// Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //

using System;
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;

#if UNITY_EDITOR
using Animancer.Editor;
using UnityEditor;
using UnityEditorInternal;
#endif

namespace Animancer
{
    /// <inheritdoc/>
    /// https://kybernetik.com.au/animancer/api/Animancer/ManualMixerTransition
    [Serializable]
    public class ManualMixerTransition : ManualMixerTransition<ManualMixerState>,
        ManualMixerState.ITransition, ICopyable<ManualMixerTransition>
    {
        /************************************************************************************************************************/

        /// <inheritdoc/>
        public override ManualMixerState CreateState()
        {
            State = new ManualMixerState();
            InitializeState();
            return State;
        }

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

        /// <inheritdoc/>
        public virtual void CopyFrom(ManualMixerTransition copyFrom)
        {
            CopyFrom((ManualMixerTransition<ManualMixerState>)copyFrom);
        }

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

        /// <inheritdoc/>
        [CustomPropertyDrawer(typeof(ManualMixerTransition), true)]
        public class Drawer : TransitionDrawer
        {
            /************************************************************************************************************************/

            /// <summary>The property this drawer is currently drawing.</summary>
            /// <remarks>Normally each property has its own drawer, but arrays share a single drawer for all elements.</remarks>
            public static SerializedProperty CurrentProperty { get; private set; }

            /// <summary>The <see cref="ManualMixerTransition{TState}.Animations"/> field.</summary>
            public static SerializedProperty CurrentAnimations { get; private set; }

            /// <summary>The <see cref="ManualMixerTransition{TState}.Speeds"/> field.</summary>
            public static SerializedProperty CurrentSpeeds { get; private set; }

            /// <summary>The <see cref="ManualMixerTransition{TState}.SynchronizeChildren"/> field.</summary>
            public static SerializedProperty CurrentSynchronizeChildren { get; private set; }

            private readonly Dictionary<string, ReorderableList>
                PropertyPathToStates = new Dictionary<string, ReorderableList>();

            private ReorderableList _MultiSelectDummyList;

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

            /// <summary>Gather the details of the `property`.</summary>
            /// <remarks>
            /// This method gets called by every <see cref="GetPropertyHeight"/> and <see cref="OnGUI"/> call since
            /// Unity uses the same <see cref="PropertyDrawer"/> instance for each element in a collection, so it
            /// needs to gather the details associated with the current property.
            /// </remarks>
            protected virtual ReorderableList GatherDetails(SerializedProperty property)
            {
                InitializeMode(property);
                GatherSubProperties(property);

                if (property.hasMultipleDifferentValues)
                {
                    if (_MultiSelectDummyList == null)
                    {
                        _MultiSelectDummyList = new ReorderableList(new List<Object>(), typeof(Object))
                        {
                            elementHeight = AnimancerGUI.LineHeight,
                            displayAdd = false,
                            displayRemove = false,
                            footerHeight = 0,
                            drawHeaderCallback = DoAnimationHeaderGUI,
                            drawNoneElementCallback = area => EditorGUI.LabelField(area,
                                "Multi-editing animations is not supported"),
                        };
                    }

                    return _MultiSelectDummyList;
                }

                if (CurrentAnimations == null)
                    return null;

                var path = property.propertyPath;

                if (!PropertyPathToStates.TryGetValue(path, out var states))
                {
                    states = new ReorderableList(CurrentAnimations.serializedObject, CurrentAnimations)
                    {
                        drawHeaderCallback = DoChildListHeaderGUI,
                        elementHeightCallback = GetElementHeight,
                        drawElementCallback = DoElementGUI,
                        onAddCallback = OnAddElement,
                        onRemoveCallback = OnRemoveElement,
                        onReorderCallbackWithDetails = OnReorderList,
                        drawFooterCallback = DoChildListFooterGUI,
                    };

                    PropertyPathToStates.Add(path, states);
                }

                states.serializedProperty = CurrentAnimations;

                return states;
            }

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

            /// <summary>
            /// Called every time a `property` is drawn to find the relevant child properties and store them to be
            /// used in <see cref="GetPropertyHeight"/> and <see cref="OnGUI"/>.
            /// </summary>
            protected virtual void GatherSubProperties(SerializedProperty property)
            {
                CurrentProperty = property;
                CurrentAnimations = property.FindPropertyRelative(AnimationsField);
                CurrentSpeeds = property.FindPropertyRelative(SpeedsField);
                CurrentSynchronizeChildren = property.FindPropertyRelative(SynchronizeChildrenField);

                if (!property.hasMultipleDifferentValues &&
                    CurrentAnimations != null &&
                    CurrentSpeeds != null &&
                    CurrentSpeeds.arraySize != 0)
                    CurrentSpeeds.arraySize = CurrentAnimations.arraySize;
            }

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

            /// <summary>
            /// Adds a menu item that will call <see cref="GatherSubProperties"/> then run the specified
            /// `function`.
            /// </summary>
            protected void AddPropertyModifierFunction(GenericMenu menu, string label,
                MenuFunctionState state, Action<SerializedProperty> function)
            {
                Serialization.AddPropertyModifierFunction(menu, CurrentProperty, label, state, (property) =>
                {
                    GatherSubProperties(property);
                    function(property);
                });
            }

            /// <summary>
            /// Adds a menu item that will call <see cref="GatherSubProperties"/> then run the specified
            /// `function`.
            /// </summary>
            protected void AddPropertyModifierFunction(GenericMenu menu, string label,
                Action<SerializedProperty> function)
            {
                Serialization.AddPropertyModifierFunction(menu, CurrentProperty, label, (property) =>
                {
                    GatherSubProperties(property);
                    function(property);
                });
            }

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

            /// <inheritdoc/>
            public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
            {
                var height = EditorGUI.GetPropertyHeight(property, label);

                if (property.isExpanded)
                {
                    var states = GatherDetails(property);
                    if (states != null)
                        height += AnimancerGUI.StandardSpacing + states.GetHeight();

                    if (CurrentAnimations != null)
                        height -= AnimancerGUI.StandardSpacing + EditorGUI.GetPropertyHeight(CurrentAnimations, label);

                    if (CurrentSpeeds != null)
                        height -= AnimancerGUI.StandardSpacing + EditorGUI.GetPropertyHeight(CurrentSpeeds, label);

                    if (CurrentSynchronizeChildren != null)
                        height -= AnimancerGUI.StandardSpacing + EditorGUI.GetPropertyHeight(CurrentSynchronizeChildren, label);
                }

                return height;
            }

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

            private SerializedProperty _RootProperty;
            private ReorderableList _CurrentChildList;

            /// <inheritdoc/>
            public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
            {
                _RootProperty = null;

                base.OnGUI(area, property, label);

                if (_RootProperty == null ||
                    !_RootProperty.isExpanded)
                    return;

                using (DrawerContext.Get(_RootProperty))
                {
                    if (Context.Transition == null)
                        return;

                    _CurrentChildList = GatherDetails(_RootProperty);
                    if (_CurrentChildList == null)
                        return;

                    var indentLevel = EditorGUI.indentLevel;

                    area.yMin = area.yMax - _CurrentChildList.GetHeight();

                    EditorGUI.indentLevel++;
                    area = EditorGUI.IndentedRect(area);

                    EditorGUI.indentLevel = 0;
                    _CurrentChildList.DoList(area);

                    EditorGUI.indentLevel = indentLevel;

                    TryCollapseArrays();
                }
            }

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

            /// <inheritdoc/>
            protected override void DoChildPropertyGUI(ref Rect area,
                SerializedProperty rootProperty, SerializedProperty property, GUIContent label)
            {
                if (Context?.Transition != null)
                {
                    area.height = 0;

                    // If we find the Animations property, hide it to draw it last.

                    var path = property.propertyPath;
                    if (path.EndsWith("." + AnimationsField))
                    {
                        _RootProperty = rootProperty;
                        return;
                    }
                    else if (_RootProperty != null)
                    {
                        // If we already found the Animations property, also hide Speeds and Synchronize Children.
                        if (path.EndsWith("." + SpeedsField) ||
                            path.EndsWith("." + SynchronizeChildrenField))
                            return;
                    }
                }

                base.DoChildPropertyGUI(ref area, rootProperty, property, label);
            }

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

            private static float _SpeedLabelWidth;
            private static float _SyncLabelWidth;

            /// <summary>Splits the specified `area` into separate sections.</summary>
            protected static void SplitListRect(Rect area, bool isHeader, out Rect animation, out Rect speed, out Rect sync)
            {
                if (_SpeedLabelWidth == 0)
                    _SpeedLabelWidth = AnimancerGUI.CalculateWidth(EditorStyles.popup, "Speed");

                if (_SyncLabelWidth == 0)
                    _SyncLabelWidth = AnimancerGUI.CalculateWidth(EditorStyles.popup, "Sync");

                var spacing = AnimancerGUI.StandardSpacing;

                var syncWidth = isHeader ?
                    _SyncLabelWidth :
                    AnimancerGUI.ToggleWidth - spacing;

                var speedWidth = _SpeedLabelWidth + _SyncLabelWidth - syncWidth;
                if (!isHeader)
                {
                    // Don't use Clamp because the max might be smaller than the min.
                    var max = Math.Max(area.height, area.width * 0.25f - 30);
                    speedWidth = Math.Min(speedWidth, max);
                }

                area.width += spacing;
                sync = AnimancerGUI.StealFromRight(ref area, syncWidth, spacing);
                speed = AnimancerGUI.StealFromRight(ref area, speedWidth, spacing);
                animation = area;
            }

            /************************************************************************************************************************/
            #region Headers
            /************************************************************************************************************************/

            /// <summary>Draws the headdings of the child list.</summary>
            protected virtual void DoChildListHeaderGUI(Rect area)
            {
                SplitListRect(area, true, out var animationArea, out var speedArea, out var syncArea);

                DoAnimationHeaderGUI(animationArea);
                DoSpeedHeaderGUI(speedArea);
                DoSyncHeaderGUI(syncArea);
            }

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

            /// <summary>Draws an "Animation" header.</summary>
            protected static void DoAnimationHeaderGUI(Rect area)
            {
                using (ObjectPool.Disposable.AcquireContent(out var label, "Animation",
                    $"The animations that will be used for each child state" +
                    $"\n\nCtrl + Click to allow picking Transition Assets (or anything that implements {nameof(ITransition)})"))
                {
                    DoHeaderDropdownGUI(area, CurrentAnimations, label, null);
                }
            }

            /************************************************************************************************************************/
            #region Speeds
            /************************************************************************************************************************/

            /// <summary>Draws a "Speed" header.</summary>
            protected void DoSpeedHeaderGUI(Rect area)
            {
                using (ObjectPool.Disposable.AcquireContent(out var label, "Speed", Strings.Tooltips.Speed))
                {
                    DoHeaderDropdownGUI(area, CurrentSpeeds, label, (menu) =>
                    {
                        AddPropertyModifierFunction(menu, "Reset All to 1",
                            CurrentSpeeds.arraySize == 0 ? MenuFunctionState.Selected : MenuFunctionState.Normal,
                            (_) => CurrentSpeeds.arraySize = 0);

                        AddPropertyModifierFunction(menu, "Normalize Durations", MenuFunctionState.Normal, NormalizeDurations);
                    });
                }
            }

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

            /// <summary>
            /// Recalculates the <see cref="CurrentSpeeds"/> depending on the <see cref="AnimationClip.length"/> of
            /// their animations so that they all take the same amount of time to play fully.
            /// </summary>
            private static void NormalizeDurations(SerializedProperty property)
            {
                var speedCount = CurrentSpeeds.arraySize;

                var lengths = new float[CurrentAnimations.arraySize];
                if (lengths.Length <= 1)
                    return;

                int nonZeroLengths = 0;
                float totalLength = 0;
                float totalSpeed = 0;
                for (int i = 0; i < lengths.Length; i++)
                {
                    var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
                    if (AnimancerUtilities.TryGetLength(state, out var length) &&
                        length > 0)
                    {
                        nonZeroLengths++;
                        totalLength += length;
                        lengths[i] = length;

                        if (speedCount > 0)
                            totalSpeed += CurrentSpeeds.GetArrayElementAtIndex(i).floatValue;
                    }
                }

                if (nonZeroLengths == 0)
                    return;

                var averageLength = totalLength / nonZeroLengths;
                var averageSpeed = speedCount > 0 ? totalSpeed / nonZeroLengths : 1;

                CurrentSpeeds.arraySize = lengths.Length;
                InitializeSpeeds(speedCount);

                for (int i = 0; i < lengths.Length; i++)
                {
                    if (lengths[i] == 0)
                        continue;

                    CurrentSpeeds.GetArrayElementAtIndex(i).floatValue = averageSpeed * lengths[i] / averageLength;
                }

                TryCollapseArrays();
            }

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

            /// <summary>
            /// Initializes every element in the <see cref="CurrentSpeeds"/> array from the `start` to the end of
            /// the array to contain a value of 1.
            /// </summary>
            public static void InitializeSpeeds(int start)
            {
                var count = CurrentSpeeds.arraySize;
                while (start < count)
                    CurrentSpeeds.GetArrayElementAtIndex(start++).floatValue = 1;
            }

            /************************************************************************************************************************/
            #endregion
            /************************************************************************************************************************/
            #region Sync
            /************************************************************************************************************************/

            /// <summary>Draws a "Sync" header.</summary>
            protected void DoSyncHeaderGUI(Rect area)
            {
                using (ObjectPool.Disposable.AcquireContent(out var label, "Sync",
                    "Determines which child states have their normalized times constantly synchronized"))
                {
                    DoHeaderDropdownGUI(area, CurrentSpeeds, label, (menu) =>
                    {
                        var syncCount = CurrentSynchronizeChildren.arraySize;

                        var allState = syncCount == 0 ? MenuFunctionState.Selected : MenuFunctionState.Normal;
                        AddPropertyModifierFunction(menu, "All", allState,
                            (_) => CurrentSynchronizeChildren.arraySize = 0);

                        var syncNone = syncCount == CurrentAnimations.arraySize;
                        if (syncNone)
                        {
                            for (int i = 0; i < syncCount; i++)
                            {
                                if (CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue)
                                {
                                    syncNone = false;
                                    break;
                                }
                            }
                        }
                        var noneState = syncNone ? MenuFunctionState.Selected : MenuFunctionState.Normal;
                        AddPropertyModifierFunction(menu, "None", noneState, (_) =>
                        {
                            var count = CurrentSynchronizeChildren.arraySize = CurrentAnimations.arraySize;
                            for (int i = 0; i < count; i++)
                                CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue = false;
                        });

                        AddPropertyModifierFunction(menu, "Invert", MenuFunctionState.Normal, (_) =>
                        {
                            var count = CurrentSynchronizeChildren.arraySize;
                            for (int i = 0; i < count; i++)
                            {
                                var property = CurrentSynchronizeChildren.GetArrayElementAtIndex(i);
                                property.boolValue = !property.boolValue;
                            }

                            var newCount = CurrentSynchronizeChildren.arraySize = CurrentAnimations.arraySize;
                            for (int i = count; i < newCount; i++)
                                CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue = false;
                        });

                        AddPropertyModifierFunction(menu, "Non-Stationary", MenuFunctionState.Normal, (_) =>
                        {
                            var count = CurrentAnimations.arraySize;

                            for (int i = 0; i < count; i++)
                            {
                                var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
                                if (state == null)
                                    continue;

                                if (i >= syncCount)
                                {
                                    CurrentSynchronizeChildren.arraySize = i + 1;
                                    for (int j = syncCount; j < i; j++)
                                        CurrentSynchronizeChildren.GetArrayElementAtIndex(j).boolValue = true;
                                    syncCount = i + 1;
                                }

                                CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue =
                                    AnimancerUtilities.TryGetAverageVelocity(state, out var velocity) &&
                                    velocity != default;
                            }

                            TryCollapseSync();
                        });
                    });
                }
            }

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

            private static void SyncNone()
            {
                var count = CurrentSynchronizeChildren.arraySize = CurrentAnimations.arraySize;
                for (int i = 0; i < count; i++)
                    CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue = false;
            }

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

            /// <summary>Draws the GUI for a header dropdown button.</summary>
            public static void DoHeaderDropdownGUI(Rect area, SerializedProperty property, GUIContent content,
                Action<GenericMenu> populateMenu)
            {
                if (property != null)
                    EditorGUI.BeginProperty(area, GUIContent.none, property);

                if (populateMenu != null)
                {
                    if (EditorGUI.DropdownButton(area, content, FocusType.Passive))
                    {
                        var menu = new GenericMenu();
                        populateMenu(menu);
                        menu.ShowAsContext();
                    }
                }
                else
                {
                    GUI.Label(area, content);
                }

                if (property != null)
                    EditorGUI.EndProperty();
            }

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

            /// <summary>Draws the footer of the child list.</summary>
            protected virtual void DoChildListFooterGUI(Rect area)
            {
                ReorderableList.defaultBehaviours.DrawFooter(area, _CurrentChildList);

                EditorGUI.BeginChangeCheck();

                area.xMax = EditorGUIUtility.labelWidth + AnimancerGUI.IndentSize;

                area.y++;
                area.height = AnimancerGUI.LineHeight;

                using (ObjectPool.Disposable.AcquireContent(out var label, "Count"))
                {
                    var indentLevel = EditorGUI.indentLevel;
                    EditorGUI.indentLevel = 0;

                    var labelWidth = EditorGUIUtility.labelWidth;
                    EditorGUIUtility.labelWidth = AnimancerGUI.CalculateLabelWidth(label.text);

                    var count = EditorGUI.DelayedIntField(area, label, _CurrentChildList.count);

                    if (EditorGUI.EndChangeCheck())
                        ResizeList(count);

                    EditorGUIUtility.labelWidth = labelWidth;

                    EditorGUI.indentLevel = indentLevel;
                }
            }

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

            /// <summary>Calculates the height of the state at the specified `index`.</summary>
            protected virtual float GetElementHeight(int index) => AnimancerGUI.LineHeight;

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

            /// <summary>Draws the GUI of the state at the specified `index`.</summary>
            private void DoElementGUI(Rect area, int index, bool isActive, bool isFocused)
            {
                if (index < 0 || index > CurrentAnimations.arraySize)
                    return;

                area.height = AnimancerGUI.LineHeight;

                var state = CurrentAnimations.GetArrayElementAtIndex(index);
                var speed = CurrentSpeeds.arraySize > 0 ? CurrentSpeeds.GetArrayElementAtIndex(index) : null;
                DoElementGUI(area, index, state, speed);
            }

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

            /// <summary>Draws the GUI of the state at the specified `index`.</summary>
            protected virtual void DoElementGUI(Rect area, int index,
                SerializedProperty state, SerializedProperty speed)
            {
                SplitListRect(area, false, out var animationArea, out var speedArea, out var syncArea);

                DoElementGUI(animationArea, speedArea, syncArea, index, state, speed);
            }

            /// <summary>Draws the GUI of the state at the specified `index`.</summary>
            protected void DoElementGUI(Rect animationArea, Rect speedArea, Rect syncArea, int index,
                SerializedProperty state, SerializedProperty speed)
            {
                DoAnimationField(animationArea, state);

                if (speed != null)
                {
                    EditorGUI.PropertyField(speedArea, speed, GUIContent.none);
                }
                else// If this element doesn't have its own speed property, just show 1.
                {
                    EditorGUI.BeginProperty(speedArea, GUIContent.none, CurrentSpeeds);

                    var value = Units.UnitsAttribute.DoSpecialFloatField(
                        speedArea, null, 1, Units.AnimationSpeedAttribute.DisplayConverters[0]);

                    // Middle Click toggles from 1 to -1.
                    if (AnimancerGUI.TryUseClickEvent(speedArea, 2))
                        value = -1;

                    if (value != 1)
                    {
                        CurrentSpeeds.InsertArrayElementAtIndex(0);
                        CurrentSpeeds.GetArrayElementAtIndex(0).floatValue = 1;
                        CurrentSpeeds.arraySize = CurrentAnimations.arraySize;
                        CurrentSpeeds.GetArrayElementAtIndex(index).floatValue = value;
                    }

                    EditorGUI.EndProperty();
                }

                DoSyncToggleGUI(syncArea, index);
            }

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

            /// <summary>
            /// Draws an <see cref="EditorGUI.ObjectField(Rect, GUIContent, Object, Type, bool)"/> that accepts
            /// <see cref="AnimationClip"/>s and <see cref="ITransition"/>s
            /// </summary>
            public static void DoAnimationField(Rect area, SerializedProperty property)
            {
                EditorGUI.BeginProperty(area, GUIContent.none, property);

                var targetObject = property.serializedObject.targetObject;
                var oldReference = property.objectReferenceValue;

                var currentEvent = Event.current;
                var isDrag =
                    currentEvent.type == EventType.DragUpdated ||
                    currentEvent.type == EventType.DragPerform;
                var type =
                    isDrag ||
                    currentEvent.control ||
                    currentEvent.commandName == "ObjectSelectorUpdated" ?
                    typeof(Object) : typeof(AnimationClip);

                var allowSceneObjects = targetObject != null && !EditorUtility.IsPersistent(targetObject);

                EditorGUI.BeginChangeCheck();
                var newReference = EditorGUI.ObjectField(area, GUIContent.none, oldReference, type, allowSceneObjects);
                if (EditorGUI.EndChangeCheck())
                {
                    if (newReference == null || (IsClipOrTransition(newReference) && newReference != targetObject))
                        property.objectReferenceValue = newReference;
                }

                if (isDrag && area.Contains(currentEvent.mousePosition))
                {
                    var objects = DragAndDrop.objectReferences;
                    if (objects.Length != 1 ||
                        !IsClipOrTransition(objects[0]) ||
                        objects[0] == targetObject)
                        DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
                }

                EditorGUI.EndProperty();
            }

            /// <summary>Is the `clipOrTransition` an <see cref="AnimationClip"/> or <see cref="ITransition"/>?</summary>
            public static bool IsClipOrTransition(object clipOrTransition)
                => clipOrTransition is AnimationClip || clipOrTransition is ITransition;

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

            /// <summary>
            /// Draws a toggle to enable or disable <see cref="MixerState.SynchronizedChildren"/> for the child at
            /// the specified `index`.
            /// </summary>
            protected void DoSyncToggleGUI(Rect area, int index)
            {
                var syncProperty = CurrentSynchronizeChildren;
                var syncFlagCount = syncProperty.arraySize;

                var enabled = true;

                if (index < syncFlagCount)
                {
                    syncProperty = syncProperty.GetArrayElementAtIndex(index);
                    enabled = syncProperty.boolValue;
                }

                EditorGUI.BeginChangeCheck();
                EditorGUI.BeginProperty(area, GUIContent.none, syncProperty);

                enabled = GUI.Toggle(area, enabled, GUIContent.none);

                EditorGUI.EndProperty();
                if (EditorGUI.EndChangeCheck())
                {
                    if (index < syncFlagCount)
                    {
                        syncProperty.boolValue = enabled;
                    }
                    else
                    {
                        syncProperty.arraySize = index + 1;

                        for (int i = syncFlagCount; i < index; i++)
                        {
                            syncProperty.GetArrayElementAtIndex(i).boolValue = true;
                        }

                        syncProperty.GetArrayElementAtIndex(index).boolValue = enabled;
                    }
                }
            }

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

            /// <summary>
            /// Called when adding a new state to the list to ensure that any other relevant arrays have new
            /// elements added as well.
            /// </summary>
            private void OnAddElement(ReorderableList list)
            {
                var index = list.index;
                if (index < 0 || Event.current.button == 1)// Right Click to add at the end.
                {
                    index = CurrentAnimations.arraySize - 1;
                    if (index < 0)
                        index = 0;
                }

                OnAddElement(index);
            }

            /// <summary>
            /// Called when adding a new state to the list to ensure that any other relevant arrays have new
            /// elements added as well.
            /// </summary>
            protected virtual void OnAddElement(int index)
            {
                CurrentAnimations.InsertArrayElementAtIndex(index);

                if (CurrentSpeeds.arraySize > 0)
                    CurrentSpeeds.InsertArrayElementAtIndex(index);

                if (CurrentSynchronizeChildren.arraySize > index)
                    CurrentSynchronizeChildren.InsertArrayElementAtIndex(index);
            }

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

            /// <summary>
            /// Called when removing a state from the list to ensure that any other relevant arrays have elements
            /// removed as well.
            /// </summary>
            protected virtual void OnRemoveElement(ReorderableList list)
            {
                var index = list.index;

                Serialization.RemoveArrayElement(CurrentAnimations, index);

                if (CurrentSpeeds.arraySize > index)
                    Serialization.RemoveArrayElement(CurrentSpeeds, index);

                if (CurrentSynchronizeChildren.arraySize > index)
                    Serialization.RemoveArrayElement(CurrentSynchronizeChildren, index);
            }

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

            /// <summary>Sets the number of items in the child list.</summary>
            protected virtual void ResizeList(int size)
            {
                CurrentAnimations.arraySize = size;

                if (CurrentSpeeds.arraySize > size)
                    CurrentSpeeds.arraySize = size;

                if (CurrentSynchronizeChildren.arraySize > size)
                    CurrentSynchronizeChildren.arraySize = size;
            }

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

            /// <summary>
            /// Called when reordering states in the list to ensure that any other relevant arrays have their
            /// corresponding elements reordered as well.
            /// </summary>
            protected virtual void OnReorderList(ReorderableList list, int oldIndex, int newIndex)
            {
                CurrentSpeeds.MoveArrayElement(oldIndex, newIndex);

                var syncCount = CurrentSynchronizeChildren.arraySize;
                if (Math.Max(oldIndex, newIndex) >= syncCount)
                {
                    CurrentSynchronizeChildren.arraySize++;
                    CurrentSynchronizeChildren.GetArrayElementAtIndex(syncCount).boolValue = true;
                    CurrentSynchronizeChildren.arraySize = newIndex + 1;
                }

                CurrentSynchronizeChildren.MoveArrayElement(oldIndex, newIndex);
            }

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

            /// <summary>
            /// Calls <see cref="TryCollapseSpeeds"/> and <see cref="TryCollapseSync"/>.
            /// </summary>
            public static void TryCollapseArrays()
            {
                if (CurrentProperty == null ||
                    CurrentProperty.hasMultipleDifferentValues)
                    return;

                TryCollapseSpeeds();
                TryCollapseSync();
            }

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

            /// <summary>
            /// If every element in the <see cref="CurrentSpeeds"/> array is 1, this method sets the array size to 0.
            /// </summary>
            public static void TryCollapseSpeeds()
            {
                var property = CurrentSpeeds;
                if (property == null)
                    return;

                var speedCount = property.arraySize;
                if (speedCount <= 0)
                    return;

                for (int i = 0; i < speedCount; i++)
                {
                    if (property.GetArrayElementAtIndex(i).floatValue != 1)
                        return;
                }

                property.arraySize = 0;
            }

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

            /// <summary>
            /// Removes any true elements from the end of the <see cref="CurrentSynchronizeChildren"/> array.
            /// </summary>
            public static void TryCollapseSync()
            {
                var property = CurrentSynchronizeChildren;
                if (property == null)
                    return;

                var count = property.arraySize;
                var changed = false;

                for (int i = count - 1; i >= 0; i--)
                {
                    if (property.GetArrayElementAtIndex(i).boolValue)
                    {
                        count = i;
                        changed = true;
                    }
                    else
                    {
                        break;
                    }
                }

                if (changed)
                    property.arraySize = count;
            }

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

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