// Animancer // Copyright 2020 Kybernetik //

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

using System;
using UnityEngine;

namespace Animancer.Examples.Events
{
    /// <summary>
    /// Base class for various scripts that use different event systems to have a character hit a golf ball.
    /// </summary>
    [AddComponentMenu(Strings.MenuPrefix + "Examples/Golf Events - Golf Hit Controller")]
    [HelpURL(Strings.APIDocumentationURL + ".Examples.AnimationEvents/GolfHitController")]
    public abstract class GolfHitController : MonoBehaviour
    {
        /************************************************************************************************************************/

        // Normally it would be good to make read-only properties to wrap fields you want other classes to access so
        // that those classes do not accidentally change any of the fields this script does not expect to change.
        // But for this example, it is easier to just let the inheriting classes access protected fields directly.

        [SerializeField] protected AnimancerComponent _Animancer;
        [SerializeField] protected ClipState.Transition _Ready;
        [SerializeField] protected ClipState.Transition _Swing;
        [SerializeField] protected ClipState.Transition _Idle;
        [SerializeField] private Rigidbody _Ball;
        [SerializeField] private Vector3 _HitVelocity;
        [SerializeField] private AudioSource _HitSound;

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

        public enum State { Ready, Swing, Idle, }

        private State _State;
        private Vector3 _BallStartPosition;

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

        /// <summary>
        /// Stores the position of the ball on startup so that it can be teleported back there when necessary.
        /// <para></para>
        /// This method is <c>virtual</c> in case any inheriting scripts need to do anything else on startup.
        /// <para></para>
        /// Most of them register <see cref="EndSwing"/> to be called when the <see cref="_Swing"/> animation ends,
        /// but <see cref="GolfHitControllerAnimancer"/> assumes that the event was already set up in the Inspector.
        /// </summary>
        protected virtual void Awake()
        {
            _BallStartPosition = _Ball.position;

            // A "Kinematic" Rigidbody essentially means that it is not currently being controlled by physics.
            // So while the character is ready we make the ball Kinematic to prevent it from rolling away.
            // Then when they hit the ball we set isKinematic = false to let regular physics take over.
            _Ball.isKinematic = true;
        }

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

        /// <summary>
        /// After <see cref="Awake"/>, we also want to enter the ready state on startup.
        /// <para></para>
        /// The difference is that this method is called every time the object is enabled instead of only the first
        /// time. It does not matter in the Golf Events example, but the Hybrid Mini Game example reuses this script and
        /// deactivates it while the Mini Game is not being played so we want to always enter the ready state when the
        /// Mini Game starts.
        /// </summary>
        /// <remarks>
        /// The contents of the <see cref="ReturnToReady"/> method could simply be here instead of needing a separate
        /// method, but when other methods (like <see cref="Update"/>) call it we would rather be clear that we
        /// specifically want to "return to the ready state" instead of some arbitrary "do what we did on startup".
        /// <para></para>
        /// Also note that this method is <c>protected</c> instead of <c>private</c>. Being <c>protected</c> allows
        /// inheriting classes to call it which we do not want, but it also means that if such a class tries to
        /// declare its own <see cref="OnEnable"/> method the compiler will give them a warning that this method
        /// already exists so they can come and make this method <c>virtual</c> if necessary. Otherwise Unity would
        /// call the <see cref="OnEnable"/> method in the derived class but not this one, which would very likely lead
        /// to errors that can be annoying to track down.
        /// </remarks>
        protected void OnEnable()
        {
            ReturnToReady();
        }

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

        /// <summary>
        /// When the player clicks the mouse, go to the next <see cref="State"/>: Ready -> Swing -> Idle -> Ready.
        /// </summary>
        /// <remarks>
        /// This method is <c>protected</c> for the same reason as <see cref="OnEnable"/>.
        /// </remarks>
        protected void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                switch (_State)
                {
                    case State.Ready: StartSwing(); break;
                    case State.Swing: TryCancelSwing(); break;
                    case State.Idle: ReturnToReady(); break;
                    default: throw new ArgumentException("Unhandled State: " + _State);
                }
            }
        }

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

        /// <summary>
        /// Enter the swing state and play the appropriate animation.
        /// <para></para>
        /// This method is <c>virtual</c> so that <see cref="GolfHitControllerAnimationSimple"/> can <c>override</c> it
        /// to register the <see cref="HitBall"/> method to be called by the event.
        /// </summary>
        protected virtual void StartSwing()
        {
            _State = State.Swing;
            _Animancer.Play(_Swing);
        }

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

        /// <summary>
        /// If the ball has not been hit yet, the swing can be cancelled to immediately return to the ready state. But
        /// after the ball has been hit, the character must fully complete the swing animation.
        /// </summary>
        private void TryCancelSwing()
        {
            if (_Ball.isKinematic)
            {
                _State = State.Ready;
                _Animancer.Play(_Ready);
            }
        }

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

        /// <summary>
        /// When the player clicks the mouse again after entering the idle state, we return to the ready state,
        /// teleport the ball back to its starting position, and make it Kinematic again so it does not roll away.
        /// </summary>
        private void ReturnToReady()
        {
            _State = State.Ready;
            _Animancer.Play(_Ready);

            _Ball.isKinematic = true;
            _Ball.position = _BallStartPosition;
        }

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

        /// <summary>
        /// Once the swing animation is started, each of the classes that inherit from this one use a different system
        /// for determining how this method gets called.
        /// </summary>
        public void HitBall()
        {
            _Ball.isKinematic = false;

            // In a real golf game you would probably calculate the hit velocity based on player input.
            _Ball.velocity = _HitVelocity;

            _HitSound.Play();
        }

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

        /// <summary>
        /// As with <see cref="HitBall"/>, this method is called in various different ways depending on which event
        /// system is being used.
        /// <para></para>
        /// Most of them register this method to be called when the <see cref="_Swing"/> animation ends, but
        /// <see cref="GolfHitControllerAnimancer"/> assumes that the event was already set up in the Inspector.
        /// </summary>
        public void EndSwing()
        {
            _State = State.Idle;

            // Since the swing animation is ending early, we want it to calculate the fade duration to fade out over
            // the remainder of that animation instead of the value specified by the _Idle transition.
            var fadeDuration = AnimancerPlayable.GetFadeOutDuration();
            _Animancer.Play(_Idle, fadeDuration);
        }

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

        /// <summary>
        /// If you add a second script derived from this type to the same object, it will instead change the type of
        /// the existing component, allowing you to easily swap between any components that inherit from
        /// <see cref="GolfHitController"/> without losing the values of their serialized fields.
        /// </summary>
        protected void Reset()
        {
            AnimancerUtilities.IfMultiComponentThenChangeType(this);
        }

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