using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

namespace UnityEditor.Timeline
{
    class TimelineClipGUI : TimelineItemGUI, IClipCurveEditorOwner, ISnappable, IAttractable
    {
        EditorClip m_EditorItem;

        Rect m_ClipCenterSection;
        readonly List<Rect> m_LoopRects = new List<Rect>();

        ClipDrawData m_ClipDrawData;
        Rect m_MixOutRect = new Rect();
        Rect m_MixInRect = new Rect();
        int m_MinLoopIndex = 1;

        // clip dirty detection
        int m_LastDirtyIndex = Int32.MinValue;
        bool m_ClipViewDirty = true;

        bool supportResize { get; }
        public ClipCurveEditor clipCurveEditor { get; set; }
        public TimelineClipGUI previousClip { get; set; }
        public TimelineClipGUI nextClip { get; set; }

        static readonly float k_MinMixWidth = 2;
        static readonly float k_MaxHandleWidth = 10f;
        static readonly float k_MinHandleWidth = 1f;

        bool? m_ShowDrillIcon;
        ClipEditor m_ClipEditor;

        static List<PlayableDirector> s_TempSubDirectors = new List<PlayableDirector>();

        static readonly IconData k_DiggableClipIcon = new IconData(DirectorStyles.LoadIcon("TimelineDigIn"));

        string name
        {
            get
            {
                if (string.IsNullOrEmpty(clip.displayName))
                    return "(Empty)";

                return clip.displayName;
            }
        }

        public bool inlineCurvesSelected
        {
            get { return SelectionManager.IsCurveEditorFocused(this); }
            set
            {
                if (!value && SelectionManager.IsCurveEditorFocused(this))
                    SelectionManager.SelectInlineCurveEditor(null);
                else
                    SelectionManager.SelectInlineCurveEditor(this);
            }
        }

        public Rect mixOutRect
        {
            get
            {
                var percent = clip.mixOutPercentage;
                var x = Mathf.Round(treeViewRect.width * (1 - percent));
                var width = Mathf.Round(treeViewRect.width * percent);
                m_MixOutRect.Set(x, 0.0f, width, treeViewRect.height);
                return m_MixOutRect;
            }
        }

        public Rect mixInRect
        {
            get
            {
                var width = Mathf.Round(treeViewRect.width * clip.mixInPercentage);
                m_MixInRect.Set(0.0f, 0.0f, width, treeViewRect.height);
                return m_MixInRect;
            }
        }

        public ClipBlends GetClipBlends()
        {
            var _mixInRect = mixInRect;
            var _mixOutRect = mixOutRect;

            var blendInKind = BlendKind.None;
            if (_mixInRect.width > k_MinMixWidth && clip.hasBlendIn)
                blendInKind = BlendKind.Mix;
            else if (_mixInRect.width > k_MinMixWidth)
                blendInKind = BlendKind.Ease;

            var blendOutKind = BlendKind.None;
            if (_mixOutRect.width > k_MinMixWidth && clip.hasBlendOut)
                blendOutKind = BlendKind.Mix;
            else if (_mixOutRect.width > k_MinMixWidth)
                blendOutKind = BlendKind.Ease;

            return new ClipBlends(blendInKind, _mixInRect, blendOutKind, _mixOutRect);
        }

        public override double start
        {
            get { return clip.start; }
        }

        public override double end
        {
            get { return clip.end; }
        }

        public bool supportsLooping
        {
            get { return clip.SupportsLooping(); }
        }

        // for the inline curve editor, only show loops if we recorded the asset
        bool IClipCurveEditorOwner.showLoops
        {
            get { return clip.SupportsLooping() && (clip.asset is AnimationPlayableAsset);  }
        }

        TrackAsset IClipCurveEditorOwner.owner
        {
            get { return clip.parentTrack; }
        }

        public bool supportsSubTimelines
        {
            get { return m_ClipEditor.supportsSubTimelines; }
        }


        public int minLoopIndex
        {
            get { return m_MinLoopIndex; }
        }

        public TrackDrawer drawer
        {
            get { return ((TimelineTrackGUI)parent).drawer; }
        }

        public Rect clippedRect { get; private set; }

        public override void Select()
        {
            zOrder = zOrderProvider.Next();
            SelectionManager.Add(clip);
        }

        public override bool IsSelected()
        {
            return SelectionManager.Contains(clip);
        }

        public override void Deselect()
        {
            SelectionManager.Remove(clip);
        }

        public override ITimelineItem item
        {
            get { return ItemsUtils.ToItem(clip); }
        }

        IZOrderProvider zOrderProvider { get; }

        public TimelineClipHandle leftHandle { get; }
        public TimelineClipHandle rightHandle { get; }

        public TimelineClipGUI(TimelineClip clip, IRowGUI parent, IZOrderProvider provider) : base(parent)
        {
            zOrderProvider = provider;
            zOrder = provider.Next();

            m_EditorItem = EditorClipFactory.GetEditorClip(clip);
            m_ClipEditor = CustomTimelineEditorCache.GetClipEditor(clip);

            supportResize = true;

            leftHandle = new TimelineClipHandle(this, TrimEdge.Start);
            rightHandle = new TimelineClipHandle(this, TrimEdge.End);

            ItemToItemGui.Add(clip, this);
        }

        void CreateInlineCurveEditor(WindowState state)
        {
            if (clipCurveEditor != null)
                return;

            var animationClip = clip.animationClip;

            if (animationClip != null && animationClip.empty)
                animationClip = null;

            // prune out clips coming from FBX
            if (animationClip != null && !clip.recordable)
                return; // don't show, even if there are curves

            if (animationClip == null && !clip.HasAnyAnimatableParameters())
                return; // nothing to show

            state.AddEndFrameDelegate((istate, currentEvent) =>
            {
                clipCurveEditor = new ClipCurveEditor(CurveDataSource.Create(this), TimelineWindow.instance, clip.parentTrack);
                return true;
            });
        }

        public TimelineClip clip
        {
            get { return m_EditorItem.clip; }
        }

        // Draw the actual clip. Defers to the track drawer for customization
        void UpdateDrawData(WindowState state, Rect drawRect, string title, bool selected, bool previousClipSelected, float rectXOffset)
        {
            m_ClipDrawData.clip = clip;
            m_ClipDrawData.targetRect = drawRect;
            m_ClipDrawData.clipCenterSection = m_ClipCenterSection;
            m_ClipDrawData.unclippedRect = treeViewRect;
            m_ClipDrawData.title = title;
            m_ClipDrawData.selected = selected;
            m_ClipDrawData.inlineCurvesSelected = inlineCurvesSelected;
            m_ClipDrawData.previousClip = previousClip != null ? previousClip.clip : null;
            m_ClipDrawData.previousClipSelected = previousClipSelected;

            Vector3 shownAreaTime = state.timeAreaShownRange;
            m_ClipDrawData.localVisibleStartTime = clip.ToLocalTimeUnbound(Math.Max(clip.start, shownAreaTime.x));
            m_ClipDrawData.localVisibleEndTime = clip.ToLocalTimeUnbound(Math.Min(clip.end, shownAreaTime.y));

            m_ClipDrawData.clippedRect = new Rect(clippedRect.x - rectXOffset, 0.0f, clippedRect.width, clippedRect.height);

            m_ClipDrawData.minLoopIndex = minLoopIndex;
            m_ClipDrawData.loopRects = m_LoopRects;
            m_ClipDrawData.supportsLooping = supportsLooping;
            m_ClipDrawData.clipBlends = GetClipBlends();
            m_ClipDrawData.clipEditor = m_ClipEditor;
            m_ClipDrawData.ClipDrawOptions = UpdateClipDrawOptions(m_ClipEditor, clip);

            UpdateClipIcons(state);
        }

        void UpdateClipIcons(WindowState state)
        {
            // Pass 1 - gather size
            int required = 0;
            bool requiresDigIn = ShowDrillIcon(state.editSequence.director);
            if (requiresDigIn)
                required++;

            var icons = m_ClipDrawData.ClipDrawOptions.icons;
            foreach (var icon in icons)
            {
                if (icon != null)
                    required++;
            }

            // Pass 2 - copy icon data
            if (required == 0)
            {
                m_ClipDrawData.rightIcons = null;
                return;
            }

            if (m_ClipDrawData.rightIcons == null || m_ClipDrawData.rightIcons.Length != required)
                m_ClipDrawData.rightIcons = new IconData[required];

            int index = 0;
            if (requiresDigIn)
                m_ClipDrawData.rightIcons[index++] = k_DiggableClipIcon;

            foreach (var icon in icons)
            {
                if (icon != null)
                    m_ClipDrawData.rightIcons[index++] = new IconData(icon);
            }
        }

        static ClipDrawOptions UpdateClipDrawOptions(ClipEditor clipEditor, TimelineClip clip)
        {
            try
            {
                return clipEditor.GetClipOptions(clip);
            }
            catch (Exception e)
            {
                Debug.LogException(e);
            }

            return CustomTimelineEditorCache.GetDefaultClipEditor().GetClipOptions(clip);
        }

        static void DrawClip(ClipDrawData drawData)
        {
            ClipDrawer.DrawDefaultClip(drawData);

            if (drawData.clip.asset is AnimationPlayableAsset)
            {
                var state = TimelineWindow.instance.state;
                if (state.recording && state.IsArmedForRecord(drawData.clip.parentTrack))
                {
                    ClipDrawer.DrawAnimationRecordBorder(drawData);
                    ClipDrawer.DrawRecordProhibited(drawData);
                }
            }
        }

        public void DrawGhostClip(Rect targetRect)
        {
            DrawSimpleClip(targetRect, ClipBorder.Selection(), new Color(1.0f, 1.0f, 1.0f, 0.5f));
        }

        public void DrawInvalidClip(Rect targetRect)
        {
            DrawSimpleClip(targetRect, ClipBorder.Selection(), DirectorStyles.Instance.customSkin.colorInvalidClipOverlay);
        }

        void DrawSimpleClip(Rect targetRect, ClipBorder border, Color overlay)
        {
            var drawOptions = UpdateClipDrawOptions(CustomTimelineEditorCache.GetClipEditor(clip), clip);
            ClipDrawer.DrawSimpleClip(clip, targetRect, border, overlay, drawOptions);
        }

        void DrawInto(Rect drawRect, WindowState state)
        {
            if (Event.current.type != EventType.Repaint)
                return;

            // create the inline curve editor if not already created
            CreateInlineCurveEditor(state);

            // @todo optimization, most of the calculations (rect, offsets, colors, etc.) could be cached
            // and rebuilt when the hash of the clip changes.

            if (isInvalid)
            {
                DrawInvalidClip(treeViewRect);
                return;
            }

            GUI.BeginClip(drawRect);

            var originRect = new Rect(0.0f, 0.0f, drawRect.width, drawRect.height);
            string clipLabel = name;
            var selected = SelectionManager.Contains(clip);
            var previousClipSelected = previousClip != null && SelectionManager.Contains(previousClip.clip);

            if (selected && 1.0 != clip.timeScale)
                clipLabel += " " + clip.timeScale.ToString("F2") + "x";

            UpdateDrawData(state, originRect, clipLabel, selected, previousClipSelected, drawRect.x);
            DrawClip(m_ClipDrawData);

            GUI.EndClip();

            if (clip.parentTrack != null && !clip.parentTrack.lockedInHierarchy)
            {
                if (selected && supportResize)
                {
                    var cursorRect = rect;
                    cursorRect.xMin += leftHandle.boundingRect.width;
                    cursorRect.xMax -= rightHandle.boundingRect.width;
                    EditorGUIUtility.AddCursorRect(cursorRect, MouseCursor.MoveArrow);
                }

                if (supportResize)
                {
                    var handleWidth = Mathf.Clamp(drawRect.width * 0.3f, k_MinHandleWidth, k_MaxHandleWidth);

                    leftHandle.Draw(drawRect, handleWidth, state);
                    rightHandle.Draw(drawRect, handleWidth, state);
                }
            }
        }

        void CalculateClipRectangle(Rect trackRect, WindowState state)
        {
            if (m_ClipViewDirty)
            {
                var clipRect = RectToTimeline(trackRect, state);
                treeViewRect = clipRect;

                // calculate clipped rect
                clipRect.xMin = Mathf.Max(clipRect.xMin, trackRect.xMin);
                clipRect.xMax = Mathf.Min(clipRect.xMax, trackRect.xMax);

                if (clipRect.width > 0 && clipRect.width < 2)
                {
                    clipRect.width = 5.0f;
                }

                clippedRect = clipRect;
            }
        }

        void AddToSpacePartitioner(WindowState state)
        {
            if (Event.current.type == EventType.Repaint && !parent.locked)
                state.spacePartitioner.AddBounds(this, rect);
        }

        void CalculateBlendRect()
        {
            m_ClipCenterSection = treeViewRect;
            m_ClipCenterSection.x = 0;
            m_ClipCenterSection.y = 0;

            m_ClipCenterSection.xMin = Mathf.Round(treeViewRect.width * clip.mixInPercentage);
            m_ClipCenterSection.width = Mathf.Round(treeViewRect.width);
            m_ClipCenterSection.xMax -= Mathf.Round(mixOutRect.width + treeViewRect.width * clip.mixInPercentage);
        }

        // Entry point to the Clip Drawing...
        public override void Draw(Rect trackRect, bool trackRectChanged, WindowState state)
        {
            // if the clip has changed, fire the appropriate callback
            DetectClipChanged(trackRectChanged);

            // update the clip projected rectangle on the timeline
            CalculateClipRectangle(trackRect, state);

            AddToSpacePartitioner(state);

            // update the blend rects (when clip overlaps with others)
            CalculateBlendRect();

            // update the loop rects (when clip loops)
            CalculateLoopRects(trackRect, state);

            DrawExtrapolation(trackRect, treeViewRect);

            DrawInto(treeViewRect, state);

            ResetClipChanged();
        }

        void DetectClipChanged(bool trackRectChanged)
        {
            if (Event.current.type == EventType.Layout)
            {
                if (clip.DirtyIndex != m_LastDirtyIndex)
                {
                    m_ClipViewDirty = true;

                    try
                    {
                        m_ClipEditor.OnClipChanged(clip);
                    }
                    catch (Exception e)
                    {
                        Debug.LogException(e);
                    }

                    m_LastDirtyIndex = clip.DirtyIndex;
                }
                m_ClipViewDirty |= trackRectChanged;
            }
        }

        void ResetClipChanged()
        {
            if (Event.current.type == EventType.Repaint)
                m_ClipViewDirty = false;
        }

        GUIStyle GetExtrapolationIcon(TimelineClip.ClipExtrapolation mode)
        {
            GUIStyle extrapolationIcon = null;

            switch (mode)
            {
                case TimelineClip.ClipExtrapolation.None: return null;
                case TimelineClip.ClipExtrapolation.Hold: extrapolationIcon = m_Styles.extrapolationHold; break;
                case TimelineClip.ClipExtrapolation.Loop: extrapolationIcon = m_Styles.extrapolationLoop; break;
                case TimelineClip.ClipExtrapolation.PingPong: extrapolationIcon = m_Styles.extrapolationPingPong; break;
                case TimelineClip.ClipExtrapolation.Continue: extrapolationIcon = m_Styles.extrapolationContinue; break;
            }

            return extrapolationIcon;
        }

        Rect GetPreExtrapolationBounds(Rect trackRect, Rect clipRect, GUIStyle icon)
        {
            float x = clipRect.xMin - (icon.fixedWidth + 10.0f);
            float y = trackRect.yMin + (trackRect.height - icon.fixedHeight) / 2.0f;

            if (previousClip != null)
            {
                float distance = Mathf.Abs(treeViewRect.xMin - previousClip.treeViewRect.xMax);

                if (distance < icon.fixedWidth)
                    return new Rect(0.0f, 0.0f, 0.0f, 0.0f);

                if (distance < icon.fixedWidth + 20.0f)
                {
                    float delta = (distance - icon.fixedWidth) / 2.0f;
                    x = clipRect.xMin - (icon.fixedWidth + delta);
                }
            }

            return new Rect(x, y, icon.fixedWidth, icon.fixedHeight);
        }

        Rect GetPostExtrapolationBounds(Rect trackRect, Rect clipRect, GUIStyle icon)
        {
            float x = clipRect.xMax + 10.0f;
            float y = trackRect.yMin + (trackRect.height - icon.fixedHeight) / 2.0f;

            if (nextClip != null)
            {
                float distance = Mathf.Abs(nextClip.treeViewRect.xMin - treeViewRect.xMax);

                if (distance < icon.fixedWidth)
                    return new Rect(0.0f, 0.0f, 0.0f, 0.0f);

                if (distance < icon.fixedWidth + 20.0f)
                {
                    float delta = (distance - icon.fixedWidth) / 2.0f;
                    x = clipRect.xMax + delta;
                }
            }

            return new Rect(x, y, icon.fixedWidth, icon.fixedHeight);
        }

        static void DrawExtrapolationIcon(Rect rect, GUIStyle icon)
        {
            GUI.Label(rect, GUIContent.none, icon);
        }

        void DrawExtrapolation(Rect trackRect, Rect clipRect)
        {
            if (clip.hasPreExtrapolation)
            {
                GUIStyle icon = GetExtrapolationIcon(clip.preExtrapolationMode);

                if (icon != null)
                {
                    Rect iconBounds = GetPreExtrapolationBounds(trackRect, clipRect, icon);

                    if (iconBounds.width > 1 && iconBounds.height > 1)
                        DrawExtrapolationIcon(iconBounds, icon);
                }
            }

            if (clip.hasPostExtrapolation)
            {
                GUIStyle icon = GetExtrapolationIcon(clip.postExtrapolationMode);

                if (icon != null)
                {
                    Rect iconBounds = GetPostExtrapolationBounds(trackRect, clipRect, icon);

                    if (iconBounds.width > 1 && iconBounds.height > 1)
                        DrawExtrapolationIcon(iconBounds, icon);
                }
            }
        }

        static Rect ProjectRectOnTimeline(Rect rect, Rect trackRect, WindowState state)
        {
            Rect newRect = rect;
            // transform clipRect into pixel-space
            newRect.x *= state.timeAreaScale.x;
            newRect.width *= state.timeAreaScale.x;

            newRect.x += state.timeAreaTranslation.x + trackRect.xMin;

            // adjust clipRect height and vertical centering
            const int clipPadding = 2;
            newRect.y = trackRect.y + clipPadding;
            newRect.height = trackRect.height - (2 * clipPadding);
            return newRect;
        }

        void CalculateLoopRects(Rect trackRect, WindowState state)
        {
            if (!m_ClipViewDirty)
                return;

            m_LoopRects.Clear();
            if (clip.duration < WindowState.kTimeEpsilon)
                return;

            var times = TimelineHelpers.GetLoopTimes(clip);
            var loopDuration = TimelineHelpers.GetLoopDuration(clip);
            m_MinLoopIndex = -1;

            // we have a hold, no need to compute all loops
            if (!supportsLooping)
            {
                if (times.Length > 1)
                {
                    var t = times[1];
                    float loopTime = (float)(clip.duration - t);
                    m_LoopRects.Add(ProjectRectOnTimeline(new Rect((float)(t + clip.start), 0, loopTime, 0), trackRect, state));
                }
                return;
            }

            var range = state.timeAreaShownRange;
            var visibleStartTime = range.x - clip.start;
            var visibleEndTime = range.y - clip.start;

            for (int i = 1; i < times.Length; i++)
            {
                var t = times[i];

                // don't draw off screen loops
                if (t > visibleEndTime)
                    break;

                float loopTime = Mathf.Min((float)(clip.duration - t), (float)loopDuration);
                var loopEnd = t + loopTime;

                if (loopEnd < visibleStartTime)
                    continue;

                m_LoopRects.Add(ProjectRectOnTimeline(new Rect((float)(t + clip.start), 0, loopTime, 0), trackRect, state));

                if (m_MinLoopIndex == -1)
                    m_MinLoopIndex = i;
            }
        }

        public override Rect RectToTimeline(Rect trackRect, WindowState state)
        {
            var offsetFromTimeSpaceToPixelSpace = state.timeAreaTranslation.x + trackRect.xMin;

            var start = (float)(DiscreteTime)clip.start;
            var end = (float)(DiscreteTime)clip.end;

            return Rect.MinMaxRect(
                Mathf.Round(start * state.timeAreaScale.x + offsetFromTimeSpaceToPixelSpace), Mathf.Round(trackRect.yMin),
                Mathf.Round(end * state.timeAreaScale.x + offsetFromTimeSpaceToPixelSpace), Mathf.Round(trackRect.yMax)
            );
        }

        public IEnumerable<Edge> SnappableEdgesFor(IAttractable attractable, ManipulateEdges manipulateEdges)
        {
            var edges = new List<Edge>();

            bool canAddEdges = !parent.muted;

            if (canAddEdges)
            {
                // Hack: Trim Start in Ripple mode should not have any snap point added
                if (EditMode.editType == EditMode.EditType.Ripple && manipulateEdges == ManipulateEdges.Left)
                    return edges;

                if (attractable != this)
                {
                    if (EditMode.editType == EditMode.EditType.Ripple)
                    {
                        bool skip = false;

                        // Hack: Since Trim End and Move in Ripple mode causes other snap point to move on the same track (which is not supported), disable snapping for this special cases...
                        // TODO Find a proper way to have different snap edges for each edit mode.
                        if (manipulateEdges == ManipulateEdges.Right)
                        {
                            var otherClipGUI = attractable as TimelineClipGUI;
                            skip = otherClipGUI != null && otherClipGUI.parent == parent;
                        }
                        else if (manipulateEdges == ManipulateEdges.Both)
                        {
                            var moveHandler = attractable as MoveItemHandler;
                            skip = moveHandler != null && moveHandler.movingItems.Any(clips => clips.targetTrack == clip.parentTrack && clip.start >= clips.start);
                        }

                        if (skip)
                            return edges;
                    }

                    AddEdge(edges, clip.start);
                    AddEdge(edges, clip.end);
                }
                else
                {
                    if (manipulateEdges == ManipulateEdges.Right)
                    {
                        var d = TimelineHelpers.GetClipAssetEndTime(clip);

                        if (d < double.MaxValue)
                        {
                            if (clip.SupportsLooping())
                            {
                                var l = TimelineHelpers.GetLoopDuration(clip);

                                var shownTime = TimelineWindow.instance.state.timeAreaShownRange;
                                do
                                {
                                    AddEdge(edges, d, false);
                                    d += l;
                                }
                                while (d < shownTime.y);
                            }
                            else
                            {
                                AddEdge(edges, d, false);
                            }
                        }
                    }

                    if (manipulateEdges == ManipulateEdges.Left)
                    {
                        var clipInfo = AnimationClipCurveCache.Instance.GetCurveInfo(clip.animationClip);
                        if (clipInfo != null && clipInfo.keyTimes.Any())
                            AddEdge(edges, clip.FromLocalTimeUnbound(clipInfo.keyTimes.Min()), false);
                    }
                }
            }
            return edges;
        }

        public bool ShouldSnapTo(ISnappable snappable)
        {
            return true;
        }

        bool ShowDrillIcon(PlayableDirector resolver)
        {
            if (!m_ShowDrillIcon.HasValue || TimelineWindow.instance.hierarchyChangedThisFrame)
            {
                var nestable = m_ClipEditor.supportsSubTimelines;
                m_ShowDrillIcon = nestable && resolver != null;
                if (m_ShowDrillIcon.Value)
                {
                    s_TempSubDirectors.Clear();
                    try
                    {
                        m_ClipEditor.GetSubTimelines(clip, resolver, s_TempSubDirectors);
                    }
                    catch (Exception e)
                    {
                        Debug.LogException(e);
                    }

                    m_ShowDrillIcon &= s_TempSubDirectors.Count > 0;
                }
            }

            return m_ShowDrillIcon.Value;
        }

        static void AddEdge(List<Edge> edges, double time, bool showEdgeHint = true)
        {
            var shownTime = TimelineWindow.instance.state.timeAreaShownRange;
            if (time >= shownTime.x && time <= shownTime.y)
                edges.Add(new Edge(time, showEdgeHint));
        }
    }
}
