// Copyright (c) Wojciech Figat. All rights reserved.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using FlaxEditor.CustomEditors;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.Options;
using FlaxEditor.Scripting;
using FlaxEngine.Utilities;
using FlaxEngine;
using FlaxEditor.GUI;

namespace FlaxEditor.Surface
{
    internal static class SurfaceUtils
    {
        internal struct GraphParameterData
        {
            public GraphParameter Parameter;
            public bool IsPublic;
            public Type Type;
            public object Tag;
            public Attribute[] Attributes;
            public EditorOrderAttribute Order;
            public EditorDisplayAttribute Display;
            public TooltipAttribute Tooltip;
            public SpaceAttribute Space;
            public HeaderAttribute Header;
            public string DisplayName;

            public GraphParameterData(GraphParameter parameter, object tag = null)
            : this(parameter, null, parameter.IsPublic, parameter.Type, SurfaceMeta.GetAttributes(parameter), tag)
            {
            }

            public GraphParameterData(GraphParameter parameter, string name, bool isPublic, Type type, Attribute[] attributes, object tag)
            {
                Parameter = parameter;
                IsPublic = isPublic;
                Type = type;
                Tag = tag;
                Attributes = attributes;
                Order = (EditorOrderAttribute)Attributes.FirstOrDefault(x => x is EditorOrderAttribute);
                Display = (EditorDisplayAttribute)Attributes.FirstOrDefault(x => x is EditorDisplayAttribute);
                Tooltip = (TooltipAttribute)Attributes.FirstOrDefault(x => x is TooltipAttribute);
                Space = (SpaceAttribute)Attributes.FirstOrDefault(x => x is SpaceAttribute);
                Header = (HeaderAttribute)Attributes.FirstOrDefault(x => x is HeaderAttribute);
                DisplayName = Display?.Name ?? name ?? parameter.Name;
            }

            public static int Compare(GraphParameterData x, GraphParameterData y)
            {
                // By order
                if (x.Order != null)
                {
                    if (y.Order != null)
                        return x.Order.Order - y.Order.Order;
                    return -1;
                }
                if (y.Order != null)
                    return 1;

                // By group name
                if (x.Display?.Group != null)
                {
                    if (y.Display?.Group != null)
                        return string.Compare(x.Display.Group, y.Display.Group, StringComparison.InvariantCulture);
                }

                // By name
                if (Editor.Instance.Options.Options.General.ScriptMembersOrder == GeneralOptions.MembersOrder.Alphabetical)
                    return string.Compare(x.DisplayName, y.DisplayName, StringComparison.InvariantCulture);

                // Keep same order
                return 0;
            }
        }

        private static Type ToType(MaterialParameterType type)
        {
            switch (type)
            {
            case MaterialParameterType.Bool: return typeof(bool);
            case MaterialParameterType.Integer: return typeof(int);
            case MaterialParameterType.Float: return typeof(float);
            case MaterialParameterType.Vector2: return typeof(Vector2);
            case MaterialParameterType.Vector3: return typeof(Vector3);
            case MaterialParameterType.Vector4: return typeof(Vector4);
            case MaterialParameterType.Color: return typeof(Color);
            case MaterialParameterType.Texture: return typeof(Texture);
            case MaterialParameterType.CubeTexture: return typeof(CubeTexture);
            case MaterialParameterType.NormalMap: return typeof(Texture);
            case MaterialParameterType.SceneTexture: return typeof(MaterialSceneTextures);
            case MaterialParameterType.GPUTexture: return typeof(GPUTexture);
            case MaterialParameterType.Matrix: return typeof(Matrix);
            case MaterialParameterType.ChannelMask: return typeof(ChannelMask);
            case MaterialParameterType.GameplayGlobal: return typeof(GameplayGlobals);
            case MaterialParameterType.TextureGroupSampler: return typeof(int);
            case MaterialParameterType.GlobalSDF: return typeof(object);
            default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
            }
        }

        internal static GroupElement InitGraphParametersGroup(LayoutElementsContainer layout)
        {
            return CustomEditors.Editors.GenericEditor.OnGroup(layout, "Parameters");
        }

        private sealed class DummyMaterialSurfaceOwner : IVisjectSurfaceOwner
        {
            public Asset SurfaceAsset => null;
            public string SurfaceName => null;
            public FlaxEditor.Undo Undo => null;
            public byte[] SurfaceData { get; set; }
            public VisjectSurfaceContext ParentContext => null;

            public void OnContextCreated(VisjectSurfaceContext context)
            {
            }

            public void OnSurfaceEditedChanged()
            {
            }

            public void OnSurfaceGraphEdited()
            {
            }

            public void OnSurfaceClose()
            {
            }
        }

        private static void FindGraphParameters(Material material, List<SurfaceParameter> surfaceParameters)
        {
            if (material == null || material.WaitForLoaded())
                return;
            var surfaceData = material.LoadSurface(false);
            if (surfaceData != null && surfaceData.Length > 0)
            {
                var surfaceOwner = new DummyMaterialSurfaceOwner { SurfaceData = surfaceData };
                var surface = new MaterialSurface(surfaceOwner);
                if (!surface.Load())
                {
                    surfaceParameters.AddRange(surface.Parameters);

                    // Search for any nested parameters (eg. via Sample Layer)
                    foreach (var node in surface.Nodes)
                    {
                        if (node.GroupArchetype.GroupID == 8 && node.Archetype.TypeID == 1) // Sample Layer
                        {
                            if (node.Values != null && node.Values.Length > 0 && node.Values[0] is Guid layerId)
                            {
                                var layer = FlaxEngine.Content.Load<MaterialBase>(layerId);
                                if (layer)
                                {
                                    FindGraphParameters(layer, surfaceParameters);
                                }
                            }
                        }
                    }
                }
                surface.Dispose();
            }
        }

        private static void FindGraphParameters(MaterialBase materialBase, List<SurfaceParameter> surfaceParameters)
        {
            while (materialBase != null && !materialBase.WaitForLoaded())
            {
                if (materialBase is MaterialInstance materialInstance)
                {
                    materialBase = materialInstance.BaseMaterial;
                }
                else if (materialBase is Material material)
                {
                    FindGraphParameters(material, surfaceParameters);
                    break;
                }
            }
        }

        internal static GraphParameterData[] InitGraphParameters(IEnumerable<MaterialParameter> parameters, Material material)
        {
            int count = parameters.Count();
            var data = new GraphParameterData[count];
            int i = 0;

            // Load material surface parameters meta to use it for material instance parameters editing
            var surfaceParameters = new List<SurfaceParameter>();
            try
            {
                Profiler.BeginEvent("Init Material Parameters UI Data");
                FindGraphParameters(material, surfaceParameters);
            }
            catch (Exception ex)
            {
                Editor.LogError("Failed to get material parameters metadata.");
                Editor.LogWarning(ex);
            }
            finally
            {
                Profiler.EndEvent();
            }

            foreach (var parameter in parameters)
            {
                var parameterId = parameter.ParameterID;
                var surfaceParameter = surfaceParameters.FirstOrDefault(x => x.ID == parameterId);
                if (surfaceParameter == null)
                {
                    // Permutate original parameter ID to reflect logic in MaterialGenerator::prepareLayer used for nested layers
                    unsafe
                    {
                        var raw = parameterId;
                        var interop = *(FlaxEngine.Json.JsonSerializer.GuidInterop*)&raw;
                        interop.A -= (uint)(i * 17 + 13);
                        parameterId = *(Guid*)&interop;
                    }
                    surfaceParameter = surfaceParameters.FirstOrDefault(x => x.ID == parameterId);
                }
                if (surfaceParameter != null)
                {
                    // Reorder so it won't be picked by other parameter that uses the same ID (eg. params from duplicated materials used as layers in other material)
                    surfaceParameters.Remove(surfaceParameter);
                    surfaceParameters.Add(surfaceParameter);
                }
                var attributes = surfaceParameter?.Meta.GetAttributes() ?? FlaxEngine.Utils.GetEmptyArray<Attribute>();
                data[i] = new GraphParameterData(null, parameter.Name, parameter.IsPublic, ToType(parameter.ParameterType), attributes, parameter);
                i++;
            }
            Array.Sort(data, GraphParameterData.Compare);
            return data;
        }

        internal static GraphParameterData[] InitGraphParameters(IEnumerable<ParticleEffectParameter> parameters)
        {
            int count = parameters.Count();
            var data = new GraphParameterData[count];
            int i = 0;
            foreach (var parameter in parameters)
            {
                data[i] = new GraphParameterData(parameter.EmitterParameter, parameter);
                i++;
            }
            Array.Sort(data, GraphParameterData.Compare);
            return data;
        }

        internal static GraphParameterData[] InitGraphParameters(IEnumerable<GraphParameter> parameters)
        {
            int count = parameters.Count();
            var data = new GraphParameterData[count];
            int i = 0;
            foreach (var parameter in parameters)
            {
                data[i] = new GraphParameterData(parameter);
                i++;
            }
            Array.Sort(data, GraphParameterData.Compare);
            return data;
        }

        internal delegate object GetGraphParameterDelegate(object instance, GraphParameter parameter, object parameterTag);

        internal delegate void SetGraphParameterDelegate(object instance, object value, GraphParameter parameter, object parameterTag);

        internal delegate void CustomPropertySpawnDelegate(LayoutElementsContainer itemLayout, ValueContainer valueContainer, ref GraphParameterData e);

        internal static void DisplayGraphParameters(LayoutElementsContainer layout, GraphParameterData[] data, GetGraphParameterDelegate getter, SetGraphParameterDelegate setter, ValueContainer values, GetGraphParameterDelegate defaultValueGetter = null, CustomPropertySpawnDelegate propertySpawn = null)
        {
            CustomEditors.Editors.GenericEditor.OnGroupsBegin();
            for (int i = 0; i < data.Length; i++)
            {
                ref var e = ref data[i];
                if (!e.IsPublic)
                    continue;
                var tag = e.Tag;
                var parameter = e.Parameter;

                // Editor Display
                var itemLayout = CustomEditors.Editors.GenericEditor.OnGroup(layout, e.Display);
                if (itemLayout is GroupElement groupElement)
                    groupElement.Panel.Open(false);

                // Space
                if (e.Space != null)
                    itemLayout.Space(e.Space.Height);

                // Header
                if (e.Header != null)
                    itemLayout.Header(e.Header);

                // Values container
                var valueType = new ScriptType(e.Type);
                var valueContainer = new CustomValueContainer(valueType, (instance, index) => getter(values[index], parameter, tag), (instance, index, value) => setter(values[index], value, parameter, tag), e.Attributes);
                for (int j = 0; j < values.Count; j++)
                    valueContainer.Add(getter(values[j], parameter, tag));

                // Default value
                if (defaultValueGetter != null)
                    valueContainer.SetDefaultValue(defaultValueGetter(values[0], parameter, tag));

                // Prefab value
                if (values.HasReferenceValue)
                {
                    var referenceValue = getter(values.ReferenceValue, parameter, tag);
                    if (referenceValue != null)
                    {
                        valueContainer.SetReferenceValue(referenceValue);
                    }
                }

                if (propertySpawn == null)
                    itemLayout.Property(e.DisplayName, valueContainer, null, e.Tooltip?.Text);
                else
                    propertySpawn(itemLayout, valueContainer, ref e);
            }
            CustomEditors.Editors.GenericEditor.OnGroupsEnd();
        }

        internal static string GetMethodDisplayName(string methodName)
        {
            if (methodName.StartsWith("get_"))
                return "Get " + methodName.Substring(4);
            if (methodName.StartsWith("set_"))
                return "Set " + methodName.Substring(4);
            if (methodName.StartsWith("op_"))
                return "Operator " + methodName.Substring(3);
            return methodName;
        }

        internal static bool IsValidVisualScriptInvokeMethod(ScriptMemberInfo member, out ScriptMemberInfo.Parameter[] parameters)
        {
            parameters = null;
            if (member.IsMethod &&
                IsValidVisualScriptType(member.ValueType) &&
                !member.HasAttribute(typeof(HideInEditorAttribute), true))
            {
                parameters = member.GetParameters();
                if (parameters.Length <= 27)
                {
                    for (int i = 0; i < parameters.Length; i++)
                    {
                        var parameter = parameters[i];
                        if (!IsValidVisualScriptType(parameter.Type))
                            return false;
                    }
                    return true;
                }
            }
            return false;
        }

        internal static bool IsValidVisualScriptOverrideMethod(ScriptMemberInfo member, out ScriptMemberInfo.Parameter[] parameters)
        {
            if (member.IsMethod &&
                member.IsVirtual &&
                IsValidVisualScriptType(member.ValueType) &&
                member.HasAttribute(typeof(UnmanagedAttribute), true) &&
                !member.HasAttribute(typeof(HideInEditorAttribute), true))
            {
                parameters = member.GetParameters();
                if (parameters.Length < 31)
                {
                    for (int i = 0; i < parameters.Length; i++)
                    {
                        var parameter = parameters[i];
                        if (!IsValidVisualScriptType(parameter.Type) || parameter.IsOut)
                            return false;
                    }
                    return true;
                }
            }
            parameters = null;
            return false;
        }

        internal static bool IsValidVisualScriptField(ScriptMemberInfo member)
        {
            return member.IsField && IsValidVisualScriptType(member.ValueType);
        }

        internal static bool IsValidVisualScriptEvent(ScriptMemberInfo member)
        {
            return member.IsEvent && member.HasAttribute(typeof(UnmanagedAttribute));
        }

        internal static bool IsValidVisualScriptType(ScriptType scriptType)
        {
            if (!scriptType.IsPublic ||
                scriptType.HasAttribute(typeof(HideInEditorAttribute), true) ||
                scriptType.HasAttribute(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), false))
                return false;
            if (scriptType.IsGenericType)
            {
                // Only Dictionary generic type is valid
                return scriptType.GetGenericTypeDefinition() == typeof(Dictionary<,>);
            }
            var managedType = TypeUtils.GetType(scriptType);
            return managedType != null && !TypeUtils.IsDelegate(managedType);
        }

        internal static bool IsValidVisualScriptFunctionType(ScriptType scriptType)
        {
            if (scriptType.IsGenericType || scriptType.IsStatic || !scriptType.IsPublic || scriptType.HasAttribute(typeof(HideInEditorAttribute), true))
                return false;
            var managedType = TypeUtils.GetType(scriptType);
            return managedType != null && !TypeUtils.IsDelegate(managedType);
        }

        internal static string GetVisualScriptTypeDescription(ScriptType type)
        {
            if (type == ScriptType.Null)
                return "null";
            var sb = new StringBuilder();
            if (type.IsStatic)
                sb.Append("static ");
            else if (type.IsAbstract)
                sb.Append("abstract ");
            sb.Append(Editor.Instance.CodeDocs.GetTooltip(type));
            return sb.ToString();
        }

        internal static string GetVisualScriptMemberInfoSignature(ScriptMemberInfo member)
        {
            var name = member.Name;
            var declaringType = member.DeclaringType;
            var valueType = member.ValueType;

            // Getter/setter method of the property - we can return early here
            if (member.IsMethod && (name.StartsWith("get_") || name.StartsWith("set_")))
            {
                var flags = member.IsStatic ? BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly : BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
                var property = declaringType.GetMembers(name.Substring(4), MemberTypes.Property, flags);
                if (property != null && property.Length != 0)
                {
                    return GetVisualScriptMemberInfoSignature(property[0]);
                }
            }

            var sb = new StringBuilder();
            if (member.IsStatic)
                sb.Append("static ");
            else if (member.IsVirtual)
                sb.Append("virtual ");
            sb.Append(valueType.Name);
            sb.Append(' ');
            if (member.IsMethod)
                sb.Append(member.DeclaringType.Namespace).Append('.');
            sb.Append(declaringType.Name);
            sb.Append('.');
            sb.Append(name);

            if (member.IsMethod)
            {
                sb.Append('(');
                var parameters = member.GetParameters();
                for (int i = 0; i < parameters.Length; i++)
                {
                    if (i != 0)
                        sb.Append(", ");
                    ref var param = ref parameters[i];
                    param.ToString(sb);
                }
                sb.Append(')');
            }
            else if (member.IsProperty)
            {
                sb.Append(' ');
                sb.Append('{');
                if (member.HasGet)
                    sb.Append(" get;");
                if (member.HasSet)
                    sb.Append(" set;");
                sb.Append(' ');
                sb.Append('}');
            }

            return sb.ToString();
        }

        internal static string GetVisualScriptMemberShortDescription(ScriptMemberInfo member)
        {
            var name = member.Name;
            var declaringType = member.DeclaringType;

            // Getter/setter method of the property - we can return early here
            if (member.IsMethod && (name.StartsWith("get_") || name.StartsWith("set_")))
            {
                var flags = member.IsStatic ? BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly : BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
                var property = declaringType.GetMembers(name.Substring(4), MemberTypes.Property, flags);
                if (property != null && property.Length != 0)
                {
                    return GetVisualScriptMemberShortDescription(property[0]);
                }
            }

            return Editor.Instance.CodeDocs.GetTooltip(member);
        }

        internal static string GetVisualScriptMemberInfoDescription(ScriptMemberInfo member)
        {
            var signature = GetVisualScriptMemberInfoSignature(member);
            var sb = new StringBuilder(signature);

            // Tooltip
            var tooltip = GetVisualScriptMemberShortDescription(member);
            if (!string.IsNullOrEmpty(tooltip))
                sb.Append("\n").Append(tooltip);

            return sb.ToString();
        }

        internal static Double4 GetDouble4(object v, float w = 0.0f)
        {
            var value = new Double4(0, 0, 0, w);
            if (v is Vector2 vec2)
                value = new Double4(vec2, 0.0f, w);
            else if (v is Vector3 vec3)
                value = new Double4(vec3, w);
            else if (v is Vector4 vec4)
                value = vec4;
            else if (v is Float2 float2)
                value = new Double4(float2, 0.0, w);
            else if (v is Float3 float3)
                value = new Double4(float3, w);
            else if (v is Float4 float4)
                value = float4;
            else if (v is Double2 double2)
                value = new Double4(double2, 0.0, w);
            else if (v is Double3 double3)
                value = new Double4(double3, w);
            else if (v is Double4 double4)
                value = double4;
            else if (v is Color col)
                value = new Double4(col.R, col.G, col.B, col.A);
            else if (v is float f)
                value = new Double4(f);
            else if (v is int i)
                value = new Double4(i);
            return value;
        }

        private static bool AreScriptTypesEqualInner(ScriptType left, ScriptType right)
        {
            // Special case for Vector types that use typedefs and might overlap
            if (left.Type == typeof(Vector2) && (right.Type == typeof(Float2) || right.Type == typeof(Double2)))
                return true;
            if (left.Type == typeof(Vector3) && (right.Type == typeof(Float3) || right.Type == typeof(Double3)))
                return true;
            if (left.Type == typeof(Vector4) && (right.Type == typeof(Float4) || right.Type == typeof(Double4)))
                return true;
            return false;
        }

        internal static bool AreScriptTypesEqual(ScriptType left, ScriptType right)
        {
            if (left == right)
                return true;
            return AreScriptTypesEqualInner(left, right) || AreScriptTypesEqualInner(right, left);
        }

        internal static void PerformCommonSetup(Windows.Assets.AssetEditorWindow window, ToolStrip toolStrip, VisjectSurface surface,
                                                out ToolStripButton saveButton, out ToolStripButton undoButton, out ToolStripButton redoButton)
        {
            var editor = window.Editor;
            var interfaceOptions = editor.Options.Options.Interface;
            var inputOptions = editor.Options.Options.Input;
            var undo = surface.Undo;
            var showSearch = () => editor.ContentFinding.ShowSearch(window);

            // Toolstrip
            saveButton = toolStrip.AddButton(editor.Icons.Save64, window.Save).LinkTooltip("Save.", ref inputOptions.Save);
            toolStrip.AddSeparator();
            undoButton = toolStrip.AddButton(editor.Icons.Undo64, undo.PerformUndo).LinkTooltip("Undo.", ref inputOptions.Undo);
            redoButton = toolStrip.AddButton(editor.Icons.Redo64, undo.PerformRedo).LinkTooltip("Redo.", ref inputOptions.Redo);
            toolStrip.AddSeparator();
            toolStrip.AddButton(editor.Icons.Search64, showSearch).LinkTooltip("Open content search tool.",  ref inputOptions.Search);
            toolStrip.AddButton(editor.Icons.CenterView64, surface.ShowWholeGraph).LinkTooltip("Show whole graph.");
            var gridSnapButton = toolStrip.AddButton(editor.Icons.Grid32, surface.ToggleGridSnapping);
            gridSnapButton.LinkTooltip("Toggle grid snapping for nodes.");
            gridSnapButton.AutoCheck = true;
            gridSnapButton.Checked = surface.GridSnappingEnabled = interfaceOptions.SurfaceGridSnapping;
            surface.GridSnappingSize = interfaceOptions.SurfaceGridSnappingSize;

            // Setup input actions
            window.InputActions.Add(options => options.Undo, undo.PerformUndo);
            window.InputActions.Add(options => options.Redo, undo.PerformRedo);
            window.InputActions.Add(options => options.Search, showSearch);
        }
    }
}
