// Animancer // Copyright 2020 Kybernetik //

using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using Object = UnityEngine.Object;

namespace Animancer
{
    /// <summary>
    /// An <see cref="AnimancerState"/> which plays an <see cref="AnimationClip"/>.
    /// </summary>
    public sealed class ClipState : AnimancerState
    {
        /************************************************************************************************************************/
        #region Fields and Properties
        /************************************************************************************************************************/

        /// <summary>The <see cref="AnimationClip"/> which this state plays.</summary>
        private AnimationClip _Clip;

        /// <summary>The <see cref="AnimationClip"/> which this state plays.</summary>
        public override AnimationClip Clip
        {
            get { return _Clip; }
            set
            {
                if (ReferenceEquals(_Clip, value))
                    return;

                if (ReferenceEquals(_Key, _Clip))
                    Key = value;

                if (_Playable.IsValid())
                    Root._Graph.DestroyPlayable(_Playable);

                CreatePlayable(value);
                SetWeightDirty();
            }
        }

        /// <summary>The <see cref="AnimationClip"/> which this state plays.</summary>
        public override Object MainObject
        {
            get { return _Clip; }
            set { Clip = (AnimationClip)value; }
        }

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

        /// <summary>The <see cref="AnimationClip.length"/>.</summary>
        public override float Length { get { return _Clip.length; } }

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

        /// <summary>The <see cref="Motion.isLooping"/>.</summary>
        public override bool IsLooping { get { return _Clip.isLooping; } }

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

        /// <summary>The average velocity of the root motion caused by this state.</summary>
        public override Vector3 AverageVelocity
        {
            get { return _Clip.averageSpeed; }
        }

        /************************************************************************************************************************/
        #region Inverse Kinematics
        /************************************************************************************************************************/

#if !UNITY_2018_1_OR_NEWER
        private const string IKNotSupported = "ApplyAnimatorIK is not supported by this version of Unity." +
            " Please upgrade to Unity 2018.1 or newer."
            ;
#endif

        /// <summary>
        /// Determines whether <c>OnAnimatorIK(int layerIndex)</c> will be called on the animated object.
        /// The initial value is determined by <see cref="AnimancerLayer.DefaultApplyAnimatorIK"/>.
        /// <para></para>
        /// This is equivalent to the "IK Pass" toggle in Animator Controller layers.
        /// <para></para>
        /// It requires Unity 2018.1 or newer, however 2018.3 or newer is recommended because a bug in earlier versions
        /// of the Playables API caused this value to only take effect while a state was at
        /// <see cref="AnimancerNode.Weight"/> == 1 which meant that IK would not work while fading between animations.
        /// </summary>
        public override bool ApplyAnimatorIK
        {
#if UNITY_2018_1_OR_NEWER
            get { return ((AnimationClipPlayable)_Playable).GetApplyPlayableIK(); }
            set { ((AnimationClipPlayable)_Playable).SetApplyPlayableIK(value); }
#else
            get { throw new NotSupportedException(IKNotSupported); }
            set { throw new NotSupportedException(IKNotSupported); }
#endif
        }

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

        /// <summary>
        /// Indicates whether this state is applying IK to the character's feet.
        /// The initial value is determined by <see cref="AnimancerLayer.DefaultApplyFootIK"/>.
        /// <para></para>
        /// This is equivalent to the "Foot IK" toggle in Animator Controller states.
        /// </summary>
        public override bool ApplyFootIK
        {
            get { return ((AnimationClipPlayable)_Playable).GetApplyFootIK(); }
            set { ((AnimationClipPlayable)_Playable).SetApplyFootIK(value); }
        }

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

        /// <summary>
        /// Applies the default IK flags from the specified `layer`.
        /// </summary>
        private void InitialiseIKDefaults(AnimancerLayer layer)
        {
            // Foot IK is actually enabled by default so we disable it if necessary.
            if (!layer.DefaultApplyFootIK)
                ApplyFootIK = false;

#if UNITY_2018_1_OR_NEWER
            if (layer.DefaultApplyAnimatorIK)
                ApplyAnimatorIK = true;
#endif
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region Methods
        /************************************************************************************************************************/

        /// <summary>
        /// Constructs a new <see cref="ClipState"/> to play the `clip` without connecting it to the
        /// <see cref="PlayableGraph"/>. You must call <see cref="AnimancerState.SetParent(AnimancerNode, int)"/> or it
        /// will not actually do anything.
        /// </summary>
        public ClipState(AnimancerPlayable root, AnimationClip clip)
            : base(root)
        {
            CreatePlayable(clip);
        }

        /// <summary>
        /// Constructs a new <see cref="ClipState"/> to play the `clip` and connects it to a new port on the `layer`s
        /// <see cref="Playable"/>.
        /// </summary>
        public ClipState(AnimancerLayer layer, AnimationClip clip)
            : this(layer.Root, clip)
        {
            layer.AddChild(this);
            InitialiseIKDefaults(layer);
        }

        /// <summary>
        /// Constructs a new <see cref="ClipState"/> to play the `clip` and connects it to the `parent`s
        /// <see cref="Playable"/> at the specified `index`.
        /// </summary>
        public ClipState(AnimancerNode parent, int index, AnimationClip clip)
            : this(parent.Root, clip)
        {
            SetParent(parent, index);
            InitialiseIKDefaults(parent.Layer);
        }

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

        private void CreatePlayable(AnimationClip clip)
        {
            if (clip == null)
                throw new ArgumentNullException("clip");

            Validate.NotLegacy(clip);

            _Clip = clip;
            _Playable = AnimationClipPlayable.Create(Root._Graph, clip);
        }

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

        /// <summary>
        /// Returns a string describing the type of this state and the name of the <see cref="Clip"/>.
        /// </summary>
        public override string ToString()
        {
            if (_Clip != null)
                return string.Concat(base.ToString(), " (", _Clip.name, ")");
            else
                return base.ToString() + " (null)";
        }

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

        /// <summary>Destroys the <see cref="Playable"/>.</summary>
        public override void Destroy()
        {
            _Clip = null;
            base.Destroy();
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region Inspector
#if UNITY_EDITOR
        /************************************************************************************************************************/

        /// <summary>[Editor-Only] Returns a <see cref="Drawer"/> for this state.</summary>
        protected internal override Editor.IAnimancerNodeDrawer GetDrawer()
        {
            return new Drawer(this);
        }

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

        /// <summary>[Editor-Only] Draws the Inspector GUI for a <see cref="ClipState"/>.</summary>
        public sealed class Drawer : Editor.AnimancerStateDrawer<ClipState>
        {
            /************************************************************************************************************************/

            /// <summary>Indicates whether the animation has an event called "End".</summary>
            private bool _HasEndEvent;

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

            /// <summary>
            /// Constructs a new <see cref="Drawer"/> to manage the Inspector GUI for the `state`.
            /// </summary>
            public Drawer(ClipState state) : base(state)
            {
                var events = state._Clip.events;
                for (int i = events.Length - 1; i >= 0; i--)
                {
                    if (events[i].functionName == "End")
                    {
                        _HasEndEvent = true;
                        break;
                    }
                }
            }

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

            /// <summary> Draws the details of the target state in the GUI.</summary>
            protected override void DoDetailsGUI(IAnimancerComponent owner)
            {
                base.DoDetailsGUI(owner);
                DoAnimationTypeWarningGUI(owner);
                DoEndEventWarningGUI();
            }

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

            private string _AnimationTypeWarning;
            private Animator _AnimationTypeWarningOwner;

            /// <summary>
            /// Validates the <see cref="Clip"/> type compared to the owner's <see cref="Animator"/> type.
            /// </summary>
            private void DoAnimationTypeWarningGUI(IAnimancerComponent owner)
            {
                // Validate the clip type compared to the owner.
                if (owner.Animator == null)
                {
                    _AnimationTypeWarning = null;
                    return;
                }

                if (_AnimationTypeWarningOwner != owner.Animator)
                {
                    _AnimationTypeWarning = null;
                    _AnimationTypeWarningOwner = owner.Animator;
                }

                if (_AnimationTypeWarning == null)
                {
                    var ownerAnimationType = Editor.AnimancerEditorUtilities.GetAnimationType(_AnimationTypeWarningOwner);
                    var clipAnimationType = Editor.AnimancerEditorUtilities.GetAnimationType(Target._Clip);

                    if (ownerAnimationType == clipAnimationType)
                    {
                        _AnimationTypeWarning = "";
                    }
                    else
                    {
                        var text = new StringBuilder()
                            .Append("Possible animation type mismatch:\n - Animator type is ")
                            .Append(ownerAnimationType)
                            .Append("\n - AnimationClip type is ")
                            .Append(clipAnimationType)
                            .Append("\nThis means that the clip may not work correctly," +
                                " however this check is not totally accurate. Click here for more info.");

                        _AnimationTypeWarning = text.ToString();
                    }
                }

                if (_AnimationTypeWarning != "")
                {
                    UnityEditor.EditorGUILayout.HelpBox(_AnimationTypeWarning, UnityEditor.MessageType.Warning);

                    if (Editor.AnimancerGUI.TryUseClickEventInLastRect())
                        UnityEditor.EditorUtility.OpenWithDefaultApp(
                            Strings.DocsURLs.AnimationTypes);
                }
            }

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

            private void DoEndEventWarningGUI()
            {
                if (_HasEndEvent && Target.Events.OnEnd == null && Target.TargetWeight != 0)
                {
                    UnityEditor.EditorGUILayout.HelpBox("This animation has an event called 'End'" +
                        " but no 'OnEnd' callback is currently registered for this state. Click here for more info.",
                        UnityEditor.MessageType.Warning);

                    if (Editor.AnimancerGUI.TryUseClickEventInLastRect())
                        UnityEditor.EditorUtility.OpenWithDefaultApp(
                            Strings.DocsURLs.EndEvents);
                }
            }

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

            /// <summary>Adds the details of this state to the menu.</summary>
            protected override void AddContextMenuFunctions(UnityEditor.GenericMenu menu)
            {
                menu.AddDisabledItem(new GUIContent(DetailsPrefix + "Animation Type: " +
                    Editor.AnimancerEditorUtilities.GetAnimationType(Target._Clip)));

                base.AddContextMenuFunctions(menu);

                menu.AddItem(new GUIContent("Inverse Kinematics/Apply Animator IK"),
                    Target.ApplyAnimatorIK,
                    () => Target.ApplyAnimatorIK = !Target.ApplyAnimatorIK);
                menu.AddItem(new GUIContent("Inverse Kinematics/Apply Foot IK"),
                    Target.ApplyFootIK,
                    () => Target.ApplyFootIK = !Target.ApplyFootIK);
            }

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

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

        /// <summary>
        /// A serializable <see cref="ITransition"/> which can create a <see cref="ClipState"/> 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 class Transition : Transition<ClipState>, IAnimationClipCollection
        {
            /************************************************************************************************************************/

            [SerializeField, Tooltip("The animation to play")]
            private AnimationClip _Clip;

            /// <summary>[<see cref="SerializeField"/>] The animation to play.</summary>
            public AnimationClip Clip
            {
                get { return _Clip; }
                set
                {
                    if (value != null)
                        Validate.NotLegacy(value);

                    _Clip = value;
                }
            }

            /// <summary>
            /// The <see cref="Clip"/> will be used as the <see cref="AnimancerState.Key"/> for the created state to be
            /// registered with.
            /// </summary>
            public override object Key { get { return _Clip; } }

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

            [SerializeField, Tooltip(Strings.ProOnlyTag +
                "How fast the animation plays (1x = normal speed, 2x = double speed)")]
            private float _Speed = 1;

            /// <summary>[<see cref="SerializeField"/>]
            /// Determines how fast the animation plays (1x = normal speed, 2x = double speed).
            /// </summary>
            public override float Speed
            {
                get { return _Speed; }
                set { _Speed = value; }
            }

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

            [SerializeField, Tooltip(Strings.ProOnlyTag + "If enabled, the animation's time will start at this value when played")]
            [UnityEngine.Serialization.FormerlySerializedAs("_StartTime")]
            private float _NormalizedStartTime = float.NaN;

            /// <summary>[<see cref="SerializeField"/>]
            /// Determines what <see cref="AnimancerState.NormalizedTime"/> to start the animation at.
            /// <para></para>
            /// The default value is <see cref="float.NaN"/> which indicates that this value is not used so the
            /// animation will continue from its current time.
            /// </summary>
            public override float NormalizedStartTime
            {
                get { return _NormalizedStartTime; }
                set { _NormalizedStartTime = value; }
            }

            /// <summary>
            /// If this transition will set the <see cref="AnimancerState.NormalizedTime"/>, then it needs to use
            /// <see cref="FadeMode.FromStart"/>.
            /// </summary>
            public override FadeMode FadeMode
            {
                get
                {
                    return float.IsNaN(_NormalizedStartTime) ? FadeMode.FixedSpeed : FadeMode.FromStart;
                }
            }

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

            /// <summary>[<see cref="ITransitionDetailed"/>] Returns <see cref="Motion.isLooping"/>.</summary>
            public override bool IsLooping
            {
                get
                {
                    return _Clip != null ? _Clip.isLooping : false;
                }
            }

            /// <summary>[<see cref="ITransitionDetailed"/>]
            /// The maximum amount of time the animation is expected to take (in seconds).
            /// </summary>
            public override float MaximumDuration
            {
                get
                {
                    return _Clip != null ? _Clip.length : 0;
                }
            }

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

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

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

            /// <summary>
            /// Called by <see cref="AnimancerPlayable.Play(ITransition)"/> to apply the <see cref="Speed"/>
            /// and <see cref="NormalizedStartTime"/>.
            /// </summary>
            public override void Apply(AnimancerState state)
            {
                base.Apply(state);

                if (!float.IsNaN(_Speed))
                    state.Speed = _Speed;

                if (!float.IsNaN(_NormalizedStartTime))
                    state.NormalizedTime = _NormalizedStartTime;
                else if (state.Weight == 0)
                    state.NormalizedTime = AnimancerEvent.Sequence.GetDefaultNormalizedStartTime(_Speed);
            }

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

            /// <summary>Adds the <see cref="Clip"/> to the collection.</summary>
            void IAnimationClipCollection.GatherAnimationClips(ICollection<AnimationClip> clips)
            {
                clips.Gather(_Clip);
            }

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

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

                /// <summary>Constructs a new <see cref="Drawer"/>.</summary>
                public Drawer() : base("_Clip") { }

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

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

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