using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEditor.ShaderGraph.Internal;
using UnityEditor.Graphing;

namespace UnityEditor.ShaderGraph.Serialization
{
    static class MultiJsonInternal
    {
        #region Unknown Data Handling
        public class UnknownJsonObject : JsonObject
        {
            public string typeInfo;
            public string jsonData;
            public JsonData<JsonObject> castedObject;

            public UnknownJsonObject(string typeInfo)
            {
                this.typeInfo = typeInfo;
            }
            public override void Deserailize(string typeInfo, string jsonData)
            {
                this.jsonData = jsonData;
            }

            public override string Serialize()
            {
                return jsonData;
            }

            public override void OnAfterDeserialize(string json)
            {
                if (castedObject.value != null)
                {
                    Enqueue(castedObject, json.Trim());
                }
            }

            public override void OnAfterMultiDeserialize(string json)
            {
                if(castedObject.value == null)
                {
                    //Never got casted so nothing ever reffed this object
                    //likely that some other unknown json object had a ref
                    //to this thing. Need to include it in the serialization
                    //step of the object still.
                    if(jsonBlobs.TryGetValue(currentRoot.objectId, out var blobs))
                    {
                        blobs[objectId] = jsonData.Trim();
                    }
                    else
                    {
                        var lookup = new Dictionary<string, string>();
                        lookup[objectId] = jsonData.Trim();
                        jsonBlobs.Add(currentRoot.objectId, lookup) ;
                    }
                }
            }

            public override T CastTo<T>()
            {
                if (castedObject.value != null)
                    return castedObject.value.CastTo<T>();

                Type t = typeof(T);
                if(t == typeof(AbstractMaterialNode) || t.IsSubclassOf(typeof(AbstractMaterialNode)))
                {
                    UnknownNodeType unt = new UnknownNodeType(jsonData);
                    valueMap[objectId] = unt;
                    s_ObjectIdField.SetValue(unt, objectId);
                    castedObject = unt;
                    return unt.CastTo<T>();
                }
                else if(t == typeof(Target) || t.IsSubclassOf(typeof(Target)))
                {
                    UnknownTargetType utt = new UnknownTargetType(typeInfo, jsonData);
                    valueMap[objectId] = utt;
                    s_ObjectIdField.SetValue(utt, objectId);
                    castedObject = utt;
                    return utt.CastTo<T>();
                }
                else if(t == typeof(SubTarget) || t.IsSubclassOf(typeof(SubTarget)))
                {
                    UnknownSubTargetType ustt = new UnknownSubTargetType(typeInfo, jsonData);
                    valueMap[objectId] = ustt;
                    s_ObjectIdField.SetValue(ustt, objectId);
                    castedObject = ustt;
                    return ustt.CastTo<T>();
                }
                else if (t == typeof(ShaderInput) || t.IsSubclassOf(typeof(ShaderInput)))
                {
                    UnknownShaderPropertyType usp = new UnknownShaderPropertyType(typeInfo, jsonData);
                    valueMap[objectId] = usp;
                    s_ObjectIdField.SetValue(usp, objectId);
                    castedObject = usp;
                    return usp.CastTo<T>();
                }
                else if (t == typeof(MaterialSlot) || t.IsSubclassOf(typeof(MaterialSlot)))
                {
                    UnknownMaterialSlotType umst = new UnknownMaterialSlotType(typeInfo, jsonData);
                    valueMap[objectId] = umst;
                    s_ObjectIdField.SetValue(umst, objectId);
                    castedObject = umst;
                    return umst.CastTo<T>();
                }
                else
                {
                    Debug.LogError($"Unable to evaluate type {typeInfo} : {jsonData}");
                }
                return null;
            }
        }

        public class UnknownTargetType : Target
        {
            public string jsonData;
            public UnknownTargetType() : base ()
            {
                isHidden = true;
            }

            private List<BlockFieldDescriptor> m_activeBlocks = null;

            public UnknownTargetType(string displayName, string jsonData)
            {
                var split = displayName.Split('.');
                var last = split[split.Length - 1];
                this.displayName = last.Replace("Target", "") + " (Unknown)";
                isHidden = false;
                this.jsonData = jsonData;
            }

            public override void Deserailize(string typeInfo, string jsonData)
            {
                this.jsonData = jsonData;
                base.Deserailize(typeInfo, jsonData);
            }
            public override string Serialize()
            {
                return jsonData.Trim();
            }
            //When we first call GetActiveBlocks, we assume any unknown blockfielddescriptors are owned by this target
            public override void GetActiveBlocks(ref TargetActiveBlockContext context)
            {
                if (m_activeBlocks == null)
                {
                    m_activeBlocks = new List<BlockFieldDescriptor>();
                    foreach (var cur in context.currentBlocks)
                    {
                        if (cur.isUnknown && !string.IsNullOrEmpty(cur.displayName))
                        {
                            m_activeBlocks.Add(cur);
                        }
                    }
                }

                foreach(var block in m_activeBlocks)
                {
                    context.AddBlock(block);
                }
            }

            public override void GetFields(ref TargetFieldContext context)
            {
            }

            public override void GetPropertiesGUI(ref TargetPropertyGUIContext context, Action onChange, Action<string> registerUndo)
            {
                context.AddHelpBox(MessageType.Warning, "Cannot find the code for this Target, a package may be missing.");
            }

            public override bool IsActive() => false;

            public override void Setup(ref TargetSetupContext context)
            {
            }

            public override bool WorksWithSRP(RenderPipelineAsset scriptableRenderPipeline) => false;
        }

        private class UnknownSubTargetType : SubTarget
        {
            public string jsonData;
            public UnknownSubTargetType() : base()
            {
                isHidden = true;
            }
            public UnknownSubTargetType(string displayName, string jsonData) : base()
            {
                isHidden = false;
                this.displayName = displayName;
                this.jsonData = jsonData;
            }

            public override void Deserailize(string typeInfo, string jsonData)
            {
                this.jsonData = jsonData;
                base.Deserailize(typeInfo, jsonData);
            }

            public override string Serialize()
            {
                return jsonData.Trim();
            }

            internal override Type targetType => typeof(UnknownTargetType);

            public override void GetActiveBlocks(ref TargetActiveBlockContext context)
            {
            }

            public override void GetFields(ref TargetFieldContext context)
            {
            }

            public override void GetPropertiesGUI(ref TargetPropertyGUIContext context, Action onChange, Action<string> registerUndo)
            {
                context.AddHelpBox(MessageType.Warning, "Cannot find the code for this SubTarget, a package may be missing.");
            }

            public override bool IsActive() => false;

            public override void Setup(ref TargetSetupContext context)
            {
            }
        }

        internal class UnknownShaderPropertyType : AbstractShaderProperty
        {
            public string jsonData;

            public UnknownShaderPropertyType(string displayName, string jsonData) : base()
            {
                this.displayName = displayName;
                this.jsonData = jsonData;
            }

            public override void Deserailize(string typeInfo, string jsonData)
            {
                this.jsonData = jsonData;
                base.Deserailize(typeInfo, jsonData);
            }

            public override string Serialize()
            {
                return jsonData.Trim();
            }

            internal override ConcreteSlotValueType concreteShaderValueType => ConcreteSlotValueType.Vector1;
            internal override bool isExposable => false;
            internal override bool isRenamable => false;
            internal override ShaderInput Copy()
            {
                // we CANNOT copy ourselves, as the serialized GUID in the jsonData would not match the json GUID
                return null;
            }

            public override PropertyType propertyType => PropertyType.Float;
            internal override void GetPropertyReferenceNames(List<string> result) { }
            internal override void GetPropertyDisplayNames(List<string> result) { }
            internal override string GetPropertyBlockString() { return ""; }
            internal override void AppendPropertyBlockStrings(ShaderStringBuilder builder)
            {
                builder.AppendLine("/* UNKNOWN PROPERTY: " + referenceName + " */");
            }
            internal override bool AllowHLSLDeclaration(HLSLDeclaration decl) => false;
            internal override void ForeachHLSLProperty(Action<HLSLProperty> action)
            {
                action(new HLSLProperty(HLSLType._float, referenceName, HLSLDeclaration.Global, concretePrecision));
            }
            internal override string GetPropertyAsArgumentString() { return ""; }
            internal override AbstractMaterialNode ToConcreteNode() { return null; }

            internal override PreviewProperty GetPreviewMaterialProperty()
            {
                return new PreviewProperty(propertyType)
                {
                    name = referenceName,
                    floatValue = 0.0f
                };
            }

            public override string GetPropertyTypeString() { return ""; }
        }

        internal class UnknownMaterialSlotType : MaterialSlot
        {
            // used to deserialize some data out of an unknown MaterialSlot
            class SerializerHelper
            {
                [SerializeField]
                public string m_DisplayName = null;

                [SerializeField]
                public SlotType m_SlotType = SlotType.Input;

                [SerializeField]
                public bool m_Hidden = false;

                [SerializeField]
                public string m_ShaderOutputName = null;

                [SerializeField]
                public ShaderStageCapability m_StageCapability = ShaderStageCapability.All;
            }

            public string jsonData;

            public UnknownMaterialSlotType(string displayName, string jsonData) : base()
            {
                // copy some minimal information to try to keep the UI as similar as possible
                var helper = new SerializerHelper();
                JsonUtility.FromJsonOverwrite(jsonData, helper);
                this.displayName = helper.m_DisplayName;
                this.hidden = helper.m_Hidden;
                this.stageCapability = helper.m_StageCapability;
                this.SetInternalData(helper.m_SlotType, helper.m_ShaderOutputName);

                // save the original json for saving
                this.jsonData = jsonData;
            }

            public override void Deserailize(string typeInfo, string jsonData)
            {
                this.jsonData = jsonData;
                base.Deserailize(typeInfo, jsonData);
            }

            public override string Serialize()
            {
                return jsonData.Trim();
            }

            public override bool isDefaultValue => true;

            public override SlotValueType valueType => SlotValueType.Vector1;

            public override ConcreteSlotValueType concreteValueType => ConcreteSlotValueType.Vector1;

            public override void AddDefaultProperty(PropertyCollector properties, GenerationMode generationMode) {}

            public override void CopyValuesFrom(MaterialSlot foundSlot)
            {
                // we CANNOT copy data from another slot, as the GUID in the serialized jsonData would not match our real GUID
                throw new NotSupportedException();
            }
        }

        [NeverAllowedByTarget]
        internal class UnknownNodeType : AbstractMaterialNode
        {
            public string jsonData;

            public UnknownNodeType() : base()
            {
                jsonData = null;
                isValid = false;
                SetOverrideActiveState(ActiveState.ExplicitInactive, false);
                SetActive(false, false);
            }
            public UnknownNodeType(string jsonData) 
            {
                this.jsonData = jsonData;
                isValid = false;
                SetOverrideActiveState(ActiveState.ExplicitInactive, false);
                SetActive(false, false);
            }

            public override void OnAfterDeserialize(string json)
            {
                jsonData = json;
                base.OnAfterDeserialize(json);
            }

            public override string Serialize()
            {
                EnqueSlotsForSerialization();
                return jsonData.Trim();
            }

            public override void ValidateNode()
            {
                base.ValidateNode();
                owner.AddValidationError(objectId, "This node type could not be found. No function will be generated in the shader.", ShaderCompilerMessageSeverity.Warning);
            }

            // unknown node types cannot be copied, or else their GUID would not match the GUID in the serialized jsonDAta
            public override bool canCutNode => false;
            public override bool canCopyNode => false;
        }
        #endregion //Unknown Data Handling

        static readonly Dictionary<string, Type> k_TypeMap = CreateTypeMap();

        internal static bool isDeserializing;

        internal static readonly Dictionary<string, JsonObject> valueMap = new Dictionary<string, JsonObject>();

        static List<MultiJsonEntry> s_Entries;

        internal static bool isSerializing;

        internal static readonly List<JsonObject> serializationQueue = new List<JsonObject>();

        internal static readonly HashSet<string> serializedSet = new HashSet<string>();

        static JsonObject currentRoot = null;

        static Dictionary<string, Dictionary<string, string>>jsonBlobs = new Dictionary<string, Dictionary<string,string>>();

        static Dictionary<string, Type> CreateTypeMap()
        {
            var map = new Dictionary<string, Type>();
            foreach (var type in TypeCache.GetTypesDerivedFrom<JsonObject>())
            {
                if (type.FullName != null)
                {
                    map[type.FullName] = type;
                }
            }

            foreach (var type in TypeCache.GetTypesWithAttribute(typeof(FormerNameAttribute)))
            {
                if (type.IsAbstract || !typeof(JsonObject).IsAssignableFrom(type))
                {
                    continue;
                }

                foreach (var attribute in type.GetCustomAttributes(typeof(FormerNameAttribute), false))
                {
                    var legacyAttribute = (FormerNameAttribute)attribute;
                    map[legacyAttribute.fullName] = type;
                }
            }

            return map;
        }

        public static Type ParseType(string typeString)
        {
            k_TypeMap.TryGetValue(typeString, out var type);
            return type;
        }

        public static List<MultiJsonEntry> Parse(string str)
        {
            var result = new List<MultiJsonEntry>();
            const string separatorStr = "\n\n";
            var startIndex = 0;
            var raw = new FakeJsonObject();

            while (startIndex < str.Length)
            {
                var jsonBegin = str.IndexOf("{", startIndex, StringComparison.Ordinal);
                if (jsonBegin == -1)
                {
                    break;
                }

                var jsonEnd = str.IndexOf(separatorStr, jsonBegin, StringComparison.Ordinal);
                if (jsonEnd == -1)
                {
                    jsonEnd = str.IndexOf("\n\r\n", jsonBegin, StringComparison.Ordinal);
                    if (jsonEnd == -1)
                    {
                        jsonEnd = str.LastIndexOf("}", StringComparison.Ordinal) + 1;
                    }
                }

                var json = str.Substring(jsonBegin, jsonEnd - jsonBegin);

                JsonUtility.FromJsonOverwrite(json, raw);
                if (startIndex != 0 && string.IsNullOrWhiteSpace(raw.type))
                {
                    throw new InvalidOperationException($"Type is null or whitespace in JSON:\n{json}");
                }

                result.Add(new MultiJsonEntry(raw.type, raw.id, json));
                raw.Reset();

                startIndex = jsonEnd + separatorStr.Length;
            }

            return result;
        }

        public static void Enqueue(JsonObject jsonObject, string json)
        {
            if (s_Entries == null)
            {
                throw new InvalidOperationException("Can only Enqueue during JsonObject.OnAfterDeserialize.");
            }

            valueMap.Add(jsonObject.objectId, jsonObject);
            s_Entries.Add(new MultiJsonEntry(jsonObject.GetType().FullName, jsonObject.objectId, json));
        }

        public static JsonObject CreateInstance(string typeString)
        {
            if (!k_TypeMap.TryGetValue(typeString, out var type))
            {
                return new UnknownJsonObject(typeString);
            }
            var output = (JsonObject)Activator.CreateInstance(type, true);
            //This CreateInstance function is supposed to esentially create a blank copy of whatever class we end up deserializing into.
            //when we typically create new JsonObjects in all other cases, we want that object to be assumed to be the latest version.
            //This doesnt work if any json object was serialized before we had the idea of version, as the blank copy would have the
            //latest version on creation and since the serialized version wouldnt have a version member, it would not get overwritten
            //and we would automatically upgrade all previously serialized json objects incorrectly and without user action. To avoid this,
            //we default jsonObject version to 0, and if the serialized value has a different saved version it gets changed and if the serialized
            //version does not have a different saved value it remains 0 (earliest version)
            output.ChangeVersion(0);
            return output;
        }

        private static FieldInfo s_ObjectIdField =
            typeof(JsonObject).GetField("m_ObjectId", BindingFlags.Instance | BindingFlags.NonPublic);

        public static void Deserialize(JsonObject root, List<MultiJsonEntry> entries, bool rewriteIds)
        {
            if (isDeserializing)
            {
                throw new InvalidOperationException("Nested MultiJson deserialization is not supported.");
            }

            try
            {
                isDeserializing = true;
                currentRoot = root;
                root.ChangeVersion(0); //Same issue as described in CreateInstance
                for (var index = 0; index < entries.Count; index++)
                {
                    var entry = entries[index];
                    try
                    {
                        JsonObject value = null;
                        if(index == 0)
                        {
                            value = root;
                        }
                        else
                        {
                            value = CreateInstance(entry.type);
                        }

                        var id = entry.id;

                        if (id != null)
                        {
                            // Need to make sure that references looking for the old ID will find it in spite of
                            // ID rewriting.
                            valueMap[id] = value;
                        }

                        if (rewriteIds || entry.id == null)
                        {
                            id = value.objectId;
                            entries[index] = new MultiJsonEntry(entry.type, id, entry.json);
                            valueMap[id] = value;
                        }

                        s_ObjectIdField.SetValue(value, id);
                    }
                    catch (Exception e)
                    {
                        // External code could throw exceptions, but we don't want that to fail the whole thing.
                        // Potentially, the fallback type should also be used here.
                        Debug.LogException(e);
                    }
                }

                s_Entries = entries;

                // Not a foreach because `entries` can be populated by calls to `Enqueue` as we go.
                for (var i = 0; i < entries.Count; i++)
                {
                    var entry = entries[i];
                    try
                    {
                        var value = valueMap[entry.id];
                        value.Deserailize(entry.type, entry.json);
                        // Set ID again as it could be overwritten from JSON.
                        s_ObjectIdField.SetValue(value, entry.id);
                        value.OnAfterDeserialize(entry.json);
                    }
                    catch (Exception e)
                    {
                        if(!String.IsNullOrEmpty(entry.id))
                        {
                            var value = valueMap[entry.id];
                            if(value != null)
                            {
                                Debug.LogError($"Exception thrown while deserialize object of type {entry.type}: {e.Message}");
                            }
                        }
                        Debug.LogException(e);
                    }
                }

                s_Entries = null;

                foreach (var entry in entries)
                {
                    try
                    {
                        var value = valueMap[entry.id];
                        value.OnAfterMultiDeserialize(entry.json);
                    }
                    catch (Exception e)
                    {
                        Debug.LogException(e);
                    }
                }
            }
            finally
            {
                valueMap.Clear();
                currentRoot = null;
                isDeserializing = false;
            }
        }

        public static string Serialize(JsonObject mainObject)
        {
            if (isSerializing)
            {
                throw new InvalidOperationException("Nested MultiJson serialization is not supported.");
            }

            try
            {
                isSerializing = true;

                serializedSet.Add(mainObject.objectId);
                serializationQueue.Add(mainObject);

                var idJsonList = new List<(string, string)>();

                // Not a foreach because the queue is populated by `JsonData<T>`s as we go.
                for (var i = 0; i < serializationQueue.Count; i++)
                {
                    var value = serializationQueue[i];
                    var json = value.Serialize();
                    idJsonList.Add((value.objectId, json));
                }

                if(jsonBlobs.TryGetValue(mainObject.objectId, out var blobs))
                {
                    foreach(var blob in blobs)
                    {
                        if(!idJsonList.Contains((blob.Key, blob.Value)))
                            idJsonList.Add((blob.Key, blob.Value));
                    }
                }



                idJsonList.Sort((x, y) =>
                    // Main object needs to be placed first
                    x.Item1 == mainObject.objectId ? -1 :
                    y.Item1 == mainObject.objectId ? 1 :
                    // We sort everything else by ID to consistently maintain positions in the output
                    x.Item1.CompareTo(y.Item1));

                var sb = new StringBuilder();
                foreach (var (id, json) in idJsonList)
                {
                    sb.AppendLine(json);
                    sb.AppendLine();
                }

                return sb.ToString();
            }
            finally
            {
                serializationQueue.Clear();
                serializedSet.Clear();
                isSerializing = false;
            }
        }

        public static void PopulateValueMap(JsonObject mainObject)
        {
            if (isSerializing)
            {
                throw new InvalidOperationException("Nested MultiJson serialization is not supported.");
            }

            try
            {
                isSerializing = true;

                serializedSet.Add(mainObject.objectId);
                serializationQueue.Add(mainObject);

                // Not a foreach because the queue is populated by `JsonRef<T>`s as we go.
                for (var i = 0; i < serializationQueue.Count; i++)
                {
                    var value = serializationQueue[i];
                    value.Serialize();
                    valueMap[value.objectId] = value;
                }
            }
            finally
            {
                serializationQueue.Clear();
                serializedSet.Clear();
                isSerializing = false;
            }
        }
    }
}
