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

namespace UnityEditor.Timeline
{
    class MoveItemHandler : IAttractable, IAttractionHandler
    {
        bool m_Grabbing;

        MovingItems m_LeftMostMovingItems;
        MovingItems m_RightMostMovingItems;

        HashSet<TimelineItemGUI> m_ItemGUIs;
        ItemsGroup m_ItemsGroup;

        public TrackAsset targetTrack { get; private set; }

        public bool allowTrackSwitch { get; private set; }

        int m_GrabbedModalUndoGroup = -1;

        readonly WindowState m_State;

        public MovingItems[] movingItems { get; private set; }

        public MoveItemHandler(WindowState state)
        {
            m_State = state;
        }

        public void Grab(IEnumerable<ITimelineItem> items, TrackAsset referenceTrack)
        {
            Grab(items, referenceTrack, Vector2.zero);
        }

        public void Grab(IEnumerable<ITimelineItem> items, TrackAsset referenceTrack, Vector2 mousePosition)
        {
            if (items == null) return;

            items = items.ToArray(); // Cache enumeration result

            if (!items.Any()) return;

            m_GrabbedModalUndoGroup = Undo.GetCurrentGroup();

            var trackItems = items.GroupBy(c => c.parentTrack).ToArray();
            var trackItemsCount = trackItems.Length;
            var tracks = items.Select(c => c.parentTrack).Where(x => x != null).Distinct();

            movingItems = new MovingItems[trackItemsCount];

            allowTrackSwitch = trackItemsCount == 1 && !trackItems.SelectMany(x => x).Any(x => x is MarkerItem); // For now, track switch is only supported when all items are on the same track and there are no items

            // one push per track handles all the clips on the track
            UndoExtensions.RegisterTracks(tracks, L10n.Tr("Move Items"));
            foreach (var sourceTrack in tracks)
            {
                // push all markers on the track because of ripple
                UndoExtensions.RegisterMarkers(sourceTrack.GetMarkers(), L10n.Tr("Move Items"));
            }

            for (var i = 0; i < trackItemsCount; ++i)
            {
                var track = trackItems[i].Key;
                var grabbedItems = new MovingItems(m_State, track, trackItems[i].ToArray(), referenceTrack, mousePosition, allowTrackSwitch);
                movingItems[i] = grabbedItems;
            }

            m_LeftMostMovingItems = null;
            m_RightMostMovingItems = null;

            foreach (var grabbedTrackItems in movingItems)
            {
                if (m_LeftMostMovingItems == null || m_LeftMostMovingItems.start > grabbedTrackItems.start)
                    m_LeftMostMovingItems = grabbedTrackItems;

                if (m_RightMostMovingItems == null || m_RightMostMovingItems.end < grabbedTrackItems.end)
                    m_RightMostMovingItems = grabbedTrackItems;
            }

            m_ItemGUIs = new HashSet<TimelineItemGUI>();
            m_ItemsGroup = new ItemsGroup(items);

            foreach (var item in items)
                m_ItemGUIs.Add(item.gui);

            targetTrack = referenceTrack;

            EditMode.BeginMove(this);
            m_Grabbing = true;
        }

        public void Drop()
        {
            if (IsValidDrop())
            {
                foreach (var grabbedItems in movingItems)
                {
                    var track = grabbedItems.targetTrack;
                    UndoExtensions.RegisterTrack(track, L10n.Tr("Move Items"));

                    if (EditModeUtils.IsInfiniteTrack(track) && grabbedItems.clips.Any())
                        ((AnimationTrack)track).ConvertToClipMode();
                }

                EditMode.FinishMove();

                Done();
            }
            else
            {
                Cancel();
            }

            EditMode.ClearEditMode();
        }

        bool IsValidDrop()
        {
            return movingItems.All(g => g.canDrop);
        }

        void Cancel()
        {
            if (!m_Grabbing)
                return;

            // TODO fix undo reselection persistency
            // identify the clips by their playable asset, since that reference will survive the undo
            // This is a workaround, until a more persistent fix for selection of clips across Undo can be found
            var assets = movingItems.SelectMany(x => x.clips).Select(x => x.asset);

            Undo.RevertAllDownToGroup(m_GrabbedModalUndoGroup);

            // reselect the clips from the original clip
            var clipsToSelect = movingItems.Select(x => x.originalTrack).SelectMany(x => x.GetClips()).Where(x => assets.Contains(x.asset)).ToArray();
            SelectionManager.RemoveTimelineSelection();

            foreach (var c in clipsToSelect)
                SelectionManager.Add(c);

            Done();
        }

        void Done()
        {
            foreach (var movingItem in movingItems)
            {
                foreach (var item in movingItem.items)
                {
                    if (item.gui != null)
                        item.gui.isInvalid = false;
                }
            }

            movingItems = null;
            m_LeftMostMovingItems = null;
            m_RightMostMovingItems = null;
            m_Grabbing = false;

            m_State.Refresh();
            m_State.analytics.SendManipulationEndedEvent();
        }

        public double start { get { return m_ItemsGroup.start; } }

        public double end { get { return m_ItemsGroup.end; } }

        public bool ShouldSnapTo(ISnappable snappable)
        {
            var itemGUI = snappable as TimelineItemGUI;
            return itemGUI != null && !m_ItemGUIs.Contains(itemGUI);
        }

        public void UpdateTrackTarget(TrackAsset track)
        {
            if (!EditMode.AllowTrackSwitch())
                return;

            targetTrack = track;

            var targetTracksChanged = false;

            foreach (var grabbedItem in movingItems)
            {
                var prevTrackGUI = grabbedItem.targetTrack;

                grabbedItem.SetReferenceTrack(track);

                targetTracksChanged = grabbedItem.targetTrack != prevTrackGUI;
            }

            if (targetTracksChanged)
                EditMode.HandleTrackSwitch(movingItems);

            RefreshPreviewItems();

            m_State.rebuildGraph |= targetTracksChanged;
        }

        public void OnGUI(Event evt)
        {
            if (!m_Grabbing)
                return;

            if (evt.type != EventType.Repaint)
                return;

            var isValid = IsValidDrop();

            using (new GUIViewportScope(m_State.GetWindow().sequenceContentRect))
            {
                foreach (var grabbedClip in movingItems)
                {
                    grabbedClip.RefreshBounds(m_State, evt.mousePosition);

                    if (!grabbedClip.HasAnyDetachedParents())
                        continue;

                    grabbedClip.Draw(isValid);
                }

                if (isValid)
                {
                    EditMode.DrawMoveGUI(m_State, movingItems);
                }
                else
                {
                    TimelineCursors.ClearCursor();
                }
            }
        }

        public void OnAttractedEdge(IAttractable attractable, ManipulateEdges manipulateEdges, AttractedEdge edge, double time)
        {
            double offset;

            if (edge == AttractedEdge.Right)
            {
                var duration = end - start;
                var startTime = time - duration;
                startTime = EditMode.AdjustStartTime(m_State, m_RightMostMovingItems, startTime);

                offset = startTime + duration - end;
            }
            else
            {
                if (edge == AttractedEdge.Left)
                    time = EditMode.AdjustStartTime(m_State, m_LeftMostMovingItems, time);

                offset = time - start;
            }

            if (start + offset < 0.0)
                offset = -start;

            if (!offset.Equals(0.0))
            {
                foreach (var grabbedClips in movingItems)
                    grabbedClips.start += offset;

                EditMode.UpdateMove();

                RefreshPreviewItems();
            }
        }

        public void RefreshPreviewItems()
        {
            foreach (var movingItemsGroup in movingItems)
            {
                // Check validity
                var valid = ValidateItemDrag(movingItemsGroup);

                foreach (var item in movingItemsGroup.items)
                {
                    if (item.gui != null)
                        item.gui.isInvalid = !valid;
                }

                movingItemsGroup.canDrop = valid;
            }
        }

        static bool ValidateItemDrag(ItemsPerTrack itemsGroup)
        {
            //TODO-marker: this is to prevent the drag operation from being canceled when moving only markers
            if (itemsGroup.clips.Any())
            {
                if (itemsGroup.targetTrack == null)
                    return false;

                if (itemsGroup.targetTrack.lockedInHierarchy)
                    return false;

                if (itemsGroup.items.Any(i => !i.IsCompatibleWithTrack(itemsGroup.targetTrack)))
                    return false;

                return EditMode.ValidateDrag(itemsGroup);
            }

            return true;
        }

        public void OnTrackDetach()
        {
            EditMode.OnTrackDetach(movingItems);
        }
    }
}
