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

namespace UnityEditor.Timeline
{
    enum CurveChangeType
    {
        None,
        CurveModified,
        CurveAddedOrRemoved
    }

    abstract class CurveDataSource
    {
        public static CurveDataSource Create(IRowGUI trackGUI)
        {
            if (trackGUI.asset is AnimationTrack)
                return new InfiniteClipCurveDataSource(trackGUI);

            return new TrackParametersCurveDataSource(trackGUI);
        }

        public static CurveDataSource Create(TimelineClipGUI clipGUI)
        {
            if (clipGUI.clip.animationClip != null)
                return new ClipAnimationCurveDataSource(clipGUI);

            return new ClipParametersCurveDataSource(clipGUI);
        }

        int? m_ID = null;
        public int id
        {
            get
            {
                if (!m_ID.HasValue)
                    m_ID = CreateHashCode();

                return m_ID.Value;
            }
        }

        readonly IRowGUI m_TrackGUI;
        protected CurveDataSource(IRowGUI trackGUI)
        {
            m_TrackGUI = trackGUI;
        }

        public abstract AnimationClip animationClip { get; }

        public abstract float start { get; }
        public abstract float timeScale { get; }
        public abstract string groupingName { get; }

        // Applies changes from the visual curve in the curve wrapper back to the animation clips
        public virtual void ApplyCurveChanges(IEnumerable<CurveWrapper> updatedCurves)
        {
            Undo.RegisterCompleteObjectUndo(animationClip, "Edit Clip Curve");
            foreach (CurveWrapper c in updatedCurves)
            {
                if (c.curve.length > 0)
                    AnimationUtility.SetEditorCurve(animationClip, c.binding, c.curve);
                else
                    RemoveCurves(new[] {c.binding});
                c.changed = false;
            }
        }

        /// <summary>The clip version is a value that will change when a curve gets updated.
        /// it's used to detect when an animation clip has been changed externally </summary>
        /// <returns>A versioning value indicating the state of the curve. If the curve is updated externally this value will change. </returns>
        public virtual UInt64 GetClipVersion()
        {
            return animationClip.ClipVersion();
        }

        /// <summary>Call this method to check if the underlying clip has changed</summary>
        /// <param name="curveVersion">A versioning value. This will be updated to the latest version</param>
        /// <returns>A value indicating how the clip has changed</returns>
        public virtual CurveChangeType UpdateExternalChanges(ref UInt64 curveVersion)
        {
            return animationClip.GetChangeType(ref curveVersion);
        }

        public virtual string ModifyPropertyDisplayName(string path, string propertyName) => propertyName;

        public virtual void RemoveCurves(IEnumerable<EditorCurveBinding> bindings)
        {
            Undo.RegisterCompleteObjectUndo(animationClip, "Remove Curve(s)");
            foreach (var binding in bindings)
            {
                if (binding.isPPtrCurve)
                    AnimationUtility.SetObjectReferenceCurve(animationClip, binding, null);
                else
                    AnimationUtility.SetEditorCurve(animationClip, binding, null);
            }
        }

        public Rect GetBackgroundRect(WindowState state)
        {
            var trackRect = m_TrackGUI.boundingRect;
            return new Rect(
                state.timeAreaTranslation.x + trackRect.xMin,
                trackRect.y,
                (float)state.editSequence.asset.duration * state.timeAreaScale.x,
                trackRect.height
            );
        }

        public List<CurveWrapper> GenerateWrappers(IEnumerable<EditorCurveBinding> bindings)
        {
            var wrappers = new List<CurveWrapper>(bindings.Count());
            int curveWrapperId = 0;

            foreach (EditorCurveBinding b in bindings)
            {
                // General configuration
                var wrapper = new CurveWrapper
                {
                    id = curveWrapperId++,
                    binding = b,
                    groupId = -1,
                    hidden = false,
                    readOnly = false,
                    getAxisUiScalarsCallback = () => new Vector2(1, 1)
                };

                // Specific configuration
                ConfigureCurveWrapper(wrapper);

                wrappers.Add(wrapper);
            }

            return wrappers;
        }

        protected virtual void ConfigureCurveWrapper(CurveWrapper wrapper)
        {
            wrapper.color = CurveUtility.GetPropertyColor(wrapper.binding.propertyName);
            wrapper.renderer = new NormalCurveRenderer(AnimationUtility.GetEditorCurve(animationClip, wrapper.binding));
            wrapper.renderer.SetCustomRange(0.0f, animationClip.length);
        }

        protected virtual int CreateHashCode()
        {
            return m_TrackGUI.asset.GetHashCode();
        }
    }

    class ClipAnimationCurveDataSource : CurveDataSource
    {
        static readonly string k_GroupingName = L10n.Tr("Animated Values");

        readonly TimelineClipGUI m_ClipGUI;

        public ClipAnimationCurveDataSource(TimelineClipGUI clipGUI) : base(clipGUI.parent)
        {
            m_ClipGUI = clipGUI;
        }

        public override AnimationClip animationClip
        {
            get { return m_ClipGUI.clip.animationClip; }
        }

        public override float start
        {
            get { return (float)m_ClipGUI.clip.FromLocalTimeUnbound(0.0); }
        }

        public override float timeScale
        {
            get { return (float)m_ClipGUI.clip.timeScale; }
        }

        public override string groupingName
        {
            get { return k_GroupingName; }
        }

        protected override int CreateHashCode()
        {
            return base.CreateHashCode().CombineHash(m_ClipGUI.clip.GetHashCode());
        }

        public override string ModifyPropertyDisplayName(string path, string propertyName)
        {
            if (!AnimatedPropertyUtility.IsMaterialProperty(propertyName))
                return propertyName;

            var track = m_ClipGUI.clip.GetParentTrack();
            if (track == null)
                return propertyName;

            var gameObjectBinding = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, track);
            if (gameObjectBinding == null)
                return propertyName;

            if (!string.IsNullOrEmpty(path))
            {
                var transform = gameObjectBinding.transform.Find(path);
                if (transform == null)
                    return propertyName;
                gameObjectBinding = transform.gameObject;
            }

            return AnimatedPropertyUtility.RemapMaterialName(gameObjectBinding, propertyName);
        }
    }

    class ClipParametersCurveDataSource : CurveDataSource
    {
        static readonly string k_GroupingName = L10n.Tr("Clip Properties");

        readonly TimelineClipGUI m_ClipGUI;
        readonly CurvesProxy m_CurvesProxy;

        private int m_ClipDirtyVersion;

        public ClipParametersCurveDataSource(TimelineClipGUI clipGUI) : base(clipGUI.parent)
        {
            m_ClipGUI = clipGUI;
            m_CurvesProxy = new CurvesProxy(clipGUI.clip);
        }

        public override AnimationClip animationClip
        {
            get { return m_CurvesProxy.curves; }
        }

        public override UInt64 GetClipVersion()
        {
            return sourceAnimationClip.ClipVersion();
        }

        public override CurveChangeType UpdateExternalChanges(ref ulong curveVersion)
        {
            if (m_ClipGUI == null || m_ClipGUI.clip == null)
                return CurveChangeType.None;

            var changeType = sourceAnimationClip.GetChangeType(ref curveVersion);
            if (changeType != CurveChangeType.None)
            {
                m_CurvesProxy.ApplyExternalChangesToProxy();
            }
            else if (m_ClipDirtyVersion != m_ClipGUI.clip.DirtyIndex)
            {
                m_CurvesProxy.UpdateProxyCurves();
                if (changeType == CurveChangeType.None)
                    changeType = CurveChangeType.CurveModified;
            }
            m_ClipDirtyVersion = m_ClipGUI.clip.DirtyIndex;
            return changeType;
        }

        public override float start
        {
            get { return (float)m_ClipGUI.clip.FromLocalTimeUnbound(0.0); }
        }

        public override float timeScale
        {
            get { return (float)m_ClipGUI.clip.timeScale; }
        }

        public override string groupingName
        {
            get { return k_GroupingName; }
        }

        public override void RemoveCurves(IEnumerable<EditorCurveBinding> bindings)
        {
            m_CurvesProxy.RemoveCurves(bindings);
        }

        public override void ApplyCurveChanges(IEnumerable<CurveWrapper> updatedCurves)
        {
            m_CurvesProxy.UpdateCurves(updatedCurves);
        }

        protected override void ConfigureCurveWrapper(CurveWrapper wrapper)
        {
            m_CurvesProxy.ConfigureCurveWrapper(wrapper);
        }

        protected override int CreateHashCode()
        {
            return base.CreateHashCode().CombineHash(m_ClipGUI.clip.GetHashCode());
        }

        private AnimationClip sourceAnimationClip
        {
            get
            {
                if (m_ClipGUI == null || m_ClipGUI.clip == null || m_ClipGUI.clip.curves == null)
                    return null;
                return m_ClipGUI.clip.curves;
            }
        }
    }

    class InfiniteClipCurveDataSource : CurveDataSource
    {
        static readonly string k_GroupingName = L10n.Tr("Animated Values");

        readonly AnimationTrack m_AnimationTrack;

        public InfiniteClipCurveDataSource(IRowGUI trackGui) : base(trackGui)
        {
            m_AnimationTrack = trackGui.asset as AnimationTrack;
        }

        public override AnimationClip animationClip
        {
            get { return m_AnimationTrack.infiniteClip; }
        }

        public override float start
        {
            get { return 0.0f; }
        }

        public override float timeScale
        {
            get { return 1.0f; }
        }

        public override string groupingName
        {
            get { return k_GroupingName; }
        }

        public override string ModifyPropertyDisplayName(string path, string propertyName)
        {
            if (m_AnimationTrack == null || !AnimatedPropertyUtility.IsMaterialProperty(propertyName))
                return propertyName;

            var binding = m_AnimationTrack.GetBinding(TimelineEditor.inspectedDirector);
            if (binding == null)
                return propertyName;

            var target = binding.transform;
            if (!string.IsNullOrEmpty(path))
                target = target.Find(path);

            if (target == null)
                return propertyName;

            return AnimatedPropertyUtility.RemapMaterialName(target.gameObject, propertyName);
        }
    }

    class TrackParametersCurveDataSource : CurveDataSource
    {
        static readonly string k_GroupingName = L10n.Tr("Track Properties");

        readonly CurvesProxy m_CurvesProxy;
        private int m_TrackDirtyVersion;

        public TrackParametersCurveDataSource(IRowGUI trackGui) : base(trackGui)
        {
            m_CurvesProxy = new CurvesProxy(trackGui.asset);
        }

        public override AnimationClip animationClip
        {
            get { return m_CurvesProxy.curves; }
        }

        public override UInt64 GetClipVersion()
        {
            return sourceAnimationClip.ClipVersion();
        }

        public override CurveChangeType UpdateExternalChanges(ref ulong curveVersion)
        {
            if (m_CurvesProxy.targetTrack == null)
                return CurveChangeType.None;

            var changeType = sourceAnimationClip.GetChangeType(ref curveVersion);
            if (changeType != CurveChangeType.None)
            {
                m_CurvesProxy.ApplyExternalChangesToProxy();
            }
            // track property has changed externally, update the curve proxies
            else if (m_TrackDirtyVersion != m_CurvesProxy.targetTrack.DirtyIndex)
            {
                if (changeType == CurveChangeType.None)
                    changeType = CurveChangeType.CurveModified;
                m_CurvesProxy.UpdateProxyCurves();
            }
            m_TrackDirtyVersion = m_CurvesProxy.targetTrack.DirtyIndex;
            return changeType;
        }

        public override float start
        {
            get { return 0.0f; }
        }

        public override float timeScale
        {
            get { return 1.0f; }
        }

        public override string groupingName
        {
            get { return k_GroupingName; }
        }

        public override void RemoveCurves(IEnumerable<EditorCurveBinding> bindings)
        {
            m_CurvesProxy.RemoveCurves(bindings);
        }

        public override void ApplyCurveChanges(IEnumerable<CurveWrapper> updatedCurves)
        {
            m_CurvesProxy.UpdateCurves(updatedCurves);
        }

        protected override void ConfigureCurveWrapper(CurveWrapper wrapper)
        {
            m_CurvesProxy.ConfigureCurveWrapper(wrapper);
        }

        private AnimationClip sourceAnimationClip
        {
            get
            {
                if (m_CurvesProxy.targetTrack == null || m_CurvesProxy.targetTrack.curves == null)
                    return null;
                return m_CurvesProxy.targetTrack.curves;
            }
        }
    }
}