using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Timeline;
using UnityEngine;
using UnityEngine.Timeline;
using Graphics = UnityEditor.Timeline.Graphics;

namespace UnityEditor
{
    class ClipCurveEditor
    {
        internal readonly CurveEditor m_CurveEditor;
        static readonly CurveEditorSettings s_CurveEditorSettings = new CurveEditorSettings();

        static readonly float s_GridLabelWidth = 40.0f;

        readonly BindingSelector m_BindingHierarchy;
        public BindingSelector bindingHierarchy
        {
            get { return m_BindingHierarchy; }
        }

        public Rect shownAreaInsideMargins
        {
            get { return m_CurveEditor != null ? m_CurveEditor.shownAreaInsideMargins : new Rect(1, 1, 1, 1); }
        }

        Vector2 m_ScrollPosition = Vector2.zero;

        readonly CurveDataSource m_DataSource;

        float m_LastFrameRate = 30.0f;
        int m_LastClipVersion = -1;
        int m_LastCurveCount = -1;
        TrackViewModelData m_ViewModel;

        bool isNewSelection
        {
            get
            {
                if (m_ViewModel == null || m_DataSource == null)
                    return true;

                return m_ViewModel.lastInlineCurveDataID != m_DataSource.id;
            }
        }

        internal CurveEditor curveEditor
        {
            get { return m_CurveEditor; }
        }

        public ClipCurveEditor(CurveDataSource dataSource, TimelineWindow parentWindow, TrackAsset hostTrack)
        {
            m_DataSource = dataSource;

            m_CurveEditor = new CurveEditor(new Rect(0, 0, 1000, 100), new CurveWrapper[0], false);

            s_CurveEditorSettings.hSlider = false;
            s_CurveEditorSettings.vSlider = false;
            s_CurveEditorSettings.hRangeLocked = false;
            s_CurveEditorSettings.vRangeLocked = false;
            s_CurveEditorSettings.scaleWithWindow = true;
            s_CurveEditorSettings.hRangeMin = 0.0f;
            s_CurveEditorSettings.showAxisLabels = true;
            s_CurveEditorSettings.allowDeleteLastKeyInCurve = true;
            s_CurveEditorSettings.rectangleToolFlags = CurveEditorSettings.RectangleToolFlags.MiniRectangleTool;

            s_CurveEditorSettings.vTickStyle = new TickStyle
            {
                tickColor = { color = DirectorStyles.Instance.customSkin.colorInlineCurveVerticalLines },
                distLabel = 20,
                stubs = true
            };

            s_CurveEditorSettings.hTickStyle = new TickStyle
            {
                // hide horizontal lines by giving them a transparent color
                tickColor = { color = new Color(0.0f, 0.0f, 0.0f, 0.0f) },
                distLabel = 0
            };

            m_CurveEditor.settings = s_CurveEditorSettings;

            m_ViewModel = TimelineWindowViewPrefs.GetTrackViewModelData(hostTrack);

            if (isNewSelection)
                m_CurveEditor.shownArea = new Rect(1, 1, 1, 1);
            else
                m_CurveEditor.shownAreaInsideMargins = m_ViewModel.inlineCurvesShownAreaInsideMargins;

            m_CurveEditor.ignoreScrollWheelUntilClicked = true;
            m_CurveEditor.curvesUpdated = OnCurvesUpdated;

            m_BindingHierarchy = new BindingSelector(parentWindow, m_CurveEditor, m_ViewModel.inlineCurvesState);
        }

        public void SelectAllKeys()
        {
            m_CurveEditor.SelectAll();
        }

        public void FrameClip()
        {
            m_CurveEditor.InvalidateBounds();
            m_CurveEditor.FrameClip(false, true);
        }

        public bool HasSelection()
        {
            return m_CurveEditor.hasSelection;
        }

        public Vector2 GetSelectionRange()
        {
            Bounds b = m_CurveEditor.selectionBounds;
            return new Vector2(b.min.x, b.max.x);
        }

        public CurveDataSource dataSource
        {
            get { return m_DataSource; }
        }

        internal void OnCurvesUpdated()
        {
            if (m_DataSource == null)
                return;

            if (m_CurveEditor == null)
                return;

            if (m_CurveEditor.animationCurves.Length == 0)
                return;

            List<CurveWrapper> curvesToUpdate = m_CurveEditor.animationCurves.Where(c => c.changed).ToList();

            // nothing changed, return.
            if (curvesToUpdate.Count == 0)
                return;

            AnimationClip clip = m_DataSource.animationClip;

            // something changed, manage the undo properly.
            Undo.RegisterCompleteObjectUndo(clip, "Edit Clip Curve");

            foreach (CurveWrapper c in curvesToUpdate)
            {
                AnimationUtility.SetEditorCurve(clip, c.binding, c.curve);
                c.changed = false;
            }

            m_DataSource.UpdateCurves(curvesToUpdate);
        }

        public void DrawHeader(Rect headerRect)
        {
            m_BindingHierarchy.InitIfNeeded(headerRect, m_DataSource, isNewSelection);

            try
            {
                GUILayout.BeginArea(headerRect);
                m_ScrollPosition = GUILayout.BeginScrollView(m_ScrollPosition, GUIStyle.none, GUI.skin.verticalScrollbar);
                m_BindingHierarchy.OnGUI(new Rect(0, 0, headerRect.width, headerRect.height));
                GUILayout.EndScrollView();
                GUILayout.EndArea();
            }
            catch (Exception e)
            {
                Debug.LogException(e);
            }
        }

        class FrameFormatCurveEditorState : ICurveEditorState
        {
            public TimeArea.TimeFormat timeFormat
            {
                get { return TimeArea.TimeFormat.Frame; }
            }
            public Vector2 timeRange { get { return new Vector2(0, 1); } }
            public bool rippleTime { get { return false; } }
        }

        class UnformattedCurveEditorState : ICurveEditorState
        {
            public TimeArea.TimeFormat timeFormat
            {
                get { return TimeArea.TimeFormat.None; }
            }
            public Vector2 timeRange { get { return new Vector2(0, 1); } }
            public bool rippleTime { get { return false; } }
        }

        void UpdateCurveEditorIfNeeded(WindowState state)
        {
            if ((Event.current.type != EventType.Layout) || (m_DataSource == null) || (m_BindingHierarchy == null) || (m_DataSource.animationClip == null))
                return;

            AnimationClipCurveInfo curveInfo = AnimationClipCurveCache.Instance.GetCurveInfo(m_DataSource.animationClip);
            int version = curveInfo.version;
            if (version != m_LastClipVersion)
            {
                // tree has changed
                if (m_LastCurveCount != curveInfo.curves.Length)
                {
                    m_BindingHierarchy.RefreshTree();
                    m_LastCurveCount = curveInfo.curves.Length;
                }
                else
                {
                    // update just the curves
                    m_BindingHierarchy.RefreshCurves();
                }

                if (isNewSelection)
                    FrameClip();

                m_LastClipVersion = version;
            }

            if (state.timeInFrames)
                m_CurveEditor.state = new FrameFormatCurveEditorState();
            else
                m_CurveEditor.state = new UnformattedCurveEditorState();

            m_CurveEditor.invSnap = state.referenceSequence.frameRate;
        }

        public void DrawCurveEditor(Rect animEditorRect, WindowState state, Vector2 activeRange, bool loop, bool selected)
        {
            var curveStart = state.TimeToPixel(m_DataSource.start);
            var minCurveStart = animEditorRect.xMax - 1.0f;

            if (curveStart > minCurveStart) // Prevent the curve from drawing inside small rect
                animEditorRect.xMax += curveStart - minCurveStart;

            UpdateCurveEditorIfNeeded(state);

            DrawCurveEditorBackground(animEditorRect);

            // adjust the top margin so smaller rectangle have smaller top / bottom margins.
            m_CurveEditor.topmargin = m_CurveEditor.bottommargin = CalculateTopMargin(animEditorRect.height);

            //align the curve with the clip.
            var localCurveStart = curveStart - animEditorRect.xMin;
            m_CurveEditor.leftmargin = 0.0f;
            m_CurveEditor.rect = new Rect(localCurveStart, 0.0f, animEditorRect.width - localCurveStart, animEditorRect.height);
            m_CurveEditor.SetShownHRangeInsideMargins(0.0f, (state.PixelToTime(animEditorRect.xMax) - m_DataSource.start) * m_DataSource.timeScale);

            if (m_LastFrameRate != state.referenceSequence.frameRate)
            {
                m_CurveEditor.hTicks.SetTickModulosForFrameRate(state.referenceSequence.frameRate);
                m_LastFrameRate = state.referenceSequence.frameRate;
            }

            foreach (CurveWrapper cw in m_CurveEditor.animationCurves)
                cw.renderer.SetWrap(WrapMode.Default, loop ? WrapMode.Loop : WrapMode.Default);

            m_CurveEditor.BeginViewGUI();

            Color oldColor = GUI.color;
            GUI.color = Color.white;

            GUI.BeginGroup(animEditorRect);

            // Draw a line at 0
            Graphics.DrawLine(new Vector2(localCurveStart, 0.0f), new Vector2(localCurveStart, animEditorRect.height), new Color(1.0f, 1.0f, 1.0f, 0.5f));

            float rangeStart = activeRange.x - animEditorRect.x;
            float rangeWidth = activeRange.y - activeRange.x;

            // draw selection outline underneath the curves.
            if (selected)
            {
                var selectionRect = new Rect(rangeStart, 0.0f, rangeWidth, animEditorRect.height);
                DrawOutline(selectionRect);
            }

            EditorGUI.BeginChangeCheck();

            Event evt = Event.current;
            if ((evt.type == EventType.Layout) || (evt.type == EventType.Repaint) || selected)
                m_CurveEditor.CurveGUI();

            m_CurveEditor.EndViewGUI();

            if (EditorGUI.EndChangeCheck())
                OnCurvesUpdated();

            // draw overlays on top of curves
            var overlayColor = DirectorStyles.Instance.customSkin.colorInlineCurveOutOfRangeOverlay;

            var leftSide = new Rect(localCurveStart, 0.0f, rangeStart - localCurveStart, animEditorRect.height);
            EditorGUI.DrawRect(leftSide, overlayColor);

            var rightSide = new Rect(rangeStart + rangeWidth, 0.0f, animEditorRect.width - rangeStart - rangeWidth, animEditorRect.height);
            EditorGUI.DrawRect(rightSide, overlayColor);

            GUI.color = oldColor;

            GUI.EndGroup();

            // draw the grid labels last
            var gridRect = animEditorRect;
            gridRect.width = s_GridLabelWidth;
            var offset = localCurveStart - s_GridLabelWidth;
            if (offset > 0.0f)
                gridRect.x = animEditorRect.x + offset;
            m_CurveEditor.rect = new Rect(0.0f, 0.0f, animEditorRect.width, animEditorRect.height);

            GUI.BeginGroup(gridRect);
            m_CurveEditor.GridGUI();
            GUI.EndGroup();
        }

        static void DrawCurveEditorBackground(Rect animEditorRect)
        {
            if (EditorGUIUtility.isProSkin)
                return;

            var animEditorBackgroundRect = Rect.MinMaxRect(
                0.0f, animEditorRect.yMin, animEditorRect.xMax, animEditorRect.yMax);

            // Curves are not legible in Personal Skin so we need to darken the background a bit.
            EditorGUI.DrawRect(animEditorBackgroundRect, DirectorStyles.Instance.customSkin.colorInlineCurvesBackground);
        }

        float CalculateTopMargin(float height)
        {
            return Mathf.Clamp(0.15f * height, 10.0f, 40.0f);
        }

        // todo move this in a utility class?
        static void DrawOutline(Rect rect, float thickness = 2.0f)
        {
            // Draw top selected lines.
            EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMin, rect.width, thickness), Color.white);

            // Draw bottom selected lines.
            EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMax - thickness, rect.width, thickness), Color.white);

            // Draw Left Selected Lines
            EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMin, thickness, rect.height), Color.white);

            // Draw Right Selected Lines
            EditorGUI.DrawRect(new Rect(rect.xMax - thickness, rect.yMin, thickness, rect.height), Color.white);
        }
    }
}
