// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //

#if UNITY_EDITOR

using System;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;

namespace Animancer.Editor
{
    /// <summary>[Editor-Only] Utility for drawing tables.</summary>
    /// https://kybernetik.com.au/animancer/api/Animancer.Editor/TableGUI
    [Serializable]
    public class TableGUI
    {
        /************************************************************************************************************************/

        /// <summary>The pixel spacing between cells.</summary>
        public static float Padding
            => 0;// StandardSpacing;

        /// <summary>The style for the horizontal scroll bar.</summary>
        public static GUIStyle HorizontalScrollBar
            => GUI.skin.horizontalScrollbar;

        /// <summary>The style for the vertical scroll bar.</summary>
        public static GUIStyle VerticalScrollBar
            => GUI.skin.verticalScrollbar;

        /************************************************************************************************************************/

        /// <summary>Draws the GUI for a specified cell.</summary>
        /// <remarks>`column` and `row` are given <c>-1</c> for the labels.</remarks>
        public delegate void CellGUIDelegate(Rect area, int column, int row);

        /// <summary>Draws the GUI for a specified cell.</summary>
        /// <remarks>`column` and `row` are given <c>-1</c> for the labels.</remarks>
        public CellGUIDelegate DoCellGUI;

        [NonSerialized] private Vector2 _MinCellSize;// Pixels.
        [NonSerialized] private Vector2 _MaxCellSize;// Pixels.

        [SerializeField] private Vector2 _LabelSize = new(0.25f, 0.25f);// Normalized.
        [SerializeField] private Vector2 _ScrollPosition;

        /// <summary>The minimum pixel size of each cell.</summary>
        public ref Vector2 MinCellSize
            => ref _MinCellSize;

        /// <summary>The maximum pixel size of each cell.</summary>
        public ref Vector2 MaxCellSize
            => ref _MaxCellSize;

        /// <summary>[<see cref="SerializeField"/>] The normalized size of the header labels.</summary>
        public ref Vector2 LabelSize
            => ref _LabelSize;

        /// <summary>[<see cref="SerializeField"/>] The position the table is currently scrolled to.</summary>
        public ref Vector2 ScrollPosition
            => ref _ScrollPosition;

        /************************************************************************************************************************/

        /// <summary>Draws this table.</summary>
        public void DoTableGUI(
            Rect area,
            int columns,
            int rows)
        {
            HandleInput(area);

            var scrollBarSize = new Vector2(
                VerticalScrollBar.fixedWidth + Padding,
                HorizontalScrollBar.fixedHeight + Padding);

            area.size -= scrollBarSize;

            CalculateSizes(area, columns, rows, out var labelSize, out var cellSize);

            area.size += scrollBarSize;

            var cornerArea = new Rect(area.position, labelSize + scrollBarSize);
            DoCellGUI(cornerArea, -1, -1);

            var labelResizerArea = cornerArea;
            labelResizerArea.xMin = labelResizerArea.xMax - scrollBarSize.x;
            labelResizerArea.yMin = labelResizerArea.yMax - scrollBarSize.y;
            DoLabelResizerGUI(labelResizerArea, area);

            var columnLabelArea = new Rect(
                area.x + scrollBarSize.x + labelSize.x + Padding,
                area.y,
                cellSize.x,
                labelSize.y);
            var rowLabelArea = new Rect(
                area.x,
                area.y + scrollBarSize.y + labelSize.y + Padding,
                labelSize.x,
                cellSize.y);

            area.xMin += labelSize.x + Padding + scrollBarSize.x;
            area.yMin += labelSize.y + Padding + scrollBarSize.y;

            GUI.BeginClip(area);

            for (int x = 0; x < columns; x++)
            {
                for (int y = 0; y < rows; y++)
                {
                    var cellArea = new Rect(
                        (cellSize.x + Padding) * x - _ScrollPosition.x,
                        (cellSize.y + Padding) * y - _ScrollPosition.y,
                        cellSize.x,
                        cellSize.y);

                    DoCellGUI(cellArea, x, y);
                }
            }

            GUI.EndClip();

            DrawColumnLabels(columnLabelArea, columns, area.width, scrollBarSize.y);
            DrawRowLabels(rowLabelArea, rows, area.height, scrollBarSize.x);
        }

        /************************************************************************************************************************/

        private static readonly int LabelResizerHint = "LabelResizer".GetHashCode();

        private static GUIContent _LabelResizerIcon;

        private void DoLabelResizerGUI(
            Rect resizerArea,
            Rect tableArea)
        {
            var control = new GUIControl(resizerArea, LabelResizerHint);

            switch (control.EventType)
            {
                case EventType.MouseDown:
                    if (control.Event.button == 0 &&
                        control.TryUseMouseDown())
                    {
                        if (control.Event.clickCount == 2)
                        {
                            AutoSizeLabels(tableArea);
                            GUIUtility.hotControl = 0;
                        }
                    }

                    break;

                case EventType.MouseUp:
                    control.TryUseMouseUp();
                    break;

                case EventType.MouseDrag:
                    if (control.TryUseHotControl())
                    {
                        var offset = control.Event.mousePosition - tableArea.position;
                        LabelSize = new(
                            offset.x / tableArea.width,
                            offset.y / tableArea.height);
                    }

                    break;

                case EventType.KeyDown:
                    if (control.TryUseKey(KeyCode.Escape))
                        Deselect();
                    break;

                case EventType.Repaint:
                    EditorGUIUtility.AddCursorRect(resizerArea, MouseCursor.ResizeUpLeft);

                    AnimancerIcons.IconContent(ref _LabelResizerIcon, "MoveTool");
                    if (_LabelResizerIcon != null)
                        GUI.DrawTexture(resizerArea, _LabelResizerIcon.image);
                    break;
            }
        }

        /************************************************************************************************************************/

        private static readonly Matrix4x4
            Rotate90LeftMatrix = Matrix4x4.Rotate(Quaternion.Euler(0, 0, -90));

        private void DrawColumnLabels(Rect area, int count, float availableSize, float scrollSize)
        {
            var previousClip = GetGUIClipRect();
            GUI.EndClip();

            var totalSize =
                area.width * count +
                Padding * (count - 1);

            var totalArea = new Rect(
                area.x,
                area.y + area.height + Padding,
                availableSize,
                scrollSize);

            var translation = previousClip.position + area.position;
            translation = -translation.GetPerpendicular();
            translation.x -= area.height;

            area.x = 0;
            area.y = -_ScrollPosition.x;
            (area.width, area.height) = (area.height, area.width);

            GUI.BeginClip(new(0, 0, area.width, availableSize));

            var matrix = GUI.matrix;
            GUI.matrix =
                Rotate90LeftMatrix *
                Matrix4x4.Translate(translation);

            for (int i = 0; i < count; i++)
            {
                DoCellGUI(area, i, -1);

                area.y += area.height + Padding;
            }

            GUI.matrix = matrix;

            GUI.EndClip();
            GUI.BeginClip(previousClip);

            var enabled = GUI.enabled;
            GUI.enabled = availableSize < totalSize;

            _ScrollPosition.x = GUI.HorizontalScrollbar(
                totalArea,
                _ScrollPosition.x,
                availableSize,
                0,
                totalSize);

            GUI.enabled = enabled;
        }

        /************************************************************************************************************************/

        private void DrawRowLabels(Rect area, int count, float availableSize, float scrollSize)
        {
            var totalArea = area;
            totalArea.height = availableSize;

            GUI.BeginClip(totalArea);

            area.x = 0;
            area.y = -_ScrollPosition.y;

            for (int i = 0; i < count; i++)
            {
                DoCellGUI(area, -1, i);

                area.y += area.height + Padding;
            }

            GUI.EndClip();

            var totalSize =
                area.height * count +
                Padding * (count - 1);

            totalArea.x += totalArea.width + Padding;
            totalArea.width = scrollSize;

            var enabled = GUI.enabled;
            GUI.enabled = availableSize < totalSize;

            _ScrollPosition.y = GUI.VerticalScrollbar(
                totalArea,
                _ScrollPosition.y,
                availableSize,
                0,
                totalSize);

            GUI.enabled = enabled;
        }

        /************************************************************************************************************************/

        private static readonly int ControlHint = nameof(TableGUI).GetHashCode();

        private void HandleInput(Rect area)
        {
            var control = new GUIControl(area, ControlHint);

            switch (control.EventType)
            {
                case EventType.ScrollWheel:
                    if (control.ContainsMousePosition)
                    {
                        var delta = control.Event.delta * 5;
                        if (control.Event.shift)
                            delta = delta.GetPerpendicular();

                        _ScrollPosition += delta;

                        control.Event.Use();
                    }
                    break;

                case EventType.MouseDown:
                    if (control.Event.IsMiddleClick())
                        control.TryUseMouseDown();
                    break;

                case EventType.MouseUp:
                    control.TryUseMouseUp();
                    break;

                case EventType.MouseDrag:
                    if (control.TryUseHotControl())
                        _ScrollPosition -= control.Event.delta;
                    break;
            }
        }

        /************************************************************************************************************************/
        #region Size Calculation
        /************************************************************************************************************************/

        /// <summary>Calculates the current label and cell sizes for the given `area`.</summary>
        public void CalculateSizes(
            Rect area,
            int columns,
            int rows,
            out Vector2 labelSize,
            out Vector2 cellSize)
        {
            // Min cell size.
            cellSize = _MinCellSize;
            if (cellSize.x < 1)
                cellSize.x = LineHeight;
            if (cellSize.y < 1)
                cellSize.y = LineHeight;

            // Min label size.
            labelSize.x = Mathf.Clamp(_LabelSize.x, 0, 0.9f);
            labelSize.y = Mathf.Clamp(_LabelSize.y, 0, 0.9f);

            labelSize = Vector2.Scale(area.size, labelSize);
            if (labelSize == default)
                labelSize = cellSize;

            // Expand cells if there is more area available, up to the max.
            var availableSize = area.size - labelSize;
            cellSize.x = StretchCellSize(availableSize.x, cellSize.x, _MaxCellSize.x, columns);
            cellSize.y = StretchCellSize(availableSize.y, cellSize.y, _MaxCellSize.y, rows);

            // Expand labels if there is more area available.
            labelSize.x = StretchLabelSize(area.width, labelSize.x, cellSize.x, columns);
            labelSize.y = StretchLabelSize(area.height, labelSize.y, cellSize.y, rows);
        }

        /************************************************************************************************************************/

        private static float StretchCellSize(
            float availableSize,
            float cellSize,
            float maxCellSize,
            int cellCount)
        {
            if (cellSize < maxCellSize)
            {
                availableSize -= Padding * (cellCount - 1);
                if (availableSize > cellSize * cellCount)
                    cellSize = Math.Min(availableSize / cellCount, maxCellSize);
            }

            return cellSize;
        }

        /************************************************************************************************************************/

        private static float StretchLabelSize(
            float availableSize,
            float labelSize,
            float cellSize,
            int cellCount)
        {
            labelSize = Math.Max(labelSize, availableSize - (cellSize + Padding) * cellCount);
            labelSize = Math.Max(labelSize, cellSize);
            return labelSize;
        }

        /************************************************************************************************************************/

        /// <summary>A delegate to calculate the largest pixel width of the header labels.</summary>
        public Func<float> CalculateWidestLabel { get; set; }

        private void AutoSizeLabels(Rect tableArea)
        {
            if (CalculateWidestLabel == null)
                return;

            var targetLabelSize = CalculateWidestLabel();

            _LabelSize.x = targetLabelSize / tableArea.width;
            _LabelSize.y = targetLabelSize / tableArea.height;
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
    }
}

#endif

