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

using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Input;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Utilities;

namespace FlaxEditor.Surface.ContextMenu
{
    /// <summary>
    /// The Visject Surface dedicated context menu for nodes spawning.
    /// </summary>
    /// <seealso cref="ContextMenuBase" />
    [HideInEditor]
    public class VisjectCM : ContextMenuBase
    {
        /// <summary>
        /// Visject context menu item clicked delegate.
        /// </summary>
        /// <param name="clickedItem">The item that was clicked</param>
        /// <param name="selectedBox">The currently user-selected box. Can be null.</param>
        public delegate void ItemClickedDelegate(VisjectCMItem clickedItem, Elements.Box selectedBox);

        /// <summary>
        /// Visject Surface node archetype spawn ability checking delegate.
        /// </summary>
        /// <param name="groupArch">The nodes group archetype to check.</param>
        /// <param name="arch">The node archetype to check.</param>
        /// <returns>True if can use this node to spawn it on a surface, otherwise false..</returns>
        public delegate bool NodeSpawnCheckDelegate(GroupArchetype groupArch, NodeArchetype arch);

        /// <summary>
        /// Visject Surface parameters getter delegate.
        /// </summary>
        /// <returns>TThe list of surface parameters or null if failed (readonly).</returns>
        public delegate List<SurfaceParameter> ParameterGetterDelegate();

        private readonly List<VisjectCMGroup> _groups = new List<VisjectCMGroup>(16);
        private readonly TextBox _searchBox;
        private bool _waitingForInput;
        private VisjectCMGroup _surfaceParametersGroup;
        private Panel _panel1;
        private VerticalPanel _groupsPanel;
        private readonly ParameterGetterDelegate _parametersGetter;
        private Elements.Box _selectedBox;
        private NodeArchetype _parameterGetNodeArchetype;
        private NodeArchetype _parameterSetNodeArchetype;

        /// <summary>
        /// The selected item
        /// </summary>
        public VisjectCMItem SelectedItem;

        /// <summary>
        /// Event fired when any item in this popup menu gets clicked.
        /// </summary>
        public event ItemClickedDelegate ItemClicked;

        /// <summary>
        /// Gets or sets a value indicating whether show groups expanded or collapsed.
        /// </summary>
        public bool ShowExpanded { get; set; }

        /// <summary>
        /// Gets the groups (read-only).
        /// </summary>
        public IList<VisjectCMGroup> Groups => _groups;

        /// <summary>
        /// The surface context menu initialization parameters.
        /// </summary>
        public struct InitInfo
        {
            /// <summary>
            /// True if surface parameters are not read-only and can be modified via setter node.
            /// </summary>
            public bool CanSetParameters;

            /// <summary>
            /// The groups archetypes. Cannot be null.
            /// </summary>
            public List<GroupArchetype> Groups;

            /// <summary>
            /// The custom callback to handle node types validation for spawning. Cannot be null.
            /// </summary>
            public NodeSpawnCheckDelegate CanSpawnNode;

            /// <summary>
            /// The surface parameters getter. Can be null.
            /// </summary>
            public ParameterGetterDelegate ParametersGetter;

            /// <summary>
            /// The group with custom nodes group. Can be null.
            /// </summary>
            public GroupArchetype CustomNodesGroup;

            /// <summary>
            /// The parameter getter node archetype to spawn when adding the parameter getter. Can be null.
            /// </summary>
            public NodeArchetype ParameterGetNodeArchetype;

            /// <summary>
            /// The parameter setter node archetype to spawn when adding the parameter getter. Can be null.
            /// </summary>
            public NodeArchetype ParameterSetNodeArchetype;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="VisjectCM"/> class.
        /// </summary>
        /// <param name="info">The initialization info data.</param>
        public VisjectCM(InitInfo info)
        {
            if (info.Groups == null)
                throw new ArgumentNullException(nameof(info.Groups));
            if (info.CanSpawnNode == null)
                throw new ArgumentNullException(nameof(info.CanSpawnNode));
            _parametersGetter = info.ParametersGetter;
            _parameterGetNodeArchetype = info.ParameterGetNodeArchetype ?? Archetypes.Parameters.Nodes[0];
            if (info.CanSetParameters)
                _parameterSetNodeArchetype = info.ParameterSetNodeArchetype ?? Archetypes.Parameters.Nodes[3];

            // Context menu dimensions
            Size = new Float2(320, 248);

            var headerPanel = new Panel(ScrollBars.None)
            {
                Parent = this,
                Height = 20,
                Width = Width - 4,
                X = 2,
                Y = 1,
            };

            // Title bar
            var titleLabel = new Label
            {
                Width = Width - 8,
                Height = 20,
                X = 4,
                Parent = headerPanel,
                Text = "Select Node",
                HorizontalAlignment = TextAlignment.Center,
                Font = new FontReference(Style.Current.FontLarge.Asset, 10),
            };

            // Search box
            _searchBox = new SearchBox(false, 2, 22)
            {
                Width = Width - 4,
                Parent = this
            };
            _searchBox.TextChanged += OnSearchFilterChanged;

            // Create first panel (for scrollbar)
            var panel1 = new Panel(ScrollBars.Vertical)
            {
                Bounds = new Rectangle(0, _searchBox.Bottom + 1, Width, Height - _searchBox.Bottom - 2),
                Parent = this
            };

            _panel1 = panel1;

            // Create second panel (for groups arrangement)
            var panel2 = new VerticalPanel
            {
                Parent = panel1,
                AnchorPreset = AnchorPresets.HorizontalStretchTop,
                IsScrollable = true,
            };
            _groupsPanel = panel2;

            // Init groups
            var nodes = new List<NodeArchetype>();
            foreach (var groupArchetype in info.Groups)
            {
                // Get valid nodes
                nodes.Clear();
                foreach (var nodeArchetype in groupArchetype.Archetypes)
                {
                    if ((nodeArchetype.Flags & NodeFlags.NoSpawnViaGUI) == 0 && info.CanSpawnNode(groupArchetype, nodeArchetype))
                    {
                        nodes.Add(nodeArchetype);
                    }
                }

                // Check if can create group for them
                if (nodes.Count > 0)
                {
                    var group = CreateGroup(groupArchetype);
                    group.Close(false);
                    for (int i = 0; i < nodes.Count; i++)
                    {
                        var item = new VisjectCMItem(group, groupArchetype, nodes[i])
                        {
                            Parent = group
                        };
                    }
                    group.SortChildren();
                    group.Parent = panel2;

                    _groups.Add(group);
                }
            }

            // Add custom nodes (special handling)
            if (info.CustomNodesGroup?.Archetypes != null)
            {
                foreach (var nodeArchetype in info.CustomNodesGroup.Archetypes)
                {
                    if ((nodeArchetype.Flags & NodeFlags.NoSpawnViaGUI) != 0)
                        continue;

                    var groupName = Archetypes.Custom.GetNodeGroup(nodeArchetype);

                    // Find group to reuse
                    VisjectCMGroup group = null;
                    for (int j = 0; j < _groups.Count; j++)
                    {
                        if (string.Equals(_groups[j].Name, groupName, StringComparison.OrdinalIgnoreCase))
                        {
                            group = _groups[j];
                            break;
                        }
                    }

                    // Create new group if name is unique
                    if (group == null)
                    {
                        group = CreateGroup(info.CustomNodesGroup, true, groupName);
                        group.Close(false);
                        group.Parent = _groupsPanel;
                        _groups.Add(group);
                    }

                    // Add new item
                    var item = new VisjectCMItem(group, info.CustomNodesGroup, nodeArchetype)
                    {
                        Parent = group
                    };

                    // Order items
                    group.SortChildren();
                }
            }
        }

        /// <summary>
        /// Adds the group archetype to add to the menu.
        /// </summary>
        /// <param name="groupArchetype">The group.</param>
        /// <param name="withGroupMerge">True if merge group items into any existing group of the same name.</param>
        public void AddGroup(GroupArchetype groupArchetype, bool withGroupMerge = true)
        {
            // Check if can create group for them to be spawned via GUI
            if (groupArchetype.Archetypes.Any(x => (x.Flags & NodeFlags.NoSpawnViaGUI) == 0))
            {
                Profiler.BeginEvent("VisjectCM.AddGroup");

                var group = CreateGroup(groupArchetype, withGroupMerge);
                group.Close(false);

                foreach (var nodeArchetype in groupArchetype.Archetypes)
                {
                    var item = new VisjectCMItem(group, groupArchetype, nodeArchetype)
                    {
                        Parent = group
                    };
                }
                group.SortChildren();
                group.Parent = _groupsPanel;

                _groups.Add(group);

                if (!IsLayoutLocked)
                {
                    group.UnlockChildrenRecursive();
                    SortGroups();
                    if (ShowExpanded)
                        group.Open(false);
                    group.PerformLayout();
                    if (_searchBox.TextLength != 0)
                    {
                        OnSearchFilterChanged();
                    }
                }

                Profiler.EndEvent();
            }
        }

        /// <summary>
        /// Adds the group archetypes to add to the menu.
        /// </summary>
        /// <param name="groupArchetypes">The groups.</param>
        /// <param name="withGroupMerge">True if merge group items into any existing group of the same name.</param>
        public void AddGroups(IEnumerable<GroupArchetype> groupArchetypes, bool withGroupMerge = true)
        {
            // Check if can create group for them to be spawned via GUI
            if (groupArchetypes.Any(g => g.Archetypes.Any(x => (x.Flags & NodeFlags.NoSpawnViaGUI) == 0)))
            {
                Profiler.BeginEvent("VisjectCM.AddGroups");
                var isLayoutLocked = IsLayoutLocked;
                LockChildrenRecursive();

                Profiler.BeginEvent("Create Groups");
                var groups = new List<VisjectCMGroup>();
                foreach (var groupArchetype in groupArchetypes)
                {
                    var group = CreateGroup(groupArchetype, withGroupMerge);
                    group.Close(false);

                    foreach (var nodeArchetype in groupArchetype.Archetypes)
                    {
                        var item = new VisjectCMItem(group, groupArchetype, nodeArchetype)
                        {
                            Parent = group
                        };
                    }
                    group.SortChildren();
                    if (ShowExpanded)
                        group.Open(false);
                    group.Parent = _groupsPanel;
                    _groups.Add(group);

                    groups.Add(group);
                }
                Profiler.EndEvent();

                if (!isLayoutLocked)
                {
                    SortGroups();
                    Profiler.BeginEvent("Perform Layout");
                    UnlockChildrenRecursive();
                    foreach (var group in groups)
                        group.PerformLayout();
                    PerformLayout();
                    Profiler.EndEvent();

                    if (_searchBox.TextLength != 0)
                    {
                        OnSearchFilterChanged();
                    }
                }

                Profiler.EndEvent();
            }
        }

        /// <summary>
        /// Removes the group archetype to from to the menu.
        /// </summary>
        /// <param name="groupArchetype">The group.</param>
        public void RemoveGroup(GroupArchetype groupArchetype)
        {
            for (int i = 0; i < _groups.Count; i++)
            {
                var group = _groups[i];
                if (group.Archetypes.Remove(groupArchetype))
                {
                    Profiler.BeginEvent("VisjectCM.RemoveGroup");
                    if (group.Archetypes.Count == 0)
                    {
                        _groups.RemoveAt(i);
                        group.Dispose();
                    }
                    else
                    {
                        var children = group.Children.ToArray();
                        foreach (var child in children)
                        {
                            if (child is VisjectCMItem item && item.GroupArchetype == groupArchetype)
                                item.Dispose();
                        }
                    }
                    Profiler.EndEvent();
                    break;
                }
            }
        }

        /// <summary>
        /// Removes the group to from to the menu.
        /// </summary>
        /// <param name="group">The group.</param>
        public void RemoveGroup(VisjectCMGroup group)
        {
            Profiler.BeginEvent("VisjectCM.RemoveGroup");
            group.Dispose();
            _groups.Remove(group);
            Profiler.EndEvent();
        }

        private VisjectCMGroup CreateGroup(GroupArchetype groupArchetype, bool withGroupMerge = true, string name = null)
        {
            if (name == null)
                name = groupArchetype.Name;
            if (withGroupMerge)
            {
                for (int i = 0; i < _groups.Count; i++)
                {
                    if (string.Equals(_groups[i].HeaderText, name, StringComparison.Ordinal))
                        return _groups[i];
                }
            }
            return new VisjectCMGroup(this, groupArchetype)
            {
                HeaderText = name
            };
        }

        private void OnSearchFilterChanged()
        {
            // Skip events during setup or init stuff
            if (IsLayoutLocked)
                return;

            Profiler.BeginEvent("VisjectCM.OnSearchFilterChanged");
            
            if (string.IsNullOrEmpty(_searchBox.Text))
            {
                ResetView();
                Profiler.EndEvent();
                return;
            }

            // Update groups
            LockChildrenRecursive();
            for (int i = 0; i < _groups.Count; i++)
            {
                _groups[i].UpdateFilter(_searchBox.Text);
                _groups[i].UpdateItemSort(_selectedBox);
            }
            SortGroups();
            UnlockChildrenRecursive();

            // If no item is selected (or it's not visible anymore), select the top one
            Profiler.BeginEvent("VisjectCM.Layout");
            if (SelectedItem == null || !SelectedItem.VisibleInHierarchy)
                SelectedItem = _groups.Find(g => g.Visible)?.Children.Find(c => c.Visible && c is VisjectCMItem) as VisjectCMItem;
            PerformLayout();
            if (SelectedItem != null)
                _panel1.ScrollViewTo(SelectedItem);
            _searchBox.Focus();
            Profiler.EndEvent();

            Profiler.EndEvent();
        }

        /// <summary>
        /// Sort the groups and keeps <see cref="_groups"/> in sync
        /// </summary>
        private void SortGroups()
        {
            Profiler.BeginEvent("VisjectCM.SortGroups");

            // Sort groups
            _groupsPanel.SortChildren();

            // Synchronize with _groups[]
            for (int i = 0, groupsIndex = 0; i < _groupsPanel.ChildrenCount; i++)
            {
                if (_groupsPanel.Children[i] is VisjectCMGroup group)
                {
                    _groups[groupsIndex] = group;
                    groupsIndex++;
                }
            }

            Profiler.EndEvent();
        }

        /// <summary>
        /// Called when user clicks on an item.
        /// </summary>
        /// <param name="item">The item.</param>
        public void OnClickItem(VisjectCMItem item)
        {
            Hide();
            ItemClicked?.Invoke(item, _selectedBox);
        }

        /// <summary>
        /// Expands all the groups.
        /// </summary>
        /// <param name="animate">Enable/disable animation feature.</param>
        public void ExpandAll(bool animate = false)
        {
            for (int i = 0; i < _groups.Count; i++)
                _groups[i].Open(animate);
        }

        /// <summary>
        /// Resets the view.
        /// </summary>
        public void ResetView()
        {
            Profiler.BeginEvent("VisjectCM.ResetView");

            LockChildrenRecursive();
            _searchBox.Clear();
            SelectedItem = null;
            for (int i = 0; i < _groups.Count; i++)
                _groups[i].ResetView();
            UnlockChildrenRecursive();

            SortGroups();
            PerformLayout();

            Profiler.EndEvent();
        }

        /// <summary>
        /// Updates the surface parameters group.
        /// </summary>
        private void UpdateSurfaceParametersGroup()
        {
            Profiler.BeginEvent("VisjectCM.UpdateSurfaceParametersGroup");

            // Remove the old one
            if (_surfaceParametersGroup != null)
            {
                _groups.Remove(_surfaceParametersGroup);
                _surfaceParametersGroup.Dispose();
                _surfaceParametersGroup = null;
            }

            // Check if surface has any parameters
            var parameters = _parametersGetter?.Invoke();
            int count = parameters?.Count(x => x.IsPublic) ?? 0;
            if (count > 0)
            {
                // TODO: cache the allocated memory to reduce dynamic allocations
                if (_parameterSetNodeArchetype != null)
                    count *= 2;
                var archetypes = new NodeArchetype[count];
                int archetypeIndex = 0;

                // ReSharper disable once PossibleNullReferenceException
                for (int i = 0; i < parameters.Count; i++)
                {
                    var param = parameters[i];
                    if (!param.IsPublic)
                        continue;

                    var node = (NodeArchetype)_parameterGetNodeArchetype.Clone();
                    node.Title = "Get " + param.Name;
                    node.DefaultValues[0] = param.ID;
                    archetypes[archetypeIndex++] = node;

                    if (_parameterSetNodeArchetype != null)
                    {
                        node = (NodeArchetype)_parameterSetNodeArchetype.Clone();
                        node.Title = "Set " + param.Name;
                        node.DefaultValues[0] = param.ID;
                        node.DefaultValues[1] = TypeUtils.GetDefaultValue(param.Type);
                        archetypes[archetypeIndex++] = node;
                    }
                }

                var groupArchetype = new GroupArchetype
                {
                    GroupID = 6,
                    Name = "Surface Parameters",
                    Color = new Color(52, 73, 94),
                    Archetypes = archetypes
                };

                var group = CreateGroup(groupArchetype);
                group.ArrowImageOpened = new SpriteBrush(Style.Current.ArrowDown);
                group.ArrowImageClosed = new SpriteBrush(Style.Current.ArrowRight);
                group.Close(false);
                archetypeIndex = 0;
                for (int i = 0; i < parameters.Count; i++)
                {
                    var param = parameters[i];
                    if (!param.IsPublic)
                        continue;

                    var item = new VisjectCMItem(group, groupArchetype, archetypes[archetypeIndex++])
                    {
                        Parent = group
                    };

                    if (_parameterSetNodeArchetype != null)
                    {
                        item = new VisjectCMItem(group, groupArchetype, archetypes[archetypeIndex++])
                        {
                            Parent = group
                        };
                    }
                }
                group.SortChildren();
                group.UnlockChildrenRecursive();
                group.Parent = _groupsPanel;
                _groups.Add(group);
                _surfaceParametersGroup = group;
            }

            Profiler.EndEvent();
        }

        /// <inheritdoc />
        public override void Show(Control parent, Float2 location)
        {
            Show(parent, location, null);
        }

        /// <summary>
        /// Show context menu over given control.
        /// </summary>
        /// <param name="parent">Parent control to attach to it.</param>
        /// <param name="location">Popup menu origin location in parent control coordinates.</param>
        /// <param name="startBox">The currently selected box that the new node will get connected to. Can be null</param>
        public void Show(Control parent, Float2 location, Elements.Box startBox)
        {
            _selectedBox = startBox;
            base.Show(parent, location);
        }

        /// <inheritdoc />
        protected override void OnShow()
        {
            // Prepare
            UpdateSurfaceParametersGroup();
            ResetView();
            _panel1.VScrollBar.TargetValue = 0;
            Focus();
            _waitingForInput = true;

            base.OnShow();
        }

        /// <inheritdoc />
        public override void Hide()
        {
            Focus(null);

            base.Hide();
        }

        /// <inheritdoc />
        public override bool OnKeyDown(KeyboardKeys key)
        {
            if (key == KeyboardKeys.Escape)
            {
                Hide();
                return true;
            }
            else if (key == KeyboardKeys.Return)
            {
                if (SelectedItem != null)
                    OnClickItem(SelectedItem);
                else
                    Hide();
                return true;
            }
            else if (key == KeyboardKeys.ArrowUp)
            {
                if (SelectedItem == null)
                    return true;

                var previousSelectedItem = GetPreviousSiblings<VisjectCMItem>(SelectedItem).FirstOrDefault(c => c.Visible) ??
                                           (GetPreviousSiblings<VisjectCMGroup>(SelectedItem.Group).FirstOrDefault(c => c.Visible)?.Children
                                                                                                   .FindLast(c => c.Visible && c is VisjectCMItem) as VisjectCMItem);

                if (previousSelectedItem != null)
                {
                    SelectedItem = previousSelectedItem;

                    // Scroll into view (without smoothing)
                    _panel1.VScrollBar.SmoothingScale = 0;
                    _panel1.ScrollViewTo(SelectedItem);
                    _panel1.VScrollBar.SmoothingScale = 1;
                }
                return true;
            }
            else if (key == KeyboardKeys.ArrowDown)
            {
                if (SelectedItem == null)
                    return true;

                var nextSelectedItem = GetNextSiblings<VisjectCMItem>(SelectedItem).FirstOrDefault(c => c.Visible) ??
                                       (GetNextSiblings<VisjectCMGroup>(SelectedItem.Group).FirstOrDefault(c => c.Visible)?.Children
                                                                                           .OfType<VisjectCMItem>().FirstOrDefault(c => c.Visible));

                if (nextSelectedItem != null)
                {
                    SelectedItem = nextSelectedItem;
                    _panel1.ScrollViewTo(SelectedItem);
                }
                return true;
            }

            if (_waitingForInput)
            {
                _waitingForInput = false;
                _searchBox.Focus();
                return _searchBox.OnKeyDown(key);
            }

            return base.OnKeyDown(key);
        }

        /// <summary>
        /// Gets the next siblings of a control.
        /// </summary>
        /// <param name="item">A control that is attached to a parent</param>
        /// <returns>An <see cref="IEnumerable{Control}"/> with the siblings that come after the current one.</returns>
        private IEnumerable<Control> GetNextSiblings(Control item)
        {
            if (item?.Parent == null)
                yield break;

            var parent = item.Parent;
            for (int i = item.IndexInParent + 1; i < parent.ChildrenCount; i++)
            {
                yield return parent.GetChild(i);
            }
        }

        /// <summary>
        /// Gets the next siblings of a control that have a specific type.
        /// </summary>
        /// <typeparam name="T">The type that the controls should have.</typeparam>
        /// <param name="item">A control that is attached to a parent</param>
        /// <returns>An <see cref="IEnumerable{T}"/> with the siblings that come after the current one.</returns>
        private IEnumerable<T> GetNextSiblings<T>(Control item) where T : Control
        {
            return GetNextSiblings(item).OfType<T>();
        }

        /// <summary>
        /// Gets the previous siblings of a control.
        /// </summary>
        /// <param name="item">A control that is attached to a parent</param>
        /// <returns>An <see cref="IEnumerable{Control}"/> with the siblings that come before the current one.</returns>
        private IEnumerable<Control> GetPreviousSiblings(Control item)
        {
            if (item?.Parent == null)
                yield break;

            var parent = item.Parent;
            for (int i = item.IndexInParent - 1; i >= 0; i--)
            {
                yield return parent.GetChild(i);
            }
        }

        /// <summary>
        /// Gets the previous sibling of a control that have a specific type.
        /// </summary>
        /// <typeparam name="T">The type that the controls should have.</typeparam>
        /// <param name="item">A control that is attached to a parent</param>
        /// <returns>An <see cref="IEnumerable{T}"/> with the siblings that come before the current one.</returns>
        private IEnumerable<T> GetPreviousSiblings<T>(Control item) where T : Control
        {
            return GetPreviousSiblings(item).OfType<T>();
        }
    }
}
