/**
 * 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 Live2D.Cubism.Core;
using System;
using System.IO;
using Live2D.Cubism.Framework.MouthMovement;
using Live2D.Cubism.Framework.Physics;
using Live2D.Cubism.Framework.UserData;
using Live2D.Cubism.Framework.Pose;
using Live2D.Cubism.Framework.Expression;
using Live2D.Cubism.Framework.MotionFade;
using Live2D.Cubism.Framework.Raycasting;
using Live2D.Cubism.Rendering;
using Live2D.Cubism.Rendering.Masking;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
namespace Live2D.Cubism.Framework.Json
{
    /// 
    /// Exposes moc3.json asset data.
    /// 
    [Serializable]
    // ReSharper disable once ClassCannotBeInstantiated
    public sealed class CubismModel3Json
    {
        #region Delegates
        /// 
        /// Handles the loading of assets.
        /// 
        /// The asset type to load.
        /// The path to the asset.
        /// 
        public delegate object LoadAssetAtPathHandler(Type assetType, string assetPath);
        /// 
        /// Picks a  for a .
        /// 
        /// Event source.
        /// Drawable to pick for.
        /// Picked material.
        public delegate Material MaterialPicker(CubismModel3Json sender, CubismDrawable drawable);
        /// 
        /// Picks a  for a .
        /// 
        /// Event source.
        /// Drawable to pick for.
        /// Picked texture.
        public delegate Texture2D TexturePicker(CubismModel3Json sender, CubismDrawable drawable);
        #endregion
        #region Load Methods
        /// 
        /// Loads a model.json asset.
        /// 
        /// The path to the asset.
        /// The  on success;  otherwise.
        public static CubismModel3Json LoadAtPath(string assetPath)
        {
            // Use default asset load handler.
            return LoadAtPath(assetPath, BuiltinLoadAssetAtPath);
        }
        /// 
        /// Loads a model.json asset.
        /// 
        /// The path to the asset.
        /// Handler for loading assets.
        /// The  on success;  otherwise.
        public static CubismModel3Json LoadAtPath(string assetPath, LoadAssetAtPathHandler loadAssetAtPath)
        {
            // Load Json asset.
            var modelJsonAsset = loadAssetAtPath(typeof(string), assetPath) as string;
            // Return early in case Json asset wasn't loaded.
            if (modelJsonAsset == null)
            {
                return null;
            }
            // Deserialize Json.
            var modelJson = JsonUtility.FromJson(modelJsonAsset);
            // Finalize deserialization.
            modelJson.AssetPath = assetPath;
            modelJson.LoadAssetAtPath = loadAssetAtPath;
            // Set motion references.
            var value = CubismJsonParser.ParseFromString(modelJsonAsset);
            // Return early if there is no references.
            if (!value.Get("FileReferences").GetMap(null).ContainsKey("Motions"))
            {
                return modelJson;
            }
            var motionGroupNames = value.Get("FileReferences").Get("Motions").KeySet().ToArray();
            modelJson.FileReferences.Motions.GroupNames = motionGroupNames;
            var motionGroupNamesCount = motionGroupNames.Length;
            modelJson.FileReferences.Motions.Motions = new SerializableMotion[motionGroupNamesCount][];
            for (var i = 0; i < motionGroupNamesCount; i++)
            {
                var motionGroup = value.Get("FileReferences").Get("Motions").Get(motionGroupNames[i]);
                var motionCount = motionGroup.GetVector(null).ToArray().Length;
                modelJson.FileReferences.Motions.Motions[i] = new SerializableMotion[motionCount];
                for (var j = 0; j < motionCount; j++)
                {
                    if (motionGroup.Get(j).GetMap(null).ContainsKey("File"))
                    {
                        modelJson.FileReferences.Motions.Motions[i][j].File = motionGroup.Get(j).Get("File").toString();
                    }
                    if (motionGroup.Get(j).GetMap(null).ContainsKey("Sound"))
                    {
                        modelJson.FileReferences.Motions.Motions[i][j].Sound = motionGroup.Get(j).Get("Sound").toString();
                    }
                    if (motionGroup.Get(j).GetMap(null).ContainsKey("FadeInTime"))
                    {
                        modelJson.FileReferences.Motions.Motions[i][j].FadeInTime = motionGroup.Get(j).Get("FadeInTime").ToFloat();
                    }
                    if (motionGroup.Get(j).GetMap(null).ContainsKey("FadeOutTime"))
                    {
                        modelJson.FileReferences.Motions.Motions[i][j].FadeOutTime = motionGroup.Get(j).Get("FadeOutTime").ToFloat();
                    }
                }
            }
            return modelJson;
        }
        #endregion
        /// 
        /// Path to .
        /// 
        public string AssetPath { get; private set; }
        /// 
        /// Method for loading assets.
        /// 
        private LoadAssetAtPathHandler LoadAssetAtPath { get; set; }
        #region Json Data
        /// 
        /// The motion3.json format version.
        /// 
        [SerializeField]
        public int Version;
        /// 
        /// The file references.
        /// 
        [SerializeField]
        public SerializableFileReferences FileReferences;
        /// 
        /// Groups.
        /// 
        [SerializeField]
        public SerializableGroup[] Groups;
        /// 
        /// Hit areas.
        /// 
        [SerializeField]
        public SerializableHitArea[] HitAreas;
        #endregion
        /// 
        /// The contents of the referenced moc3 asset.
        /// 
        /// 
        /// The contents isn't cached internally.
        /// 
        public byte[] Moc3
        {
            get
            {
                return LoadReferencedAsset(FileReferences.Moc);
            }
        }
        /// 
        ///  backing field.
        /// 
        [NonSerialized]
        private CubismPose3Json _pose3Json;
        /// 
        /// The contents of pose3.json asset.
        /// 
        public CubismPose3Json Pose3Json
        {
            get
            {
                if(_pose3Json != null)
                {
                    return _pose3Json;
                }
                var jsonString = string.IsNullOrEmpty(FileReferences.Pose) ? null : LoadReferencedAsset(FileReferences.Pose);
                _pose3Json = CubismPose3Json.LoadFrom(jsonString);
                return _pose3Json;
            }
        }
        /// 
        ///  backing field.
        /// 
        [NonSerialized]
        private CubismExp3Json[] _expression3Jsons;
        /// 
        /// The referenced expression assets.
        /// 
        /// 
        /// The references aren't cached internally.
        /// 
        public CubismExp3Json[] Expression3Jsons
        {
            get
            {
                // Fail silently...
                if(FileReferences.Expressions == null)
                {
                    return null;
                }
                // Load expression only if necessary.
                if (_expression3Jsons == null)
                {
                    _expression3Jsons = new CubismExp3Json[FileReferences.Expressions.Length];
                    for (var i = 0; i < _expression3Jsons.Length; ++i)
                    {
                        var expressionJson = (string.IsNullOrEmpty(FileReferences.Expressions[i].File))
                                                ? null
                                                : LoadReferencedAsset(FileReferences.Expressions[i].File);
                        _expression3Jsons[i] = CubismExp3Json.LoadFrom(expressionJson);
                    }
                }
                return _expression3Jsons;
            }
        }
        /// 
        /// The contents of physics3.json asset.
        /// 
        public string Physics3Json
        {
            get
            {
                return string.IsNullOrEmpty(FileReferences.Physics) ? null : LoadReferencedAsset(FileReferences.Physics);
            }
        }
        public string UserData3Json
        {
            get
            {
                return string.IsNullOrEmpty(FileReferences.UserData) ? null : LoadReferencedAsset(FileReferences.UserData);
            }
        }
        /// 
        /// The contents of cdi3.json asset.
        /// 
        public string DisplayInfo3Json
        {
            get
            {
                return string.IsNullOrEmpty(FileReferences.DisplayInfo) ? null : LoadReferencedAsset(FileReferences.DisplayInfo);
            }
        }
        /// 
        ///  backing field.
        /// 
        [NonSerialized]
        private Texture2D[] _textures;
        /// 
        /// The referenced texture assets.
        /// 
        /// 
        /// The references aren't cached internally.
        /// 
        public Texture2D[] Textures
        {
            get
            {
                // Load textures only if necessary.
                if (_textures == null)
                {
                    _textures = new Texture2D[FileReferences.Textures.Length];
                    for (var i = 0; i < _textures.Length; ++i)
                    {
                        _textures[i] = LoadReferencedAsset(FileReferences.Textures[i]);
                    }
                }
                return _textures;
            }
        }
        #region Constructors
        /// 
        /// Makes construction only possible through factories.
        /// 
        private CubismModel3Json()
        {
        }
        #endregion
        /// 
        /// Instantiates a model source and a model with the default texture set.
        /// 
        /// Should import as original workflow.
        /// The instantiated model on success;  otherwise.
        public CubismModel ToModel(bool shouldImportAsOriginalWorkflow = false)
        {
            return ToModel(CubismBuiltinPickers.MaterialPicker, CubismBuiltinPickers.TexturePicker, shouldImportAsOriginalWorkflow);
        }
        /// 
        /// Instantiates a model source and a model.
        /// 
        /// The material mapper to use.
        /// The texture mapper to use.
        /// Should import as original workflow.
        /// The instantiated model on success;  otherwise.
        public CubismModel ToModel(MaterialPicker pickMaterial, TexturePicker pickTexture, bool shouldImportAsOriginalWorkflow = false)
        {
            // Initialize model source and instantiate it.
            var mocAsBytes = Moc3;
            if (mocAsBytes == null)
            {
                return null;
            }
            var moc = CubismMoc.CreateFrom(mocAsBytes);
            var model = CubismModel.InstantiateFrom(moc);
            model.name = Path.GetFileNameWithoutExtension(FileReferences.Moc);
#if UNITY_EDITOR
            // Add parameters and parts inspectors.
            model.gameObject.AddComponent();
            model.gameObject.AddComponent();
#endif
            // Create renderers.
            var rendererController = model.gameObject.AddComponent();
            var renderers = rendererController.Renderers;
            var drawables = model.Drawables;
            // Initialize materials.
            for (var i = 0; i < renderers.Length; ++i)
            {
                renderers[i].Material = pickMaterial(this, drawables[i]);
            }
            // Initialize textures.
            for (var i = 0; i < renderers.Length; ++i)
            {
                renderers[i].MainTexture = pickTexture(this, drawables[i]);
            }
            // Initialize drawables.
            if(HitAreas != null)
            {
                for (var i = 0; i < HitAreas.Length; i++)
                {
                    for (var j = 0; j < drawables.Length; j++)
                    {
                        if (drawables[j].Id == HitAreas[i].Id)
                        {
                            // Add components for hit judgement to HitArea target Drawables.
                            var hitDrawable = drawables[j].gameObject.AddComponent();
                            hitDrawable.Name = HitAreas[i].Name;
                            drawables[j].gameObject.AddComponent();
                            break;
                        }
                    }
                }
            }
            //Load from cdi3.json
            var DisplayInfo3JsonAsString = DisplayInfo3Json;
            var cdi3Json = CubismDisplayInfo3Json.LoadFrom(DisplayInfo3JsonAsString);
            // Initialize groups.
            var parameters = model.Parameters;
            for (var i = 0; i < parameters.Length; ++i)
            {
                if (IsParameterInGroup(parameters[i], "EyeBlink"))
                {
                    if (model.gameObject.GetComponent() == null)
                    {
                        model.gameObject.AddComponent();
                    }
                    parameters[i].gameObject.AddComponent();
                }
                // Set up mouth parameters.
                if (IsParameterInGroup(parameters[i], "LipSync"))
                {
                    if (model.gameObject.GetComponent() == null)
                    {
                        model.gameObject.AddComponent();
                    }
                    parameters[i].gameObject.AddComponent();
                }
                // Setting up the parameter name for display.
                if (cdi3Json != null)
                {
                    var cubismDisplayInfoParameterName = parameters[i].gameObject.AddComponent();
                    cubismDisplayInfoParameterName.Name = cdi3Json.Parameters[i].Name;
                    cubismDisplayInfoParameterName.DisplayName = string.Empty;
                }
            }
            // Setting up the part name for display.
            if (cdi3Json != null)
            {
                // Initialize groups.
                var parts = model.Parts;
                for (var i = 0; i < parts.Length; i++)
                {
                    var cubismDisplayInfoPartNames = parts[i].gameObject.AddComponent();
                    cubismDisplayInfoPartNames.Name = cdi3Json.Parts[i].Name;
                    cubismDisplayInfoPartNames.DisplayName = string.Empty;
                }
            }
            // Add mask controller if required.
            for (var i = 0; i < drawables.Length; ++i)
            {
                if (!drawables[i].IsMasked)
                {
                    continue;
                }
                // Add controller exactly once...
                model.gameObject.AddComponent();
                break;
            }
            // Add original workflow component if is original workflow.
            if(shouldImportAsOriginalWorkflow)
            {
                // Add cubism update manager.
                var updateManager = model.gameObject.GetComponent();
                if(updateManager == null)
                {
                    model.gameObject.AddComponent();
                }
                // Add parameter store.
                var parameterStore = model.gameObject.GetComponent();
                if(parameterStore == null)
                {
                    parameterStore = model.gameObject.AddComponent();
                }
                // Add pose controller.
                var poseController = model.gameObject.GetComponent();
                if(poseController == null)
                {
                    poseController = model.gameObject.AddComponent();
                }
                // Add expression controller.
                var expressionController = model.gameObject.GetComponent();
                if(expressionController == null)
                {
                    expressionController = model.gameObject.AddComponent();
                }
                // Add fade controller.
                var motionFadeController = model.gameObject.GetComponent();
                if(motionFadeController == null)
                {
                    motionFadeController = model.gameObject.AddComponent();
                }
            }
            // Initialize physics if JSON exists.
            var physics3JsonAsString = Physics3Json;
            if (!string.IsNullOrEmpty(physics3JsonAsString))
            {
                var physics3Json = CubismPhysics3Json.LoadFrom(physics3JsonAsString);
                var physicsController = model.gameObject.GetComponent();
                if (physicsController == null)
                {
                    physicsController = model.gameObject.AddComponent();
                }
                physicsController.Initialize(physics3Json.ToRig());
            }
            var userData3JsonAsString = UserData3Json;
            if (!string.IsNullOrEmpty(userData3JsonAsString))
            {
                var userData3Json = CubismUserData3Json.LoadFrom(userData3JsonAsString);
                var drawableBodies = userData3Json.ToBodyArray(CubismUserDataTargetType.ArtMesh);
                for (var i = 0; i < drawables.Length; ++i)
                {
                    var index = GetBodyIndexById(drawableBodies, drawables[i].Id);
                    if (index >= 0)
                    {
                        var tag = drawables[i].gameObject.GetComponent();
                        if (tag == null)
                        {
                            tag = drawables[i].gameObject.AddComponent();
                        }
                        tag.Initialize(drawableBodies[index]);
                    }
                }
            }
            if (model.gameObject.GetComponent() == null)
            {
                model.gameObject.AddComponent();
            }
            // Make sure model is 'fresh'
            model.ForceUpdateNow();
            return model;
        }
        #region Helper Methods
        /// 
        /// Type-safely loads an asset.
        /// 
        /// Asset type.
        /// Path to asset.
        /// The asset on success;  otherwise.
        private T LoadReferencedAsset(string referencedFile) where T : class
        {
            var assetPath = Path.GetDirectoryName(AssetPath) + "/" + referencedFile;
            return LoadAssetAtPath(typeof(T), assetPath) as T;
        }
        /// 
        /// Builtin method for loading assets.
        /// 
        /// Asset type.
        /// Path to asset.
        /// The asset on success;  otherwise.
        private static object BuiltinLoadAssetAtPath(Type assetType, string assetPath)
        {
            // Explicitly deal with byte arrays.
            if (assetType == typeof(byte[]))
            {
#if UNITY_EDITOR
                return File.ReadAllBytes(assetPath);
#else
                var textAsset = Resources.Load(assetPath, typeof(TextAsset)) as TextAsset;
                return (textAsset != null)
                    ? textAsset.bytes
                    : null;
#endif
            }
            else if (assetType == typeof(string))
            {
#if UNITY_EDITOR
                return File.ReadAllText(assetPath);
#else
                var textAsset = Resources.Load(assetPath, typeof(TextAsset)) as TextAsset;
                return (textAsset != null)
                    ? textAsset.text
                    : null;
#endif
            }
#if UNITY_EDITOR
            return AssetDatabase.LoadAssetAtPath(assetPath, assetType);
#else
            return Resources.Load(assetPath, assetType);
#endif
        }
        /// 
        /// Checks whether the parameter is an eye blink parameter.
        /// 
        /// Parameter to check.
        /// Name of group to query for.
        ///  if parameter is an eye blink parameter;  otherwise.
        private bool IsParameterInGroup(CubismParameter parameter, string groupName)
        {
            // Return early if groups aren't available...
            if (Groups == null || Groups.Length == 0)
            {
                return false;
            }
            for (var i = 0; i < Groups.Length; ++i)
            {
                if (Groups[i].Name != groupName)
                {
                    continue;
                }
                if(Groups[i].Ids != null)
                {
                    for (var j = 0; j < Groups[i].Ids.Length; ++j)
                    {
                        if (Groups[i].Ids[j] == parameter.name)
                        {
                            return true;
                        }
                    }
                }
            }
            return false;
        }
        /// 
        /// Get body index from body array by Id.
        /// 
        /// Target body array.
        /// Id for find.
        /// Array index if Id found; -1 otherwise.
        private int GetBodyIndexById(CubismUserDataBody[] bodies, string id)
        {
            for (var i = 0; i < bodies.Length; ++i)
            {
                if (bodies[i].Id == id)
                {
                    return i;
                }
            }
            return -1;
        }
        #endregion
        #region Json Helpers
        /// 
        /// File references data.
        /// 
        [Serializable]
        public struct SerializableFileReferences
        {
            /// 
            /// Relative path to the moc3 asset.
            /// 
            [SerializeField]
            public string Moc;
            /// 
            /// Relative paths to texture assets.
            /// 
            [SerializeField]
            public string[] Textures;
            /// 
            /// Relative path to the pose3.json.
            /// 
            [SerializeField]
            public string Pose;
            /// 
            /// Relative path to the expression asset.
            /// 
            [SerializeField]
            public SerializableExpression[] Expressions;
            /// 
            /// Relative path to the pose motion3.json.
            /// 
            [SerializeField]
            public SerializableMotions Motions;
            /// 
            /// Relative path to the physics asset.
            /// 
            [SerializeField]
            public string Physics;
            /// 
            /// Relative path to the user data asset.
            /// 
            [SerializeField]
            public string UserData;
            /// 
            /// Relative path to the cdi3.json.
            /// 
            [SerializeField]
            public string DisplayInfo;
        }
        /// 
        /// Group data.
        /// 
        [Serializable]
        public struct SerializableGroup
        {
            /// 
            /// Target type.
            /// 
            [SerializeField]
            public string Target;
            /// 
            /// Group name.
            /// 
            [SerializeField]
            public string Name;
            /// 
            /// Referenced IDs.
            /// 
            [SerializeField]
            public string[] Ids;
        }
        /// 
        /// Expression data.
        /// 
        [Serializable]
        public struct SerializableExpression
        {
            /// 
            /// Expression Name.
            /// 
            [SerializeField]
            public string Name;
            /// 
            /// Expression File.
            /// 
            [SerializeField]
            public string File;
            /// 
            /// Expression FadeInTime.
            /// 
            [SerializeField]
            public float FadeInTime;
            /// 
            /// Expression FadeOutTime.
            /// 
            [SerializeField]
            public float FadeOutTime;
        }
        /// 
        /// Motion data.
        /// 
        [Serializable]
        public struct SerializableMotions
        {
            /// 
            /// Motion group names.
            /// 
            [SerializeField]
            public string[] GroupNames;
            /// 
            /// Motion groups.
            /// 
            [SerializeField]
            public SerializableMotion[][] Motions;
        }
        /// 
        /// Motion data.
        /// 
        [Serializable]
        public struct SerializableMotion
        {
            /// 
            /// File path.
            /// 
            [SerializeField]
            public string File;
            /// 
            /// Sound path.
            /// 
            [SerializeField]
            public string Sound;
            /// 
            /// Fade in time.
            /// 
            [SerializeField]
            public float FadeInTime;
            /// 
            /// Fade out time.
            /// 
            [SerializeField]
            public float FadeOutTime;
        }
        /// 
        /// Hit Area.
        /// 
        [Serializable]
        public struct SerializableHitArea
        {
            /// 
            /// Hit area name.
            /// 
            [SerializeField]
            public string Name;
            /// 
            /// Hit area id.
            /// 
            [SerializeField]
            public string Id;
        }
        #endregion
    }
}