using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using UnityEngine.Assertions;
using Unity.PerformanceTesting.Data;
#if UNITY_6000_2_OR_NEWER
using TreeView = UnityEditor.IMGUI.Controls.TreeView<int>;
using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem<int>;
using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState<int>;
#endif

namespace Unity.PerformanceTesting.Editor
{
    internal class TestListTableItem : TreeViewItem
    {
        public PerformanceTestResult performanceTest;
        public double deviation;
        public double standardDeviation;
        public double median;
        public double min;
        public double max;

        public TestListTableItem(int id, int depth, string displayName, PerformanceTestResult performanceTest)
            : base(id, depth,
                displayName)
        {
            this.performanceTest = performanceTest;

            deviation = 0f;
            if (performanceTest != null)
            {
                foreach (var sampleGroup in performanceTest.SampleGroups)
                {
                    var thisDeviation =
                        sampleGroup.Median == 0 ? 0 : sampleGroup.StandardDeviation / sampleGroup.Median;

                    if (sampleGroup.Name == "Time" || sampleGroup.Samples.Count <= 1 || thisDeviation > deviation)
                    {
                        standardDeviation = sampleGroup.StandardDeviation;
                        median = sampleGroup.Median;
                        min = sampleGroup.Min;
                        max = sampleGroup.Max;
                        deviation = thisDeviation;

                        break;
                    }
                }
            }
        }
    }

    internal class TestListTable : TreeView
    {
        TestReportWindow m_testReportWindow;

        const float kRowHeights = 20f;
        readonly List<TreeViewItem> m_Rows = new List<TreeViewItem>(100);

        // All columns
        public enum MyColumns
        {
            Name,
            SampleCount,
            StandardDeviation,
            Deviation,
            Median,
            Min,
            Max,
        }

        public enum SortOption
        {
            Name,
            SampleCount,
            StandardDeviation,
            Deviation,
            Median,
            Min,
            Max,
        }

        // Sort options per column
        SortOption[] m_SortOptions =
        {
            SortOption.Name,
            SortOption.SampleCount,
            SortOption.StandardDeviation,
            SortOption.Deviation,
            SortOption.Median,
            SortOption.Min,
            SortOption.Max,
        };

        public TestListTable(TreeViewState state, MultiColumnHeader multicolumnHeader,
            TestReportWindow testReportWindow)
            : base(state, multicolumnHeader)
        {
            m_testReportWindow = testReportWindow;

            Assert.AreEqual(m_SortOptions.Length, Enum.GetValues(typeof(MyColumns)).Length,
                "Ensure number of sort options are in sync with number of MyColumns enum values");

            // Custom setup
            rowHeight = kRowHeights;
            showAlternatingRowBackgrounds = true;
            showBorder = true;
            customFoldoutYOffset =
                (kRowHeights - EditorGUIUtility.singleLineHeight) *
                0.5f; // center foldout in the row since we also center content. See RowGUI

            // extraSpaceBeforeIconAndLabel = 0;
            multicolumnHeader.sortingChanged += OnSortingChanged;

            Reload();
        }

        protected override TreeViewItem BuildRoot()
        {
            var root = new TestListTableItem(-1, -1, "root", null);
            var results = m_testReportWindow.GetResults();
            var searchText = m_testReportWindow.searchString?.ToLower();
            var methodItemId = 0;

            if (results == null) return root;

            var classItemId = results.Results.Count(); // Class ID will always be bigger than the count of results (from methods) we get
            var classGroups = results.Results.GroupBy(r => r.ClassName);

            foreach (var classGroup in classGroups)
            {
                var classItem = new TestListTableItem(classItemId++, 0, classGroup.Key, null);
                root.AddChild(classItem);

                // Create method items and add them to the class item if they match the search text
                // Currently Name provides both MethodName and Test parameter - example would be - ValueSource(Cube)
                // We are using Name and simply removing the ClassPart as we need to keep that parameter
                var methodItems = classGroup
                    .Select(r =>
                    {
                        // Calculate the starting index for the substring to extract methodname from full name
                        int startIndex = r.Name.IndexOf(classGroup.Key, StringComparison.Ordinal) + classGroup.Key.Length + 1;

                        // Check if the calculated startIndex is within the bounds of the string
                        // If it's within bounds, extract the substring; otherwise, return full name
                        string methodName = startIndex < r.Name.Length ? r.Name.Substring(startIndex) : r.Name;

                        // Create a TestListTableItem using the extracted methodName and other parameters
                        return new TestListTableItem(methodItemId++, 1, methodName, r);
                    });

                foreach (var methodItem in methodItems)
                {
                    var matchesSearchText = string.IsNullOrEmpty(searchText) ||
                                            methodItem.displayName.ToLower().Contains(searchText);
                    if (matchesSearchText) classItem.AddChild(methodItem);
                }
            }

            // Remove class items with no children
            root.children?.RemoveAll(c => !c.hasChildren);

            return root;
        }

        protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
        {
            m_Rows.Clear();

            if (rootItem != null && rootItem.children != null)
            {
                m_Rows.AddRange(base.BuildRows(root));
            }

            SortIfNeeded(m_Rows);

            return m_Rows;
        }

        void OnSortingChanged(MultiColumnHeader _multiColumnHeader)
        {
            SortIfNeeded(GetRows());
        }

        void SortIfNeeded(IList<TreeViewItem> rows)
        {
            if (rows.Count <= 1)
            {
                return;
            }

            if (multiColumnHeader.sortedColumnIndex == -1)
            {
                return; // No column to sort for (just use the order the data are in)
            }

            // Sort the roots of the existing tree items
            SortByMultipleColumns();

            // Update the data with the sorted content
            rows.Clear();
            AddExpandedRows(rootItem, rows);

            Repaint();
        }

        void SortByMultipleColumns()
        {
            var sortedColumns = multiColumnHeader.state.sortedColumns;

            if (sortedColumns.Length == 0)
            {
                return;
            }

            var classItems = rootItem.children;

            foreach (var classItem in classItems)
            {
                var myTypes = classItem.children.Cast<TestListTableItem>();
                var orderedQuery = InitialOrder(myTypes, sortedColumns);
                for (int i = 1; i < sortedColumns.Length; i++)
                {
                    SortOption sortOption = m_SortOptions[sortedColumns[i]];
                    bool ascending = multiColumnHeader.IsSortedAscending(sortedColumns[i]);

                    switch (sortOption)
                    {
                        case SortOption.Name:
                            orderedQuery = orderedQuery.ThenBy(l => l.displayName, ascending);
                            break;
                        case SortOption.SampleCount:
                            orderedQuery = orderedQuery.ThenBy(l => l.performanceTest.SampleGroups.Count, ascending);
                            break;
                        case SortOption.Deviation:
                            orderedQuery = orderedQuery.ThenBy(l => l.deviation, ascending);
                            break;
                        case SortOption.StandardDeviation:
                            orderedQuery = orderedQuery.ThenBy(l => l.standardDeviation, ascending);
                            break;
                        case SortOption.Median:
                            orderedQuery = orderedQuery.ThenBy(l => l.median, ascending);
                            break;
                        case SortOption.Min:
                            orderedQuery = orderedQuery.ThenBy(l => l.min, ascending);
                            break;
                        case SortOption.Max:
                            orderedQuery = orderedQuery.ThenBy(l => l.max, ascending);
                            break;
                    }
                }

                classItem.children = orderedQuery.Cast<TreeViewItem>().ToList();
            }
        }

        IOrderedEnumerable<TestListTableItem> InitialOrder(IEnumerable<TestListTableItem> myTypes, int[] history)
        {
            SortOption sortOption = m_SortOptions[history[0]];
            bool ascending = multiColumnHeader.IsSortedAscending(history[0]);
            switch (sortOption)
            {
                case SortOption.Name:
                    return myTypes.Order(l => l.displayName, ascending);
                case SortOption.SampleCount:
                    return myTypes.Order(l => l.performanceTest.SampleGroups.Count, ascending);
                case SortOption.Deviation:
                    return myTypes.Order(l => l.deviation, ascending);
                case SortOption.StandardDeviation:
                    return myTypes.Order(l => l.standardDeviation, ascending);
                case SortOption.Median:
                    return myTypes.Order(l => l.median, ascending);
                case SortOption.Min:
                    return myTypes.Order(l => l.min, ascending);
                case SortOption.Max:
                    return myTypes.Order(l => l.max, ascending);
                default:
                    Assert.IsTrue(false, "Unhandled enum");
                    break;
            }

            // default
            return myTypes.Order(l => l.displayName, ascending);
        }

        protected override void RowGUI(RowGUIArgs args)
        {
            var item = (TestListTableItem)args.item;
            var nameRect = args.GetCellRect(0);
            nameRect.x += GetContentIndent(item);
            nameRect.xMax -= GetContentIndent(item);

            if (item.depth == 0)
            {
                EditorGUI.LabelField(nameRect, item.displayName);
            }

            if (item.depth == 1)
            {
                for (var i = 0; i < args.GetNumVisibleColumns(); ++i)
                {
                    var column = (MyColumns)args.GetColumn(i);

                    if (column == MyColumns.Name)
                    {
                        EditorGUI.LabelField(nameRect, item.displayName);
                    }
                    else
                    {
                        CellGUI(args.GetCellRect(i), item, column);
                    }
                }
            }
        }

        void CellGUI(Rect cellRect, TestListTableItem item, MyColumns column)
        {
            // Center cell rect vertically (makes it easier to place controls, icons etc in the cells)
            CenterRectUsingSingleLineHeight(ref cellRect);

            switch (column)
            {
                case MyColumns.SampleCount:
                    EditorGUI.LabelField(cellRect, $"{item.performanceTest.SampleGroups.Count}");
                    break;
                case MyColumns.Deviation:
                    EditorGUI.LabelField(cellRect, $"{item.deviation:f2}");
                    break;
                case MyColumns.StandardDeviation:
                    EditorGUI.LabelField(cellRect, $"{item.standardDeviation:f2}");
                    break;
                case MyColumns.Median:
                    EditorGUI.LabelField(cellRect, $"{item.median:f2}");
                    break;
                case MyColumns.Min:
                    EditorGUI.LabelField(cellRect, $"{item.min:f2}");
                    break;
                case MyColumns.Max:
                    EditorGUI.LabelField(cellRect, $"{item.max:f2}");
                    break;
            }
        }

        // Misc
        //--------

        protected override bool CanMultiSelect(TreeViewItem item)
        {
            return false;
        }

        struct HeaderData
        {
            public readonly GUIContent content;
            public readonly float width;
            public readonly float minWidth;
            public readonly bool autoResize;
            public readonly bool allowToggleVisibility;
            public readonly bool ascending;

            public HeaderData(string name, string tooltip = "", float width = 100, float minWidth = 50, bool autoResize = true, bool allowToggleVisibility = true, bool ascending = false)
            {
                content = new GUIContent(name, tooltip);
                this.width = width;
                this.minWidth = minWidth;
                this.autoResize = autoResize;
                this.allowToggleVisibility = allowToggleVisibility;
                this.ascending = ascending;
            }
        }

        public static MultiColumnHeaderState CreateDefaultMultiColumnHeaderState(float treeViewWidth)
        {
            var columnList = new List<MultiColumnHeaderState.Column>();
            HeaderData[] headerData = new HeaderData[]
            {
                new HeaderData("Name", "Name of test", width : 500, minWidth : 100, autoResize : false, allowToggleVisibility : false, ascending : true),
                new HeaderData("Groups", "Number of Sample Groups", width : 60, minWidth : 50),
                new HeaderData("SD", "Standard Deviation"), //  (of sample group with largest deviation)
                new HeaderData("Deviation", "Standard Deviation / Median"),
                new HeaderData("Median", "Median value"),
                new HeaderData("Min", "Min value"),
                new HeaderData("Max", "Max value"),
            };
            foreach (var header in headerData)
            {
                columnList.Add(new MultiColumnHeaderState.Column
                {
                    headerContent = header.content,
                    headerTextAlignment = TextAlignment.Left,
                    sortedAscending = header.ascending,
                    sortingArrowAlignment = TextAlignment.Left,
                    width = header.width,
                    minWidth = header.minWidth,
                    autoResize = header.autoResize,
                    allowToggleVisibility = header.allowToggleVisibility
                });
            };
            var columns = columnList.ToArray();

            Assert.AreEqual(columns.Length, Enum.GetValues(typeof(MyColumns)).Length,
                "Number of columns should match number of enum values: You probably forgot to update one of them.");

            var state = new MultiColumnHeaderState(columns);
            state.visibleColumns = new int[]
            {
                (int)MyColumns.Name,
                (int)MyColumns.SampleCount,
                (int)MyColumns.Deviation,
                (int)MyColumns.StandardDeviation,
                (int)MyColumns.Median,
                (int)MyColumns.Min,
                (int)MyColumns.Max,
            };
            return state;
        }

        protected override void SelectionChanged(IList<int> selectedIds)
        {
            base.SelectionChanged(selectedIds);

            if (selectedIds.Count > 0)
                m_testReportWindow.SelectTest(selectedIds[0]);
        }
    }

    internal static class MyExtensionMethods
    {
        public static IOrderedEnumerable<T> Order<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector,
            bool ascending)
        {
            if (ascending)
            {
                return source.OrderBy(selector);
            }
            else
            {
                return source.OrderByDescending(selector);
            }
        }

        public static IOrderedEnumerable<T> ThenBy<T, TKey>(this IOrderedEnumerable<T> source, Func<T, TKey> selector,
            bool ascending)
        {
            if (ascending)
            {
                return source.ThenBy(selector);
            }
            else
            {
                return source.ThenByDescending(selector);
            }
        }
    }
}