/**
 * 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.Framework;
using System;
using UnityEngine;
#if UNITY_2019_3_OR_NEWER
using UnityEngine.LowLevel;
using UnityEngine.PlayerLoop;
#elif UNITY_2018_1_OR_NEWER
using UnityEngine.Experimental.LowLevel;
using UnityEngine.Experimental.PlayerLoop;
#endif
namespace Live2D.Cubism.Core
{
    /// 
    /// Runtime Cubism model.
    /// 
    [ExecuteInEditMode, CubismDontMoveOnReimport]
    public sealed class CubismModel : MonoBehaviour
    {
        #region Delegates
        /// 
        /// Handler for .
        /// 
        /// Model the dymanic data applies to.
        /// New data.
        public delegate void DynamicDrawableDataHandler(CubismModel sender, CubismDynamicDrawableData[] data);
        #endregion
        #region Events
        /// 
        /// Event triggered if new  is available for instance.
        /// 
        public event DynamicDrawableDataHandler OnDynamicDrawableData;
        #endregion
        #region Factory Methods
        /// 
        /// Instantiates a .
        /// 
        /// Cubism moc to instantiate.
        /// Instance.
        public static CubismModel InstantiateFrom(CubismMoc moc)
        {
            // Return if argument is invalid.
            if (moc == null)
            {
                return null;
            }
            // Create model.
            var model = new GameObject(moc.name)
                .AddComponent();
            // Initialize it by resetting it.
            model.Reset(moc);
            return model;
        }
        #endregion
        /// 
        /// Resets a  reference in .
        /// 
        /// Target Cubism model.
        /// Cubism moc to reset.
        public static void ResetMocReference(CubismModel model, CubismMoc moc)
        {
            model.Moc = moc;
        }
        /// 
        ///  backing field.
        /// 
        [SerializeField, HideInInspector]
        private CubismMoc _moc;
        /// 
        /// Moc the instance was instantiated from.
        /// 
        public CubismMoc Moc
        {
            get { return _moc; }
            private set { _moc = value; }
        }
        /// 
        /// TaskableModel for unmanaged backend.
        /// 
        private CubismTaskableModel TaskableModel { get; set; }
        /// 
        ///  backing field.
        /// 
        [NonSerialized]
        private CubismParameter[] _parameters;
        /// 
        /// Drawables of model.
        /// 
        public CubismParameter[] Parameters
        {
            get
            {
                if (_parameters == null)
                {
                    Revive();
                }
                return _parameters;
            }
            private set { _parameters = value; }
        }
        /// 
        ///  backing field.
        /// 
        [NonSerialized]
        private CubismPart[] _parts;
        /// 
        /// Drawables of model.
        /// 
        public CubismPart[] Parts
        {
            get
            {
                if (_parts == null)
                {
                    Revive();
                }
                return _parts;
            }
            private set { _parts = value; }
        }
        /// 
        ///  backing field.
        /// 
        [NonSerialized]
        private CubismDrawable[] _drawables;
        /// 
        /// Drawables of model.
        /// 
        public CubismDrawable[] Drawables
        {
            get
            {
                if (_drawables == null)
                {
                    Revive();
                }
                return _drawables;
            }
            private set { _drawables = value; }
        }
        /// 
        ///  backing field.
        /// 
        [NonSerialized]
        private CubismCanvasInformation _canvasInformation;
        /// 
        /// Canvas information of model.
        /// 
        public CubismCanvasInformation CanvasInformation
        {
            get
            {
                if (_canvasInformation == null)
                {
                    Revive();
                }
                return _canvasInformation;
            }
            private set { _canvasInformation = value; }
        }
        /// 
        /// Parameter store cache.
        /// 
        CubismParameterStore _parameterStore;
        /// 
        /// True if instance is revived.
        /// 
        public bool IsRevived
        {
            get { return TaskableModel != null; }
        }
        /// 
        /// True if instance can revive.
        /// 
        private bool CanRevive
        {
            get { return Moc != null; }
        }
#if UNITY_2018_1_OR_NEWER
        /// 
        /// Model update functions for player loop.
        /// 
        [NonSerialized]
        private static Action _modelUpdateFunctions;
        private bool WasAttachedModelUpdateFunction { get; set; }
#endif
        /// 
        /// True on the frame the instance was enabled.
        /// 
        private bool WasJustEnabled { get; set; }
        /// 
        /// Frame number last update was done.
        /// 
        private int LastTick { get; set; }
        /// 
        /// Revives instance.
        /// 
        private void Revive()
        {
            // Return if already revive.
            if (IsRevived)
            {
                return;
            }
            // Return if revive isn't possible.
            if (!CanRevive)
            {
                return;
            }
            // Revive unmanaged model.
            TaskableModel = new CubismTaskableModel(Moc);
            // Revive proxies.
            Parameters = GetComponentsInChildren();
            Parts = GetComponentsInChildren();
            Drawables = GetComponentsInChildren();
            Parameters.Revive(TaskableModel.UnmanagedModel);
            Parts.Revive(TaskableModel.UnmanagedModel);
            Drawables.Revive(TaskableModel.UnmanagedModel);
            CanvasInformation = new CubismCanvasInformation(TaskableModel.UnmanagedModel);
            _parameterStore = GetComponent();
        }
        /// 
        /// Initializes instance for first use.
        /// 
        /// Moc to instantiate from.
        private void Reset(CubismMoc moc)
        {
            Moc = moc;
            name = moc.name;
            TaskableModel = new CubismTaskableModel(moc);
            // Create and initialize proxies.
            var parameters = CubismParameter.CreateParameters(TaskableModel.UnmanagedModel);
            var parts = CubismPart.CreateParts(TaskableModel.UnmanagedModel);
            var drawables = CubismDrawable.CreateDrawables(TaskableModel.UnmanagedModel);
            parameters.transform.SetParent(transform);
            parts.transform.SetParent(transform);
            drawables.transform.SetParent(transform);
            Parameters = parameters.GetComponentsInChildren();
            Parts = parts.GetComponentsInChildren();
            Drawables = drawables.GetComponentsInChildren();
            CanvasInformation = new CubismCanvasInformation(TaskableModel.UnmanagedModel);
        }
        /// 
        /// Forces update.
        /// 
        public void ForceUpdateNow()
        {
            WasJustEnabled = true;
            LastTick = -1;
            Revive();
#if UNITY_2018_1_OR_NEWER
            OnModelUpdate();
#else
            OnRenderObject();
#endif
        }
#if UNITY_2018_1_OR_NEWER
        /// 
        /// Calls model update functions for player loop.
        /// 
        private static void OnModelsUpdate()
        {
            if (_modelUpdateFunctions != null)
            {
                _modelUpdateFunctions.Invoke();
            }
        }
        /// 
        /// Register the model update function into the player loop.
        /// 
        [RuntimeInitializeOnLoadMethod]
        private static void RegisterCallbackFunction()
        {
            // Prepare the function for using player loop.
            var myPlayerLoopSystem = new PlayerLoopSystem()
            {
                type = typeof(CubismModel),     // Identifier for Profiler Hierarchy view.
                updateDelegate = OnModelsUpdate    // Register the function.
            };
            // Get the default player loop.
            var playerLoopSystem =
#if UNITY_2019_3_OR_NEWER
                PlayerLoop.GetCurrentPlayerLoop();
#else
                PlayerLoop.GetDefaultPlayerLoop();
#endif
            var playerLoopIndex = -1;
            for (var i = 0; i < playerLoopSystem.subSystemList.Length; i++)
            {
                if (playerLoopSystem.subSystemList[i].type != typeof(PreLateUpdate))
                {
                    continue;
                }
                playerLoopIndex = i;
                break;
            }
            if (playerLoopIndex < 0)
            {
                Debug.LogError("CubismModel : Failed to add processing to PlayerLoop.");
                return;
            }
            // Get the "PreLateUpdate" system.
            var playerLoopSubSystem = playerLoopSystem.subSystemList[playerLoopIndex];
            var subSystemList = playerLoopSubSystem.subSystemList;
            // Register the model update function after "PreLateUpdate" system.
            Array.Resize(ref subSystemList, subSystemList.Length + 1);
            subSystemList[subSystemList.Length - 1] = myPlayerLoopSystem;
            // Restore the "PreLateUpdate" sytem.
            playerLoopSubSystem.subSystemList = subSystemList;
            playerLoopSystem.subSystemList[playerLoopIndex] = playerLoopSubSystem;
            PlayerLoop.SetPlayerLoop(playerLoopSystem);
        }
#endif
        #region Unity Event Handling
        /// 
        /// Called by Unity. Triggers  to update.
        /// 
        private void Update()
        {
#if UNITY_2018_1_OR_NEWER
            if (!WasAttachedModelUpdateFunction)
            {
                _modelUpdateFunctions += OnModelUpdate;
                WasAttachedModelUpdateFunction = true;
            }
#endif
            // Return on first frame enabled.
            if (WasJustEnabled)
            {
                return;
            }
            // Return unless revived.
            if (!IsRevived)
            {
                return;
            }
            // Return if backend is ticking.
            if (!TaskableModel.DidExecute)
            {
                return;
            }
            // Sync parameters back.
            TaskableModel.TryReadParameters(Parameters);
            // restore last frame parameters value and parts opacity.
            if (_parameterStore != null)
            {
                _parameterStore.RestoreParameters();
            }
            // Trigger event.
            if (OnDynamicDrawableData == null)
            {
                return;
            }
            OnDynamicDrawableData(this, TaskableModel.DynamicDrawableData);
        }
        /// 
        /// Called by Unity. Blockingly updates  on first frame enabled; otherwise tries async update.
        /// 
        private void OnRenderObject()
        {
#if !UNITY_2018_1_OR_NEWER
            OnModelUpdate();
#endif
        }
        /// 
        /// Update model states.
        /// 
        private void OnModelUpdate()
        {
            // Return unless revived.
            if (!IsRevived)
            {
                return;
            }
            // Return if already ticked this frame.
            if (LastTick == Time.frameCount && Application.isPlaying)
            {
                return;
            }
            LastTick = Time.frameCount;
            // Try to sync parameters and parts (without caring whether task is executing or not).
            TaskableModel.TryWriteParametersAndParts(Parameters, Parts);
            // Return if task is executing.
            if (TaskableModel.IsExecuting)
            {
                return;
            }
            // Force blocking update on first frame enabled.
            if (WasJustEnabled)
            {
                // Force sync update.
                TaskableModel.UpdateNow();
                // Unset condition.
                WasJustEnabled = false;
                // Fetch results by calling own 'Update()'.
                Update();
                return;
            }
            // Enqueue update task.
            TaskableModel.Update();
        }
        /// 
        /// Called by Unity. Revives instance.
        /// 
        private void OnEnable()
        {
            WasJustEnabled = true;
            Revive();
        }
        private void OnDisable()
        {
#if UNITY_2018_1_OR_NEWER
            if (WasAttachedModelUpdateFunction)
            {
                _modelUpdateFunctions -= OnModelUpdate;
                WasAttachedModelUpdateFunction = false;
            }
#endif
        }
        /// 
        /// Called by Unity. Releases unmanaged memory.
        /// 
        private void OnDestroy()
        {
            if (!IsRevived)
            {
                return;
            }
            TaskableModel.ReleaseUnmanaged();
            TaskableModel = null;
        }
        /// 
        /// Called by Unity. Triggers .
        /// 
        private void OnValidate()
        {
            OnEnable();
        }
        #endregion
    }
}