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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
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 const float DefaultWidth = 300;
        private const float DefaultHeight = 400;

        private readonly List<VisjectCMGroup> _groups = new List<VisjectCMGroup>(16);
        private CheckBox _contextSensitiveToggle;
        private bool _contextSensitiveSearchEnabled = true;
        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;

        // Description panel elements
        private readonly bool _useDescriptionPanel;
        private bool _descriptionPanelVisible;
        private readonly Panel _descriptionPanel;
        private readonly Panel _descriptionPanelSeparator;
        private readonly Image _descriptionDeclaringClassImage;
        private readonly Label _descriptionSignatureLabel;
        private readonly Label _descriptionLabel;
        private readonly VerticalPanel _descriptionInputPanel;
        private readonly VerticalPanel _descriptionOutputPanel;
        private readonly SurfaceStyle _surfaceStyle;
        private VisjectCMItem _selectedItem;

        /// <summary>
        /// The selected item
        /// </summary>
        public VisjectCMItem SelectedItem
        {
            get => _selectedItem;
            private set
            {
                _selectedItem = value;
                _selectedItem?.OnSelect();
            }
        }

        /// <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>
            /// True if the surface should make use of a description panel drawn at the bottom of the context menu
            /// </summary>
            public bool UseDescriptionPanel;

            /// <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>
            /// The surface style to use to draw images in the description panel
            /// </summary>
            public SurfaceStyle Style;
        }

        /// <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];
            _useDescriptionPanel = info.UseDescriptionPanel;
            _surfaceStyle = info.Style;

            // Context menu dimensions
            Size = new Float2(_useDescriptionPanel ? DefaultWidth + 50f : DefaultWidth, DefaultHeight);

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

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

            // Context sensitive toggle
            var contextSensitiveLabel = new Label
            {
                Width = Width * 0.5f - 28,
                Height = 20,
                X = Width * 0.5f,
                Parent = headerPanel,
                Text = "Context Sensitive",
                TooltipText = "Should the nodes be filtered to only show those that can be connected in the current context?",
                HorizontalAlignment = TextAlignment.Far,
                Font = titleFontReference,
            };

            _contextSensitiveToggle = new CheckBox
            {
                Width = 20,
                Height = 20,
                X = Width - 24,
                Parent = headerPanel,
                Checked = _contextSensitiveSearchEnabled,
            };
            _contextSensitiveToggle.StateChanged += OnContextSensitiveToggleStateChanged;

            // 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;

            // Create description panel elements only when description panel is about to be used
            if (_useDescriptionPanel)
            {
                _descriptionPanel = new Panel(ScrollBars.None)
                {
                    Parent = this,
                    Bounds = new Rectangle(0, Height, Width, 0),
                    BackgroundColor = Style.Current.BackgroundNormal,
                };

                _descriptionDeclaringClassImage = new Image(8, 12, 20, 20)
                {
                    Parent = _descriptionPanel,
                    Brush = new SpriteBrush(info.Style.Icons.BoxClose),
                };

                var descriptionFontReference = new FontReference(Style.Current.FontMedium.Asset, 9f);
                _descriptionSignatureLabel = new Label(32, 8, Width - 40, 0)
                {
                    Parent = _descriptionPanel,
                    HorizontalAlignment = TextAlignment.Near,
                    VerticalAlignment = TextAlignment.Near,
                    Wrapping = TextWrapping.WrapWords,
                    Font = descriptionFontReference,
                    Bold = true,
                    AutoHeight = true,
                };
                _descriptionSignatureLabel.SetAnchorPreset(AnchorPresets.TopLeft, true);

                _descriptionLabel = new Label(32, 0, Width - 40, 0)
                {
                    Parent = _descriptionPanel,
                    HorizontalAlignment = TextAlignment.Near,
                    VerticalAlignment = TextAlignment.Near,
                    Wrapping = TextWrapping.WrapWords,
                    Font = descriptionFontReference,
                    AutoHeight = true,
                };
                _descriptionLabel.SetAnchorPreset(AnchorPresets.TopLeft, true);

                _descriptionPanelSeparator = new Panel(ScrollBars.None)
                {
                    Parent = _descriptionPanel,
                    Bounds = new Rectangle(8, Height, Width - 16, 2),
                    BackgroundColor = Style.Current.BackgroundHighlighted,
                };

                _descriptionInputPanel = new VerticalPanel()
                {
                    Parent = _descriptionPanel,
                    X = 8,
                    Width = Width * 0.5f - 16,
                    AutoSize = true,
                };

                _descriptionOutputPanel = new VerticalPanel()
                {
                    Parent = _descriptionPanel,
                    X = Width * 0.5f + 8,
                    Width = Width * 0.5f - 16,
                    AutoSize = true,
                };
            }

            // 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();
                    if (_contextSensitiveSearchEnabled && _selectedBox != null)
                        UpdateFilters();
                    else
                        SortGroups();
                    if (ShowExpanded)
                        group.Open(false);
                    group.PerformLayout();
                    if (_searchBox.TextLength != 0)
                    {
                        OnSearchFilterChanged();
                    }
                }
                else if (_contextSensitiveSearchEnabled)
                {
                    group.EvaluateVisibilityWithBox(_selectedBox);
                }

                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
                        };
                    }
                    if (_contextSensitiveSearchEnabled)
                        group.EvaluateVisibilityWithBox(_selectedBox);
                    group.SortChildren();
                    if (ShowExpanded)
                        group.Open(false);
                    group.Parent = _groupsPanel;
                    _groups.Add(group);

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

                if (!isLayoutLocked)
                {
                    if (_contextSensitiveSearchEnabled && _selectedBox != null)
                        UpdateFilters();
                    else
                        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");
            UpdateFilters();
            _searchBox.Focus();
            Profiler.EndEvent();
        }

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

            Profiler.BeginEvent("VisjectCM.OnContextSensitiveToggleStateChanged");
            _contextSensitiveSearchEnabled = checkBox.Checked;
            UpdateFilters();
            Profiler.EndEvent();
        }

        private void UpdateFilters()
        {
            if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBox == null)
            {
                ResetView();
                Profiler.EndEvent();
                return;
            }

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

            // If no item is selected (or it's not visible anymore), select the top one
            Profiler.BeginEvent("VisjectCM.Layout");
            SelectedItem = _groups.Find(g => g.Visible)?.Children.Find(c => c.Visible && c is VisjectCMItem) as VisjectCMItem;
            PerformLayout();
            if (SelectedItem != null)
                _panel1.ScrollViewTo(SelectedItem);
            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();
                if (_contextSensitiveSearchEnabled)
                    _groups[i].EvaluateVisibilityWithBox(_selectedBox);
            }
            UnlockChildrenRecursive();

            if (_contextSensitiveSearchEnabled && _selectedBox != null)
                UpdateFilters();
            else
                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 ?? 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;

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

                var group = CreateGroup(groupArchetype, false);
                group.ArrowImageOpened = new SpriteBrush(Style.Current.ArrowDown);
                group.ArrowImageClosed = new SpriteBrush(Style.Current.ArrowRight);
                group.Close(false);

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

                    // Define Getter node and create CM item
                    var node = (NodeArchetype)_parameterGetNodeArchetype.Clone();
                    node.Title = "Get " + param.Name;
                    node.DefaultValues[0] = param.ID;
                    archetypes[archetypeIndex++] = node;

                    var item = new VisjectCMItem(group, groupArchetype, node)
                    {
                        Parent = group
                    };

                    // Define Setter node and create CM item if parameter has a setter
                    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;

                        item = new VisjectCMItem(group, groupArchetype, node)
                        {
                            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);

            if (_useDescriptionPanel)
                HideDescriptionPanel();

            base.Hide();
        }

        /// <summary>
        /// Updates the description panel and shows information about the set archetype
        /// </summary>
        /// <param name="archetype">The node archetype</param>
        public void SetDescriptionPanelArchetype(NodeArchetype archetype)
        {
            if (!_useDescriptionPanel)
                return;

            if (archetype == null || !Editor.Instance.Options.Options.Interface.NodeDescriptionPanel)
            {
                HideDescriptionPanel();
                return;
            }

            Profiler.BeginEvent("VisjectCM.SetDescriptionPanelArchetype");

            _descriptionInputPanel.RemoveChildren();
            _descriptionOutputPanel.RemoveChildren();

            // Fetch description and information from the memberInfo - mainly from nodes that got fetched asynchronously by the visual scripting editor
            ScriptType declaringType;
            if (archetype.Tag is ScriptMemberInfo memberInfo)
            {
                var name = memberInfo.Name;
                if (memberInfo.IsMethod && memberInfo.Name.StartsWith("get_") || memberInfo.Name.StartsWith("set_"))
                {
                    name = memberInfo.Name.Substring(4);
                }

                declaringType = memberInfo.DeclaringType;
                _descriptionSignatureLabel.Text = memberInfo.DeclaringType + "." + name;

                // We have to add the Instance information manually for members that aren't static
                if (!memberInfo.IsStatic)
                    AddInputOutputElement(archetype, declaringType, false, $"Instance ({memberInfo.DeclaringType.Name})");

                // We also have to manually add the Return information as well.
                if (memberInfo.ValueType != ScriptType.Null && memberInfo.ValueType != ScriptType.Void)
                {
                    // When a field has a setter we don't want it to show the input as a return output.
                    if (memberInfo.IsField && archetype.Title.StartsWith("Set "))
                        AddInputOutputElement(archetype, memberInfo.ValueType, false, $"({memberInfo.ValueType.Name})");
                    else
                        AddInputOutputElement(archetype, memberInfo.ValueType, true, $"Return ({memberInfo.ValueType.Name})");
                }

                for (int i = 0; i < memberInfo.ParametersCount; i++)
                {
                    var param = memberInfo.GetParameters()[i];
                    AddInputOutputElement(archetype, param.Type, param.IsOut, $"{param.Name} ({param.Type.Name})");
                }
            }
            else
            {
                // Try to fetch as many informations as possible from the predefined hardcoded nodes
                _descriptionSignatureLabel.Text = string.IsNullOrEmpty(archetype.Signature) ? archetype.Title : archetype.Signature;
                declaringType = archetype.DefaultType;

                // Some nodes are more delicate. Like Arrays or Dictionaries. In this case we let them fetch the inputs/outputs for us.
                // Otherwise, it is not possible to show a proper return type on some nodes.
                if (archetype.GetInputOutputDescription != null)
                {
                    archetype.GetInputOutputDescription.Invoke(archetype, out var inputs, out var outputs);

                    foreach (var input in inputs ?? [])
                    {
                        AddInputOutputElement(archetype, input.Type, false, $"{input.Name} ({input.Type.Name})");
                    }

                    foreach (var output in outputs ?? [])
                    {
                        AddInputOutputElement(archetype, output.Type, true, $"{output.Name} ({output.Type.Name})");
                    }
                }
                else if (archetype.Elements != null) // Skip if no Elements (ex: Comment node)
                {
                    foreach (var element in archetype.Elements)
                    {
                        if (element.Type is not (NodeElementType.Input or NodeElementType.Output))
                            continue;

                        var typeText = element.ConnectionsType ? element.ConnectionsType.Name : archetype.ConnectionsHints.ToString();
                        AddInputOutputElement(archetype, element.ConnectionsType, element.Type == NodeElementType.Output, $"{element.Text} ({typeText})");
                    }
                }
            }

            // Set declaring type icon color
            _surfaceStyle.GetConnectionColor(declaringType, archetype.ConnectionsHints, out var declaringTypeColor);
            _descriptionDeclaringClassImage.Color = declaringTypeColor;
            _descriptionDeclaringClassImage.MouseOverColor = declaringTypeColor;

            // Calculate the description panel height. (I am doing this manually since working with autoSize on horizontal/vertical panels didn't work, especially with nesting - Nils)
            float panelHeight = _descriptionSignatureLabel.Height;

            // If thee is no description we move the signature label down a bit to align it with the icon. Just a cosmetic check
            if (string.IsNullOrEmpty(archetype.Description))
            {
                _descriptionSignatureLabel.Y = 15;
                _descriptionLabel.Text = "";
            }
            else
            {
                _descriptionSignatureLabel.Y = 8;
                _descriptionLabel.Y = _descriptionSignatureLabel.Bounds.Bottom + 6f;
                // Replacing multiple whitespaces with a linebreak for better readability. (Mainly doing this because the ToolTip fetching system replaces linebreaks with whitespaces - Nils)
                _descriptionLabel.Text = Regex.Replace(archetype.Description, @"\s{2,}", "\n");
            }

            // Some padding and moving elements around
            _descriptionPanelSeparator.Y = _descriptionLabel.Bounds.Bottom + 8f;
            panelHeight += _descriptionLabel.Height + 32f;
            _descriptionInputPanel.Y = panelHeight;
            _descriptionOutputPanel.Y = panelHeight;
            panelHeight += Mathf.Max(_descriptionInputPanel.Height, _descriptionOutputPanel.Height);

            // Forcing the description panel to at least have a minimum height to not make the window size change too much in order to reduce jittering
            // TODO: Remove the Mathf.Max and just set the height to panelHeight once the window jitter issue is fixed - Nils
            _descriptionPanel.Height = Mathf.Max(140f, panelHeight);
            Height = DefaultHeight + _descriptionPanel.Height;
            UpdateWindowSize();
            _descriptionPanelVisible = true;

            Profiler.EndEvent();
        }

        private void AddInputOutputElement(NodeArchetype nodeArchetype, ScriptType type, bool isOutput, string text)
        {
            // Using a panel instead of a vertical panel because auto sizing is unreliable - so we are doing this manually here as well
            var elementPanel = new Panel()
            {
                Parent = isOutput ? _descriptionOutputPanel : _descriptionInputPanel,
                Width = Width * 0.5f,
                Height = 16,
                AnchorPreset = AnchorPresets.TopLeft
            };

            _surfaceStyle.GetConnectionColor(type, nodeArchetype.ConnectionsHints, out var typeColor);
            elementPanel.AddChild(new Image(2, 0, 12, 12)
            {
                Brush = new SpriteBrush(_surfaceStyle.Icons.BoxOpen),
                Color = typeColor,
                MouseOverColor = typeColor,
                AutoFocus = false,
            }).SetAnchorPreset(AnchorPresets.TopLeft, true);

            // Forcing the first letter to be capital and removing the '&' char from pointer-references
            text = (char.ToUpper(text[0]) + text.Substring(1)).Replace("&", "");
            var elementText = new Label(16, 0, Width * 0.5f - 32, 16)
            {
                Text = text,
                HorizontalAlignment = TextAlignment.Near,
                VerticalAlignment = TextAlignment.Near,
                Wrapping = TextWrapping.WrapWords,
                AutoHeight = true,
            };
            elementText.SetAnchorPreset(AnchorPresets.TopLeft, true);
            elementPanel.AddChild(elementText);
            elementPanel.Height = elementText.Height;
        }

        /// <summary>
        /// Hides the description panel and resets the context menu to its original size
        /// </summary>
        private void HideDescriptionPanel()
        {
            if (!_descriptionPanelVisible)
                return;

            _descriptionInputPanel.RemoveChildren();
            _descriptionOutputPanel.RemoveChildren();
            Height = DefaultHeight;
            UpdateWindowSize();
            _descriptionPanelVisible = false;
        }

        /// <inheritdoc />
        public override bool OnKeyDown(KeyboardKeys key)
        {
            if (key == KeyboardKeys.Escape)
            {
                Hide();
                return true;
            }
            else if (key == KeyboardKeys.Return || key == KeyboardKeys.Tab)
            {
                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.ScrollViewTo(SelectedItem, true);
                }
                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>();
        }

        /// <inheritdoc />
        public override void OnDestroy()
        {
            _contextSensitiveToggle.StateChanged -= OnContextSensitiveToggleStateChanged;
            base.OnDestroy();
        }
    }
}
