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

//#define DEBUG_SEARCH_TIME

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Scripting;
using FlaxEditor.Surface.ContextMenu;
using FlaxEditor.Surface.Elements;
using FlaxEditor.Surface.Undo;
using FlaxEngine;
using FlaxEngine.GUI;

namespace FlaxEditor.Surface
{
    public partial class VisjectSurface
    {
        /// <summary>
        /// Utility for easy nodes archetypes generation for Visject Surface based on scripting types.
        /// </summary>
        [HideInEditor]
        public class NodesCache
        {
            /// <summary>
            /// Delegate for scripting types filtering into cache.
            /// </summary>
            /// <param name="scriptType">The input type to process.</param>
            /// <param name="cache">Node groups cache that can be used for reusing groups for different nodes.</param>
            /// <param name="version">The cache version number. Can be used to reject any cached data after.<see cref="NodesCache"/> rebuilt.</param>
            public delegate void IterateType(ScriptType scriptType, Dictionary<KeyValuePair<string, ushort>, GroupArchetype> cache, int version);

            internal static readonly List<NodesCache> Caches = new List<NodesCache>(8);

            private readonly object _locker = new object();
            private readonly IterateType _iterator;
            private int _version;
            private Task _task;
            private VisjectCM _taskContextMenu;
            private Dictionary<KeyValuePair<string, ushort>, GroupArchetype> _cache;

            /// <summary>
            /// Initializes a new instance of the <see cref="NodesCache"/> class.
            /// </summary>
            /// <param name="iterator">The iterator callback to build node types from Scripting.</param>
            public NodesCache(IterateType iterator)
            {
                _iterator = iterator;
            }

            /// <summary>
            /// Waits for the async caching job to finish.
            /// </summary>
            public void Wait()
            {
                if (_task != null)
                {
                    Profiler.BeginEvent("Setup Context Menu");
                    _task.Wait();
                    Profiler.EndEvent();
                }
            }

            /// <summary>
            /// Clears cache.
            /// </summary>
            public void Clear()
            {
                Wait();

                if (_cache != null && _cache.Count != 0)
                {
                    OnCodeEditingTypesCleared();
                }
                lock (_locker)
                {
                    Caches.Remove(this);
                }
            }

            /// <summary>
            /// Updates the Visject Context Menu to contain current nodes.
            /// </summary>
            /// <param name="contextMenu">The output context menu to setup.</param>
            public void Get(VisjectCM contextMenu)
            {
                Profiler.BeginEvent("Setup Context Menu");

                Wait();

                lock (_locker)
                {
                    if (_cache == null)
                    {
                        if (!Caches.Contains(this))
                            Caches.Add(this);
                        _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.Archetypes[0].Tag is int asInt && asInt == _version))
                        {
                            var groups = contextMenu.Groups.Where(g => g.Archetypes.Count != 0 && g.Archetypes[0].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.Archetypes.Count != 0 && g.Archetypes[0].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();
                }

                Profiler.EndEvent();
            }

            private void OnActiveContextMenuShowAsync()
            {
                Profiler.BeginEvent("Setup Context Menu (async)");
#if DEBUG_SEARCH_TIME
                var searchStartTime = DateTime.Now;
#endif

                foreach (var scriptType in Editor.Instance.CodeEditing.AllWithStd.Get())
                {
                    if (SurfaceUtils.IsValidVisualScriptType(scriptType))
                    {
                        _iterator(scriptType, _cache, _version);
                    }
                }

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

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

                lock (_locker)
                {
                    _task = null;
                }
            }

            private void OnCodeEditingTypesCleared()
            {
                Wait();

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

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

        private ContextMenuButton _cmCopyButton;
        private ContextMenuButton _cmDuplicateButton;
        private ContextMenuChildMenu _cmFormatNodesMenu;
        private ContextMenuButton _cmFormatNodesConnectionButton;
        private ContextMenuButton _cmAlignNodesTopButton;
        private ContextMenuButton _cmAlignNodesMiddleButton;
        private ContextMenuButton _cmAlignNodesBottomButton;
        private ContextMenuButton _cmAlignNodesLeftButton;
        private ContextMenuButton _cmAlignNodesCenterButton;
        private ContextMenuButton _cmAlignNodesRightButton;
        private ContextMenuButton _cmDistributeNodesHorizontallyButton;
        private ContextMenuButton _cmDistributeNodesVerticallyButton;
        private ContextMenuButton _cmRemoveNodeConnectionsButton;
        private ContextMenuButton _cmRemoveBoxConnectionsButton;
        private readonly Float2 ContextMenuOffset = new Float2(5);

        /// <summary>
        /// Gets a value indicating whether the primary surface context menu is being opened (eg. user is adding nodes).
        /// </summary>
        public virtual bool IsPrimaryMenuOpened => _activeVisjectCM != null && _activeVisjectCM.Visible;

        /// <summary>
        /// Sets the primary menu for the Visject nodes spawning. Can be overriden per surface or surface context. Set to null to restore the default menu.
        /// </summary>
        /// <param name="menu">The menu to override with (use null if restore the default value).</param>
        protected virtual void SetPrimaryMenu(VisjectCM menu)
        {
            menu = menu ?? _cmPrimaryMenu;

            if (menu == _activeVisjectCM)
                return;

            if (_activeVisjectCM != null)
            {
                _activeVisjectCM.ItemClicked -= OnPrimaryMenuButtonClick;
                _activeVisjectCM.VisibleChanged -= OnPrimaryMenuVisibleChanged;
            }

            _activeVisjectCM = menu;

            if (_activeVisjectCM != null)
            {
                _activeVisjectCM.ItemClicked += OnPrimaryMenuButtonClick;
                _activeVisjectCM.VisibleChanged += OnPrimaryMenuVisibleChanged;
            }
        }

        /// <summary>
        /// Creates the default primary context menu for the surface. Override this to provide the custom implementation.
        /// </summary>
        /// <remarks>This method is being called in <see cref="ShowPrimaryMenu"/> on first time when need to show the default menu (no overrides specified for the surface context).</remarks>
        /// <returns>The created menu.</returns>
        protected virtual VisjectCM CreateDefaultPrimaryMenu()
        {
            return new VisjectCM(new VisjectCM.InitInfo
            {
                CanSetParameters = CanSetParameters,
                UseDescriptionPanel = UseContextMenuDescriptionPanel,
                Groups = NodeArchetypes,
                CanSpawnNode = CanUseNodeType,
                ParametersGetter = () => Parameters,
                CustomNodesGroup = GetCustomNodes(),
                ParameterGetNodeArchetype = GetParameterGetterNodeArchetype(out _),
                Style = Style,
            });
        }

        /// <summary>
        /// Called when showing primary context menu.
        /// </summary>
        /// <param name="activeCM">The active context menu to show.</param>
        /// <param name="location">The display location on the surface control.</param>
        /// <param name="startBoxes">The start boxes.</param>
        protected virtual void OnShowPrimaryMenu(VisjectCM activeCM, Float2 location, List<Box> startBoxes)
        {
            activeCM.Show(this, location, startBoxes);
        }

        /// <summary>
        /// Shows the primary menu.
        /// </summary>
        /// <param name="location">The location in the Surface Space.</param>
        /// <param name="moveSurface">If the surface should be moved to accommodate for the menu.</param>
        /// <param name="input">The user text input for nodes search.</param>
        public virtual void ShowPrimaryMenu(Float2 location, bool moveSurface = false, string input = null)
        {
            if (!CanEdit)
                return;

            // Check if need to create default context menu (no override specified)
            if (_activeVisjectCM == null && _cmPrimaryMenu == null)
            {
                _activeVisjectCM = _cmPrimaryMenu = CreateDefaultPrimaryMenu();

                _activeVisjectCM.ItemClicked += OnPrimaryMenuButtonClick;
                _activeVisjectCM.VisibleChanged += OnPrimaryMenuVisibleChanged;
            }

            if (moveSurface)
            {
                const float leftPadding = 20;
                var delta = Float2.Min(location - leftPadding, Float2.Zero) + Float2.Max((location + _activeVisjectCM.Size) - Size, Float2.Zero);

                location -= delta;
                _rootControl.Location -= delta;
            }

            _cmStartPos = location;

            List<Box> startBoxes = new List<Box>(_connectionInstigators.Where(c => c is Box).Cast<Box>());

            // Position offset added so the user can quickly close the menu by clicking
            OnShowPrimaryMenu(_activeVisjectCM, _cmStartPos + ContextMenuOffset, startBoxes);

            if (!string.IsNullOrEmpty(input))
            {
                foreach (char character in input)
                {
                    // OnKeyDown -> VisjectCM focuses on the text-thingy
                    _activeVisjectCM.OnKeyDown(KeyboardKeys.None);
                    _activeVisjectCM.OnCharInput(character);
                    _activeVisjectCM.OnKeyUp(KeyboardKeys.None);
                }
            }
        }

        /// <summary>
        /// Shows the secondary context menu.
        /// </summary>
        /// <param name="location">The location in the Surface Space.</param>
        /// <param name="controlUnderMouse">The Surface Control that is under the cursor. Used to customize the menu.</param>
        public virtual void ShowSecondaryCM(Float2 location, SurfaceControl controlUnderMouse)
        {
            var selection = SelectedNodes;
            if (selection.Count == 0)
                return;

            // Create secondary context menu
            var menu = new FlaxEditor.GUI.ContextMenu.ContextMenu();

            if (_onSave != null)
            {
                menu.AddButton("Save", _onSave).Enabled = CanEdit;
                menu.AddSeparator();
            }
            _cmCopyButton = menu.AddButton("Copy", Copy);
            menu.AddButton("Paste", Paste).Enabled = CanEdit && CanPaste();
            _cmDuplicateButton = menu.AddButton("Duplicate", Duplicate);
            _cmDuplicateButton.Enabled = CanEdit && selection.Any(node => (node.Archetype.Flags & NodeFlags.NoSpawnViaPaste) == 0);
            var canRemove = CanEdit && selection.All(node => (node.Archetype.Flags & NodeFlags.NoRemove) == 0);
            menu.AddButton("Cut", Cut).Enabled = canRemove;
            menu.AddButton("Delete", Delete).Enabled = canRemove;

            if (_supportsDebugging)
            {
                menu.AddSeparator();
                if (selection.Count == 1)
                {
                    menu.AddButton(selection[0].Breakpoint.Set ? "Delete breakpoint" : "Add breakpoint", () =>
                    {
                        foreach (var node in Nodes)
                        {
                            if (node.IsSelected)
                            {
                                node.Breakpoint.Set = !node.Breakpoint.Set;
                                node.Breakpoint.Enabled = true;
                                OnNodeBreakpointEdited(node);
                                break;
                            }
                        }
                    });
                    menu.AddButton("Toggle breakpoint", () =>
                    {
                        foreach (var node in Nodes)
                        {
                            if (node.IsSelected)
                            {
                                node.Breakpoint.Enabled = !node.Breakpoint.Enabled;
                                OnNodeBreakpointEdited(node);
                                break;
                            }
                        }
                    }).Enabled = selection[0].Breakpoint.Set;
                }
                menu.AddSeparator();
                menu.AddButton("Delete all breakpoints", () =>
                {
                    foreach (var node in Nodes)
                    {
                        if (node.Breakpoint.Set)
                        {
                            node.Breakpoint.Set = false;
                            OnNodeBreakpointEdited(node);
                        }
                    }
                }).Enabled = Nodes.Any(x => x.Breakpoint.Set);
                menu.AddButton("Enable all breakpoints", () =>
                {
                    foreach (var node in Nodes)
                    {
                        if (!node.Breakpoint.Enabled)
                        {
                            node.Breakpoint.Enabled = true;
                            OnNodeBreakpointEdited(node);
                        }
                    }
                }).Enabled = Nodes.Any(x => x.Breakpoint.Set && !x.Breakpoint.Enabled);
                menu.AddButton("Disable all breakpoints", () =>
                {
                    foreach (var node in Nodes)
                    {
                        if (node.Breakpoint.Enabled)
                        {
                            node.Breakpoint.Enabled = false;
                            OnNodeBreakpointEdited(node);
                        }
                    }
                }).Enabled = Nodes.Any(x => x.Breakpoint.Set && x.Breakpoint.Enabled);
            }
            menu.AddSeparator();

            bool allNodesNoMove = SelectedNodes.All(n => n.Archetype.Flags.HasFlag(NodeFlags.NoMove));
            bool clickedNodeNoMove = ((SelectedNodes.Count == 1 && controlUnderMouse is SurfaceNode n && n.Archetype.Flags.HasFlag(NodeFlags.NoMove)));

            _cmFormatNodesMenu = menu.AddChildMenu("Format nodes");
            _cmFormatNodesMenu.Enabled = CanEdit && HasNodesSelection && !(allNodesNoMove || clickedNodeNoMove);

            _cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Auto format", Editor.Instance.Options.Options.Input.NodesAutoFormat, () => { FormatGraph(SelectedNodes); });
            _cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Straighten connections", Editor.Instance.Options.Options.Input.NodesStraightenConnections, () => { StraightenGraphConnections(SelectedNodes); });

            _cmFormatNodesMenu.ContextMenu.AddSeparator();
            _cmAlignNodesTopButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align top", Editor.Instance.Options.Options.Input.NodesAlignTop, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Top); });
            _cmAlignNodesMiddleButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align middle", Editor.Instance.Options.Options.Input.NodesAlignMiddle, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Middle); });
            _cmAlignNodesBottomButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align bottom", Editor.Instance.Options.Options.Input.NodesAlignBottom, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Bottom); });

            _cmFormatNodesMenu.ContextMenu.AddSeparator();
            _cmAlignNodesLeftButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align left", Editor.Instance.Options.Options.Input.NodesAlignLeft, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Left); });
            _cmAlignNodesCenterButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align center", Editor.Instance.Options.Options.Input.NodesAlignCenter, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Center); });
            _cmAlignNodesRightButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align right", Editor.Instance.Options.Options.Input.NodesAlignRight, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Right); });

            _cmFormatNodesMenu.ContextMenu.AddSeparator();
            _cmDistributeNodesHorizontallyButton = _cmFormatNodesMenu.ContextMenu.AddButton("Distribute horizontally", Editor.Instance.Options.Options.Input.NodesDistributeHorizontal, () => { DistributeNodes(SelectedNodes, false); });
            _cmDistributeNodesVerticallyButton = _cmFormatNodesMenu.ContextMenu.AddButton("Distribute vertically", Editor.Instance.Options.Options.Input.NodesDistributeVertical, () => { DistributeNodes(SelectedNodes, true); });

            _cmRemoveNodeConnectionsButton = menu.AddButton("Remove all connections", () =>
            {
                var nodes = ((List<SurfaceNode>)menu.Tag);

                if (Undo != null)
                {
                    var actions = new List<IUndoAction>(nodes.Count);
                    foreach (var node in nodes)
                    {
                        var action = new EditNodeConnections(Context, node);
                        node.RemoveConnections();
                        action.End();
                        actions.Add(action);
                    }
                    Undo.AddAction(new MultiUndoAction(actions, actions[0].ActionString));
                }
                else
                {
                    foreach (var node in nodes)
                    {
                        node.RemoveConnections();
                    }
                }

                MarkAsEdited();
            });
            bool anyConnection = SelectedNodes.Any(n => n.GetBoxes().Any(b => b.HasAnyConnection));
            _cmRemoveNodeConnectionsButton.Enabled = CanEdit && anyConnection;

            _cmRemoveBoxConnectionsButton = menu.AddButton("Remove all socket connections", () =>
            {
                var boxUnderMouse = (Box)_cmRemoveBoxConnectionsButton.Tag;
                if (Undo != null)
                {
                    var action = new EditNodeConnections(Context, boxUnderMouse.ParentNode);
                    boxUnderMouse.RemoveConnections();
                    action.End();
                    Undo.AddAction(action);
                }
                else
                {
                    boxUnderMouse.RemoveConnections();
                }
                MarkAsEdited();
            });
            _cmRemoveBoxConnectionsButton.Enabled = CanEdit;
            {
                var boxUnderMouse = GetChildAtRecursive(location) as Box;
                _cmRemoveBoxConnectionsButton.Enabled = boxUnderMouse != null && boxUnderMouse.HasAnyConnection;
                _cmRemoveBoxConnectionsButton.Tag = boxUnderMouse;

                if (boxUnderMouse != null)
                {
                    boxUnderMouse.ParentNode.highlightBox = boxUnderMouse; 
                    menu.VisibleChanged += (c) =>
                    {
                        if (!c.Visible)
                            boxUnderMouse.ParentNode.highlightBox = null;
                    };
                }
            }

            controlUnderMouse?.OnShowSecondaryContextMenu(menu, controlUnderMouse.PointFromParent(location));

            OnShowSecondaryContextMenu(menu, controlUnderMouse);

            // Show secondary context menu
            _cmStartPos = location;
            menu.Tag = selection;
            menu.MaximumItemsInViewCount = 24;
            menu.Show(this, location);
        }

        /// <summary>
        /// Called when editor is showing secondary context menu. Can be used to inject custom options for surface logic.
        /// </summary>
        /// <param name="controlUnderMouse">The Surface Control that is under the cursor. Used to customize the menu.</param>
        /// <param name="menu">The menu.</param>
        protected virtual void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, SurfaceControl controlUnderMouse)
        {
        }

        private void OnPrimaryMenuVisibleChanged(Control primaryMenu)
        {
            if (!primaryMenu.Visible)
                _connectionInstigators.Clear();
        }

        /// <summary>
        /// Handles Visject CM item click event by spawning the selected item.
        /// </summary>
        /// <param name="visjectCmItem">The item.</param>
        /// <param name="selectedBoxes">The selected boxes.</param>
        protected virtual void OnPrimaryMenuButtonClick(VisjectCMItem visjectCmItem, List<Box> selectedBoxes)
        {
            if (!CanEdit)
                return;
            var node = Context.SpawnNode(
                                         visjectCmItem.GroupArchetype,
                                         visjectCmItem.NodeArchetype,
                                         _rootControl.PointFromParent(ref _cmStartPos),
                                         visjectCmItem.Data
                                        );
            if (node == null)
                return;

            // If the user entered a comment
            if (node is SurfaceComment surfaceComment && HasNodesSelection)
            {
                // Note how the user input exactly mimics the other comment creation way. This is very much desired.
                // Select node --> Type // --> Type the comment text --> Hit Enter
                string title = surfaceComment.Title;
                Delete(node, false);
                CommentSelection(title);
                return;
            }

            // Auto select new node
            Select(node);

            for (int i = 0; i < selectedBoxes.Count; i++)
            {
                Box currentBox = selectedBoxes[i];
                if (currentBox != null)
                {
                    Box endBox = null;
                    foreach (var box in node.GetBoxes().Where(box => box.IsOutput != currentBox.IsOutput))
                    {
                        if (currentBox.IsOutput)
                        {
                            if (box.CanUseType(currentBox.CurrentType))
                            {
                                endBox = box;
                                break;
                            }
                        }
                        else
                        {
                            if (currentBox.CanUseType(box.CurrentType))
                            {
                                endBox = box;
                                break;
                            }
                        }

                        if (endBox == null && currentBox.CanUseType(box.CurrentType))
                            endBox = box;
                    }
                    TryConnect(currentBox, endBox);
                }
            }
        }

        private void TryConnect(Box startBox, Box endBox)
        {
            if (startBox == null || endBox == null)
            {
                if (IsConnecting)
                {
                    ConnectingEnd(null);
                }
                return;
            }

            // If the user is patiently waiting for his box to get connected to the newly created one fulfill his wish!
            if (!IsConnecting)
                ConnectingStart(startBox);
            ConnectingEnd(endBox);

            // Smart-Select next box
            /*
             * Output and Output => undefined
             * Output and Input => Connect and move to next on input-node
             * Input and Output => Connect and move to next on input-node
             * Input and Input => undefined, cannot happen
             */
            Box inputBox = endBox.IsOutput ? startBox : endBox;
            Box nextBox = inputBox.ParentNode.GetNextBox(inputBox);

            // If we are going backwards and the end-node has an input box we want to edit backwards
            if (!startBox.IsOutput)
            {
                Box endNodeInputBox = endBox.ParentNode.GetBoxes().DefaultIfEmpty(null).FirstOrDefault(b => !b.IsOutput);
                if (endNodeInputBox != null)
                {
                    nextBox = endNodeInputBox;
                }
            }

            // TODO: What if we reached the end (nextBox == null)? Do we travel along the nodes?
            /*
             * while (nextBox == null && _inputBoxStack.Count > 0)
                {
                    // We found the last box on this node but there are still boxes on previous nodes on the stack
                    nextBox = GetNextBox(_inputBoxStack.Pop());
                }
                */

            if (nextBox != null)
            {
                Select(nextBox.ParentNode);
                nextBox.ParentNode.SelectBox(nextBox);

                var padding = new Float2(20);
                var delta = Float2.Min(_rootControl.PointToParent(nextBox.ParentNode.Location) - padding, Float2.Zero) + Float2.Max((_rootControl.PointToParent(nextBox.ParentNode.BottomRight) + padding) - Size, Float2.Zero);

                _rootControl.Location -= delta;
            }
        }
    }
}
