using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditorInternal;
using UnityEngine;
using Object = UnityEngine.Object;

namespace UnityEditor.Timeline
{
    // Utility class for editing animation clips from serialized properties
    static class CurveEditUtility
    {
        public static bool IsRotationKey(EditorCurveBinding binding)
        {
            return binding.propertyName.Contains("localEulerAnglesRaw");
        }

        public static void AddKey(AnimationClip clip, EditorCurveBinding sourceBinding, SerializedProperty prop, double time)
        {
            if (sourceBinding.isPPtrCurve)
            {
                AddObjectKey(clip, sourceBinding, prop, time);
            }
            else if (IsRotationKey(sourceBinding))
            {
                AddRotationKey(clip, sourceBinding, prop, time);
            }
            else
            {
                AddFloatKey(clip, sourceBinding, prop, time);
            }
        }

        static void AddObjectKey(AnimationClip clip, EditorCurveBinding sourceBinding, SerializedProperty prop, double time)
        {
            if (prop.propertyType != SerializedPropertyType.ObjectReference)
                return;

            ObjectReferenceKeyframe[] curve = null;
            var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
            var curveIndex = Array.IndexOf(info.objectBindings, sourceBinding);
            if (curveIndex >= 0)
            {
                curve = info.objectCurves[curveIndex];

                // where in the array does the evaluation land?
                var evalIndex = EvaluateIndex(curve, (float)time);

                if (KeyCompare(curve[evalIndex].time, (float)time, clip.frameRate) == 0)
                {
                    curve[evalIndex].value = prop.objectReferenceValue;
                }
                // check the next key (always return the minimum value)
                else if (evalIndex < curve.Length - 1 && KeyCompare(curve[evalIndex + 1].time, (float)time, clip.frameRate) == 0)
                {
                    curve[evalIndex + 1].value = prop.objectReferenceValue;
                }
                // resize the array
                else
                {
                    if (time > curve[0].time)
                        evalIndex++;
                    var key = new ObjectReferenceKeyframe();
                    key.time = (float)time;
                    key.value = prop.objectReferenceValue;
                    ArrayUtility.Insert(ref curve, evalIndex, key);
                }
            }
            else // curve doesn't exist, add it
            {
                curve = new ObjectReferenceKeyframe[1];
                curve[0].time = (float)time;
                curve[0].value = prop.objectReferenceValue;
            }

            AnimationUtility.SetObjectReferenceCurve(clip, sourceBinding, curve);
            EditorUtility.SetDirty(clip);
        }

        static void AddRotationKey(AnimationClip clip, EditorCurveBinding sourceBind, SerializedProperty prop, double time)
        {
            if (prop.propertyType != SerializedPropertyType.Quaternion)
            {
                return;
            }

            var updateCurves = new List<AnimationCurve>();
            var updateBindings = new List<EditorCurveBinding>();

            var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
            for (var i = 0; i < info.bindings.Length; i++)
            {
                if (sourceBind.type != info.bindings[i].type)
                    continue;

                if (info.bindings[i].propertyName.Contains("localEuler"))
                {
                    updateBindings.Add(info.bindings[i]);
                    updateCurves.Add(info.curves[i]);
                }
            }

            // use this instead of serialized properties because the editor will attempt to maintain
            // correct localeulers
            var eulers = ((Transform)prop.serializedObject.targetObject).localEulerAngles;
            if (updateBindings.Count == 0)
            {
                var propName = AnimationWindowUtility.GetPropertyGroupName(sourceBind.propertyName);
                updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".x"));
                updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".y"));
                updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".z"));

                var curveX = new AnimationCurve();
                var curveY = new AnimationCurve();
                var curveZ = new AnimationCurve();
                AddKeyFrameToCurve(curveX, (float)time, clip.frameRate, eulers.x, false);
                AddKeyFrameToCurve(curveY, (float)time, clip.frameRate, eulers.y, false);
                AddKeyFrameToCurve(curveZ, (float)time, clip.frameRate, eulers.z, false);

                updateCurves.Add(curveX);
                updateCurves.Add(curveY);
                updateCurves.Add(curveZ);
            }

            for (var i = 0; i < updateBindings.Count; i++)
            {
                var c = updateBindings[i].propertyName.Last();
                var value = eulers.x;
                if (c == 'y') value = eulers.y;
                else if (c == 'z') value = eulers.z;
                AddKeyFrameToCurve(updateCurves[i], (float)time, clip.frameRate, value, false);
            }

            UpdateEditorCurves(clip, updateBindings, updateCurves);
        }

        // Add a floating point curve key
        static void AddFloatKey(AnimationClip clip, EditorCurveBinding sourceBind, SerializedProperty prop, double time)
        {
            var updateCurves = new List<AnimationCurve>();
            var updateBindings = new List<EditorCurveBinding>();

            var updated = false;
            var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
            for (var i = 0; i < info.bindings.Length; i++)
            {
                var binding = info.bindings[i];
                if (binding.type != sourceBind.type)
                    continue;

                SerializedProperty valProp = null;
                var curve = info.curves[i];

                // perfect match on property path, editting a float
                if (prop.propertyPath.Equals(binding.propertyName))
                {
                    valProp = prop;
                }
                // this is a child object
                else if (binding.propertyName.Contains(prop.propertyPath))
                {
                    valProp = prop.serializedObject.FindProperty(binding.propertyName);
                }

                if (valProp != null)
                {
                    var value = GetKeyValue(valProp);
                    if (!float.IsNaN(value)) // Nan indicates an error retrieving the property value
                    {
                        updated = true;
                        AddKeyFrameToCurve(curve, (float)time, clip.frameRate, value, valProp.propertyType == SerializedPropertyType.Boolean);
                        updateCurves.Add(curve);
                        updateBindings.Add(binding);
                    }
                }
            }

            // Curves don't exist, add them
            if (!updated)
            {
                var propName = AnimationWindowUtility.GetPropertyGroupName(sourceBind.propertyName);
                if (!prop.hasChildren)
                {
                    var value = GetKeyValue(prop);
                    if (!float.IsNaN(value))
                    {
                        updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, sourceBind.propertyName));
                        var curve = new AnimationCurve();
                        AddKeyFrameToCurve(curve, (float)time, clip.frameRate, value, prop.propertyType == SerializedPropertyType.Boolean);
                        updateCurves.Add(curve);
                    }
                }
                else
                {
                    // special case because subproperties on color aren't 'visible' so you can't iterate over them
                    if (prop.propertyType == SerializedPropertyType.Color)
                    {
                        updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".r"));
                        updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".g"));
                        updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".b"));
                        updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".a"));

                        var c = prop.colorValue;
                        for (var i = 0; i < 4; i++)
                        {
                            var curve = new AnimationCurve();
                            AddKeyFrameToCurve(curve, (float)time, clip.frameRate, c[i], prop.propertyType == SerializedPropertyType.Boolean);
                            updateCurves.Add(curve);
                        }
                    }
                    else
                    {
                        prop = prop.Copy();
                        foreach (SerializedProperty cp in prop)
                        {
                            updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, cp.propertyPath));
                            var curve = new AnimationCurve();
                            AddKeyFrameToCurve(curve, (float)time, clip.frameRate, GetKeyValue(cp), cp.propertyType == SerializedPropertyType.Boolean);
                            updateCurves.Add(curve);
                        }
                    }
                }
            }

            UpdateEditorCurves(clip, updateBindings, updateCurves);
        }

        public static void RemoveKey(AnimationClip clip, EditorCurveBinding sourceBinding, SerializedProperty prop, double time)
        {
            if (sourceBinding.isPPtrCurve)
            {
                RemoveObjectKey(clip, sourceBinding, time);
            }
            else if (IsRotationKey(sourceBinding))
            {
                RemoveRotationKey(clip, sourceBinding, prop, time);
            }
            else
            {
                RemoveFloatKey(clip, sourceBinding, prop, time);
            }
        }

        public static void RemoveObjectKey(AnimationClip clip, EditorCurveBinding sourceBinding, double time)
        {
            var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
            var curveIndex = Array.IndexOf(info.objectBindings, sourceBinding);
            if (curveIndex >= 0)
            {
                var curve = info.objectCurves[curveIndex];
                var evalIndex = GetKeyframeAtTime(curve, (float)time, clip.frameRate);
                if (evalIndex >= 0)
                {
                    ArrayUtility.RemoveAt(ref curve, evalIndex);
                    AnimationUtility.SetObjectReferenceCurve(clip, sourceBinding, curve.Length == 0 ? null : curve);
                    EditorUtility.SetDirty(clip);
                }
            }
        }

        public static int GetObjectKeyCount(AnimationClip clip, EditorCurveBinding sourceBinding)
        {
            var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
            var curveIndex = Array.IndexOf(info.objectBindings, sourceBinding);
            if (curveIndex >= 0)
            {
                var curve = info.objectCurves[curveIndex];
                return curve.Length;
            }

            return 0;
        }

        static void RemoveRotationKey(AnimationClip clip, EditorCurveBinding sourceBind, SerializedProperty prop, double time)
        {
            if (prop.propertyType != SerializedPropertyType.Quaternion)
            {
                return;
            }

            var updateCurves = new List<AnimationCurve>();
            var updateBindings = new List<EditorCurveBinding>();

            var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
            for (var i = 0; i < info.bindings.Length; i++)
            {
                if (sourceBind.type != info.bindings[i].type)
                    continue;

                if (info.bindings[i].propertyName.Contains("localEuler"))
                {
                    updateBindings.Add(info.bindings[i]);
                    updateCurves.Add(info.curves[i]);
                }
            }

            foreach (var c in updateCurves)
            {
                RemoveKeyFrameFromCurve(c, (float)time, clip.frameRate);
            }

            UpdateEditorCurves(clip, updateBindings, updateCurves);
        }

        // Removes the float keys from curves
        static void RemoveFloatKey(AnimationClip clip, EditorCurveBinding sourceBind, SerializedProperty prop, double time)
        {
            var updateCurves = new List<AnimationCurve>();
            var updateBindings = new List<EditorCurveBinding>();

            var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
            for (var i = 0; i < info.bindings.Length; i++)
            {
                var binding = info.bindings[i];
                if (binding.type != sourceBind.type)
                    continue;

                SerializedProperty valProp = null;
                var curve = info.curves[i];

                // perfect match on property path, editting a float
                if (prop.propertyPath.Equals(binding.propertyName))
                {
                    valProp = prop;
                }
                // this is a child object
                else if (binding.propertyName.Contains(prop.propertyPath))
                {
                    valProp = prop.serializedObject.FindProperty(binding.propertyName);
                }
                if (valProp != null)
                {
                    RemoveKeyFrameFromCurve(curve, (float)time, clip.frameRate);
                    updateCurves.Add(curve);
                    updateBindings.Add(binding);
                }
            }

            // update the curve. Do this last to not mess with the curve caches we are iterating over
            UpdateEditorCurves(clip, updateBindings, updateCurves);
        }

        static void UpdateEditorCurve(AnimationClip clip, EditorCurveBinding binding, AnimationCurve curve)
        {
            if (curve.keys.Length == 0)
                AnimationUtility.SetEditorCurve(clip, binding, null);
            else
                AnimationUtility.SetEditorCurve(clip, binding, curve);
        }

        static void UpdateEditorCurves(AnimationClip clip, List<EditorCurveBinding> bindings, List<AnimationCurve> curves)
        {
            if (curves.Count == 0)
                return;

            for (var i = 0; i < curves.Count; i++)
            {
                UpdateEditorCurve(clip, bindings[i], curves[i]);
            }
            EditorUtility.SetDirty(clip);
        }

        public static void RemoveCurves(AnimationClip clip, SerializedProperty prop)
        {
            if (clip == null || prop == null)
                return;

            var toRemove = new List<EditorCurveBinding>();
            var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
            for (var i = 0; i < info.bindings.Length; i++)
            {
                var binding = info.bindings[i];

                // check if we match directly, or with a child object
                if (prop.propertyPath.Equals(binding.propertyName) || binding.propertyName.Contains(prop.propertyPath))
                {
                    toRemove.Add(binding);
                }
            }
            for (int i = 0; i < toRemove.Count; i++)
            {
                AnimationUtility.SetEditorCurve(clip, toRemove[i], null);
            }
        }

        // adds a stepped key frame to the given curve
        public static void AddKeyFrameToCurve(AnimationCurve curve, float time, float framerate, float value, bool stepped)
        {
            var key = new Keyframe();

            bool add = true;
            var keyIndex = GetKeyframeAtTime(curve, time, framerate);
            if (keyIndex != -1)
            {
                add = false;
                key = curve[keyIndex]; // retain the tangents and mode
                curve.RemoveKey(keyIndex);
            }

            key.value = value;
            key.time = GetKeyTime(time, framerate);
            keyIndex = curve.AddKey(key);

            if (stepped)
            {
                AnimationUtility.SetKeyBroken(curve, keyIndex, stepped);
                AnimationUtility.SetKeyLeftTangentMode(curve, keyIndex, AnimationUtility.TangentMode.Constant);
                AnimationUtility.SetKeyRightTangentMode(curve, keyIndex, AnimationUtility.TangentMode.Constant);
                key.outTangent = Mathf.Infinity;
                key.inTangent = Mathf.Infinity;
            }
            else if (add)
            {
                AnimationUtility.SetKeyLeftTangentMode(curve, keyIndex, AnimationUtility.TangentMode.ClampedAuto);
                AnimationUtility.SetKeyRightTangentMode(curve, keyIndex, AnimationUtility.TangentMode.ClampedAuto);
            }

            if (keyIndex != -1 && !stepped)
            {
                AnimationUtility.UpdateTangentsFromModeSurrounding(curve, keyIndex);
                AnimationUtility.SetKeyBroken(curve, keyIndex, false);
            }
        }

        // Removes a keyframe at the given time from the animation curve
        public static bool RemoveKeyFrameFromCurve(AnimationCurve curve, float time, float framerate)
        {
            var keyIndex = GetKeyframeAtTime(curve, time, framerate);
            if (keyIndex == -1)
                return false;

            curve.RemoveKey(keyIndex);
            return true;
        }

        // gets the value of the key
        public static float GetKeyValue(SerializedProperty prop)
        {
            switch (prop.propertyType)
            {
                case SerializedPropertyType.Integer:
                    return prop.intValue;
                case SerializedPropertyType.Boolean:
                    return prop.boolValue ? 1.0f : 0.0f;
                case SerializedPropertyType.Float:
                    return prop.floatValue;
                default:
                    Debug.LogError("Could not convert property type " + prop.propertyType.ToString() + " to float");
                    break;
            }
            return float.NaN;
        }

        public static void SetFromKeyValue(SerializedProperty prop, float keyValue)
        {
            switch (prop.propertyType)
            {
                case SerializedPropertyType.Float:
                {
                    prop.floatValue = keyValue;
                    return;
                }
                case SerializedPropertyType.Integer:
                {
                    prop.intValue = (int)keyValue;
                    return;
                }
                case SerializedPropertyType.Boolean:
                {
                    prop.boolValue = Math.Abs(keyValue) > 0.001f;
                    return;
                }
            }

            Debug.LogError("Could not convert float to property type " + prop.propertyType.ToString());
        }

        // gets the index of the key, -1 if not found
        public static int GetKeyframeAtTime(AnimationCurve curve, float time, float frameRate)
        {
            var range = 0.5f / frameRate;
            var keys = curve.keys;
            for (var i = 0; i < keys.Length; i++)
            {
                var k = keys[i];
                if (k.time >= time - range && k.time < time + range)
                {
                    return i;
                }
            }

            return -1;
        }

        public static int GetKeyframeAtTime(ObjectReferenceKeyframe[] curve, float time, float frameRate)
        {
            if (curve == null || curve.Length == 0)
                return -1;

            var range = 0.5f / frameRate;
            for (var i = 0; i < curve.Length; i++)
            {
                var t = curve[i].time;
                if (t >= time - range && t < time + range)
                {
                    return i;
                }
            }
            return -1;
        }

        public static float GetKeyTime(float time, float frameRate)
        {
            return Mathf.Round(time * frameRate) / frameRate;
        }

        public static int KeyCompare(float timeA, float timeB, float frameRate)
        {
            if (Mathf.Abs(timeA - timeB) <= 0.5f / frameRate)
                return 0;
            return timeA < timeB ? -1 : 1;
        }

        // Evaluates an object (bool curve)
        public static Object Evaluate(ObjectReferenceKeyframe[] curve, float time)
        {
            return curve[EvaluateIndex(curve, time)].value;
        }

        // returns the index from evaluation
        public static int EvaluateIndex(ObjectReferenceKeyframe[] curve, float time)
        {
            if (curve == null || curve.Length == 0)
                throw new InvalidOperationException("Can not evaluate a PPtr curve with no entries");

            // clamp conditions
            if (time <= curve[0].time)
                return 0;
            if (time >= curve.Last().time)
                return curve.Length - 1;

            // binary search
            var max = curve.Length - 1;
            var min = 0;
            while (max - min > 1)
            {
                var imid = (min + max) / 2;
                if (Mathf.Approximately(curve[imid].time, time))
                    return imid;
                if (curve[imid].time < time)
                    min = imid;
                else if (curve[imid].time > time)
                    max = imid;
            }
            return min;
        }

        // Shifts the animation clip so the time start at 0
        public static void ShiftBySeconds(this AnimationClip clip, float time)
        {
            var floatBindings = AnimationUtility.GetCurveBindings(clip);
            var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);

            // update the float curves
            foreach (var bind in floatBindings)
            {
                var curve = AnimationUtility.GetEditorCurve(clip, bind);
                var keys = curve.keys;
                for (var i = 0; i < keys.Length; i++)
                    keys[i].time += time;
                curve.keys = keys;
                AnimationUtility.SetEditorCurve(clip, bind, curve);
            }

            // update the PPtr curves
            foreach (var bind in objectBindings)
            {
                var curve = AnimationUtility.GetObjectReferenceCurve(clip, bind);
                for (var i = 0; i < curve.Length; i++)
                    curve[i].time += time;
                AnimationUtility.SetObjectReferenceCurve(clip, bind, curve);
            }

            EditorUtility.SetDirty(clip);
        }

        public static void ScaleTime(this AnimationClip clip, float scale)
        {
            var floatBindings = AnimationUtility.GetCurveBindings(clip);
            var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);

            // update the float curves
            foreach (var bind in floatBindings)
            {
                var curve = AnimationUtility.GetEditorCurve(clip, bind);
                var keys = curve.keys;
                for (var i = 0; i < keys.Length; i++)
                    keys[i].time *= scale;
                curve.keys = keys.OrderBy(x => x.time).ToArray();
                AnimationUtility.SetEditorCurve(clip, bind, curve);
            }

            // update the PPtr curves
            foreach (var bind in objectBindings)
            {
                var curve = AnimationUtility.GetObjectReferenceCurve(clip, bind);
                for (var i = 0; i < curve.Length; i++)
                    curve[i].time *= scale;
                curve = curve.OrderBy(x => x.time).ToArray();
                AnimationUtility.SetObjectReferenceCurve(clip, bind, curve);
            }

            EditorUtility.SetDirty(clip);
        }

        // Creates an opposing blend curve that matches the given curve to make sure the result is normalized
        public static AnimationCurve CreateMatchingCurve(AnimationCurve curve)
        {
            Keyframe[] keys = curve.keys;

            for (var i = 0; i != keys.Length; i++)
            {
                if (!Single.IsPositiveInfinity(keys[i].inTangent))
                    keys[i].inTangent = -keys[i].inTangent;
                if (!Single.IsPositiveInfinity(keys[i].outTangent))
                    keys[i].outTangent = -keys[i].outTangent;
                keys[i].value = 1.0f - keys[i].value;
            }
            return new AnimationCurve(keys);
        }

        // Sanitizes the keys on an animation to force the property to be normalized
        public static Keyframe[] SanitizeCurveKeys(Keyframe[] keys, bool easeIn)
        {
            if (keys.Length < 2)
            {
                if (easeIn)
                    keys = new[] { new Keyframe(0, 0), new Keyframe(1, 1) };
                else
                    keys = new[] { new Keyframe(0, 1), new Keyframe(1, 0) };
            }
            else if (easeIn)
            {
                keys[0].time = 0;
                keys[keys.Length - 1].time = 1;
                keys[keys.Length - 1].value = 1;
            }
            else
            {
                keys[0].time = 0;
                keys[0].value = 1;
                keys[keys.Length - 1].time = 1;
            }
            return keys;
        }
    }
}
