// Animancer // Copyright 2020 Kybernetik //

using System;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;

namespace Animancer
{
    /// <summary>[Pro-Only]
    /// An <see cref="AnimancerState"/> which blends an array of other states together using linear interpolation
    /// between the specified thresholds.
    /// <para></para>
    /// This mixer type is similar to the 1D Blend Type in Mecanim Blend Trees.
    /// </summary>
    public sealed class LinearMixerState : MixerState<float>
    {
        /************************************************************************************************************************/

        /// <summary>
        /// Constructs a new <see cref="LinearMixerState"/> without connecting it to the <see cref="PlayableGraph"/>.
        /// </summary>
        private LinearMixerState(AnimancerPlayable root) : base(root) { }

        /// <summary>
        /// Constructs a new <see cref="LinearMixerState"/> and connects it to the `layer`.
        /// </summary>
        public LinearMixerState(AnimancerLayer layer) : base(layer) { }

        /// <summary>
        /// Constructs a new <see cref="LinearMixerState"/> and connects it to the `parent` at the specified
        /// `index`.
        /// </summary>
        public LinearMixerState(AnimancerNode parent, int index) : base(parent, index) { }

        /************************************************************************************************************************/

        /// <summary>
        /// Initialises the <see cref="AnimationMixerPlayable"/> and <see cref="ManualMixerState.States"/> with one
        /// state per clip and assigns thresholds evenly spaced between the specified min and max (inclusive).
        /// </summary>
        public void Initialise(AnimationClip[] clips, float minThreshold = 0, float maxThreshold = 1)
        {
            Initialise(clips);
            AssignLinearThresholds(minThreshold, maxThreshold);
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Initialises the <see cref="AnimationMixerPlayable"/> with two ports and connects two states to them for
        /// the specified clips at the specified thresholds (default 0 and 1).
        /// </summary>
        public void Initialise(AnimationClip clip0, AnimationClip clip1, float threshold0 = 0, float threshold1 = 1)
        {
            _Playable.SetInputCount(2);

            States = new AnimancerState[2];
            new ClipState(this, 0, clip0);
            new ClipState(this, 1, clip1);

            SetThresholds(new float[]
            {
                threshold0,
                threshold1,
            });
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Initialises the <see cref="AnimationMixerPlayable"/> with three ports and connects three states to them for
        /// the specified clips at the specified thresholds (default -1, 0, and 1).
        /// </summary>
        public void Initialise(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2, float threshold0 = -1, float threshold1 = 0, float threshold2 = 1)
        {
            _Playable.SetInputCount(3);

            States = new AnimancerState[3];
            new ClipState(this, 0, clip0);
            new ClipState(this, 1, clip1);
            new ClipState(this, 2, clip2);

            SetThresholds(new float[]
            {
                threshold0,
                threshold1,
                threshold2,
            });
        }

        /************************************************************************************************************************/

#if UNITY_EDITOR
        /// <summary>
        /// Called whenever the thresholds are changed. In the Unity Editor this method calls
        /// <see cref="AssertThresholdsSorted"/> then <see cref="RecalculateWeights"/> while at runtime it only calls
        /// the latter.
        /// </summary>
        public override void OnThresholdsChanged()
        {
            AssertThresholdsSorted();

            base.OnThresholdsChanged();
        }
#endif

        /************************************************************************************************************************/

        /// <summary>
        /// Throws an <see cref="ArgumentException"/> unless the thresholds are sorted from lowest to highest.
        /// </summary>
        /// <exception cref="ArgumentException"/>
        public void AssertThresholdsSorted()
        {
            if (!HasThresholds())
                throw new InvalidOperationException("LinearAnimationMixer: no Thresholds have been assigned");

            var previous = float.NegativeInfinity;

            int count = States.Length;
            for (int i = 0; i < count; i++)
            {
                var state = States[i];
                if (state == null)
                    continue;

                var next = GetThreshold(i);
                if (next > previous)
                    previous = next;
                else
                    throw new ArgumentException("LinearAnimationMixer: Thresholds are out of order. They must be sorted from lowest to highest with no equal values.");
            }
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Recalculates the weights of all <see cref="ManualMixerState.States"/> based on the current value of the
        /// <see cref="MixerState{TParameter}.Parameter"/> and the thresholds.
        /// </summary>
        public override void RecalculateWeights()
        {
            WeightsAreDirty = false;

            // Go through all states, figure out how much weight to give those with thresholds adjacent to the
            // current parameter value using linear interpolation, and set all others to 0 weight.

            var index = 0;
            var previousState = GetNextState(ref index);
            if (previousState == null)
                return;

            var previousThreshold = GetThreshold(index);

            if (Parameter <= previousThreshold)
            {
                previousState.Weight = 1;
                DisableRemainingStates(index);
                return;
            }

            var count = States.Length;
            while (++index < count)
            {
                var nextState = GetNextState(ref index);
                if (nextState == null)
                    break;

                var nextThreshold = GetThreshold(index);

                if (Parameter > previousThreshold && Parameter <= nextThreshold)
                {
                    var t = (Parameter - previousThreshold) / (nextThreshold - previousThreshold);
                    previousState.Weight = 1 - t;
                    nextState.Weight = t;
                    DisableRemainingStates(index);
                    return;
                }
                else
                {
                    previousState.Weight = 0;
                }

                previousState = nextState;
                previousThreshold = nextThreshold;
            }

            previousState.Weight = Parameter > previousThreshold ? 1 : 0;
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Assigns the thresholds to be evenly spaced between the specified min and max (inclusive).
        /// </summary>
        public void AssignLinearThresholds(float min = 0, float max = 1)
        {
            var count = States.Length;

            var thresholds = new float[count];

            var increment = (max - min) / (count - 1);

            for (int i = 0; i < count; i++)
            {
                thresholds[i] =
                    i < count - 1 ?
                    min + i * increment :// Assign each threshold linearly spaced between the min and max.
                    max;// and ensure that the last one is exactly at the max (to avoid floating-point error).
            }

            SetThresholds(thresholds);
        }

        /************************************************************************************************************************/
        #region Inspector
        /************************************************************************************************************************/

        /// <summary>The number of parameters being managed by this state.</summary>
        protected override int ParameterCount { get { return 1; } }

        /// <summary>Returns the name of a parameter being managed by this state.</summary>
        /// <exception cref="NotSupportedException">Thrown if this state doesn't manage any parameters.</exception>
        protected override string GetParameterName(int index) { return "Parameter"; }

        /// <summary>Returns the type of a parameter being managed by this state.</summary>
        /// <exception cref="NotSupportedException">Thrown if this state doesn't manage any parameters.</exception>
        protected override AnimatorControllerParameterType GetParameterType(int index) { return AnimatorControllerParameterType.Float; }

        /// <summary>Returns the value of a parameter being managed by this state.</summary>
        /// <exception cref="NotSupportedException">Thrown if this state doesn't manage any parameters.</exception>
        protected override object GetParameterValue(int index) { return Parameter; }

        /// <summary>Sets the value of a parameter being managed by this state.</summary>
        /// <exception cref="NotSupportedException">Thrown if this state doesn't manage any parameters.</exception>
        protected override void SetParameterValue(int index, object value) { Parameter = (float)value; }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region Transition
        /************************************************************************************************************************/

        /// <summary>
        /// A serializable <see cref="ITransition"/> which can create a <see cref="LinearMixerState"/> when
        /// passed into <see cref="AnimancerPlayable.Play(ITransition)"/>.
        /// </summary>
        /// <remarks>
        /// Unfortunately the tool used to generate this documentation does not currently support nested types with
        /// identical names, so only one <c>Transition</c> class will actually have a documentation page.
        /// </remarks>
        [Serializable]
        public new class Transition : Transition<LinearMixerState, float>
        {
            /************************************************************************************************************************/

            /// <summary>
            /// Creates and returns a new <see cref="LinearMixerState"/> connected to the `layer`.
            /// <para></para>
            /// This method also assigns it as the <see cref="AnimancerState.Transition{TState}.State"/>.
            /// </summary>
            public override LinearMixerState CreateState(AnimancerLayer layer)
            {
                State = new LinearMixerState(layer);
                InitialiseState();
                return State;
            }

            /************************************************************************************************************************/
            #region Drawer
#if UNITY_EDITOR
            /************************************************************************************************************************/

            /// <summary>[Editor-Only] Adds context menu functions for this transition.</summary>
            protected override void AddItemsToContextMenu(UnityEditor.GenericMenu menu, UnityEditor.SerializedProperty property,
                Editor.Serialization.PropertyAccessor accessor)
            {
                base.AddItemsToContextMenu(menu, property, accessor);
                Drawer.AddItemsToContextMenu(menu);
            }

            /************************************************************************************************************************/

            /// <summary>[Editor-Only] Draws the Inspector GUI for a <see cref="Transition"/>.</summary>
            [UnityEditor.CustomPropertyDrawer(typeof(Transition), true)]
            public class Drawer : TransitionDrawer
            {
                /************************************************************************************************************************/

                /// <summary>
                /// Fills the `menu` with functions relevant to the `rootProperty`.
                /// </summary>
                public static void AddItemsToContextMenu(UnityEditor.GenericMenu menu, string prefix = "Calculate Thresholds/")
                {
                    AddPropertyModifierFunction(menu, prefix + "Evenly Spaced", (_) =>
                    {
                        var count = CurrentThresholds.arraySize;
                        if (count <= 1)
                            return;

                        var first = CurrentThresholds.GetArrayElementAtIndex(0).floatValue;
                        var last = CurrentThresholds.GetArrayElementAtIndex(count - 1).floatValue;
                        for (int i = 0; i < count; i++)
                        {
                            CurrentThresholds.GetArrayElementAtIndex(i).floatValue = Mathf.Lerp(first, last, i / (float)(count - 1));
                        }
                    });

                    AddCalculateThresholdsFunction(menu, prefix + "From Speed",
                        (clip, threshold) => clip.apparentSpeed);
                    AddCalculateThresholdsFunction(menu, prefix + "From Velocity X",
                        (clip, threshold) => clip.averageSpeed.x);
                    AddCalculateThresholdsFunction(menu, prefix + "From Velocity Y",
                        (clip, threshold) => clip.averageSpeed.z);
                    AddCalculateThresholdsFunction(menu, prefix + "From Velocity Z",
                        (clip, threshold) => clip.averageSpeed.z);
                    AddCalculateThresholdsFunction(menu, prefix + "From Angular Speed (Rad)",
                        (clip, threshold) => clip.averageAngularSpeed * Mathf.Deg2Rad);
                    AddCalculateThresholdsFunction(menu, prefix + "From Angular Speed (Deg)",
                        (clip, threshold) => clip.averageAngularSpeed);
                }

                /************************************************************************************************************************/

                /// <summary><see cref="AddThresholdItemsToMenu"/> will add some functions to the menu.</summary>
                protected override bool HasThresholdContextMenu { get { return true; } }

                /// <summary>Adds functions to the `menu` relating to the thresholds.</summary>
                protected override void AddThresholdItemsToMenu(UnityEditor.GenericMenu menu)
                {
                    AddItemsToContextMenu(menu, null);
                }

                /************************************************************************************************************************/

                private static void AddCalculateThresholdsFunction(UnityEditor.GenericMenu menu, string label,
                    Func<AnimationClip, float, float> calculateThreshold)
                {
                    AddPropertyModifierFunction(menu, label, (property) =>
                    {
                        var count = CurrentClips.arraySize;
                        for (int i = 0; i < count; i++)
                        {
                            var clip = CurrentClips.GetArrayElementAtIndex(i).objectReferenceValue as AnimationClip;
                            if (clip == null)
                                continue;

                            var threshold = CurrentThresholds.GetArrayElementAtIndex(i);

                            threshold.floatValue = calculateThreshold(clip, threshold.floatValue);
                        }
                    });
                }

                /************************************************************************************************************************/
            }

            /************************************************************************************************************************/
#endif
            #endregion
            /************************************************************************************************************************/
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
    }
}