/**
 * Copyright(c) Live2D Inc. All rights reserved.
 *
 * Use of this source code is governed by the Live2D Open Software license
 * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html.
 */
using System;
using System.Collections.Generic;
using Live2D.Cubism.Core;
using Live2D.Cubism.Framework.MouthMovement;
using Live2D.Cubism.Rendering;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
namespace Live2D.Cubism.Framework.Json
{
    /// 
    /// Contains Cubism motion3.json data.
    /// 
    [Serializable]
    // ReSharper disable once ClassCannotBeInstantiated
    public sealed class CubismMotion3Json
    {
        #region Load Methods
        /// 
        /// Loads a motion3.json asset.
        /// 
        /// motion3.json to deserialize.
        /// Deserialized motion3.json on success;  otherwise.
        public static CubismMotion3Json LoadFrom(string motion3Json)
        {
            if (string.IsNullOrEmpty(motion3Json))
            {
                return null;
            }
            var cubismMotion3Json = JsonUtility.FromJson(motion3Json);
            cubismMotion3Json.Meta.FadeInTime = -1.0f;
            cubismMotion3Json.Meta.FadeOutTime = -1.0f;
            for (var i = 0; i < cubismMotion3Json.Curves.Length; ++i)
            {
                cubismMotion3Json.Curves[i].FadeInTime = -1.0f;
                cubismMotion3Json.Curves[i].FadeOutTime = -1.0f;
            }
            JsonUtility.FromJsonOverwrite(motion3Json, cubismMotion3Json);
            return cubismMotion3Json;
        }
        /// 
        /// Loads a motion3.json asset.
        /// 
        /// motion3.json to deserialize.
        /// Deserialized motion3.json on success;  otherwise.
        public static CubismMotion3Json LoadFrom(TextAsset motion3JsonAsset)
        {
            return (motion3JsonAsset == null)
                ? null
                : LoadFrom(motion3JsonAsset.text);
        }
        #endregion
        #region Json Data
        /// 
        /// The model3.json format version.
        /// 
        [SerializeField]
        public int Version;
        /// 
        /// Motion meta info.
        /// 
        [SerializeField]
        public SerializableMeta Meta;
        /// 
        /// Curves.
        /// 
        [SerializeField]
        public SerializableCurve[] Curves;
        /// 
        /// User data.
        /// 
        [SerializeField]
        public SerializableUserData[] UserData;
        #endregion
        #region Constructors
        /// 
        /// Makes construction only possible through factories.
        /// 
        private CubismMotion3Json()
        {
        }
        #endregion
        /// 
        /// Converts motion curve segments into s.
        /// 
        /// Data to convert.
        /// Keyframes.
        public static Keyframe[] ConvertCurveSegmentsToKeyframes(float[] segments)
        {
            // Return early on invalid input.
            if (segments.Length < 1)
            {
                return new Keyframe[0];
            }
            // Initialize container for keyframes.
            var keyframes = new List { new Keyframe(segments[0], segments[1]) };
            // Parse segments.
            for (var i = 2; i < segments.Length;)
            {
                Parsers[segments[i]](segments, keyframes, ref i);
            }
            // Return result.
            return keyframes.ToArray();
        }
        /// 
        /// Converts stepped curves to liner curves.
        /// 
        /// Data to convert.
        /// Animation curve.
        public static AnimationCurve ConvertSteppedCurveToLinerCurver(CubismMotion3Json.SerializableCurve curve, float poseFadeInTime)
        {
            poseFadeInTime = (poseFadeInTime < 0) ? 0.5f : poseFadeInTime;
            var segments = curve.Segments;
            var segmentsCount = 2;
            for(var index = 2; index < curve.Segments.Length; index += 3)
            {
                // if current segment type is stepped and
                // next segment type is stepped or next segment is last segment
                // then convert segment type to liner.
                var currentSegmentTypeIsStepped = (curve.Segments[index] == 2);
                var currentSegmentIsLast = (index == (curve.Segments.Length - 3));
                var nextSegmentTypeIsStepped = (currentSegmentIsLast) ? false : (curve.Segments[index + 3] == 2);
                var nextSegmentIsLast = (currentSegmentIsLast) ? false : ((index + 3) == (curve.Segments.Length - 3));
                if ( currentSegmentTypeIsStepped && (nextSegmentTypeIsStepped || nextSegmentIsLast) )
                {
                    Array.Resize(ref segments, segments.Length + 3);
                    segments[segmentsCount + 0] = 0;
                    segments[segmentsCount + 1] = curve.Segments[index + 1];
                    segments[segmentsCount + 2] = curve.Segments[index - 1];
                    segments[segmentsCount + 3] = 0;
                    segments[segmentsCount + 4] = curve.Segments[index + 1] + poseFadeInTime;
                    segments[segmentsCount + 5] = curve.Segments[index + 2];
                    segmentsCount += 6;
                }
                else if(curve.Segments[index] == 1)
                {
                    segments[segmentsCount + 0] = curve.Segments[index + 0];
                    segments[segmentsCount + 1] = curve.Segments[index + 1];
                    segments[segmentsCount + 2] = curve.Segments[index + 2];
                    segments[segmentsCount + 3] = curve.Segments[index + 3];
                    segments[segmentsCount + 4] = curve.Segments[index + 4];
                    segments[segmentsCount + 5] = curve.Segments[index + 5];
                    segments[segmentsCount + 6] = curve.Segments[index + 6];
                    index += 4;
                    segmentsCount += 7;
                }
                else
                {
                    segments[segmentsCount + 0] = curve.Segments[index + 0];
                    segments[segmentsCount + 1] = curve.Segments[index + 1];
                    segments[segmentsCount + 2] = curve.Segments[index + 2];
                    segmentsCount += 3;
                }
            }
            return new AnimationCurve(ConvertCurveSegmentsToKeyframes(segments));
        }
        /// 
        /// Instantiates an .
        /// 
        /// Should import as original workflow.
        /// Should clear animation clip curves.
        /// Is function call form .
        /// pose3.json asset.
        /// The instantiated clip on success;  otherwise.
        /// 
        /// Note this method generates  clips when called at runtime.
        /// 
        public AnimationClip ToAnimationClip(bool shouldImportAsOriginalWorkflow = false, bool shouldClearAnimationCurves = false,
                                             bool isCallFormModelJson = false, CubismPose3Json poseJson = null)
        {
            // Check béziers restriction flag.
            if (!Meta.AreBeziersRestricted)
            {
                Debug.LogWarning("Béziers are not restricted and curves might be off. Please export motions from Cubism in restricted mode for perfect match.");
            }
            // Create animation clip.
            var animationClip = new AnimationClip
            {
#if UNITY_EDITOR
                frameRate = Meta.Fps
#else
                frameRate = Meta.Fps,
                legacy = true,
                wrapMode = (Meta.Loop)
                  ? WrapMode.Loop
                  : WrapMode.Default
#endif
            };
            return ToAnimationClip(animationClip, shouldImportAsOriginalWorkflow, shouldClearAnimationCurves, isCallFormModelJson, poseJson);
        }
        /// 
        /// Instantiates an .
        /// 
        /// Previous animation clip.
        /// Should import as original workflow.
        /// Should clear animation clip curves.
        /// Is function call form .
        /// pose3.json asset.
        /// The instantiated clip on success;  otherwise.
        /// 
        /// Note this method generates  clips when called at runtime.
        /// 
        public AnimationClip ToAnimationClip(AnimationClip animationClip, bool shouldImportAsOriginalWorkflow = false, bool shouldClearAnimationCurves = false
                                                                        , bool isCallFormModelJson = false, CubismPose3Json poseJson = null)
        {
            // Clear curves.
            if (shouldClearAnimationCurves && (!shouldImportAsOriginalWorkflow || (isCallFormModelJson && shouldImportAsOriginalWorkflow)))
            {
                animationClip.ClearCurves();
            }
            // Convert curves.
            for (var i = 0; i < Curves.Length; ++i)
            {
                var curve = Curves[i];
                // If should import as original workflow mode, skip add part opacity curve when call not from model3.json.
                if (curve.Target == "PartOpacity" && shouldImportAsOriginalWorkflow && !isCallFormModelJson)
                {
                    continue;
                }
                var relativePath = string.Empty;
                var type = default(Type);
                var propertyName = string.Empty;
                var animationCurve = new AnimationCurve(ConvertCurveSegmentsToKeyframes(curve.Segments));
                // Create model binding.
                if (curve.Target == "Model")
                {
                    // Bind opacity.
                    if (curve.Id == "Opacity")
                    {
                        relativePath = string.Empty;
                        propertyName = "Opacity";
                        type = typeof(CubismRenderController);
                    }
                    // Bind eye-blink.
                    else if (curve.Id == "EyeBlink")
                    {
                        relativePath = string.Empty;
                        propertyName = "EyeOpening";
                        type = typeof(CubismEyeBlinkController);
                    }
                    // Bind lip-sync.
                    else if (curve.Id == "LipSync")
                    {
                        relativePath = string.Empty;
                        propertyName = "MouthOpening";
                        type = typeof(CubismMouthController);
                    }
                }
                // Create parameter binding.
                else if (curve.Target == "Parameter")
                {
                    relativePath = "Parameters/" + curve.Id;
                    propertyName = "Value";
                    type = typeof(CubismParameter);
                }
                // Create part opacity binding.
                else if (curve.Target == "PartOpacity")
                {
                    relativePath = "Parts/" + curve.Id;
                    propertyName = "Opacity";
                    type = typeof(CubismPart);
                    // original workflow.
                    if (shouldImportAsOriginalWorkflow && poseJson != null && poseJson.FadeInTime != 0.0f)
                    {
                        animationCurve = ConvertSteppedCurveToLinerCurver(curve, poseJson.FadeInTime);
                    }
                }
#if UNITY_EDITOR
                var curveBinding = new EditorCurveBinding
                {
                    path = relativePath,
                    propertyName = propertyName,
                    type = type
                };
                AnimationUtility.SetEditorCurve(animationClip, curveBinding, animationCurve);
#else
                animationClip.SetCurve(relativePath, type, propertyName, animationCurve);
#endif
            }
#if UNITY_EDITOR
            // Apply settings.
            var animationClipSettings = new AnimationClipSettings
            {
                loopTime = Meta.Loop,
                stopTime = Meta.Duration
            };
            AnimationUtility.SetAnimationClipSettings(animationClip, animationClipSettings);
#endif
#if UNITY_EDITOR
            // Add animation events from user data.
            if (UserData != null)
            {
                var animationEvents = new List();
                for (var i = 0; i < UserData.Length; ++i)
                {
                    var animationEvent = new AnimationEvent
                    {
                        time = UserData[i].Time,
                        stringParameter = UserData[i].Value,
                    };
                    animationEvents.Add(animationEvent);
                }
                if (animationEvents.Count > 0)
                {
                    AnimationUtility.SetAnimationEvents(animationClip, animationEvents.ToArray());
                }
            }
#endif
            return animationClip;
        }
        #region Segment Parsing
        /// 
        /// Offset to use for setting of keyframes.
        /// 
        private const float OffsetGranularity = 0.01f;
        /// 
        /// Handles parsing of a single segment.
        /// 
        /// Curve segments.
        /// Buffer to append result to.
        /// Offset of segment.
        private delegate void SegmentParser(float[] segments, List result, ref int position);
        /// 
        /// Available segment parsers.
        /// 
        // ReSharper disable once InconsistentNaming
        private static Dictionary Parsers = new Dictionary
        {
            {0f, ParseLinearSegment},
            {1f, ParseBezierSegment},
            {2f, ParseSteppedSegment},
            {3f, ParseInverseSteppedSegment}
        };
        /// 
        /// Parses a linear segment.
        /// 
        /// Curve segments.
        /// Buffer to append result to.
        /// Offset of segment.
        private static void ParseLinearSegment(float[] segments, List result, ref int position)
        {
            // Compute slope.
            var length = (segments[position + 1] - result[result.Count - 1].time);
            var slope = (segments[position + 2] - result[result.Count - 1].value) / length;
            // Determine tangents.
            var outTangent = slope;
            var inTangent = outTangent;
            // Create keyframes.
            var keyframe = new Keyframe(
                result[result.Count - 1].time,
                result[result.Count - 1].value,
                result[result.Count - 1].inTangent,
                outTangent);
            result[result.Count - 1] = keyframe;
            keyframe = new Keyframe(
                segments[position + 1],
                segments[position + 2],
                inTangent,
                0);
            result.Add(keyframe);
            // Update position.
            position += 3;
        }
        /// 
        /// Parses a bezier segment.
        /// 
        /// Curve segments.
        /// Buffer to append result to.
        /// Offset of segment.
        private static void ParseBezierSegment(float[] segments, List result, ref int position)
        {
            // Compute tangents.
            var tangentLength = Mathf.Abs(result[result.Count - 1].time - segments[position + 5]) * 0.333333f;
            var outTangent = (segments[position + 2] - result[result.Count - 1].value) / tangentLength;
            var inTangent = (segments[position + 6] - segments[position + 4]) / tangentLength;
            // Create keyframes.
            var keyframe = new Keyframe(
                result[result.Count - 1].time,
                result[result.Count - 1].value,
                result[result.Count - 1].inTangent,
                outTangent);
            result[result.Count - 1] = keyframe;
            keyframe = new Keyframe(
                segments[position + 5],
                segments[position + 6],
                inTangent,
                0);
            result.Add(keyframe);
            // Update position.
            position += 7;
        }
        /// 
        /// Parses a stepped segment.
        /// 
        /// Curve segments.
        /// Buffer to append result to.
        /// Offset of segment.
        private static void ParseSteppedSegment(float[] segments, List result, ref int position)
        {
            // Create keyframe.
            result.Add(
                new Keyframe(segments[position + 1], segments[position + 2])
                {
                    inTangent = float.PositiveInfinity
                });
            // Update position.
            position += 3;
        }
        /// 
        /// Parses a inverse-stepped segment.
        /// 
        /// Curve segments.
        /// Buffer to append result to.
        /// Offset of segment.
        private static void ParseInverseSteppedSegment(float[] segments, List result, ref int position)
        {
            // Compute tangents.
            var keyframe = result[result.Count - 1];
            var tangent = (float)Math.Atan2(
                (segments[position + 2] - keyframe.value),
                (segments[position + 1] - keyframe.time));
            keyframe.outTangent = tangent;
            result[result.Count - 1] = keyframe;
            result.Add(
                new Keyframe(keyframe.time + OffsetGranularity, segments[position + 2])
                {
                    inTangent = tangent,
                    outTangent = 0
                });
            result.Add(
                new Keyframe(segments[position + 1], segments[position + 2])
                {
                    inTangent = 0
                });
            // Update position.
            position += 3;
        }
        #endregion
        #region Json Object Types
        /// 
        /// Motion meta info.
        /// 
        [Serializable]
        public struct SerializableMeta
        {
            /// 
            /// Duration in seconds.
            /// 
            [SerializeField]
            public float Duration;
            /// 
            /// Framerate in seconds.
            /// 
            [SerializeField]
            public float Fps;
            /// 
            /// True if motion is looping.
            /// 
            [SerializeField]
            public bool Loop;
            /// 
            /// Number of curves.
            /// 
            [SerializeField]
            public int CurveCount;
            /// 
            /// Total number of curve segments.
            /// 
            [SerializeField]
            public int TotalSegmentCount;
            /// 
            /// Total number of curve points.
            /// 
            [SerializeField]
            public int TotalPointCount;
            /// 
            /// True if beziers are restricted.
            /// 
            [SerializeField]
            public bool AreBeziersRestricted;
            /// 
            /// Total number of UserData.
            /// 
            [SerializeField]
            public int UserDataCount;
            /// 
            /// Total size of UserData in bytes.
            /// 
            [SerializeField]
            public int TotalUserDataSize;
            /// 
            /// [Optional] Time of the Fade-In for easing in seconds.
            /// 
            [SerializeField]
            public float FadeInTime;
            /// 
            /// [Optional] Time of the Fade-Out for easing in seconds.
            /// 
            [SerializeField]
            public float FadeOutTime;
        };
        /// 
        /// Single motion curve.
        /// 
        [Serializable]
        public struct SerializableCurve
        {
            /// 
            /// Target type.
            /// 
            [SerializeField]
            public string Target;
            /// 
            /// Id within target.
            /// 
            [SerializeField]
            public string Id;
            /// 
            /// Flattened curve segments.
            /// 
            [SerializeField]
            public float[] Segments;
            /// 
            /// [Optional] Time of the overall Fade-In for easing in seconds.
            /// 
            [SerializeField]
            public float FadeInTime;
            /// 
            /// [Optional] Time of the overall Fade-Out for easing in seconds.
            /// 
            [SerializeField]
            public float FadeOutTime;
        };
        /// 
        /// User data.
        /// 
        [Serializable]
        public struct SerializableUserData
        {
            /// 
            /// Time in seconds.
            /// 
            [SerializeField]
            public float Time;
            /// 
            /// Content of user data.
            /// 
            [SerializeField]
            public string Value;
        }
        #endregion
    }
}