// Animancer // Copyright 2020 Kybernetik //

#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.

//#define ANIMANCER_ULT_EVENTS

// If you edit this file to change the callback type to something other than UltEvents, you will need to change this
// alias as well as the HasPersistentCalls method below.
#if ANIMANCER_ULT_EVENTS
using SerializableCallback = UltEvents.UltEvent;
#else
using SerializableCallback = UnityEngine.Events.UnityEvent;
#endif

using UnityEngine;
using System;

namespace Animancer
{
    partial struct AnimancerEvent
    {
        partial class Sequence
        {
            /// <summary>
            /// An <see cref="AnimancerEvent.Sequence"/> that can be serialized and uses
            /// <see cref="SerializableCallback"/>s to define the <see cref="callback"/>s.
            /// </summary>
            /// <remarks>
            /// If you have Animancer Pro you can replace <see cref="SerializableCallback"/>s with
            /// <see href="https://kybernetik.com.au/ultevents">UltEvents</see> using the following procedure:
            /// <list type="number">
            /// <item>Select the <c>Assets/Plugins/Animancer/Animancer.asmdef</c> and add a Reference to the
            /// <c>UltEvents</c> Assembly Definition.</item>
            /// <item>Go into the Player Settings of your project and add <c>ANIMANCER_ULT_EVENTS</c> as a Scripting
            /// Define Symbol. Or you can simply edit this script to change the event type (it is located at
            /// <c>Assets/Plugins/Animancer/Internal/Core/AnimancerEvent.Sequence.Serializable.cs</c> by default.</item>
            /// </list>
            /// </remarks>
            [Serializable]
            public sealed class Serializable
#if UNITY_EDITOR
                : ISerializationCallbackReceiver
#endif
            {
                /************************************************************************************************************************/

                /// <summary>The serialized <see cref="normalizedTime"/>s.</summary>
                [SerializeField]
                private float[] _NormalizedTimes;

                /// <summary>The name of the array field which stores the <see cref="normalizedTime"/>s.</summary>
                public const string NormalizedTimesField = "_NormalizedTimes";

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

                /// <summary>The serialized <see cref="callback"/>s.</summary>
                /// <remarks>
                /// This array only needs to be large enough to hold the last event that actually contains any calls.
                /// Any empty or missing elements will simply use the <see cref="DummyCallback"/> at runtime.
                /// </remarks>
                [SerializeField]
                [UnityEngine.Serialization.FormerlySerializedAs("_Events")]
                private SerializableCallback[] _Callbacks;

                /// <summary>The name of the array field which stores the <see cref="callback"/>s.</summary>
                public const string Callbacks = "_Callbacks";

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

                private Sequence _Sequence;

                /// <summary>
                /// The runtime <see cref="AnimancerEvent.Sequence"/> compiled from this <see cref="Serializable"/>.
                /// Each call after the first will return the same value.
                /// <para></para>
                /// Unlike <see cref="GetSequenceOptional"/>, this method will create an empty
                /// <see cref="AnimancerEvent.Sequence"/> instead of returning null.
                /// </summary>
                public Sequence Sequence
                {
                    get
                    {
                        if (_Sequence == null)
                        {
                            GetSequenceOptional();

                            if (_Sequence == null)
                                _Sequence = new Sequence();
                        }
                        return _Sequence;
                    }
                    set { _Sequence = value; }
                }

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

                /// <summary>
                /// Returns the runtime <see cref="AnimancerEvent.Sequence"/> compiled from this
                /// <see cref="Serializable"/>. Each call after the first will return the same value.
                /// <para></para>
                /// This method returns null if the sequence would be empty anyway and is used by the implicit
                /// conversion from <see cref="Serializable"/> to <see cref="AnimancerEvent.Sequence"/>.
                /// </summary>
                public Sequence GetSequenceOptional()
                {
                    if (_Sequence != null ||
                        _NormalizedTimes == null)
                        return _Sequence;

                    var timeCount = _NormalizedTimes.Length;
                    if (timeCount == 0)
                        return null;

                    var callbackCount = _Callbacks.Length;

                    AnimancerEvent endEvent;
                    if (callbackCount >= timeCount--)
                    {
                        endEvent = new AnimancerEvent(_NormalizedTimes[timeCount], GetInvoker(_Callbacks[timeCount]));
                    }
                    else
                    {
                        endEvent = new AnimancerEvent(_NormalizedTimes[timeCount], null);
                    }

                    _Sequence = new Sequence(timeCount)
                    {
                        endEvent = endEvent,
                        Count = timeCount,
                    };

                    for (int i = 0; i < timeCount; i++)
                    {
                        var callback = i < callbackCount ? GetInvoker(_Callbacks[i]) : DummyCallback;
                        _Sequence._Events[i] = new AnimancerEvent(_NormalizedTimes[i], callback);
                    }

                    return _Sequence;
                }

                /// <summary>Calls <see cref="GetSequenceOptional"/>.</summary>
                public static implicit operator Sequence(Serializable serializable)
                {
                    return serializable != null ? serializable.GetSequenceOptional() : null;
                }

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

                /// <summary>
                /// A delegate that does nothing which is used whenever the <see cref="SerializableCallback"/> is not
                /// defined for a particular event or it is empty.
                /// </summary>
                public static readonly Action DummyCallback = () => { };

                /// <summary>
                /// If the `callback` has any persistent calls, this method returns a delegate to call its
                /// <see cref="SerializableCallback.Invoke"/> method. Otherwise it returns the
                /// <see cref="DummyCallback"/>.
                /// </summary>
                public static Action GetInvoker(SerializableCallback callback)
                {
                    return HasPersistentCalls(callback) ? callback.Invoke : DummyCallback;
                }

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

                /// <summary>
                /// Determines if the `callback` contains any method calls that will be serialized (otherwise the
                /// <see cref="DummyCallback"/> can be used instead of creating a new delegate to invoke the empty
                /// `callback`).
                /// </summary>
                public static bool HasPersistentCalls(SerializableCallback callback)
                {
                    if (callback == null)
                        return false;

                    // UnityEvents do not allow us to check if any dynamic calls are present.
                    // But we are not giving runtime access to the events so it does not really matter.
                    // UltEvents does allow it (via the HasCalls property), but we might as well be consistent.

#if ANIMANCER_ULT_EVENTS
                    var calls = callback.PersistentCallsList;
                    return calls != null && calls.Count > 0;
#else
                    return callback.GetPersistentEventCount() > 0;
#endif
                }

                /// <summary>
                /// Determines if the `callback` contains any method calls that will be serialized (otherwise the
                /// <see cref="DummyCallback"/> can be used instead of creating a new delegate to invoke the empty
                /// `callback`).
                /// <para></para>
                /// This method casts the `callback` to <see cref="SerializableCallback"/> so the caller does not need
                /// to know what type is actually being used.
                /// </summary>
                public static bool HasPersistentCalls(object callback)
                {
                    return HasPersistentCalls((SerializableCallback)callback);
                }

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

                /// <summary>
                /// Returns the <see cref="normalizedTime"/> of the <see cref="endEvent"/>.
                /// <para></para>
                /// If the value is not set, the value is determined by <see cref="GetDefaultNormalizedEndTime"/>.
                /// </summary>
                public float GetNormalizedEndTime(float speed = 0)
                {
                    if (_NormalizedTimes == null || _NormalizedTimes.Length == 0)
                        return GetDefaultNormalizedEndTime(speed);
                    else
                        return _NormalizedTimes[_NormalizedTimes.Length - 1];
                }

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

                /// <summary>Gets the internal details of the specified `serializable`.</summary>
                public static void GetDetails(Serializable serializable, out int timeCount, out int callbackCount)
                {
                    timeCount = serializable._NormalizedTimes != null ? serializable._NormalizedTimes.Length : 0;
                    callbackCount = serializable._Callbacks != null ? serializable._Callbacks.Length : 0;
                }

                /************************************************************************************************************************/
#if UNITY_EDITOR
                /************************************************************************************************************************/

                /// <summary>[Editor-Only] Does nothing.</summary>
                void ISerializationCallbackReceiver.OnAfterDeserialize() { }

                /// <summary>[Editor-Only] Ensures that the events are sorted by time (excluding the end event).</summary>
                void ISerializationCallbackReceiver.OnBeforeSerialize()
                {
                    if (_NormalizedTimes == null ||
                        _NormalizedTimes.Length <= 2)
                        return;

                    var context = Editor.EventSequenceDrawer.Context.Instance;
                    var selectedEvent = context.Property != null ? context.SelectedEvent : -1;

                    var timeCount = _NormalizedTimes.Length - 1;
                    var callbackCount = _Callbacks != null ? _Callbacks.Length : 0;

                    var previousTime = _NormalizedTimes[0];

                    // Bubble Sort based on the normalized times.
                    var modifiedCallbacks = false;
                    for (int i = 1; i < timeCount; i++)
                    {
                        var time = _NormalizedTimes[i];
                        if (time >= previousTime)
                        {
                            previousTime = time;
                            continue;
                        }

                        time = _NormalizedTimes[i];
                        _NormalizedTimes[i] = _NormalizedTimes[i - 1];
                        _NormalizedTimes[i - 1] = time;

                        if (i == callbackCount)
                        {
                            Array.Resize(ref _Callbacks, ++callbackCount);
                        }

                        if (i < callbackCount)
                        {
                            var callback = _Callbacks[i];
                            _Callbacks[i] = _Callbacks[i - 1];
                            _Callbacks[i - 1] = callback;

                            modifiedCallbacks = true;
                        }

                        if (selectedEvent == i)
                            selectedEvent = i - 1;
                        else if (selectedEvent == i - 1)
                            selectedEvent = i;

                        if (i == 1)
                        {
                            i = 0;
                            previousTime = float.NegativeInfinity;
                        }
                        else
                        {
                            i -= 2;
                            previousTime = _NormalizedTimes[i];
                        }
                    }

                    if (context.Property != null && context.SelectedEvent != selectedEvent)
                    {
                        context.SelectedEvent = selectedEvent;
                        Editor.TransitionPreviewWindow.SetPreviewNormalizedTime(_NormalizedTimes[selectedEvent]);
                    }

                    if (modifiedCallbacks)
                    {
                        while (callbackCount >= 1)
                        {
                            var callback = _Callbacks[callbackCount - 1];
                            if (callback != null && HasPersistentCalls(callback))
                                break;
                            else
                                callbackCount--;
                        }

                        Array.Resize(ref _Callbacks, callbackCount);
                    }
                }

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

    /************************************************************************************************************************/
#if UNITY_EDITOR
    /************************************************************************************************************************/

    namespace Editor
    {
        /// <summary>[Editor-Only] [Internal]
        /// A serializable container which holds a <see cref="SerializableCallback"/> in a field named "_Callback".
        /// </summary>
        /// <remarks>
        /// <see cref="DummySerializableCallback"/> needs to be in a file with the same name as it (otherwise it can't
        /// draw the callback properly) and this class needs to be in the same file as
        /// <see cref="AnimancerEvent.Sequence.Serializable"/> to use the <see cref="SerializableCallback"/> alias.
        /// </remarks>
        [Serializable]
        internal sealed class SerializableCallbackHolder
        {
#pragma warning disable CS0169 // Field is never used.
            [SerializeField]
            private SerializableCallback _Callback;
        }
    }

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