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

#if ! UNITY_EDITOR
#pragma warning disable CS0618 // Type or member is obsolete (for TransitionLibraries in Animancer Lite).
#endif
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.

using Animancer.TransitionLibraries;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace Animancer.Samples.TransitionLibraries
{
    /// <summary>
    /// Animancer's flexibility is usually quite helpful, but that's not the case in situations
    /// where you need to serialize the animation details because anything that can be changed
    /// will need to be serialized somehow.
    /// 
    /// There are two main reasons you might want to serialize the current details of
    /// Animancer, each with different priorities in their implementation:
    /// 
    /// - Files - When saving a file, the most important factor is generally forwards compatibility,
    /// i.e. the ability for files saved in an old version of your application to be loaded in a
    /// newer version.
    /// 
    /// - Networking - When synchronising animations over a network, you can generally force everyone
    /// to be using the same application version so it's generally much more important to minimise
    /// the amount of data you need to send in order to maximise speed.
    /// 
    /// Networking is a much more common use case for animation serialization so that's what this sample
    /// focusses on. However, actually setting up networking stuff is far beyond the scope of Animancer
    /// so the data structure focuses on efficiency but is then simply saved to a file for the sake of demonstration.
    /// 
    /// The reason that distinction is important is because data efficiency can be greatly improved by
    /// limiting what it can represent. For example, you could write a script to create 7 different
    /// `AnimancerState`s on the same layer, set some of their weights to 0.1, others to 0.2,
    /// have some states fading to other weights, some playing, some with different speeds, and so on.
    /// If you do that, then your serialized data would need to include all of those values.
    /// But most applications just stick with calling `animancerComponent.Play(transition)` which
    /// reduces the required serializable data to something like this:
    /// - Number of active states - Usually 1, can be 2 during a fade, or more if a fade is interrupted by another fade.
    /// - Remaining duration of the current fade - 0 when a fade isn't in progress.
    /// - Identifier of each state - This is an index retrieved from the Transition Library.
    /// - Current Time of each state.
    /// - Current Weight of each state.
    /// That allows the data to skip some things which can be inferred or recreated on the other side:
    /// - If a fade is in progress, sort the states so that the first one is the one that's fading in.
    /// That way the data doesn't need to include the target weight of each state.
    /// - If speeds aren't being controlled, they can just be initialized from their transitions.
    /// Or if they are, then the systems controlling them will probably also need to be synchronised
    /// so those systems can simply control the speed value in the same way on the other side.
    /// - If multiple layers were being used, then we would also need data for their weights and states.
    /// </summary>
    [Serializable]
    public class SerializablePose
    {
        /************************************************************************************************************************/

        public static StringReference Speed = "Speed";

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

        [SerializeField]
        private float _FadeDuration;

        [SerializeField]
        private float _SpeedParameter;

        [SerializeField]
        private List<StateData> _States = new();

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

        [Serializable]
        public struct StateData
        {
            /************************************************************************************************************************/

            /// <summary>
            /// The index of the state in the <see cref="Animancer.TransitionLibraries.TransitionLibrary"/>.
            /// </summary>
            /// <remarks>
            /// This is a <c>byte</c> because a library probably won't have more than 256 transitions.
            /// If it does, you would need a <c>short</c> or <c>int</c> instead.
            /// </remarks>
            [SerializeField]
            public byte index;

            /// <summary>The <see cref="AnimancerState.Time"/>.</summary>
            [SerializeField]
            public float time;

            /// <summary>The <see cref="AnimancerNode.Weight"/>.</summary>
            [SerializeField]
            public float weight;

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

            public readonly void Serialize(BinaryWriter stream)
            {
                stream.Write(index);
                stream.Write(time);
                stream.Write(weight);
            }

            public static StateData Deserialize(BinaryReader stream)
            {
                return new StateData()
                {
                    index = stream.ReadByte(),
                    time = stream.ReadSingle(),
                    weight = stream.ReadSingle(),
                };
            }

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

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

        public void GatherFrom(AnimancerComponent animancer)
        {
            _States.Clear();

            TransitionLibrary library = animancer.Transitions.Library;

            _FadeDuration = 0;
            int fadeInIndex = -1;

            IReadOnlyIndexedList<AnimancerState> activeStates = animancer.Layers[0].ActiveStates;
            for (int i = 0; i < activeStates.Count; i++)
            {
                AnimancerState state = activeStates[i];

                if (state.FadeGroup != null &&
                    state.TargetWeight == 1)
                {
                    fadeInIndex = i;
                    _FadeDuration = state.FadeGroup.FadeDuration;
                }

                _States.Add(new()
                {
                    index = (byte)library.IndexOf(state.Key),
                    time = state.Time,
                    weight = state.Weight,
                });
            }

            // If we have a state fading in, swap it with the first state
            // so we know which one it is after deserialization.
            if (fadeInIndex > 0)
            {
                (_States[0], _States[fadeInIndex]) = (_States[fadeInIndex], _States[0]);
            }

            _SpeedParameter = animancer.Parameters.GetFloat(Speed);
        }

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

        public void ApplyTo(AnimancerComponent animancer)
        {
            TransitionLibrary library = animancer.Transitions.Library;

            AnimancerLayer layer = animancer.Layers[0];
            layer.Stop();

            for (int i = _States.Count - 1; i >= 0; i--)
            {
                StateData stateData = _States[i];
                if (!library.TryGetTransition(stateData.index, out TransitionModifierGroup transition))
                    continue;

                AnimancerState state = layer.Play(transition.Transition);
                state.Time = stateData.time;
                state.SetWeight(stateData.weight);
            }

            animancer.Parameters.SetValue(Speed, _SpeedParameter);
        }

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

        public void Serialize(BinaryWriter stream)
        {
            stream.Write(_FadeDuration);

            stream.Write(_SpeedParameter);

            stream.Write((byte)_States.Count);

            for (int i = 0; i < _States.Count; i++)
                _States[i].Serialize(stream);
        }

        public void Deserialize(BinaryReader stream)
        {
            _FadeDuration = stream.ReadSingle();

            _SpeedParameter = stream.ReadSingle();

            byte count = stream.ReadByte();
            if (count < _States.Count)
            {
                _States.RemoveRange(count, _States.Count - count);
            }
            else
            {
                while (_States.Count < count)
                    _States.Add(new());
            }

            for (int i = 0; i < count; i++)
                _States[i] = StateData.Deserialize(stream);
        }

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