using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEngine;

namespace UnityEditor.Timeline
{
    class TimelineTreeView : ITreeViewGUI
    {
        float m_FoldoutWidth;
        Rect m_DraggingInsertionMarkerRect;
        readonly TreeViewController m_TreeView;

        List<Rect> m_RowRects = new List<Rect>();
        List<Rect> m_ExpandedRowRects = new List<Rect>();

        float m_MaxWidthOfRows;
        readonly WindowState m_State;

        static readonly float kMinTrackHeight = 25.0f;
        static readonly float kFoldOutOffset = 14.0f;

        static DirectorStyles m_Styles;

        public bool showInsertionMarker { get; set; }
        public virtual float topRowMargin { get; private set; }
        public virtual float bottomRowMargin { get; private set; }

        public TimelineTreeView(TimelineWindow sequencerWindow, TreeViewController treeView)
        {
            m_TreeView = treeView;
            m_TreeView.useExpansionAnimation = true;

            m_TreeView.selectionChangedCallback += SelectionChangedCallback;
            m_TreeView.contextClickOutsideItemsCallback += ContextClickOutsideItemsCallback;
            m_TreeView.itemDoubleClickedCallback += ItemDoubleClickedCallback;
            m_TreeView.contextClickItemCallback += ContextClickItemCallback;

            m_TreeView.SetConsumeKeyDownEvents(false);
            m_Styles = DirectorStyles.Instance;
            m_State = sequencerWindow.state;

            m_FoldoutWidth = DirectorStyles.Instance.foldout.fixedWidth;
        }

        void ItemDoubleClickedCallback(int id)
        {
            var trackGUI = m_TreeView.FindItem(id) as TimelineTrackGUI;
            if (trackGUI == null)
                return;

            if (trackGUI.track == null || trackGUI.track.lockedInHierarchy)
                return;

            var selection = SelectionManager.SelectedItems().ToList();
            var items = ItemsUtils.GetItems(trackGUI.track).ToList();
            var addToSelection = !selection.SequenceEqual(items);

            foreach (var i in items)
            {
                if (addToSelection)
                    SelectionManager.Add(i);
                else
                    SelectionManager.Remove(i);
            }
        }

        void ContextClickOutsideItemsCallback()
        {
            SequencerContextMenu.ShowNewTracksContextMenu(null, m_State);
            Event.current.Use();
        }

        void ContextClickItemCallback(int id)
        {
            // may not occur if another menu is active
            if (!m_TreeView.IsSelected(id))
                SelectionChangedCallback(new[] {id});

            SequencerContextMenu.ShowTrackContextMenu(Event.current.mousePosition);

            Event.current.Use();
        }

        void SelectionChangedCallback(int[] ids)
        {
            if (Event.current.button == 1 && PickerUtils.TopmostPickedItem() is ISelectable)
                return;

            if (Event.current.command || Event.current.control || Event.current.shift)
                SelectionManager.UnSelectTracks();
            else
                SelectionManager.Clear();

            foreach (var id in ids)
            {
                var trackGUI = (TimelineTrackBaseGUI)m_TreeView.FindItem(id);
                SelectionManager.Add(trackGUI.track);
            }

            m_State.GetWindow().Repaint();
        }

        public void OnInitialize() {}

        public Rect GetRectForFraming(int row)
        {
            return GetRowRect(row, 1); // We ignore width by default when framing (only y scroll is affected)
        }

        protected virtual Vector2 GetSizeOfRow(TreeViewItem item)
        {
            if (item.displayName == "root")
                return new Vector2(m_TreeView.GetTotalRect().width, 0.0f);

            var trackGroupGui = item as TimelineGroupGUI;
            if (trackGroupGui != null)
            {
                return new Vector2(m_TreeView.GetTotalRect().width, trackGroupGui.GetHeight(m_State));
            }

            float height = TrackEditor.DefaultTrackHeight;
            if (item.hasChildren && m_TreeView.data.IsExpanded(item))
            {
                height = Mathf.Min(TrackEditor.DefaultTrackHeight, kMinTrackHeight);
            }

            return new Vector2(m_TreeView.GetTotalRect().width, height);
        }

        public virtual void BeginRowGUI()
        {
            if (m_TreeView.GetTotalRect().width != GetRowRect(0).width)
            {
                CalculateRowRects();
            }

            m_DraggingInsertionMarkerRect.x = -1;

            m_TreeView.SetSelection(SelectionManager.SelectedTrackGUI().Select(t => t.id).ToArray(), false);
        }

        public virtual void EndRowGUI()
        {
            // Draw row marker when dragging
            if (m_DraggingInsertionMarkerRect.x >= 0 && Event.current.type == EventType.Repaint)
            {
                Rect insertionRect = m_DraggingInsertionMarkerRect;
                const float insertionHeight = 1.0f;
                insertionRect.height = insertionHeight;

                if (m_TreeView.dragging.drawRowMarkerAbove)
                    insertionRect.y -= insertionHeight * 0.5f + 2.0f;
                else
                    insertionRect.y += m_DraggingInsertionMarkerRect.height - insertionHeight * 0.5f + 1.0f;

                EditorGUI.DrawRect(insertionRect, Color.white);
            }
        }

        public virtual void OnRowGUI(Rect rowRect, TreeViewItem item, int row, bool selected, bool focused)
        {
            using (new EditorGUI.DisabledScope(TimelineWindow.instance.currentMode.TrackState(TimelineWindow.instance.state) == TimelineModeGUIState.Disabled))
            {
                var sqvi = (TimelineTrackBaseGUI)item;
                sqvi.treeViewToWindowTransformation = m_TreeView.GetTotalRect().position - m_TreeView.state.scrollPos;

                // this may be called because an encompassing parent is visible
                if (!sqvi.visibleExpanded)
                    return;

                Rect headerRect = rowRect;
                Rect contentRect = rowRect;

                headerRect.width = m_State.sequencerHeaderWidth - 2.0f;
                contentRect.xMin += m_State.sequencerHeaderWidth;
                contentRect.width = rowRect.width - m_State.sequencerHeaderWidth - 1.0f;

                Rect foldoutRect = rowRect;

                var indent = GetFoldoutIndent(item);
                var headerRectWithIndent = headerRect;
                headerRectWithIndent.xMin = indent;
                var rowRectWithIndent = new Rect(rowRect.x + indent, rowRect.y, rowRect.width - indent, rowRect.height);
                sqvi.Draw(headerRectWithIndent, contentRect, m_State);
                sqvi.DrawInsertionMarkers(rowRectWithIndent);

                if (Event.current.type == EventType.Repaint)
                {
                    m_State.spacePartitioner.AddBounds(sqvi);

                    // Show marker below this Item
                    if (showInsertionMarker)
                    {
                        if (m_TreeView.dragging != null && m_TreeView.dragging.GetRowMarkerControlID() == TreeViewController.GetItemControlID(item))
                            m_DraggingInsertionMarkerRect = rowRectWithIndent;
                    }
                }

                // Draw foldout (after text content above to ensure drop down icon is rendered above selection highlight)
                DrawFoldout(item, foldoutRect, indent);

                sqvi.ClearDrawFlags();
            }
        }

        void DrawFoldout(TreeViewItem item, Rect foldoutRect, float indent)
        {
            var showFoldout = m_TreeView.data.IsExpandable(item);
            if (showFoldout)
            {
                foldoutRect.x = indent - kFoldOutOffset;
                foldoutRect.width = m_FoldoutWidth;
                EditorGUI.BeginChangeCheck();
                float foldoutIconHeight = DirectorStyles.Instance.foldout.fixedHeight;
                foldoutRect.y += foldoutIconHeight / 2.0f;
                foldoutRect.height = foldoutIconHeight;

                if (foldoutRect.xMax > m_State.sequencerHeaderWidth)
                    return;

                //Override Disable state for TrakGroup toggle button to expand/collapse group.
                bool previousEnableState = GUI.enabled;
                GUI.enabled = true;
                bool newExpandedValue = GUI.Toggle(foldoutRect, m_TreeView.data.IsExpanded(item), GUIContent.none, m_Styles.foldout);
                GUI.enabled = previousEnableState;

                if (EditorGUI.EndChangeCheck())
                {
                    if (Event.current.alt)
                        m_TreeView.data.SetExpandedWithChildren(item, newExpandedValue);
                    else
                        m_TreeView.data.SetExpanded(item, newExpandedValue);
                }
            }
        }

        public Rect GetRenameRect(Rect rowRect, int row, TreeViewItem item)
        {
            return rowRect;
        }

        public void BeginPingItem(TreeViewItem item, float topPixelOfRow, float availableWidth) {}
        public void EndPingItem() {}

        public Rect GetRowRect(int row,  float rowWidth)
        {
            return GetRowRect(row);
        }

        public Rect GetRowRect(int row)
        {
            if (m_RowRects.Count == 0)
                return new Rect();

            if (row >= m_RowRects.Count)
                return new Rect();

            return m_RowRects[row];
        }

        static float GetSpacing(TreeViewItem item)
        {
            var trackBase = item as TimelineTrackBaseGUI;
            if (trackBase != null)
                return trackBase.GetVerticalSpacingBetweenTracks();

            return 3.0f;
        }

        public void CalculateRowRects()
        {
            if (m_TreeView.isSearching)
                return;

            const float startY = 6.0f;
            IList<TreeViewItem> rows = m_TreeView.data.GetRows();
            m_RowRects = new List<Rect>(rows.Count);
            m_ExpandedRowRects = new List<Rect>(rows.Count);

            float curY = startY;
            m_MaxWidthOfRows = 1f;

            // first pass compute the row rects
            for (int i = 0; i < rows.Count; ++i)
            {
                var item = rows[i];

                if (i != 0)
                    curY += GetSpacing(item);

                Vector2 rowSize = GetSizeOfRow(item);
                m_RowRects.Add(new Rect(0, curY, rowSize.x, rowSize.y));
                m_ExpandedRowRects.Add(m_RowRects[i]);

                curY += rowSize.y;

                if (rowSize.x > m_MaxWidthOfRows)
                    m_MaxWidthOfRows = rowSize.x;

                // updated the expanded state
                var groupGUI = item as TimelineGroupGUI;
                if (groupGUI != null)
                    groupGUI.SetExpanded(m_TreeView.data.IsExpanded(item));
            }

            float halfHeight = halfDropBetweenHeight;
            const float kGroupPad = 1.0f;
            const float kSkinPadding = 5.0f * 0.6f;
            // work bottom up and compute visible regions for groups
            for (int i = rows.Count - 1; i > 0; i--)
            {
                float height = 0;
                TimelineTrackBaseGUI item = (TimelineTrackBaseGUI)rows[i];
                if (item.isExpanded && item.children != null && item.children.Count > 0)
                {
                    for (var j = 0; j < item.children.Count; j++)
                    {
                        var child = item.children[j];
                        int index = rows.IndexOf(child);
                        if (index > i)
                            height += m_ExpandedRowRects[index].height + kSkinPadding;
                    }

                    height += kGroupPad;
                }
                m_ExpandedRowRects[i] = new Rect(m_RowRects[i].x, m_RowRects[i].y, m_RowRects[i].width, m_RowRects[i].height + height);

                var groupGUI = item as TimelineGroupGUI;
                if (groupGUI != null)
                {
                    var spacing = GetSpacing(item) + 1;
                    groupGUI.expandedRect = m_ExpandedRowRects[i];
                    groupGUI.rowRect = m_RowRects[i];
                    groupGUI.dropRect = new Rect(m_RowRects[i].x, m_RowRects[i].y - spacing, m_RowRects[i].width, m_RowRects[i].height + Mathf.Max(halfHeight, spacing));
                }
            }
        }

        public virtual bool BeginRename(TreeViewItem item, float delay)
        {
            return false;
        }

        public virtual void EndRename() {}

        protected virtual float GetFoldoutIndent(TreeViewItem item)
        {
            // Ignore depth when showing search results
            if (item.depth <= 1 || m_TreeView.isSearching)
                return DirectorStyles.kBaseIndent;

            int depth = item.depth;
            var trackGUI = item as TimelineTrackGUI;

            // first level subtracks are not indented
            if (trackGUI != null && trackGUI.track != null && trackGUI.track.isSubTrack)
                depth--;

            return depth * DirectorStyles.kBaseIndent;
        }

        public virtual float GetContentIndent(TreeViewItem item)
        {
            return GetFoldoutIndent(item);
        }

        public int GetNumRowsOnPageUpDown(TreeViewItem fromItem, bool pageUp, float heightOfTreeView)
        {
            return (int)Mathf.Floor(heightOfTreeView / 30); // return something
        }

        // Should return the row number of the first and last row thats fits in the pixel rect defined by top and height
        public void GetFirstAndLastRowVisible(out int firstRowVisible, out int lastRowVisible)
        {
            int rowCount = m_TreeView.data.rowCount;
            if (rowCount == 0)
            {
                firstRowVisible = lastRowVisible = -1;
                return;
            }

            if (rowCount != m_ExpandedRowRects.Count)
            {
                Debug.LogError("Mismatch in state: rows vs cached rects. Did you remember to hook up: dataSource.onVisibleRowsChanged += gui.CalculateRowRects ?");
                CalculateRowRects();
            }

            float topPixel = m_TreeView.state.scrollPos.y;
            float heightInPixels = m_TreeView.GetTotalRect().height;

            int firstVisible = -1;
            int lastVisible = -1;

            Rect visibleRect = new Rect(0, topPixel, m_ExpandedRowRects[0].width, heightInPixels);
            for (int i = 0; i < m_ExpandedRowRects.Count; ++i)
            {
                bool visible = visibleRect.Overlaps(m_ExpandedRowRects[i]);
                if (visible)
                {
                    if (firstVisible == -1)
                        firstVisible = i;
                    lastVisible = i;
                }

                TimelineTrackBaseGUI gui = m_TreeView.data.GetItem(i) as TimelineTrackBaseGUI;
                if (gui != null)
                {
                    gui.visibleExpanded = visible;
                    gui.visibleRow = visibleRect.Overlaps(m_RowRects[i]);
                }
            }

            if (firstVisible != -1 && lastVisible != -1)
            {
                firstRowVisible = firstVisible;
                lastRowVisible = lastVisible;
            }
            else
            {
                firstRowVisible = 0;
                lastRowVisible = rowCount - 1;
            }
        }

        public Vector2 GetTotalSize()
        {
            if (m_RowRects.Count == 0)
                return new Vector2(0, 0);

            return new Vector2(m_MaxWidthOfRows, m_RowRects[m_RowRects.Count - 1].yMax);
        }

        public virtual float halfDropBetweenHeight
        {
            get { return 8f; }
        }
    }
}
