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

using System;
using FlaxEngine;
using FlaxEngine.GUI;

namespace FlaxEditor.GUI.Tree
{
    /// <summary>
    /// Tree node control.
    /// </summary>
    /// <seealso cref="FlaxEngine.GUI.ContainerControl" />
    [HideInEditor]
    public class TreeNode : ContainerControl
    {
        /// <summary>
        /// The default drag insert position margin.
        /// </summary>
        public const float DefaultDragInsertPositionMargin = 3.0f;

        /// <summary>
        /// The default node offset on Y axis.
        /// </summary>
        public const float DefaultNodeOffsetY = 0;

        private const float _targetHighlightScale = 1.25f;
        private const float _highlightScaleAnimDuration = 0.85f;

        private Tree _tree;

        private bool _opened, _canChangeOrder;
        private float _animationProgress, _cachedHeight;
        private bool _isHightlighted;
        private float _targetHighlightTimeSec;
        private float _currentHighlightTimeSec;
        // Used to prevent showing highlight on double mouse click
        private float _debounceHighlightTime;
        private float _highlightScale;
        private bool _mouseOverArrow, _mouseOverHeader;
        private float _xOffset, _textWidth;
        private float _headerHeight = 16.0f;
        private Rectangle _headerRect;
        private SpriteHandle _iconCollaped, _iconOpened;
        private Margin _margin = new Margin(2.0f);
        private string _text;
        private bool _textChanged;
        private bool _isMouseDown;
        private float _mouseDownTime;
        private Float2 _mouseDownPos;

        private DragItemPositioning _dragOverMode;
        private bool _isDragOverHeader;
        private static ulong _dragEndFrame;

        /// <summary>
        /// Gets or sets the text.
        /// </summary>
        [EditorOrder(10), Tooltip("The node text.")]
        public string Text
        {
            get => _text;
            set
            {
                _text = value;
                _textChanged = true;
                PerformLayout();
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this node is expanded.
        /// </summary>
        [EditorOrder(20), Tooltip("If checked, node is expanded.")]
        public bool IsExpanded
        {
            get => _opened;
            set
            {
                if (value)
                    Expand(true);
                else
                    Collapse(true);
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this node is collapsed.
        /// </summary>
        [HideInEditor, NoSerialize]
        public bool IsCollapsed
        {
            get => !_opened;
            set
            {
                if (value)
                    Collapse(true);
                else
                    Expand(true);
            }
        }

        /// <summary>
        /// Gets a value indicating whether the node is collapsed in the hierarchy (is collapsed or any of its parents is collapsed).
        /// </summary>
        public bool IsCollapsedInHierarchy => IsCollapsed || (Parent is TreeNode parentNode && parentNode.IsCollapsedInHierarchy);

        /// <summary>
        /// Gets or sets the text margin.
        /// </summary>
        [EditorOrder(30), Tooltip("The margin of the text area.")]
        public Margin TextMargin
        {
            get => _margin;
            set
            {
                _margin = value;
                PerformLayout();
            }
        }

        /// <summary>
        /// Gets or sets the color of the text.
        /// </summary>
        [EditorDisplay("Style"), EditorOrder(2000)]
        public Color TextColor { get; set; }

        /// <summary>
        /// Gets or sets the font used to render text.
        /// </summary>
        [EditorDisplay("Style"), EditorOrder(2000)]
        public FontReference TextFont { get; set; }

        /// <summary>
        /// Gets or sets the color of the background when tree node is selected.
        /// </summary>
        [EditorDisplay("Style"), EditorOrder(2000)]
        public Color BackgroundColorSelected { get; set; }

        /// <summary>
        /// Gets or sets the color of the background when tree node is highlighted.
        /// </summary>
        [EditorDisplay("Style"), EditorOrder(2000)]
        public Color BackgroundColorHighlighted { get; set; }

        /// <summary>
        /// Gets or sets the color of the background when tree node is selected but not focused.
        /// </summary>
        [EditorDisplay("Style"), EditorOrder(2000)]
        public Color BackgroundColorSelectedUnfocused { get; set; }

        /// <summary>
        /// Gets the parent tree control.
        /// </summary>
        public Tree ParentTree
        {
            get
            {
                if (_tree == null)
                {
                    if (Parent is TreeNode upNode)
                        _tree = upNode.ParentTree;
                    else if (Parent is Tree tree)
                        _tree = tree;
                }
                return _tree;
            }
        }

        /// <summary>
        /// Gets a value indicating whether this node is root.
        /// </summary>
        public bool IsRoot => !(Parent is TreeNode);

        /// <summary>
        /// Gets the minimum width of the node sub-tree.
        /// </summary>
        public virtual float MinimumWidth
        {
            get
            {
                UpdateTextWidth();

                float minWidth = _xOffset + _textWidth + 6 + 16;
                if (_iconCollaped.IsValid)
                    minWidth += 16;

                if (_opened || _animationProgress < 1.0f)
                {
                    for (int i = 0; i < _children.Count; i++)
                    {
                        if (_children[i] is TreeNode node && node.Visible)
                        {
                            minWidth = Mathf.Max(minWidth, node.MinimumWidth);
                        }
                    }
                }

                return minWidth;
            }
        }

        /// <summary>
        /// The indent applied to the child nodes.
        /// </summary>
        [EditorOrder(30), Tooltip("The indentation applied to the child nodes.")]
        public float ChildrenIndent { get; set; } = 12.0f;

        /// <summary>
        /// The height of the tree node header area.
        /// </summary>
        [EditorOrder(40), Limit(1, 10000, 0.1f), Tooltip("The height of the tree node header area.")]
        public float HeaderHeight
        {
            get => _headerHeight;
            set
            {
                if (!Mathf.NearEqual(_headerHeight, value))
                {
                    _headerHeight = value;
                    PerformLayout();
                }
            }
        }

        /// <summary>
        /// Gets or sets the color of the icon.
        /// </summary>
        [EditorOrder(50), Tooltip("The color of the icon.")]
        public Color IconColor { get; set; } = Color.White;

        /// <summary>
        /// Gets the arrow rectangle.
        /// </summary>
        public Rectangle ArrowRect => CustomArrowRect.HasValue ? CustomArrowRect.Value : new Rectangle(_xOffset + 2 + _margin.Left, 2, 12, 12);

        /// <summary>
        /// Gets the header rectangle.
        /// </summary>
        public Rectangle HeaderRect => _headerRect;

        /// <summary>
        /// Gets the header text rectangle.
        /// </summary>
        public Rectangle TextRect
        {
            get
            {
                var left = _xOffset + 16; // offset + arrow
                var textRect = new Rectangle(left, 0, Width - left, _headerHeight);

                // Margin
                _margin.ShrinkRectangle(ref textRect);

                // Icon
                if (_iconCollaped.IsValid)
                {
                    textRect.X += 18.0f;
                    textRect.Width -= 18.0f;
                }

                return textRect;
            }
        }

        /// <summary>
        /// Custom arrow rectangle within node.
        /// </summary>
        [HideInEditor, NoSerialize]
        public Rectangle? CustomArrowRect;

        /// <summary>
        /// Gets the drag over action type.
        /// </summary>
        public DragItemPositioning DragOverMode => _dragOverMode;

        /// <summary>
        /// Gets a value indicating whether this node has any visible child. Returns false if it has no children.
        /// </summary>
        public bool HasAnyVisibleChild
        {
            get
            {
                bool result = false;
                for (int i = 0; i < _children.Count; i++)
                {
                    if (_children[i] is TreeNode node && node.Visible)
                    {
                        result = true;
                        break;
                    }
                }
                return result;
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TreeNode"/> class.
        /// </summary>
        public TreeNode()
        : this(false, SpriteHandle.Invalid, SpriteHandle.Invalid)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TreeNode"/> class.
        /// </summary>
        /// <param name="canChangeOrder">Enable/disable changing node order in parent tree node.</param>
        public TreeNode(bool canChangeOrder)
        : this(canChangeOrder, SpriteHandle.Invalid, SpriteHandle.Invalid)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TreeNode"/> class.
        /// </summary>
        /// <param name="canChangeOrder">Enable/disable changing node order in parent tree node.</param>
        /// <param name="iconCollapsed">The icon for node collapsed.</param>
        /// <param name="iconOpened">The icon for node opened.</param>
        public TreeNode(bool canChangeOrder, SpriteHandle iconCollapsed, SpriteHandle iconOpened)
        : base(0, 0, 64, 16)
        {
            AutoFocus = true;

            _canChangeOrder = canChangeOrder;
            _animationProgress = 1.0f;
            _cachedHeight = _headerHeight;
            _iconCollaped = iconCollapsed;
            _iconOpened = iconOpened;
            _mouseDownTime = -1;

            var style = Style.Current;
            TextColor = style.Foreground;
            BackgroundColorSelected = style.BackgroundSelected;
            BackgroundColorHighlighted = style.BackgroundHighlighted;
            BackgroundColorSelectedUnfocused = style.LightBackground;
            TextFont = new FontReference(style.FontSmall);
        }

        /// <summary>
        /// Expand node.
        /// </summary>
        /// <param name="noAnimation">True if skip node expanding animation.</param>
        public void Expand(bool noAnimation = false)
        {
            // Parents first
            ExpandAllParents(noAnimation);

            // Change state
            if (_opened && _animationProgress >= 1.0f)
                return;
            bool prevState = _opened;
            _opened = true;
            if (noAnimation)
                _animationProgress = 1.0f;
            else if (prevState != _opened)
                _animationProgress = 1.0f - _animationProgress;

            // Update
            OnExpandedChanged();
            OnExpandAnimationChanged();
        }

        /// <summary>
        /// Collapse node.
        /// </summary>
        /// <param name="noAnimation">True if skip node expanding animation.</param>
        public void Collapse(bool noAnimation = false)
        {
            // Change state
            if (!_opened && _animationProgress >= 1.0f)
                return;
            bool prevState = _opened;
            _opened = false;
            if (noAnimation)
                _animationProgress = 1.0f;
            else if (prevState != _opened)
                _animationProgress = 1.0f - _animationProgress;

            // Update
            OnExpandedChanged();
            OnExpandAnimationChanged();
        }

        /// <summary>
        /// Expand node and all the children.
        /// </summary>
        /// <param name="noAnimation">True if skip node expanding animation.</param>
        public void ExpandAll(bool noAnimation = false)
        {
            bool wasLayoutLocked = IsLayoutLocked;
            IsLayoutLocked = true;

            Expand(noAnimation);

            for (int i = 0; i < _children.Count; i++)
            {
                if (_children[i] is TreeNode node)
                {
                    node.ExpandAll(noAnimation);
                }
            }

            IsLayoutLocked = wasLayoutLocked;
            PerformLayout();
        }

        /// <summary>
        /// Collapse node and all the children.
        /// </summary>
        /// <param name="noAnimation">True if skip node expanding animation.</param>
        public void CollapseAll(bool noAnimation = false)
        {
            bool wasLayoutLocked = IsLayoutLocked;
            IsLayoutLocked = true;

            Collapse(noAnimation);

            for (int i = 0; i < _children.Count; i++)
            {
                if (_children[i] is TreeNode node)
                {
                    node.CollapseAll(noAnimation);
                }
            }

            IsLayoutLocked = wasLayoutLocked;
            PerformLayout();
        }

        /// <summary>
        /// Ensure that all node parents are expanded.
        /// </summary>
        /// <param name="noAnimation">True if skip node expanding animation.</param>
        public void ExpandAllParents(bool noAnimation = false)
        {
            (Parent as TreeNode)?.Expand(noAnimation);
        }

        /// <summary>
        /// Ends open/close animation by force.
        /// </summary>
        public void EndAnimation()
        {
            if (_animationProgress < 1.0f)
            {
                _animationProgress = 1.0f;
                OnExpandAnimationChanged();
            }
        }

        /// <summary>
        /// Select node in the tree.
        /// </summary>
        public void Select()
        {
            ParentTree.Select(this);
        }

        /// <summary>
        /// Called when drag and drop enters the node header area.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <returns>Drag action response.</returns>
        protected virtual DragDropEffect OnDragEnterHeader(DragData data)
        {
            return DragDropEffect.None;
        }

        /// <summary>
        /// Called when drag and drop moves over the node header area.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <returns>Drag action response.</returns>
        protected virtual DragDropEffect OnDragMoveHeader(DragData data)
        {
            return DragDropEffect.None;
        }

        /// <summary>
        /// Called when drag and drop performs over the node header area.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <returns>Drag action response.</returns>
        protected virtual DragDropEffect OnDragDropHeader(DragData data)
        {
            return DragDropEffect.None;
        }

        /// <summary>
        /// Called when drag and drop leaves the node header area.
        /// </summary>
        protected virtual void OnDragLeaveHeader()
        {
        }

        /// <summary>
        /// Begins the drag drop operation.
        /// </summary>
        protected virtual void DoDragDrop()
        {
        }

        /// <summary>
        /// Called when mouse double clicks header.
        /// </summary>
        /// <param name="location">The mouse location.</param>
        /// <param name="button">The button.</param>
        /// <returns>True if event has been handled.</returns>
        protected virtual bool OnMouseDoubleClickHeader(ref Float2 location, MouseButton button)
        {
            if (HasAnyVisibleChild)
            {
                // Toggle open state
                if (_opened)
                    Collapse();
                else
                    Expand();
            }

            // Handled
            return true;
        }

        /// <summary>
        /// Called when mouse is pressing node header for a long time.
        /// </summary>
        protected virtual void OnLongPress()
        {
        }

        /// <summary>
        /// Called when expanded/collapsed state changes.
        /// </summary>
        protected virtual void OnExpandedChanged()
        {
        }

        /// <summary>
        /// Called when expand/collapse animation progress changes.
        /// </summary>
        protected virtual void OnExpandAnimationChanged()
        {
            if (ParentTree != null)
                ParentTree.PerformLayout();
            else if (Parent != null)
                Parent.PerformLayout();
            else
                PerformLayout();
        }

        /// <summary>
        /// Tests the header hit.
        /// </summary>
        /// <param name="location">The location.</param>
        /// <returns>True if hits it.</returns>
        protected virtual bool TestHeaderHit(ref Float2 location)
        {
            return _headerRect.Contains(ref location);
        }

        /// <summary>
        /// Updates the drag over mode based on the given mouse location.
        /// </summary>
        /// <param name="location">The location.</param>
        private void UpdateDragPositioning(ref Float2 location)
        {
            // Check collision with drag areas
            if (new Rectangle(_headerRect.X, _headerRect.Y - DefaultDragInsertPositionMargin - DefaultNodeOffsetY, _headerRect.Width, DefaultDragInsertPositionMargin * 2.0f).Contains(location))
                _dragOverMode = DragItemPositioning.Above;
            else if ((IsCollapsed || !HasAnyVisibleChild) && new Rectangle(_headerRect.X, _headerRect.Bottom - DefaultDragInsertPositionMargin, _headerRect.Width, DefaultDragInsertPositionMargin * 2.0f).Contains(location))
                _dragOverMode = DragItemPositioning.Below;
            else
                _dragOverMode = DragItemPositioning.At;

            // Update DraggedOverNode
            var tree = ParentTree;
            if (_dragOverMode == DragItemPositioning.None)
            {
                if (tree != null && tree.DraggedOverNode == this)
                    tree.DraggedOverNode = null;
            }
            else if (tree != null)
                tree.DraggedOverNode = this;
        }

        private void ClearDragPositioning()
        {
            _dragOverMode = DragItemPositioning.None;
            var tree = ParentTree;
            if (tree != null && tree.DraggedOverNode == this)
                tree.DraggedOverNode = null;
        }

        /// <summary>
        /// Caches the color of the text for this node. Called during update before children nodes but after parent node so it can reuse parent tree node data.
        /// </summary>
        /// <returns>Text color.</returns>
        protected virtual Color CacheTextColor()
        {
            return Enabled ? TextColor : TextColor * 0.6f;
        }

        /// <summary>
        /// Updates the cached width of the text.
        /// </summary>
        protected void UpdateTextWidth()
        {
            if (_textChanged)
            {
                var font = TextFont.GetFont();
                if (font)
                {
                    _textWidth = font.MeasureText(_text).X;
                    _textChanged = false;
                }
            }
        }

        /// <summary>
        /// Adds a box around the text to highlight the node.
        /// </summary>
        /// <param name="durationSec">The duration of the highlight in seconds.</param>
        public void StartHighlight(float durationSec = 3)
        {
            _isHightlighted = true;
            _targetHighlightTimeSec = durationSec;
            _currentHighlightTimeSec = 0;
            _debounceHighlightTime = 0;
            _highlightScale = 2f;
        }

        /// <summary>
        /// Stops any current highlight.
        /// </summary>
        public void StopHighlight()
        {
            _isHightlighted = false;
            _targetHighlightTimeSec = 0;
            _currentHighlightTimeSec = 0;
            _debounceHighlightTime = 0;
        }

        /// <inheritdoc />
        public override void Update(float deltaTime)
        {
            // Highlight animations
            if (_isHightlighted)
            {
                _debounceHighlightTime += deltaTime;
                _currentHighlightTimeSec += deltaTime;

                // In the first second, animate the highlight to shrink into it's resting position
                if (_currentHighlightTimeSec < _highlightScaleAnimDuration)
                    _highlightScale = Mathf.Lerp(_highlightScale, _targetHighlightScale, _currentHighlightTimeSec);

                if (_currentHighlightTimeSec >= _targetHighlightTimeSec)
                    _isHightlighted = false;
            }

            // Drop/down animation
            if (_animationProgress < 1.0f)
            {
                bool isDeltaSlow = deltaTime > (1 / 20.0f);

                // Update progress
                if (isDeltaSlow)
                {
                    _animationProgress = 1.0f;
                }
                else
                {
                    const float openCloseAnimationTime = 0.1f;
                    _animationProgress += deltaTime / openCloseAnimationTime;
                    if (_animationProgress > 1.0f)
                        _animationProgress = 1.0f;
                }

                // Arrange controls
                OnExpandAnimationChanged();
            }

            // Check for long press
            const float longPressTimeSeconds = 0.6f;
            if (_isMouseDown && Time.UnscaledGameTime - _mouseDownTime > longPressTimeSeconds)
            {
                OnLongPress();
            }

            // Don't update collapsed children
            if (_opened)
            {
                base.Update(deltaTime);
            }
        }

        /// <inheritdoc />
        public override void Draw()
        {
            // Cache data
            var style = Style.Current;
            var tree = ParentTree;
            bool isSelected = tree.Selection.Contains(this);
            bool isFocused = tree.ContainsFocus;
            var left = _xOffset + 16; // offset + arrow
            var textRect = new Rectangle(left, 0, Width - left, _headerHeight);
            _margin.ShrinkRectangle(ref textRect);

            // Draw background
            if (isSelected || _mouseOverHeader)
            {
                Render2D.FillRectangle(_headerRect, (isSelected && isFocused) ? BackgroundColorSelected : (_mouseOverHeader ? BackgroundColorHighlighted : BackgroundColorSelectedUnfocused));
            }

            // Draw arrow
            if (HasAnyVisibleChild)
            {
                Render2D.DrawSprite(_opened ? style.ArrowDown : style.ArrowRight, ArrowRect, _mouseOverHeader ? style.Foreground : style.ForegroundGrey);
            }

            // Draw icon
            if (_iconCollaped.IsValid)
            {
                Render2D.DrawSprite(_opened ? _iconOpened : _iconCollaped, new Rectangle(textRect.Left, 0, 16, 16), IconColor);
                textRect.X += 18.0f;
                textRect.Width -= 18.0f;
            }

            float textWidth = TextFont.GetFont().MeasureText(_text).X;
            Rectangle trueTextRect = textRect;
            trueTextRect.Width = textWidth;
            trueTextRect.Scale(_highlightScale);

            if (_isHightlighted && _debounceHighlightTime > 0.1f)
            {
                Color highlightBackgroundColor = Editor.Instance.Options.Options.Visual.HighlightColor;
                highlightBackgroundColor = highlightBackgroundColor.AlphaMultiplied(0.3f);
                Render2D.FillRectangle(trueTextRect, highlightBackgroundColor);
            }

            // Draw text
            Color textColor = CacheTextColor();
            Render2D.DrawText(TextFont.GetFont(), _text, textRect, textColor, TextAlignment.Near, TextAlignment.Center);

            // Draw drag and drop effect
            if (IsDragOver && _tree.DraggedOverNode == this)
            {
                switch (_dragOverMode)
                {
                case DragItemPositioning.At:
                    Render2D.FillRectangle(textRect, style.Selection);
                    Render2D.DrawRectangle(textRect, style.SelectionBorder);
                    break;
                case DragItemPositioning.Above:
                    Render2D.DrawRectangle(new Rectangle(textRect.X, textRect.Top - DefaultDragInsertPositionMargin * 0.5f - DefaultNodeOffsetY - _margin.Top, textRect.Width, DefaultDragInsertPositionMargin), style.SelectionBorder);
                    break;
                case DragItemPositioning.Below:
                    Render2D.DrawRectangle(new Rectangle(textRect.X, textRect.Bottom + _margin.Bottom - DefaultDragInsertPositionMargin * 0.5f, textRect.Width, DefaultDragInsertPositionMargin), style.SelectionBorder);
                    break;
                }
            }

            // Show tree guidelines
            if (Editor.Instance.Options.Options.Interface.ShowTreeLines)
            {
                ContainerControl parent = Parent;
                TreeNode parentNode = parent as TreeNode;
                bool thisNodeIsLast = false;
                while (parentNode != null && (parentNode != tree.Children[0] || tree.DrawRootTreeLine))
                {
                    float bottomOffset = 0;
                    float topOffset = 0;

                    if (parent == parentNode && this == parent.Children[0])
                        topOffset = 2;

                    if (thisNodeIsLast && parentNode.Children.Count == 1)
                        bottomOffset = topOffset != 0 ? 4 : 2;

                    if (parent == parentNode && this == parent.Children[^1] && !_opened)
                    {
                        thisNodeIsLast = true;
                        bottomOffset = topOffset != 0 ? 4 : 2;
                    }

                    float leftOffset = 9;
                    // Adjust offset for icon image
                    if (_iconCollaped.IsValid)
                        leftOffset += 18;
                    var lineRect1 = new Rectangle(parentNode.TextRect.Left - leftOffset, parentNode.HeaderRect.Top + topOffset, 1, parentNode.HeaderRect.Height - bottomOffset);
                    if (HasAnyVisibleChild && CustomArrowRect.HasValue && CustomArrowRect.Value.Intersects(lineRect1))
                        lineRect1 = Rectangle.Empty; // Skip drawing line if it's overlapping the arrow rectangle
                    Render2D.FillRectangle(lineRect1, isSelected ? style.ForegroundGrey : style.LightBackground);
                    parentNode = parentNode.Parent as TreeNode;
                }
            }

            if (_isHightlighted && _debounceHighlightTime > 0.1f)
            {
                // Draw highlights
                Render2D.DrawRectangle(trueTextRect, Editor.Instance.Options.Options.Visual.HighlightColor, 3);
            }

            // Base
            if (_opened)
            {
                if (ClipChildren)
                {
                    Render2D.PushClip(new Rectangle(0, _headerHeight, Width, Height - _headerHeight));
                    base.Draw();
                    Render2D.PopClip();
                }
                else
                {
                    base.Draw();
                }
            }
        }

        /// <inheritdoc />
        protected override void DrawChildren()
        {
            // Draw all visible child controls
            var children = _children;
            if (children.Count == 0)
                return;
            var last = children.Count - 1;

            if (CullChildren)
            {
                Render2D.PeekClip(out var globalClipping);
                Render2D.PeekTransform(out var globalTransform);

                // Try to estimate the rough location of the first and the last nodes, assuming the node height is constant
                var firstChildGlobalRect = GetChildGlobalRectangle(children[0], ref globalTransform);
                var firstVisibleChild = Math.Clamp((int)Math.Floor((globalClipping.Top - firstChildGlobalRect.Top) / _headerHeight) + 1, 0, last);
                if (GetChildGlobalRectangle(children[firstVisibleChild], ref globalTransform).Top > globalClipping.Top || !children[firstVisibleChild].Visible)
                {
                    // Estimate overshoot, either it's partially visible or hidden in the tree
                    for (; firstVisibleChild > 0; firstVisibleChild--)
                    {
                        var child = children[firstVisibleChild];
                        if (!child.Visible)
                            continue;
                        if (GetChildGlobalRectangle(child, ref globalTransform).Top < globalClipping.Top)
                            break;
                    }
                }
                var lastVisibleChild = Math.Clamp((int)Math.Ceiling((globalClipping.Bottom - firstChildGlobalRect.Top) / _headerHeight) + 1, firstVisibleChild, last);
                if (GetChildGlobalRectangle(children[lastVisibleChild], ref globalTransform).Top < globalClipping.Bottom || !children[lastVisibleChild].Visible)
                {
                    // Estimate overshoot, either it's partially visible or hidden in the tree
                    for (; lastVisibleChild < last; lastVisibleChild++)
                    {
                        var child = children[lastVisibleChild];
                        if (!child.Visible)
                            continue;
                        if (GetChildGlobalRectangle(child, ref globalTransform).Top > globalClipping.Bottom)
                            break;
                    }
                }

                for (int i = firstVisibleChild; i <= lastVisibleChild; i++)
                {
                    var child = children[i];
                    if (!child.Visible)
                        continue;
                    Render2D.PushTransform(ref child._cachedTransform);
                    child.Draw();
                    Render2D.PopTransform();
                }

                static Rectangle GetChildGlobalRectangle(Control control, ref Matrix3x3 globalTransform)
                {
                    Matrix3x3.Multiply(ref control._cachedTransform, ref globalTransform, out var globalChildTransform);
                    return new Rectangle(globalChildTransform.M31, globalChildTransform.M32, control.Width * globalChildTransform.M11, control.Height * globalChildTransform.M22);
                }
            }
            else
            {
                for (int i = 0; i <= last; i++)
                {
                    var child = children[i];
                    if (child.Visible)
                    {
                        Render2D.PushTransform(ref child._cachedTransform);
                        child.Draw();
                        Render2D.PopTransform();
                    }
                }
            }
        }

        /// <inheritdoc />
        public override bool OnMouseDown(Float2 location, MouseButton button)
        {
            UpdateMouseOverFlags(location);

            // Check if mouse hits bar and node isn't a root
            if (_mouseOverHeader)
            {
                // Check if left button goes down
                if (button == MouseButton.Left)
                {
                    _isMouseDown = true;
                    _mouseDownPos = location;
                    _mouseDownTime = Time.UnscaledGameTime;
                }

                // Handled
                Focus();
                return true;
            }

            // Base
            if (_opened)
                return base.OnMouseDown(location, button);

            // Handled
            Focus();
            return true;
        }

        /// <inheritdoc />
        public override bool OnMouseUp(Float2 location, MouseButton button)
        {
            UpdateMouseOverFlags(location);

            // Clear flag for left button
            if (button == MouseButton.Left && _isMouseDown)
            {
                _isMouseDown = false;
                _mouseDownTime = -1;
            }

            // Check if mouse hits bar and node isn't a root
            if (_mouseOverHeader)
            {
                // Skip mouse up event right after drag drop ends
                if (button == MouseButton.Left && Engine.FrameCount - _dragEndFrame < 10)
                    return true;

                // Prevent from selecting node when user is just clicking at an arrow
                if (!_mouseOverArrow)
                {
                    // Check if user is pressing control key
                    var tree = ParentTree;
                    var window = tree.Root;
                    if (window.GetKey(KeyboardKeys.Shift))
                    {
                        // Select range
                        tree.SelectRange(this);
                    }
                    else if (window.GetKey(KeyboardKeys.Control))
                    {
                        // Add/Remove
                        tree.AddOrRemoveSelection(this);
                    }
                    else if (button == MouseButton.Right && tree.Selection.Contains(this))
                    {
                        // Do nothing
                    }
                    else
                    {
                        // Select
                        tree.Select(this);
                    }
                }

                // Check if mouse hits arrow
                if (_mouseOverArrow && HasAnyVisibleChild)
                {
                    if (ParentTree.Root.GetKey(KeyboardKeys.Alt))
                    {
                        if (_opened)
                            CollapseAll();
                        else
                            ExpandAll();
                    }
                    else
                    {
                        if (_opened)
                            Collapse();
                        else
                            Expand();
                    }
                }

                // Check if mouse hits bar
                if (button == MouseButton.Right && TestHeaderHit(ref location))
                {
                    ParentTree.OnRightClickInternal(this, ref location);
                }

                // Handled
                Focus();
                return true;
            }

            // Check if mouse hits bar
            if (button == MouseButton.Right && TestHeaderHit(ref location))
            {
                ParentTree.OnRightClickInternal(this, ref location);
            }

            // Base
            return base.OnMouseUp(location, button);
        }

        /// <inheritdoc />
        public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
        {
            // Check if mouse hits bar
            if (TestHeaderHit(ref location))
            {
                return OnMouseDoubleClickHeader(ref location, button);
            }

            // Check if animation has been finished
            if (_animationProgress >= 1.0f)
            {
                // Base
                return base.OnMouseDoubleClick(location, button);
            }

            return false;
        }

        /// <inheritdoc />
        public override void OnMouseMove(Float2 location)
        {
            UpdateMouseOverFlags(location);

            // Check if start drag and drop
            if (_isMouseDown && Float2.Distance(_mouseDownPos, location) > 10.0f)
            {
                // Clear flag
                _isMouseDown = false;
                _mouseDownTime = -1;

                // Start
                DoDragDrop();
                return;
            }

            // Check if animation has been finished
            if (_animationProgress >= 1.0f)
            {
                // Base
                if (_opened)
                    base.OnMouseMove(location);
            }
        }

        private void UpdateMouseOverFlags(Vector2 location)
        {
            // Cache flags
            _mouseOverArrow = HasAnyVisibleChild && ArrowRect.Contains(location);
            _mouseOverHeader = new Rectangle(0, 0, Width, _headerHeight - 1).Contains(location);
            if (_mouseOverHeader)
            {
                // Allow non-scrollable controls to stay on top of the header and override the mouse behaviour
                for (int i = 0; i < Children.Count; i++)
                {
                    if (!Children[i].IsScrollable && IntersectsChildContent(Children[i], location, out _))
                    {
                        _mouseOverHeader = false;
                        break;
                    }
                }
            }
        }

        /// <inheritdoc />
        public override void OnMouseLeave()
        {
            // Clear flags
            _mouseOverArrow = false;
            _mouseOverHeader = false;

            // Check if start drag and drop
            if (_isMouseDown)
            {
                // Clear flag
                _isMouseDown = false;
                _mouseDownTime = -1;

                // Start
                DoDragDrop();
            }

            // Base
            base.OnMouseLeave();
        }

        /// <inheritdoc />
        public override bool OnKeyDown(KeyboardKeys key)
        {
            // Base
            if (_opened)
                return base.OnKeyDown(key);
            return false;
        }

        /// <inheritdoc />
        public override void OnKeyUp(KeyboardKeys key)
        {
            // Base
            if (_opened)
                base.OnKeyUp(key);
        }

        /// <inheritdoc />
        public override void OnChildResized(Control control)
        {
            // Optimize if child is tree node that is not visible
            if (!_opened && control is TreeNode)
                return;

            PerformLayout();

            base.OnChildResized(control);
        }

        /// <inheritdoc />
        public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
        {
            var result = base.OnDragEnter(ref location, data);

            // Check if no children handled that event
            _dragOverMode = DragItemPositioning.None;
            if (result == DragDropEffect.None)
            {
                UpdateDragPositioning(ref location);

                // Check if mouse is over header
                _isDragOverHeader = TestHeaderHit(ref location);
                if (_isDragOverHeader)
                {
                    if (ParentTree != null)
                        ParentTree.DraggedOverNode = this;

                    // Expand node if mouse goes over arrow
                    if (ArrowRect.Contains(location) && HasAnyVisibleChild && IsCollapsed)
                    {
                        Expand(true);
                        ParentTree?.FlushPendingPerformLayout();
                    }

                    result = OnDragEnterHeader(data);
                }

                if (result == DragDropEffect.None)
                    _dragOverMode = DragItemPositioning.None;
            }

            return result;
        }

        /// <inheritdoc />
        public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
        {
            var result = base.OnDragMove(ref location, data);

            // Check if no children handled that event
            ClearDragPositioning();
            if (result == DragDropEffect.None)
            {
                UpdateDragPositioning(ref location);

                // Check if mouse is over header
                bool isDragOverHeader = TestHeaderHit(ref location);
                if (isDragOverHeader)
                {
                    if (ParentTree != null)
                        ParentTree.DraggedOverNode = this;

                    // Expand node if mouse goes over arrow
                    if (ArrowRect.Contains(location) && HasAnyVisibleChild && IsCollapsed)
                    {
                        Expand(true);
                        ParentTree?.FlushPendingPerformLayout();
                    }

                    if (!_isDragOverHeader)
                        result = OnDragEnterHeader(data);
                    else
                        result = OnDragMoveHeader(data);
                }
                else if (_isDragOverHeader)
                {
                    OnDragLeaveHeader();
                }
                _isDragOverHeader = isDragOverHeader;

                if (result == DragDropEffect.None)
                    _dragOverMode = DragItemPositioning.None;
            }

            return result;
        }

        /// <inheritdoc />
        public override DragDropEffect OnDragDrop(ref Float2 location, DragData data)
        {
            var result = base.OnDragDrop(ref location, data);

            // Check if no children handled that event
            if (result == DragDropEffect.None)
            {
                UpdateDragPositioning(ref location);
                _dragEndFrame = Engine.FrameCount;

                // Check if mouse is over header
                if (TestHeaderHit(ref location))
                {
                    result = OnDragDropHeader(data);
                }
            }

            // Clear cache
            _isDragOverHeader = false;
            ClearDragPositioning();

            return result;
        }

        /// <inheritdoc />
        public override void OnDragLeave()
        {
            // Clear cache
            if (_isDragOverHeader)
            {
                _isDragOverHeader = false;
                OnDragLeaveHeader();
            }
            ClearDragPositioning();

            base.OnDragLeave();
        }

        /// <inheritdoc />
        public override bool OnTestTooltipOverControl(ref Float2 location)
        {
            return TestHeaderHit(ref location) && ShowTooltip;
        }

        /// <inheritdoc />
        public override bool OnShowTooltip(out string text, out Float2 location, out Rectangle area)
        {
            text = TooltipText;
            location = _headerRect.Size * new Float2(0.5f, 1.0f);
            area = new Rectangle(Float2.Zero, _headerRect.Size);
            return ShowTooltip;
        }

        /// <inheritdoc />
        protected override void OnSizeChanged()
        {
            base.OnSizeChanged();

            _headerRect = new Rectangle(0, 0, Width, _headerHeight);
        }

        /// <inheritdoc />
        public override void PerformLayout(bool force = false)
        {
            if (_isLayoutLocked && !force)
                return;

            bool wasLocked = _isLayoutLocked;
            if (!wasLocked)
                LockChildrenRecursive();

            // Auto-size tree nodes to match the parent size
            var parent = Parent;
            var width = parent is TreeNode ? parent.Width : Width;

            // Optimize layout logic if node is collapsed
            if (_opened || _animationProgress < 1.0f)
            {
                Width = width;
                PerformLayoutBeforeChildren();
                for (int i = 0; i < _children.Count; i++)
                    _children[i].PerformLayout(true);
                PerformLayoutAfterChildren();
            }
            else
            {
                // TODO: perform layout for any non-TreeNode controls
                _cachedHeight = _headerHeight;
                Size = new Float2(width, _headerHeight);
            }

            if (!wasLocked)
                UnlockChildrenRecursive();
        }

        /// <inheritdoc />
        protected override void PerformLayoutBeforeChildren()
        {
            if (_opened)
            {
                // Update the nodes nesting level before the actual positioning
                float xOffset = _xOffset + ChildrenIndent;
                for (int i = 0; i < _children.Count; i++)
                {
                    if (_children[i] is TreeNode node)
                        node._xOffset = xOffset;
                }
            }

            base.PerformLayoutBeforeChildren();
        }

        /// <inheritdoc />
        protected override void PerformLayoutAfterChildren()
        {
            float y = _headerHeight;
            float height = _headerHeight;
            float xOffset = _xOffset + ChildrenIndent;

            // Skip full layout if it's fully collapsed
            if (_opened || _animationProgress < 1.0f)
            {
                y -= _cachedHeight * (_opened ? 1.0f - _animationProgress : _animationProgress);
                for (int i = 0; i < _children.Count; i++)
                {
                    if (_children[i] is TreeNode node && node.Visible)
                    {
                        node._xOffset = xOffset;
                        node.Location = new Float2(0, y);
                        float nodeHeight = node.Height + DefaultNodeOffsetY;
                        y += nodeHeight;
                        height += nodeHeight;
                    }
                }
            }

            _cachedHeight = height;
            Height = Mathf.Max(_headerHeight, y);
        }

        /// <inheritdoc />
        protected override bool CanNavigateChild(Control child)
        {
            // Closed tree node skips navigation for hidden children
            if (IsCollapsed && child is TreeNode)
                return false;
            return base.CanNavigateChild(child);
        }

        /// <inheritdoc />
        protected override void OnParentChangedInternal()
        {
            _tree = null;

            base.OnParentChangedInternal();
        }

        /// <inheritdoc />
        public override int Compare(Control other)
        {
            if (other is TreeNode node)
            {
                return string.Compare(Text, node.Text, StringComparison.InvariantCulture);
            }
            return base.Compare(other);
        }

        /// <inheritdoc />
        public override void OnDestroy()
        {
            ParentTree?.Selection.Remove(this);

            base.OnDestroy();
        }
    }
}
