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

namespace UnityEditor.Timeline
{
    // Handles Undo animated properties on Monobehaviours to create track clips
    static partial class TimelineRecording
    {
        static readonly List<PropertyModification> s_TempPropertyModifications = new List<PropertyModification>(6);

        internal static UndoPropertyModification[] ProcessUndoModification(UndoPropertyModification[] modifications, WindowState state)
        {
            if (HasAnyPlayableAssetModifications(modifications))
                return ProcessPlayableAssetModification(modifications, state, false);
            return ProcessMonoBehaviourModification(modifications, state);
        }

        static UnityEngine.Object GetTarget(UndoPropertyModification undo)
        {
            if (undo.currentValue != null)
                return undo.currentValue.target;
            if (undo.previousValue != null)
                return undo.previousValue.target;
            return null;
        }

        // Gets the appropriate track for a given game object
        static TrackAsset GetTrackForGameObject(GameObject gameObject, WindowState state)
        {
            if (gameObject == null)
                return null;

            var director = state.editSequence.director;
            if (director == null)
                return null;

            var level = int.MaxValue;

            TrackAsset result = null;

            // search the output tracks
            var outputTracks = state.editSequence.asset.flattenedTracks;
            foreach (var track in outputTracks)
            {
                if (track.GetType() != typeof(AnimationTrack))
                    continue;
                if (!state.IsTrackRecordable(track))
                    continue;

                var obj = TimelineUtility.GetSceneGameObject(director, track);
                if (obj != null)
                {
                    // checks if the effected gameobject is our child
                    var childLevel = GetChildLevel(obj, gameObject);
                    if (childLevel != -1 && childLevel < level)
                    {
                        result = track;
                        level = childLevel;
                    }
                }
            }

            // the resulting track is not armed. checking here avoids accidentally recording objects with their own
            // tracks
            if (result && !state.IsTrackRecordable(result))
            {
                result = null;
            }

            return result;
        }

        // Gets the track this property would record to.
        // Returns null if there is a track, but it's not currently active for recording
        public static TrackAsset GetRecordingTrack(SerializedProperty property, WindowState state)
        {
            var serializedObject = property.serializedObject;
            var component = serializedObject.targetObject as Component;
            if (component == null)
                return null;

            var gameObject = component.gameObject;
            return GetTrackForGameObject(gameObject, state);
        }

        // Given a serialized property, gathers all animatable properties
        static void GatherModifications(SerializedProperty property, List<PropertyModification> modifications)
        {
            // handles child properties (Vector3 is 3 recordable properties)
            if (property.hasChildren)
            {
                var iter = property.Copy();
                var end = property.GetEndProperty(false);

                // recurse over all children properties
                while (iter.Next(true) && !SerializedProperty.EqualContents(iter, end))
                {
                    GatherModifications(iter, modifications);
                }
            }

            var isObject = property.propertyType == SerializedPropertyType.ObjectReference;
            var isFloat = property.propertyType == SerializedPropertyType.Float ||
                property.propertyType == SerializedPropertyType.Boolean ||
                property.propertyType == SerializedPropertyType.Integer;

            if (isObject || isFloat)
            {
                var serializedObject = property.serializedObject;
                var modification = new PropertyModification();

                modification.target = serializedObject.targetObject;
                modification.propertyPath = property.propertyPath;
                if (isObject)
                {
                    modification.value = string.Empty;
                    modification.objectReference = property.objectReferenceValue;
                }
                else
                {
                    modification.value = TimelineUtility.PropertyToString(property);
                }

                // Path for monobehaviour based - better to grab the component to get the curvebinding to allow validation
                if (serializedObject.targetObject is Component)
                {
                    EditorCurveBinding temp;
                    var go = ((Component)serializedObject.targetObject).gameObject;
                    if (AnimationUtility.PropertyModificationToEditorCurveBinding(modification, go, out temp) != null)
                    {
                        modifications.Add(modification);
                    }
                }
                else
                {
                    modifications.Add(modification);
                }
            }
        }

        public static void AddKey(SerializedProperty prop, WindowState state)
        {
            s_TempPropertyModifications.Clear();
            GatherModifications(prop, s_TempPropertyModifications);
            if (s_TempPropertyModifications.Any())
            {
                AddKey(s_TempPropertyModifications, state);
            }
        }

        public static void AddKey(IEnumerable<PropertyModification> modifications, WindowState state)
        {
            var undos = modifications.Select(PropertyModificationToUndoPropertyModification).ToArray();
            if (HasAnyPlayableAssetModifications(undos))
                ProcessPlayableAssetModification(undos, state, true);
            ProcessMonoBehaviourModification(undos, state);
        }

        static UndoPropertyModification PropertyModificationToUndoPropertyModification(PropertyModification prop)
        {
            return new UndoPropertyModification
            {
                previousValue = prop,
                currentValue = new PropertyModification
                {
                    objectReference = prop.objectReference,
                    propertyPath = prop.propertyPath,
                    target = prop.target,
                    value = prop.value
                },
                keepPrefabOverride = true
            };
        }

        // Given an animation track, return the clip that we are currently recording to
        static AnimationClip GetRecordingClip(TrackAsset asset, WindowState state, out double startTime, out double timeScale)
        {
            startTime = 0;
            timeScale = 1;

            TimelineClip displayBackground = null;
            asset.FindRecordingClipAtTime(state.editSequence.time, out displayBackground);
            var animClip = asset.FindRecordingAnimationClipAtTime(state.editSequence.time);

            if (displayBackground != null)
            {
                startTime =  displayBackground.start;
                timeScale =  displayBackground.timeScale;
            }

            return animClip;
        }

        // Helper that finds the animation clip we are recording and the relative time to that clip
        static bool GetClipAndRelativeTime(UnityEngine.Object target, WindowState state,
            out AnimationClip outClip, out double keyTime, out bool keyInRange)
        {
            const float floatToDoubleError = 0.00001f;
            outClip = null;
            keyTime = 0;
            keyInRange = false;

            double startTime = 0;
            double timeScale = 1;
            AnimationClip clip = null;

            IPlayableAsset playableAsset = target as IPlayableAsset;
            Component component = target as Component;

            // Handle recordable playable assets
            if (playableAsset != null)
            {
                var curvesOwner = AnimatedParameterUtility.ToCurvesOwner(playableAsset, state.editSequence.asset);
                if (curvesOwner != null)
                {
                    if (curvesOwner.curves == null)
                        curvesOwner.CreateCurves(curvesOwner.GetUniqueRecordedClipName());

                    clip = curvesOwner.curves;

                    var timelineClip = curvesOwner as TimelineClip;
                    if (timelineClip != null)
                    {
                        startTime = timelineClip.start;
                        timeScale = timelineClip.timeScale;
                    }
                }
            }
            // Handle recording components, including infinite clip
            else if (component != null)
            {
                var asset = GetTrackForGameObject(component.gameObject, state);
                if (asset != null)
                {
                    clip = GetRecordingClip(asset, state, out startTime, out timeScale);
                }
            }

            if (clip == null)
                return false;

            keyTime = (state.editSequence.time - startTime) * timeScale;
            outClip = clip;
            keyInRange = keyTime >= 0 && keyTime <= (clip.length * timeScale + floatToDoubleError);

            return true;
        }

        public static bool HasCurve(IList<PropertyModification> modifications, UnityEngine.Object target,
            WindowState state)
        {
            return GetKeyTimes(modifications, state).Any();
        }

        public static bool HasKey(IList<PropertyModification> modifications,
            WindowState state)
        {
            AnimationClip clip;
            double keyTime;
            bool inRange;

            if (!GetClipAndRelativeTime(modifications[0].target, state, out clip, out keyTime, out inRange))
                return false;

            return GetKeyTimes(modifications, state).Any(t => (CurveEditUtility.KeyCompare((float)state.editSequence.time, (float)t, clip.frameRate) == 0));
        }

        // Checks if a key already exists for this property
        static bool HasBinding(UnityEngine.Object target, PropertyModification modification, AnimationClip clip, out EditorCurveBinding binding)
        {
            var component = target as Component;
            var playableAsset = target as IPlayableAsset;

            if (component != null)
            {
                var type = AnimationUtility.PropertyModificationToEditorCurveBinding(modification, component.gameObject, out binding);
                binding = RotationCurveInterpolation.RemapAnimationBindingForRotationCurves(binding, clip);
                return type != null;
            }

            if (playableAsset != null)
            {
                binding = EditorCurveBinding.FloatCurve(string.Empty, target.GetType(),
                    AnimatedParameterUtility.GetAnimatedParameterBindingName(target, modification.propertyPath));
            }
            else
            {
                binding = new EditorCurveBinding();
                return false;
            }

            return true;
        }

        public static void RemoveKey(UnityEngine.Object target, IEnumerable<PropertyModification> modifications,
            WindowState state)
        {
            AnimationClip clip;
            double keyTime;
            bool inRange;
            if (!GetClipAndRelativeTime(target, state, out clip, out keyTime, out inRange) || !inRange)
                return;
            var refreshPreview = false;
            TimelineUndo.PushUndo(clip, L10n.Tr("Remove Key"));
            foreach (var mod in modifications)
            {
                EditorCurveBinding temp;
                if (HasBinding(target, mod, clip, out temp))
                {
                    if (temp.isPPtrCurve)
                    {
                        CurveEditUtility.RemoveObjectKey(clip, temp, keyTime);
                        if (CurveEditUtility.GetObjectKeyCount(clip, temp) == 0)
                        {
                            refreshPreview = true;
                        }
                    }
                    else
                    {
                        AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, temp);
                        if (curve != null)
                        {
                            CurveEditUtility.RemoveKeyFrameFromCurve(curve, (float)keyTime, clip.frameRate);
                            AnimationUtility.SetEditorCurve(clip, temp, curve);
                            if (curve.length == 0)
                            {
                                AnimationUtility.SetEditorCurve(clip, temp, null);
                                refreshPreview = true;
                            }
                        }
                    }
                }
            }

            if (refreshPreview)
            {
                state.ResetPreviewMode();
            }
        }

        static HashSet<double> GetKeyTimes(IList<PropertyModification> modifications, WindowState state)
        {
            var keyTimes = new HashSet<double>();

            AnimationClip animationClip;
            double keyTime;
            bool inRange;

            var component = modifications[0].target as Component;
            var target = modifications[0].target;
            if (component != null)
            {
                var track = GetTrackForGameObject(component.gameObject, state);
                var go = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, track);
                if (go != null)
                {
                    target = go.transform;
                }
            }

            GetClipAndRelativeTime(target, state, out animationClip, out keyTime, out inRange);
            if (animationClip == null)
                return keyTimes;

            var playableAsset = target as IPlayableAsset;
            var info = AnimationClipCurveCache.Instance.GetCurveInfo(animationClip);

            TimelineClip clip = null;
            if (component != null)
            {
                GetTrackForGameObject(component.gameObject, state).FindRecordingClipAtTime(state.editSequence.time, out clip);
            }
            else if (playableAsset != null)
            {
                clip = FindClipWithAsset(state.editSequence.asset, playableAsset);
            }

            foreach (var mod in modifications)
            {
                EditorCurveBinding temp;
                if (HasBinding(target, mod, animationClip, out temp))
                {
                    IEnumerable<double> keys = new HashSet<double>();
                    if (temp.isPPtrCurve)
                    {
                        var curve = info.GetObjectCurveForBinding(temp);
                        if (curve != null)
                        {
                            keys = curve.Select(x => (double)x.time);
                        }
                    }
                    else
                    {
                        var curve = info.GetCurveForBinding(temp);
                        if (curve != null)
                        {
                            keys = curve.keys.Select(x => (double)x.time);
                        }
                    }

                    // Transform the times in to 'global' space using the clip
                    if (clip != null)
                    {
                        foreach (var k in keys)
                        {
                            var time = clip.FromLocalTimeUnbound(k);
                            const double eps = 1e-5;
                            if (time >= clip.start - eps && time <= clip.end + eps)
                            {
                                keyTimes.Add(time);
                            }
                        }
                    }
                    // infinite clip mode, global == local space
                    else
                    {
                        keyTimes.UnionWith(keys);
                    }
                }
            }

            return keyTimes;
        }

        public static void NextKey(UnityEngine.Object target, IList<PropertyModification> modifications, WindowState state)
        {
            const double eps = 1e-5;
            var keyTimes = GetKeyTimes(modifications, state);
            if (keyTimes.Count == 0)
                return;
            var nextKeys = keyTimes.Where(x => x > state.editSequence.time + eps);
            if (nextKeys.Any())
            {
                state.editSequence.time = nextKeys.Min();
            }
        }

        public static void PrevKey(UnityEngine.Object target, IList<PropertyModification> modifications, WindowState state)
        {
            const double eps = 1e-5;
            var keyTimes = GetKeyTimes(modifications, state);
            if (keyTimes.Count == 0)
                return;
            var prevKeys = keyTimes.Where(x => x < state.editSequence.time - eps);
            if (prevKeys.Any())
            {
                state.editSequence.time = prevKeys.Max();
            }
        }

        public static void RemoveCurve(UnityEngine.Object target, IEnumerable<PropertyModification> modifications, WindowState state)
        {
            AnimationClip clip = null;
            double keyTime = 0;
            var inRange = false; // not used for curves
            if (!GetClipAndRelativeTime(target, state, out clip, out keyTime, out inRange))
                return;

            TimelineUndo.PushUndo(clip, L10n.Tr("Remove Curve"));
            foreach (var mod in modifications)
            {
                EditorCurveBinding temp;
                if (HasBinding(target, mod, clip, out temp))
                {
                    if (temp.isPPtrCurve)
                        AnimationUtility.SetObjectReferenceCurve(clip, temp, null);
                    else
                        AnimationUtility.SetEditorCurve(clip, temp, null);
                }
            }

            state.ResetPreviewMode();
        }

        public static void KeyAllProperties(Component target, WindowState state)
        {
            var go = target is Component component ? component.gameObject : null;
            GetClipAndRelativeTime(target, state, out var animationClip, out _, out _);

            var info = AnimationClipCurveCache.Instance.GetCurveInfo(animationClip);
            if (animationClip != null && info.curves.Length > 0)
            {
                KeyProperties(go, state, info.bindings);
            }
        }

        public static void KeyProperties(GameObject go, WindowState state, IList<EditorCurveBinding> bindings)
        {
            var allKeyedProperties = new List<PropertyModification>();
            var rotationPaths = new HashSet<string>();
            for (var i = 0; i < bindings.Count; ++i)
            {
                // Skip the euler and key quaternion+hint
                if (CurveEditUtility.IsRotationKey(bindings[i]))
                {
                    rotationPaths.Add(bindings[i].path);
                    continue;
                }

                AnimationUtility.GetFloatValue(go, bindings[i], out var val);
                var compo = GetTargetFromEditorBinding(go, bindings[i]);
                allKeyedProperties.Add(new PropertyModification
                {
                    target = compo, value = val.ToString(EditorGUI.kFloatFieldFormatString),
                    propertyPath = bindings[i].propertyName
                });
            }

            foreach (var path in rotationPaths)
            {
                foreach (var binding in GetRotationBindings(path))
                {
                    var compo = GetTargetFromEditorBinding(go, binding);
                    var readBinding = binding;
                    switch (binding.propertyName)
                    {
                        case kLocalEulerHint + ".x":
                            readBinding = EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".x");
                            break;
                        case kLocalEulerHint + ".y":
                            readBinding = EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".y");
                            break;
                        case kLocalEulerHint + ".z":
                            readBinding = EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".z");
                            break;
                    }

                    AnimationUtility.GetFloatValue(go, readBinding, out var val);
                    allKeyedProperties.Add(new PropertyModification
                    {
                        target = compo, value = val.ToString(EditorGUI.kFloatFieldFormatString),
                        propertyPath = binding.propertyName
                    });
                }
            }

            AddKey(allKeyedProperties, state);
            state.Refresh();
        }

        static IEnumerable<EditorCurveBinding> GetRotationBindings(string path)
        {
            return new[]
            {
                EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".x"),
                EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".y"),
                EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".z"),
                EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".w"),
                EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalEulerHint + ".x"),
                EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalEulerHint + ".y"),
                EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalEulerHint + ".z"),
            };
        }

        static Component GetTargetFromEditorBinding(GameObject root, EditorCurveBinding binding)
        {
            GameObject go = null;
            if (string.IsNullOrEmpty(binding.path))
            {
                go = root;
            }

            var childTransform = root.transform.Find(binding.path);
            if (childTransform != null)
            {
                go = childTransform.gameObject;
            }

            if (go != null)
            {
                return go.GetComponent(binding.type);
            }

            return null;
        }

        public static IEnumerable<GameObject> GetRecordableGameObjects(WindowState state)
        {
            if (state == null || state.editSequence.asset == null || state.editSequence.director == null)
                yield break;

            var outputTracks = state.editSequence.asset.GetOutputTracks();
            foreach (var track in outputTracks)
            {
                if (track.GetType() != typeof(AnimationTrack))
                    continue;
                if (!state.IsTrackRecordable(track) && !track.GetChildTracks().Any(state.IsTrackRecordable))
                    continue;

                var obj = TimelineUtility.GetSceneGameObject(state.editSequence.director, track);
                if (obj != null)
                {
                    yield return obj;
                }
            }
        }
    }
}
