/**
 * 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 Live2D.Cubism.Framework.Motion;
using UnityEngine;
namespace Live2D.Cubism.Framework.MotionFade
{
    /// 
    /// Cubism fade controller.
    /// 
    [RequireComponent(typeof(Animator))]
    public class CubismFadeController : MonoBehaviour, ICubismUpdatable
    {
        #region Variable
        /// 
        /// Cubism fade motion list.
        /// 
        [SerializeField]
        public CubismFadeMotionList CubismFadeMotionList;
        /// 
        /// Parameters cache.
        /// 
        private CubismParameter[] DestinationParameters { get; set; }
        /// 
        /// Parts cache.
        /// 
        private CubismPart[] DestinationParts { get; set; }
        /// 
        /// Model has motion controller component.
        /// 
        private CubismMotionController _motionController;
        /// 
        /// Model has cubism update controller component.
        /// 
        [HideInInspector]
        public bool HasUpdateController { get; set; }
        /// 
        /// Fade state machine behavior set in the animator.
        /// 
        private ICubismFadeState[] _fadeStates;
        /// 
        /// Model has animator component.
        /// 
        private Animator _animator;
        /// 
        /// Restore parameter value.
        /// 
        private CubismParameterStore _parameterStore;
        /// 
        /// Fading flags for each layer.
        /// 
        private bool[] _isFading;
        #endregion
        #region Function
        /// 
        /// Refreshes the controller. Call this method after adding and/or removing s.
        /// 
        public void Refresh()
        {
            _animator = GetComponent();
            // Fail silently...
            if (_animator == null)
            {
                return;
            }
            DestinationParameters = this.FindCubismModel().Parameters;
            DestinationParts = this.FindCubismModel().Parts;
            _motionController = GetComponent();
            _parameterStore = GetComponent();
            // Get cubism update controller.
            HasUpdateController = (GetComponent() != null);
            _fadeStates = (ICubismFadeState[])_animator.GetBehaviours();
            if ((_fadeStates == null || _fadeStates.Length == 0) && _motionController != null)
            {
                _fadeStates = _motionController.GetFadeStates();
            }
            if (_fadeStates == null)
            {
                return;
            }
            _isFading = new bool[_fadeStates.Length];
        }
        /// 
        /// Called by cubism update controller. Order to invoke OnLateUpdate.
        /// 
        public int ExecutionOrder
        {
            get { return CubismUpdateExecutionOrder.CubismFadeController; }
        }
        /// 
        /// Called by cubism update controller. Needs to invoke OnLateUpdate on Editing.
        /// 
        public bool NeedsUpdateOnEditing
        {
            get { return false; }
        }
        /// 
        /// Called by cubism update controller. Updates controller.
        /// 
        /// 
        /// Make sure this method is called after any animations are evaluated.
        /// 
        public void OnLateUpdate()
        {
            // Fail silently.
            if (!enabled || _fadeStates == null || _parameterStore == null
               || DestinationParameters == null || DestinationParts == null)
            {
                return;
            }
            var time = Time.time;
            for (var i = 0; i < _fadeStates.Length; ++i)
            {
                _isFading[i] = false;
                var playingMotions = _fadeStates[i].GetPlayingMotions();
                if (playingMotions == null || playingMotions.Count <= 1)
                {
                    continue;
                }
                var latestPlayingMotion = playingMotions[playingMotions.Count - 1];
                var playingMotionData = latestPlayingMotion.Motion;
                var elapsedTime = time - latestPlayingMotion.StartTime;
                for (var j = 0; j < playingMotionData.ParameterFadeInTimes.Length; j++)
                {
                    if ((elapsedTime <= playingMotionData.FadeInTime) ||
                        ((0 <= playingMotionData.ParameterFadeInTimes[j]) &&
                          (elapsedTime <= playingMotionData.ParameterFadeInTimes[j])))
                    {
                        _isFading[i] = true;
                        break;
                    }
                }
            }
            var isFadingAllFinished = true;
            for (var i = 0; i < _fadeStates.Length; ++i)
            {
                var playingMotions = _fadeStates[i].GetPlayingMotions();
                var playingMotionCount = playingMotions.Count - 1;
                if (_isFading[i])
                {
                    isFadingAllFinished = false;
                    continue;
                }
                for (var j = playingMotionCount; j >= 0; --j)
                {
                    if (playingMotions.Count <= 1)
                    {
                        break;
                    }
                    var playingMotion = playingMotions[j];
                    if (time <= playingMotion.EndTime)
                    {
                        continue;
                    }
                    // If fade-in has been completed, delete the motion that has been played back.
                    _fadeStates[i].StopAnimation(j);
                }
            }
            if (isFadingAllFinished)
            {
                return;
            }
            _parameterStore.RestoreParameters();
            // Update sources and destinations.
            for (var i = 0; i < _fadeStates.Length; ++i)
            {
                if (!_isFading[i])
                {
                    continue;
                }
                UpdateFade(_fadeStates[i]);
            }
        }
        /// 
        /// Update motion fade.
        /// 
        /// Fade state observer.
        private void UpdateFade(ICubismFadeState fadeState)
        {
            var playingMotions = fadeState.GetPlayingMotions();
            if (playingMotions == null)
            {
                // Do not process if there is only one motion, if it does not switch.
                return;
            }
            // Weight set for the layer being processed.
            // (In the case of the layer located at the top, it is forced to 1.)
            var layerWeight = fadeState.GetLayerWeight();
            var time = Time.time;
            // Set playing motions end time.
            if ((playingMotions.Count > 0) && (playingMotions[playingMotions.Count - 1].Motion != null) && (playingMotions[playingMotions.Count - 1].IsLooping))
            {
                var motion = playingMotions[playingMotions.Count - 1];
                var newEndTime = time + motion.Motion.FadeOutTime;
                motion.EndTime = newEndTime;
                while (true)
                {
                    if ((motion.StartTime + motion.Motion.MotionLength) >= time)
                    {
                        break;
                    }
                    motion.StartTime += motion.Motion.MotionLength;
                }
                playingMotions[playingMotions.Count - 1] = motion;
            }
            // Calculate MotionFade.
            for (var i = 0; i < playingMotions.Count; i++)
            {
                var playingMotion = playingMotions[i];
                var fadeMotion = playingMotion.Motion;
                if (fadeMotion == null)
                {
                    continue;
                }
                var elapsedTime = time - playingMotion.StartTime;
                var endTime = playingMotion.EndTime - elapsedTime;
                var fadeInTime = fadeMotion.FadeInTime;
                var fadeOutTime = fadeMotion.FadeOutTime;
                var fadeInWeight = (fadeInTime <= 0.0f)
                    ? 1.0f
                    : CubismFadeMath.GetEasingSine(elapsedTime / fadeInTime);
                var fadeOutWeight = (fadeOutTime <= 0.0f)
                    ? 1.0f
                    : CubismFadeMath.GetEasingSine((playingMotion.EndTime - Time.time) / fadeOutTime);
                playingMotions[i] = playingMotion;
                var motionWeight = (i == 0)
                    ? (fadeInWeight * fadeOutWeight)
                    : (fadeInWeight * fadeOutWeight * layerWeight);
                // Apply to parameter values
                for (var j = 0; j < DestinationParameters.Length; ++j)
                {
                    var index = -1;
                    for (var k = 0; k < fadeMotion.ParameterIds.Length; ++k)
                    {
                        if (fadeMotion.ParameterIds[k] != DestinationParameters[j].Id)
                        {
                            continue;
                        }
                        index = k;
                        break;
                    }
                    if (index < 0)
                    {
                        // There is not target ID curve in motion.
                        continue;
                    }
                    DestinationParameters[j].Value = Evaluate(
                            fadeMotion.ParameterCurves[index], elapsedTime, endTime,
                            fadeInWeight, fadeOutWeight,
                            fadeMotion.ParameterFadeInTimes[index], fadeMotion.ParameterFadeOutTimes[index],
                            motionWeight, DestinationParameters[j].Value);
                }
                // Apply to part opacities
                for (var j = 0; j < DestinationParts.Length; ++j)
                {
                    var index = -1;
                    for (var k = 0; k < fadeMotion.ParameterIds.Length; ++k)
                    {
                        if (fadeMotion.ParameterIds[k] != DestinationParts[j].Id)
                        {
                            continue;
                        }
                        index = k;
                        break;
                    }
                    if (index < 0)
                    {
                        // There is not target ID curve in motion.
                        continue;
                    }
                    DestinationParts[j].Opacity = Evaluate(
                            fadeMotion.ParameterCurves[index], elapsedTime, endTime,
                            fadeInWeight, fadeOutWeight,
                            fadeMotion.ParameterFadeInTimes[index], fadeMotion.ParameterFadeOutTimes[index],
                            motionWeight, DestinationParts[j].Opacity);
                }
            }
        }
        /// 
        /// Evaluate fade curve.
        /// 
        /// Curves to be evaluated.
        /// Elapsed Time.
        /// Fading end time.
        /// Fade in time.
        /// Fade out time.
        /// Fade in time parameter.
        /// Fade out time parameter.
        /// Motion weight.
        /// Current value with weight applied.
        public float Evaluate(
            AnimationCurve curve, float elapsedTime, float endTime,
            float fadeInTime, float fadeOutTime,
            float parameterFadeInTime, float parameterFadeOutTime,
            float motionWeight, float currentValue)
        {
            if (curve.length <= 0)
            {
                return currentValue;
            }
            // Motion fade.
            if (parameterFadeInTime < 0.0f &&
                parameterFadeOutTime < 0.0f)
            {
                return currentValue + (curve.Evaluate(elapsedTime) - currentValue) * motionWeight;
            }
            // Parameter fade.
            float fadeInWeight, fadeOutWeight;
            if (parameterFadeInTime < 0.0f)
            {
                fadeInWeight = fadeInTime;
            }
            else
            {
                fadeInWeight = (parameterFadeInTime < float.Epsilon)
                    ? 1.0f
                    : CubismFadeMath.GetEasingSine(elapsedTime / parameterFadeInTime);
            }
            if (parameterFadeOutTime < 0.0f)
            {
                fadeOutWeight = fadeOutTime;
            }
            else
            {
                fadeOutWeight = (parameterFadeOutTime < float.Epsilon)
                    ? 1.0f
                    : CubismFadeMath.GetEasingSine(endTime / parameterFadeOutTime);
            }
            var parameterWeight = fadeInWeight * fadeOutWeight;
            return currentValue + (curve.Evaluate(elapsedTime) - currentValue) * parameterWeight;
        }
        #endregion
        #region Unity Events Handling
        /// 
        /// Initializes instance.
        /// 
        private void OnEnable()
        {
            // Initialize cache.
            Refresh();
        }
        /// 
        /// Called by Unity.
        /// 
        private void LateUpdate()
        {
            if (!HasUpdateController)
            {
                OnLateUpdate();
            }
        }
        #endregion
    }
}