// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.

//#define DEBUG_INVOKE_METHODS_SEARCHING
//#define DEBUG_FIELDS_SEARCHING

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using FlaxEditor.Content;
using FlaxEditor.GUI.Drag;
using FlaxEditor.SceneGraph;
using FlaxEditor.Scripting;
using FlaxEditor.Surface.ContextMenu;
using FlaxEditor.Surface.Elements;
using FlaxEngine;
using FlaxEngine.GUI;
using Object = FlaxEngine.Object;

namespace FlaxEditor.Surface
{
    /// <summary>
    /// The Visject Surface implementation for the Visual Script editor.
    /// </summary>
    /// <seealso cref="FlaxEditor.Surface.VisjectSurface" />
    [HideInEditor]
    public class VisualScriptSurface : VisjectSurface
    {
        private readonly GroupArchetype _methodOverridesGroupArchetype = new GroupArchetype
        {
            GroupID = 16,
            Name = "Method Overrides",
            Color = new Color(109, 160, 24),
            Archetypes = new List<NodeArchetype>(),
        };

        internal static class NodesCache
        {
            private static readonly object _locker = new object();
            private static int _version;
            private static Task _task;
            private static VisjectCM _taskContextMenu;
            private static Dictionary<KeyValuePair<string, ushort>, GroupArchetype> _cache;

            public static void Wait()
            {
                _task?.Wait();
            }

            public static void Clear()
            {
                Wait();

                if (_cache != null && _cache.Count != 0)
                {
                    OnCodeEditingTypesCleared();
                }
            }

            public static void Get(VisjectCM contextMenu)
            {
                Wait();

                lock (_locker)
                {
                    if (_cache == null)
                        _cache = new Dictionary<KeyValuePair<string, ushort>, GroupArchetype>();
                    contextMenu.LockChildrenRecursive();

                    // Check if has cached groups
                    if (_cache.Count != 0)
                    {
                        // Check if context menu doesn;t have the recent cached groups
                        if (!contextMenu.Groups.Any(g => g.Archetype.Tag is int asInt && asInt == _version))
                        {
                            var groups = contextMenu.Groups.Where(g => g.Archetype.Tag is int).ToArray();
                            foreach (var g in groups)
                                contextMenu.RemoveGroup(g);
                            foreach (var g in _cache.Values)
                                contextMenu.AddGroup(g);
                        }
                    }
                    else
                    {
                        // Remove any old groups from context menu
                        var groups = contextMenu.Groups.Where(g => g.Archetype.Tag is int).ToArray();
                        foreach (var g in groups)
                            contextMenu.RemoveGroup(g);

                        // Register for scripting types reload
                        Editor.Instance.CodeEditing.TypesCleared += OnCodeEditingTypesCleared;

                        // Run caching on an async
                        _task = Task.Run(OnActiveContextMenuShowAsync);
                        _taskContextMenu = contextMenu;
                    }

                    contextMenu.UnlockChildrenRecursive();
                }
            }

            private static void OnActiveContextMenuShowAsync()
            {
                Profiler.BeginEvent("Setup Visual Script Context Menu (async)");
#if DEBUG_INVOKE_METHODS_SEARCHING || DEBUG_FIELDS_SEARCHING
                var searchStartTime = DateTime.Now;
                var searchHitsCount = 0;
#endif

                foreach (var scriptType in Editor.Instance.CodeEditing.All.Get())
                {
                    if (!SurfaceUtils.IsValidVisualScriptType(scriptType))
                        continue;

                    // Skip Newtonsoft.Json stuff
                    var scriptTypeTypeName = scriptType.TypeName;
                    if (scriptTypeTypeName.StartsWith("Newtonsoft.Json."))
                        continue;
                    var scriptTypeName = scriptType.Name;

                    // Enum
                    if (scriptType.IsEnum)
                    {
                        // Create node archetype
                        var node = (NodeArchetype)Archetypes.Constants.Nodes[10].Clone();
                        node.DefaultValues[0] = Activator.CreateInstance(scriptType.Type);
                        node.Flags &= ~NodeFlags.NoSpawnViaGUI;
                        node.Title = scriptTypeName;
                        node.Description = scriptTypeTypeName;
                        var attributes = scriptType.GetAttributes(false);
                        var tooltipAttribute = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute);
                        if (tooltipAttribute != null)
                            node.Description += "\n" + tooltipAttribute.Text;

                        // Create group archetype
                        var groupKey = new KeyValuePair<string, ushort>(scriptTypeName, 2);
                        if (!_cache.TryGetValue(groupKey, out var group))
                        {
                            group = new GroupArchetype
                            {
                                GroupID = groupKey.Value,
                                Name = groupKey.Key,
                                Color = new Color(243, 156, 18),
                                Tag = _version,
                                Archetypes = new List<NodeArchetype>(),
                            };
                            _cache.Add(groupKey, group);
                        }

                        // Add node to the group
                        ((IList<NodeArchetype>)group.Archetypes).Add(node);
                        continue;
                    }

                    // Structure
                    if (scriptType.IsValueType)
                    {
                        if (scriptType.IsVoid)
                            continue;

                        // Create group archetype
                        var groupKey = new KeyValuePair<string, ushort>(scriptTypeName, 4);
                        if (!_cache.TryGetValue(groupKey, out var group))
                        {
                            group = new GroupArchetype
                            {
                                GroupID = groupKey.Value,
                                Name = groupKey.Key,
                                Color = new Color(155, 89, 182),
                                Tag = _version,
                                Archetypes = new List<NodeArchetype>(),
                            };
                            _cache.Add(groupKey, group);
                        }

                        var attributes = scriptType.GetAttributes(false);
                        var tooltipAttribute = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute);

                        // Create Pack node archetype
                        var node = (NodeArchetype)Archetypes.Packing.Nodes[6].Clone();
                        node.DefaultValues[0] = scriptTypeTypeName;
                        node.Flags &= ~NodeFlags.NoSpawnViaGUI;
                        node.Title = "Pack " + scriptTypeName;
                        node.Description = scriptTypeTypeName;
                        if (tooltipAttribute != null)
                            node.Description += "\n" + tooltipAttribute.Text;
                        ((IList<NodeArchetype>)group.Archetypes).Add(node);

                        // Create Unpack node archetype
                        node = (NodeArchetype)Archetypes.Packing.Nodes[13].Clone();
                        node.DefaultValues[0] = scriptTypeTypeName;
                        node.Flags &= ~NodeFlags.NoSpawnViaGUI;
                        node.Title = "Unpack " + scriptTypeName;
                        node.Description = scriptTypeTypeName;
                        if (tooltipAttribute != null)
                            node.Description += "\n" + tooltipAttribute.Text;
                        ((IList<NodeArchetype>)group.Archetypes).Add(node);
                    }

                    foreach (var member in scriptType.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly))
                    {
                        if (member.IsGeneric)
                            continue;

                        if (member.IsMethod)
                        {
                            // Skip methods not declared in this type
                            if (member.Type is MethodInfo m && m.GetBaseDefinition().DeclaringType != m.DeclaringType)
                                continue;
                            var name = member.Name;
                            if (name == "ToString")
                                continue;

                            // Skip if searching by name doesn't return a match
                            var members = scriptType.GetMembers(name, MemberTypes.Method, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly);
                            if (!members.Contains(member))
                                continue;

                            // Check if method is valid for Visual Script usage
                            if (SurfaceUtils.IsValidVisualScriptInvokeMethod(member, out var parameters))
                            {
                                // Create node archetype
                                var node = (NodeArchetype)Archetypes.Function.Nodes[3].Clone();
                                node.DefaultValues[0] = scriptTypeTypeName;
                                node.DefaultValues[1] = name;
                                node.DefaultValues[2] = parameters.Length;
                                node.Flags &= ~NodeFlags.NoSpawnViaGUI;
                                node.Title = SurfaceUtils.GetMethodDisplayName((string)node.DefaultValues[1]);
                                node.Description = SurfaceUtils.GetVisualScriptMemberInfoDescription(member);
                                node.SubTitle = string.Format(" (in {0})", scriptTypeName);
                                node.Tag = member;

                                // Create group archetype
                                var groupKey = new KeyValuePair<string, ushort>(scriptTypeName, 16);
                                if (!_cache.TryGetValue(groupKey, out var group))
                                {
                                    group = new GroupArchetype
                                    {
                                        GroupID = groupKey.Value,
                                        Name = groupKey.Key,
                                        Color = new Color(109, 160, 24),
                                        Tag = _version,
                                        Archetypes = new List<NodeArchetype>(),
                                    };
                                    _cache.Add(groupKey, group);
                                }

                                // Add node to the group
                                ((IList<NodeArchetype>)group.Archetypes).Add(node);
#if DEBUG_INVOKE_METHODS_SEARCHING
                                Editor.LogWarning(scriptTypeTypeName + " -> " + member.GetSignature());
                                searchHitsCount++;
#endif
                            }
                        }
                        else if (member.IsField)
                        {
                            var name = member.Name;

                            // Skip if searching by name doesn't return a match
                            var members = scriptType.GetMembers(name, MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly);
                            if (!members.Contains(member))
                                continue;

                            // Check if field is valid for Visual Script usage
                            if (SurfaceUtils.IsValidVisualScriptField(member))
                            {
                                if (member.HasGet)
                                {
                                    // Create node archetype
                                    var node = (NodeArchetype)Archetypes.Function.Nodes[6].Clone();
                                    node.DefaultValues[0] = scriptTypeTypeName;
                                    node.DefaultValues[1] = name;
                                    node.DefaultValues[2] = member.ValueType.TypeName;
                                    node.DefaultValues[3] = member.IsStatic;
                                    node.Flags &= ~NodeFlags.NoSpawnViaGUI;
                                    node.Title = "Get " + name;
                                    node.Description = SurfaceUtils.GetVisualScriptMemberInfoDescription(member);
                                    node.SubTitle = string.Format(" (in {0})", scriptTypeName);

                                    // Create group archetype
                                    var groupKey = new KeyValuePair<string, ushort>(scriptTypeName, 16);
                                    if (!_cache.TryGetValue(groupKey, out var group))
                                    {
                                        group = new GroupArchetype
                                        {
                                            GroupID = groupKey.Value,
                                            Name = groupKey.Key,
                                            Color = new Color(109, 160, 24),
                                            Tag = _version,
                                            Archetypes = new List<NodeArchetype>(),
                                        };
                                        _cache.Add(groupKey, group);
                                    }

                                    // Add node to the group
                                    ((IList<NodeArchetype>)group.Archetypes).Add(node);
#if DEBUG_FIELDS_SEARCHING
                                    Editor.LogWarning(scriptTypeTypeName + " -> Get " + member.GetSignature());
                                    searchHitsCount++;
#endif
                                }
                                if (member.HasSet)
                                {
                                    // Create node archetype
                                    var node = (NodeArchetype)Archetypes.Function.Nodes[7].Clone();
                                    node.DefaultValues[0] = scriptTypeTypeName;
                                    node.DefaultValues[1] = name;
                                    node.DefaultValues[2] = member.ValueType.TypeName;
                                    node.DefaultValues[3] = member.IsStatic;
                                    node.Flags &= ~NodeFlags.NoSpawnViaGUI;
                                    node.Title = "Set " + name;
                                    node.Description = SurfaceUtils.GetVisualScriptMemberInfoDescription(member);
                                    node.SubTitle = string.Format(" (in {0})", scriptTypeName);

                                    // Create group archetype
                                    var groupKey = new KeyValuePair<string, ushort>(scriptTypeName, 16);
                                    if (!_cache.TryGetValue(groupKey, out var group))
                                    {
                                        group = new GroupArchetype
                                        {
                                            GroupID = groupKey.Value,
                                            Name = groupKey.Key,
                                            Color = new Color(109, 160, 24),
                                            Tag = _version,
                                            Archetypes = new List<NodeArchetype>(),
                                        };
                                        _cache.Add(groupKey, group);
                                    }

                                    // Add node to the group
                                    ((IList<NodeArchetype>)group.Archetypes).Add(node);
#if DEBUG_FIELDS_SEARCHING
                                    Editor.LogWarning(scriptTypeTypeName + " -> Set " + member.GetSignature());
                                    searchHitsCount++;
#endif
                                }
                            }
                        }
                    }
                }

                // Add group to context menu (on a main thread)
                FlaxEngine.Scripting.InvokeOnUpdate(() =>
                {
#if DEBUG_INVOKE_METHODS_SEARCHING || DEBUG_FIELDS_SEARCHING
                    var addStartTime = DateTime.Now;
#endif
                    lock (_locker)
                    {
                        _taskContextMenu.AddGroups(_cache.Values);
                        _taskContextMenu = null;
                    }
#if DEBUG_INVOKE_METHODS_SEARCHING || DEBUG_FIELDS_SEARCHING
                    Editor.LogError($"Added items to VisjectCM in: {(DateTime.Now - addStartTime).TotalMilliseconds} ms");
#endif
                });

#if DEBUG_INVOKE_METHODS_SEARCHING || DEBUG_FIELDS_SEARCHING
                Editor.LogError($"Collected {searchHitsCount} items in: {(DateTime.Now - searchStartTime).TotalMilliseconds} ms");
#endif
                Profiler.EndEvent();

                lock (_locker)
                {
                    _task = null;
                }
            }

            private static void OnCodeEditingTypesCleared()
            {
                Wait();

                lock (_locker)
                {
                    _cache.Clear();
                    _version++;
                }

                Editor.Instance.CodeEditing.TypesCleared -= OnCodeEditingTypesCleared;
            }
        }

        private DragActors _dragActors;

        /// <summary>
        /// The script that is edited by the surface.
        /// </summary>
        public VisualScript Script;

        /// <summary>
        /// The list of nodes with breakpoints set.
        /// </summary>
        public readonly List<SurfaceNode> Breakpoints = new List<SurfaceNode>();

        /// <inheritdoc />
        public VisualScriptSurface(IVisjectSurfaceOwner owner, Action onSave, FlaxEditor.Undo undo)
        : base(owner, onSave, undo, null, null, true)
        {
            _supportsImplicitCastFromObjectToBoolean = true;
            DragHandlers.Add(_dragActors = new DragActors(ValidateDragActor));
        }

        private bool ValidateDragActor(ActorNode actor)
        {
            return true;
        }

        /// <inheritdoc />
        public override void OnNodeBreakpointEdited(SurfaceNode node)
        {
            base.OnNodeBreakpointEdited(node);

            if (node.Breakpoint.Set && node.Breakpoint.Enabled)
                Breakpoints.Add(node);
            else
                Breakpoints.Remove(node);
        }

        /// <inheritdoc />
        public override string GetTypeName(ScriptType type)
        {
            if (type.Type == typeof(void))
                return "Impulse";
            return base.GetTypeName(type);
        }

        /// <inheritdoc />
        public override bool GetBoxDebuggerTooltip(Box box, out string text)
        {
            if (Editor.Instance.Simulation.IsDuringBreakpointHang)
            {
                // Find any local variable from the current scope that matches this box
                var state = Windows.Assets.VisualScriptWindow.GetLocals();
                if (state.Locals != null)
                {
                    for (int i = 0; i < state.Locals.Length; i++)
                    {
                        ref var local = ref state.Locals[i];
                        if (local.BoxId == box.ID && local.NodeId == box.ParentNode.ID)
                        {
                            text = $"{local.Value ?? string.Empty} ({local.ValueTypeName})";
                            return true;
                        }
                    }
                }

                // Evaluate the value using the Visual Scripting backend
                if (box.CurrentType != typeof(void))
                {
                    var local = new Editor.VisualScriptLocal
                    {
                        NodeId = box.ParentNode.ID,
                        BoxId = box.ID,
                    };
                    var script = ((Windows.Assets.VisualScriptWindow)box.Surface.Owner).Asset;
                    if (Editor.Internal_EvaluateVisualScriptLocal(Object.GetUnmanagedPtr(script), ref local))
                    {
                        text = $"{local.Value ?? string.Empty} ({local.ValueTypeName})";
                        return true;
                    }
                }
            }
            text = null;
            return false;
        }

        /// <inheritdoc />
        public override bool CanSetParameters => true;

        /// <inheritdoc />
        public override bool CanUseNodeType(NodeArchetype nodeArchetype)
        {
            return (nodeArchetype.Flags & NodeFlags.VisualScriptGraph) != 0 && base.CanUseNodeType(nodeArchetype);
        }

        /// <inheritdoc />
        protected override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId)
        {
            groupId = 6;
            return Archetypes.Parameters.Nodes[2];
        }

        /// <inheritdoc />
        protected override void OnShowPrimaryMenu(VisjectCM activeCM, Vector2 location, Box startBox)
        {
            Profiler.BeginEvent("Setup Visual Script Context Menu");

            // Update nodes for method overrides
            activeCM.RemoveGroup(_methodOverridesGroupArchetype);
            if (Script && !Script.WaitForLoaded(100))
            {
                var nodes = (List<NodeArchetype>)_methodOverridesGroupArchetype.Archetypes;
                nodes.Clear();

                // Find methods to override from the inherited types
                var scriptMeta = Script.Meta;
                var baseType = TypeUtils.GetType(scriptMeta.BaseTypename);
                var members = baseType.GetMembers(BindingFlags.Public | BindingFlags.Instance);
                for (var i = 0; i < members.Length; i++)
                {
                    ref var member = ref members[i];
                    if (SurfaceUtils.IsValidVisualScriptOverrideMethod(member, out var parameters))
                    {
                        var name = member.Name;

                        // Skip already added override method nodes
                        bool isAlreadyAdded = false;
                        foreach (var n in Nodes)
                        {
                            if (n.GroupArchetype.GroupID == 16 &&
                                n.Archetype.TypeID == 3 &&
                                (string)n.Values[0] == name &&
                                (int)n.Values[1] == parameters.Length)
                            {
                                isAlreadyAdded = true;
                                break;
                            }
                        }
                        if (isAlreadyAdded)
                            continue;

                        var node = (NodeArchetype)Archetypes.Function.Nodes[2].Clone();
                        node.Flags &= ~NodeFlags.NoSpawnViaGUI;
                        var attributes = member.GetAttributes(true);
                        var tooltipAttribute = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute);
                        if (tooltipAttribute != null)
                            node.Description = tooltipAttribute.Text;
                        node.DefaultValues[0] = name;
                        node.DefaultValues[1] = parameters.Length;
                        node.Title = "Override " + name;
                        nodes.Add(node);
                    }
                }

                activeCM.AddGroup(_methodOverridesGroupArchetype);
            }

            // Update nodes for invoke methods (async)
            NodesCache.Get(activeCM);

            Profiler.EndEvent();

            base.OnShowPrimaryMenu(activeCM, location, startBox);

            activeCM.VisibleChanged += OnActiveContextMenuVisibleChanged;
        }

        private void OnActiveContextMenuVisibleChanged(Control activeCM)
        {
            NodesCache.Wait();
        }

        /// <inheritdoc />
        protected override bool ValidateDragItem(AssetItem assetItem)
        {
            if (assetItem.IsOfType<GameplayGlobals>())
                return true;
            if (assetItem.IsOfType<Asset>())
                return true;
            return base.ValidateDragItem(assetItem);
        }

        /// <inheritdoc />
        protected override void HandleDragDropAssets(List<AssetItem> objects, DragDropEventArgs args)
        {
            for (int i = 0; i < objects.Count; i++)
            {
                var assetItem = objects[i];
                SurfaceNode node = null;

                if (assetItem.IsOfType<GameplayGlobals>())
                {
                    node = Context.SpawnNode(7, 16, args.SurfaceLocation, new object[]
                    {
                        assetItem.ID,
                        string.Empty,
                    });
                }
                else if (assetItem.IsOfType<Asset>())
                {
                    node = Context.SpawnNode(7, 18, args.SurfaceLocation, new object[]
                    {
                        assetItem.ID,
                    });
                }

                if (node != null)
                {
                    args.SurfaceLocation.X += node.Width + 10;
                }
            }

            base.HandleDragDropAssets(objects, args);
        }

        /// <inheritdoc />
        public override DragDropEffect OnDragDrop(ref Vector2 location, DragData data)
        {
            var args = new DragDropEventArgs
            {
                SurfaceLocation = _rootControl.PointFromParent(ref location)
            };

            // Drag actors
            if (_dragActors.HasValidDrag)
            {
                for (int i = 0; i < _dragActors.Objects.Count; i++)
                {
                    var actorNode = _dragActors.Objects[i];
                    var node = Context.SpawnNode(7, 21, args.SurfaceLocation, new object[]
                    {
                        actorNode.ID
                    });
                    args.SurfaceLocation.X += node.Width + 10;
                }
                DragHandlers.OnDragDrop(args);
                return _dragActors.Effect;
            }

            return base.OnDragDrop(ref location, data);
        }

        /// <inheritdoc />
        public override void OnDestroy()
        {
            if (IsDisposing)
                return;
            NodesCache.Wait();

            base.OnDestroy();
        }
    }
}
