// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //

using System;
using System.Collections.Generic;
using UnityEngine;

namespace Animancer
{
    /// https://kybernetik.com.au/animancer/api/Animancer/ControllerState
    partial class ControllerState
    {
        /************************************************************************************************************************/

        private SerializableParameterBindings _SerializedParameterBindings;

        /// <summary>Serialized data used to create <see cref="ParameterBinding{T}"/>s at runtime.</summary>
        public SerializableParameterBindings SerializedParameterBindings
        {
            get => _SerializedParameterBindings;
            set
            {
                _SerializedParameterBindings = value;
                DeserializeParameterBindings();
            }
        }

        /// <summary>Deserializes the <see cref="SerializedParameterBindings"/>.</summary>
        private void DeserializeParameterBindings()
        {
            if (Graph == null)
                return;

            DisposeParameterBindings();
            _SerializedParameterBindings?.Deserialize(this);
        }

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

        private List<IDisposable> _ParameterBindings;

        /// <summary>
        /// Adds an object to a list for <see cref="IDisposable.Dispose"/>
        /// to be called in <see cref="Destroy"/>.
        /// </summary>
        private void AddParameterBinding(IDisposable disposable)
        {
            _ParameterBindings ??= new();
            _ParameterBindings.Add(disposable);
        }

        /// <summary>Disposes everything added by <see cref="AddParameterBinding"/>.</summary>
        private void DisposeParameterBindings()
        {
            if (_ParameterBindings == null)
                return;

            for (int i = _ParameterBindings.Count - 1; i >= 0; i--)
                _ParameterBindings[i].Dispose();

            _ParameterBindings.Clear();
        }

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

        /// <summary>
        /// Configures all parameters in the <see cref="Controller"/>
        /// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public void BindAllParameters()
        {
            var count = Playable.GetParameterCount();
            for (int i = 0; i < count; i++)
            {
                var parameter = Playable.GetParameter(i);
                BindParameter(parameter.name, parameter);
            }
        }

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

        /// <summary>
        /// Configures a parameter in the <see cref="Controller"/>
        /// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public void BindParameter(StringReference name)
            => BindParameter(name, name);

        /// <summary>
        /// Configures a parameter in the <see cref="Controller"/>
        /// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public void BindParameter(StringReference animancerParameter, string controllerParameterName)
            => BindParameter(animancerParameter, Animator.StringToHash(controllerParameterName));

        /// <summary>
        /// Configures a parameter in the <see cref="Controller"/>
        /// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public void BindParameter(StringReference animancerParameter, int controllerParameterHash)
        {
            var count = Playable.GetParameterCount();
            for (int i = 0; i < count; i++)
            {
                var parameter = Playable.GetParameter(i);
                if (parameter.nameHash == controllerParameterHash)
                {
                    BindParameter(animancerParameter, parameter);
                    break;
                }
            }
        }

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

        /// <summary>
        /// Configures all parameters in the <see cref="Controller"/>
        /// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public void BindParameter(
            StringReference animancerParameter,
            AnimatorControllerParameter controllerParameter)
        {
            switch (controllerParameter.type)
            {
                case AnimatorControllerParameterType.Float:
                    BindFloat(animancerParameter, controllerParameter.nameHash);
                    break;

                case AnimatorControllerParameterType.Int:
                    BindInt(animancerParameter, controllerParameter.nameHash);
                    break;

                case AnimatorControllerParameterType.Bool:
                case AnimatorControllerParameterType.Trigger:
                    BindBool(animancerParameter, controllerParameter.nameHash);
                    break;
            }
        }

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

        /// <summary>
        /// Configures a parameter in the <see cref="Controller"/>
        /// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public ParameterBinding<bool> BindBool(StringReference name)
            => BindBool(name, name);

        /// <summary>
        /// Configures a parameter in the <see cref="Controller"/>
        /// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public ParameterBinding<bool> BindBool(StringReference animancerParameter, string controllerParameterName)
            => BindBool(animancerParameter, Animator.StringToHash(controllerParameterName));

        /// <summary>
        /// Configures a parameter in the <see cref="Controller"/>
        /// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public ParameterBinding<bool> BindBool(StringReference animancerParameter, int controllerParameterHash)
        {
            var parameter = Graph.Parameters.GetOrCreate<bool>(animancerParameter);
            var binding = new ParameterBinding<bool>(
                parameter,
                value => Playable.SetBool(controllerParameterHash, value));
            AddParameterBinding(binding);
            return binding;
        }

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

        /// <summary>
        /// Configures a parameter in the <see cref="Controller"/>
        /// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public ParameterBinding<float> BindFloat(StringReference name)
            => BindFloat(name, name);

        /// <summary>
        /// Configures a parameter in the <see cref="Controller"/>
        /// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public ParameterBinding<float> BindFloat(StringReference animancerParameter, string controllerParameterName)
            => BindFloat(animancerParameter, Animator.StringToHash(controllerParameterName));

        /// <summary>
        /// Configures a parameter in the <see cref="Controller"/>
        /// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public ParameterBinding<float> BindFloat(StringReference animancerParameter, int controllerParameterHash)
        {
            var parameter = Graph.Parameters.GetOrCreate<float>(animancerParameter);
            var binding = new ParameterBinding<float>(
                parameter,
                value => Playable.SetFloat(controllerParameterHash, value));
            AddParameterBinding(binding);
            return binding;
        }

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

        /// <summary>
        /// Configures a parameter in the <see cref="Controller"/>
        /// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public ParameterBinding<int> BindInt(StringReference name)
            => BindInt(name, name);

        /// <summary>
        /// Configures a parameter in the <see cref="Controller"/>
        /// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public ParameterBinding<int> BindInt(StringReference animancerParameter, string controllerParameterName)
            => BindInt(animancerParameter, Animator.StringToHash(controllerParameterName));

        /// <summary>
        /// Configures a parameter in the <see cref="Controller"/>
        /// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
        /// </summary>
        public ParameterBinding<int> BindInt(StringReference animancerParameter, int controllerParameterHash)
        {
            var parameter = Graph.Parameters.GetOrCreate<int>(animancerParameter);
            var binding = new ParameterBinding<int>(
                parameter,
                value => Playable.SetInteger(controllerParameterHash, value));
            AddParameterBinding(binding);
            return binding;
        }

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

        /// <summary>An <see cref="IDisposable"/> binding to <see cref="Parameter{T}.OnValueChanged"/>.</summary>
        /// https://kybernetik.com.au/animancer/api/Animancer/ParameterBinding_1
        public class ParameterBinding<T> : IDisposable
        {
            /************************************************************************************************************************/

            /// <summary>The parameter being watched.</summary>
            public readonly Parameter<T> Parameter;

            /// <summary>The callback to invoke when the parameter changes.</summary>
            public readonly Action<T> OnParameterChanged;

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

            /// <summary>
            /// Invokes `onParameterChanged` and adds it to the <see cref="Parameter{T}.OnValueChanged"/>
            /// to be removed by <see cref="Dispose"/>.
            /// </summary>
            public ParameterBinding(
                Parameter<T> parameter,
                Action<T> onParameterChanged)
            {
                Parameter = parameter;
                OnParameterChanged = onParameterChanged;

                OnParameterChanged(Parameter.Value);
                Parameter.OnValueChanged += OnParameterChanged;
            }

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

            /// <summary>
            /// Removes <see cref="OnParameterChanged"/> from the <see cref="Parameter{T}.OnValueChanged"/>.
            /// </summary>
            public void Dispose()
            {
                Parameter.OnValueChanged -= OnParameterChanged;
            }

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

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

        /// <summary>
        /// A serializable array of data which can create <see cref="ParameterBinding{T}"/>s at runtime.
        /// </summary>
        /// <remarks>
        /// This data contains a <see cref="Bindings"/> array and <see cref="Mode"/> flag:
        /// <list type="bullet">
        /// 
        /// <item>
        /// If the array is empty,
        /// <c>true</c> will bind all parameters by name
        /// and <c>false</c> will bind nothing.
        /// </item>
        /// 
        /// <item>
        /// Otherwise, <c>true</c> will bind <c>[i * 2]</c> in the <see cref="RuntimeAnimatorController"/>
        /// to <c>[i * 2 + 1]</c> in the <see cref="AnimancerGraph.Parameters"/>.
        /// </item>
        /// 
        /// <item>
        /// And <c>false</c> will bind each of its parameters to the same name in both systems.
        /// </item>
        /// 
        /// </list>
        /// </remarks>
        /// https://kybernetik.com.au/animancer/api/Animancer/SerializableParameterBindings
        [Serializable]
        public class SerializableParameterBindings :
            ICloneable<SerializableParameterBindings>
        {
            /************************************************************************************************************************/

            [SerializeField]
            private bool _Mode;

            /// <summary>[<see cref="SerializeField"/>]
            /// Modifies the way the <see cref="Bindings"/> array is interpreted.
            /// </summary>
            /// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
            public ref bool Mode
                => ref _Mode;

#if UNITY_EDITOR
            /// <summary>[Editor-Only] The name of the serialized backing field of <see cref="Mode"/>.</summary>
            public const string ModeFieldName = nameof(_Mode);
#endif

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

            /// <summary>[<see cref="SerializeField"/>]
            /// Should all parameters in the <see cref="RuntimeAnimatorController"/> be bound by name?
            /// </summary>
            /// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
            public bool BindAllParameters
            {
                get => _Mode && _Bindings.Length == 0;
                set
                {
                    _Mode = value;

                    if (value)
                    {
                        _Bindings = Array.Empty<StringAsset>();
                    }
                    else
                    {
                        Debug.Assert(
                            _Bindings.Length == 0,
                            $"{nameof(BindAllParameters)} can't be disabled unless the {nameof(Bindings)}" +
                            $" array is empty because it changes the meaning of that array.");
                    }
                }
            }

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

            /// <summary>[<see cref="SerializeField"/>]
            /// Should the <see cref="Bindings"/> be grouped into pairs
            /// to bind each <see cref="RuntimeAnimatorController"/> parameter
            /// to the subsequent parameter in <see cref="AnimancerGraph.Parameters"/>?
            /// </summary>
            /// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
            public bool RebindNames
            {
                get => _Mode && _Bindings.Length > 0;
                set
                {
                    _Mode = value;

                    if (value)
                    {
                        if (_Bindings.Length % 2 != 0)
                            Array.Resize(ref _Bindings, _Bindings.Length + 1);
                    }
                    else
                    {
                        Debug.Assert(
                            _Bindings.Length == 0,
                            $"{nameof(RebindNames)} can't be disabled unless the {nameof(Bindings)}" +
                            $" array is empty because it changes the meaning of that array.");
                    }
                }
            }

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

            [SerializeField]
            private StringAsset[] _Bindings = Array.Empty<StringAsset>();

            /// <summary>[<see cref="SerializeField"/>]
            /// Parameter names used to have parameters in the <see cref="RuntimeAnimatorController"/>
            /// follow the value of parameters in the <see cref="AnimancerGraph.Parameters"/>.
            /// </summary>
            /// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
            public StringAsset[] Bindings
            {
                get => _Bindings;
                set
                {
                    Debug.Assert(
                        value != null,
                        $"{nameof(Bindings)} can't be null. Use Array.Empty<StringAsset>() instead.");

                    _Bindings = value;
                }
            }

#if UNITY_EDITOR
            /// <summary>[Editor-Only] The name of the serialized backing field of <see cref="Bindings"/>.</summary>
            public const string BindingsFieldName = nameof(_Bindings);
#endif

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

            /// <summary>Creates runtime bindings for the `state`.</summary>
            /// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
            public void Deserialize(ControllerState state)
            {
                if (_Bindings.Length == 0)
                {
                    if (_Mode)
                        state.BindAllParameters();
                    // Else do nothing.
                }
                else
                {
                    if (_Mode)
                    {
                        for (int i = 0; i < _Bindings.Length - 1; i += 2)
                        {
                            var controller = _Bindings[i];
                            var animancer = _Bindings[i + 1];
                            if (controller == null ||
                                animancer == null)
                                continue;

                            state.BindParameter(animancer, controller);
                        }
                    }
                    else
                    {
                        for (int i = 0; i < _Bindings.Length; i++)
                        {
                            var name = _Bindings[i];
                            if (name == null)
                                continue;

                            state.BindParameter(name);
                        }
                    }
                }
            }

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

            /// <inheritdoc/>
            public SerializableParameterBindings Clone(CloneContext context)
            {
                var bindingCount = Bindings != null ? Bindings.Length : 0;
                var clone = new SerializableParameterBindings()
                {
                    BindAllParameters = BindAllParameters,
                    Bindings = new StringAsset[bindingCount],
                };

                for (int i = 0; i < bindingCount; i++)
                    clone.Bindings[i] = context.GetCloneOrOriginal(Bindings[i]);

                return clone;
            }

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

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

