using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using Photon.Pun;
using Photon.Realtime;

/// <summary>
/// The Controller is in charge of deriving local inputs into 
/// actionable states used by the other components of the racer
/// </summary>

public struct InputState
{
    [SerializeField] KeyCode m_keyCode;
    public KeyCode InputKey { get { return m_keyCode; } }

    public bool IsDown { get { return Input.GetKey(m_keyCode); } }

    [SerializeField] [ReadOnly] bool m_pressedThisFrame;
    public bool JustPressed { get { return m_pressedThisFrame; } }

    [SerializeField] [ReadOnly] bool m_releasedThisFrame;
    public bool JustReleased { get { return m_releasedThisFrame; } }

    [SerializeField] [ReadOnly] float m_heldFor;
    public float HeldFor { get { return m_heldFor; } }

    public InputState(KeyCode _code)
    {
        m_keyCode = _code;
        m_heldFor = 0;
        m_pressedThisFrame = false;
        m_releasedThisFrame = false;
    }

    public void UpdateInputTime(float _dt)
    {
        m_pressedThisFrame = false;
        m_releasedThisFrame = false;

        if (IsDown)
        {
            if (m_heldFor == 0)
                m_pressedThisFrame = true;

            m_heldFor += _dt;
        }
        else if (m_heldFor != 0)
        {
            m_releasedThisFrame = true;
            m_heldFor = 0;
        }
    }

    IEnumerator DelayResetHeldTime()
    {
        yield return null;
        if (!IsDown)
            m_heldFor = 0;
    }
}

public class RacerController : RacerCoreFeature
{
    [Header("Input Tracking")]
    #region
    [Tooltip("Holds the current horizontal/vertical inputs.")]
    [ReadOnly]
    public Vector2 XYInput;

    [Tooltip("Distance from center that an input axis will consider to be zero.")]
    [Range(0, 1)]
    public float DeadzoneThreshold;

    [Tooltip("Length of time between a key press and release that constitutes a tap versus a hold.")]
    [Range(0, 0.5f)]
    public float TapThreshold;

    [Tooltip("Holds the current Fire input.")]
    [ReadOnly]
    [SerializeField] bool m_fireInput;
    private float m_fireHeldTime;

    [Tooltip("Holds the current Jump input.")]
    [ReadOnly]
    [SerializeField] bool m_jumpInput;
    private float m_jumpHeldTime;
    #endregion
    
    [Header("Kart Control State")]
    #region
    [Tooltip("Keeps track of whether or not the player is disabled. While disabled, movement and charging behaviors are not executed.")]
    [ReadOnly]
    public bool Disabled;
    
    [Tooltip("Keeps track of whether or not the player is resetting their position. Kart is disabled during this movement.")]
    [ReadOnly]
    public bool Resetting;
    
    [Tooltip("Keeps track of whether or not the player is drifting. While drifting, a persistent rotation is applied and charge level is increased.")]
    [ReadOnly]
    public bool Drifting;
    [SerializeField] [ReadOnly] bool m_startedDrifting = false;
    [SerializeField] [ReadOnly] bool m_endedDrifting = false;

    [Tooltip("Keeps track of whether or not the player is drifting to the left or right.")]
    [ReadOnly]
    public bool DriftRight;

    [Tooltip("Keeps track of whether or not the player is firing up an item.")]
    [ReadOnly]
    public bool Firing;
    [SerializeField] [ReadOnly] bool m_startedFiring = false;
    [SerializeField] [ReadOnly] bool m_endedFiring = false;

    [Tooltip("Keeps track of whether or not the player is boosting. While boosting, charging behaviors are disabled.")]
    [ReadOnly]
    public bool Boosting;
    [SerializeField] [ReadOnly] bool m_startedBoosting = false;
    [SerializeField] [ReadOnly] bool m_endedBoosting = false;

    [Tooltip("Keeps track of whether or not the player is charging. This only occurs while drifting and not holding down the Fire Input")]
    [ReadOnly]
    public bool Charging;
    [SerializeField] [ReadOnly] bool m_startedCharging = false;
    [SerializeField] [ReadOnly] bool m_endedCharging = false;
    #endregion
    
    [Header("Persistent Values")]
    #region
    [Tooltip("Retains Raw inputs.")]
    [ReadOnly]
    [SerializeField] Vector2 m_rawXYInput = Vector2.zero;
    [Tooltip("The remaining amount of time that the kart is disabled for.")]
    [ReadOnly]
    [SerializeField] float m_disabledTime = 0;

    public float HorizontalInput { get { return m_rawXYInput.x; } }
    public float VerticalInput { get { return m_rawXYInput.y; } }
    #endregion
    
    [Header("Debug")]
    [SerializeField] DebugChannelSO m_debugChannel;

    // DELEGATES //
    #region
    public delegate void OnKartDisabledAction(float _duration);
    public OnKartDisabledAction OnKartDisabled;
    
    public delegate void OnKartEnabledAction();
    public OnKartEnabledAction OnKartEnabled;
    
    public delegate void OnResetAction();
    public OnResetAction OnReset;

    public delegate void OnDriftStartAction(bool _right);
    public OnDriftStartAction OnDriftStart;

    public delegate void OnDriftEndAction();
    public OnDriftEndAction OnDriftEnd;

    public delegate void OnFireStartAction();
    public OnFireStartAction OnFireStart;

    public delegate void OnFireEndAction();
    public OnFireEndAction OnFireEnd;

    public delegate void OnBoostStartAction();
    public OnBoostStartAction OnBoostStart;

    public delegate void OnBoostEndAction();
    public OnBoostEndAction OnBoostEnd;
   
    public delegate void OnChargeStartAction();
    public OnChargeStartAction OnChargeStart;

    public delegate void OnChargeEndAction();
    public OnChargeEndAction OnChargeEnd;

    public delegate void OnEmoteAction(int _index);
    public OnEmoteAction OnEmote;

    public delegate void OnInputUpdateAction(Vector2 _dir, bool _boost);
    public OnInputUpdateAction OnInputUpdate;
    #endregion

    // COROUTINES //
    #region
    IEnumerator co_chargeEnergy, co_disable;
    #endregion

    // DEFAULT METHODS //
    #region
    new void OnEnable()
    {
        base.OnEnable();

        OnBoostStart += StartBoost;
        OnBoostEnd += EndBoost;

        OnDriftStart += StartDrift;
        OnDriftEnd += EndDrift;

        OnFireStart += StartFire;
        OnFireEnd += EndFire;

        OnChargeStart += StartCharge;
        OnChargeEnd += EndCharge;
        
        Core.OnRaceCompleted += DisableKart;

        Core.Motor.OnForcedMoveStart += DisableKart;
        Core.Motor.OnForcedMoveEnd += EnableKart;

        CountdownDisplay _countdownDisplay = FindObjectOfType<CountdownDisplay>();

        if (_countdownDisplay)
            _countdownDisplay.OnCountdownEnd += EnableKart;

    }
    new void OnDisable()
    {
        base.OnEnable();

        OnBoostStart -= StartBoost;
        OnBoostEnd -= EndBoost;

        OnDriftStart -= StartDrift;
        OnDriftEnd -= EndDrift;

        OnFireStart -= StartFire;
        OnFireEnd -= EndFire;
        
        Core.OnRaceCompleted -= DisableKart;

        OnChargeStart -= StartCharge;
        OnChargeEnd -= EndCharge;
        
        CountdownDisplay _countdownDisplay = FindObjectOfType<CountdownDisplay>();

        if (_countdownDisplay)
            _countdownDisplay.OnCountdownEnd -= EnableKart;
    }
    void Start()
    {
        if (FindObjectOfType<CountdownDisplay>() && !SceneManager.GetActiveScene().name.Contains("Lobby") && Core.IsLocal)
            DisableKart();
    }
    void Update()
    {
        // STANDARD PHOTON CHECK //
        #region
        if (!Core.IsLocal)
            return;
        #endregion

        // UPDATE RAW INPUTS //
        #region
        UpdateInputs();
        #endregion
        
        // DERIVE KART STATES FROM INPUTS //
        #region
        DeriveStates();
        #endregion

        // HANDLE DRIFT BEHAVIORS //
        #region
        HandleDriftAdjustments();
        #endregion
    }
    void LateUpdate()
    {
        // STANDARD PHOTON CHECK //
        #region
        if (!Core.IsLocal)
            return;
        #endregion

        // UPDATE JUMP HELD TIME
        #region
        if (!m_jumpInput && m_jumpHeldTime > 0) { m_jumpHeldTime = 0; }
        #endregion

        // UPDATE FIRE HELD TIME
        #region
        if (!m_fireInput && m_fireHeldTime > 0) { m_fireHeldTime = 0; }
        #endregion
    }
    #endregion

    // INPUT METHODS //
    #region
    /// <summary>
    /// Updates our raw inputs
    /// </summary>
    void UpdateInputs()
    {
        bool _update = false;

        // DIRECTIONAL INPUT //
        #region
        Vector2 _newInputDir = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));

        if (_newInputDir != m_rawXYInput)
            _update = true;

        XYInput = m_rawXYInput = _newInputDir;
        #endregion
        
        // JUMP INPUT //
        #region
        // If the Jump Input is pressed
        if (Input.GetAxis("Jump") > 0)
        {
            // Check if this is a fresh input
            if (!m_jumpInput)
            {
                _update = true;

                m_jumpInput = true;
            }
            else
            {
                // Increment jumpHeldTime by the time passed between frames.
                m_jumpHeldTime += Time.deltaTime;
            }
        }
        // If the Jump Input is not pressed
        else
        {
            if (m_jumpInput)
                _update = true;

            m_jumpInput = false;
        }
        #endregion

        // FIRE INPUT //
        #region
        // If the fire input is pressed
        if (Input.GetAxis("Fire3") > 0)
        {
            // Check if this is a fresh Fire Input
            if (!m_fireInput)
            {
                _update = true;

                // Update Fire Input accordingly
                m_fireInput = true;
            }
            else
            {
                // Increment fireHeldTime by the time passed between frames
                m_fireHeldTime += Time.deltaTime;
            }
        }
        // If we do not have a Fire Input
        else
        {
            if (m_fireInput)
                _update = true;

            m_fireInput = false;
        }
        #endregion

        // EMOTE INPUT //
        #region
        for (int i = 1; i < 10; i++)
        {
            if (!Input.GetKeyDown(i.ToString()))
                continue;

            if (OnEmote != null)
                OnEmote.Invoke(i);

            break;
        }
        #endregion

        // RESET INPUT //
        #region
        if (Input.GetKeyDown(KeyCode.R))
            if (OnReset != null)
                OnReset.Invoke();
            
        #endregion

        if (Disabled)
            _update = false;

        if (_update && OnInputUpdate != null)
            OnInputUpdate.Invoke(m_rawXYInput, m_jumpInput && m_fireInput);
    }
    #endregion

    // ENABLE / DISABLE KART //
    #region
    public void EnableKart()
    {
        Disabled = false;

        if (OnKartEnabled != null)
            OnKartEnabled.Invoke();

        if (OnInputUpdate != null)
            OnInputUpdate.Invoke(m_rawXYInput, Boosting);

        if (m_debugChannel)
            m_debugChannel.Raise(this, "Racer Enabled!");
    }
    public void DisableKart()
    {
        // If we were not previously disabled...
        if (!Disabled)
        {
            // Set Disabled state value
            Disabled = true;

            // End active drifting
            if (Drifting)
                if (OnDriftEnd != null)
                    OnDriftEnd.Invoke();

            // End active boosting
            if (Boosting)
                if (OnBoostEnd != null)
                    OnBoostEnd.Invoke();

            // End active firing
            if (Firing)
                if (OnFireEnd != null)
                    OnFireEnd.Invoke();

            // End active charging
            if (Charging)
                if (OnChargeEnd != null)
                    OnChargeEnd.Invoke();

            // Invoke the disabled delegate
            if (OnKartDisabled != null)
                OnKartDisabled.Invoke(m_disabledTime);

            if (m_debugChannel)
                m_debugChannel.Raise(this, "Kart Disabled!");
        }
    }
    #endregion

    // BOOST METHODS //
    #region
    void StartBoost()
    {
        if (m_debugChannel)
            m_debugChannel.Raise(this, "Start Boost!");

        Boosting = true;
    }
    void EndBoost()
    {
        if (m_debugChannel)
            m_debugChannel.Raise(this, "End Boost!");
        Boosting = false;
    }
    #endregion

    // DRIFT METHODS //
    #region
    void StartDrift(bool _right)
    {
        if (m_debugChannel)
            m_debugChannel.Raise(this, "Start Drift to the " + (_right ? "Right" : "Left") + "!");

        Drifting = true;
        DriftRight = _right;
    }
    void EndDrift()
    {
        if (m_debugChannel)
            m_debugChannel.Raise(this, "End Drift!");

        Drifting = false;
    }
    /// <summary>
    /// Adjusts the directional input values while drifting
    /// </summary>
    void HandleDriftAdjustments()
    {
        // If we are drifting
        if (Drifting)
        {
            // Offset our median X input based on whether we are drifting left or right.
            float driftOffset = DriftRight ? 0.5f : -0.5f;

            // Create a new input using a quarter of our actual input multiplied by Motor Handling.
            float inputAdjustment = (XYInput.x / 4) * Core.Motor.Handling;

            // Set the Motor's adjusted input via 
            XYInput.x = driftOffset + inputAdjustment;
        }
    }
    #endregion

    // FIRE METHODS //
    #region
    void StartFire()
    {
        if (m_debugChannel)
            m_debugChannel.Raise(this, "Start Firing!");

        Firing = true;
    }
    void EndFire()
    {
        if (m_debugChannel)
            m_debugChannel.Raise(this, "End Firing!");

        Firing = false;
    }
    #endregion

    // CHARGE METHODS //
    #region
    void StartCharge()
    {
        if (m_debugChannel)
            m_debugChannel.Raise(this, "Start Charging!");

        Charging = true;
    }
    void EndCharge()
    {
        if (m_debugChannel)
            m_debugChannel.Raise(this, "End Charging!");

        Charging = false;
    }
    #endregion

    // STATE METHODS //
    #region
    /// <summary>
    /// Interprets inputs into states
    /// </summary>
    void DeriveStates()
    {
        // If we are disabled, we don't have to derive any states
        if (Disabled)
            return;

        m_startedBoosting = m_startedDrifting = m_startedFiring = m_startedCharging = false;
        m_endedBoosting = m_endedDrifting = m_endedFiring = m_endedCharging = false;

        // Boosting updates
        switch (Boosting)
        {
            case (true):
                // If we are not firing or we are out of charge, end the boost
                if (!m_jumpInput || !m_fireInput || Core.Motor.AdjustedCharge <= 0)
                {
                    m_endedBoosting = true;

                    if (OnBoostEnd != null)
                        OnBoostEnd.Invoke();
                }
                break;
            case (false):
                // If we are holding the jump and fire input together and we have charge, start the boost
                if (m_jumpInput && m_fireInput && Core.Motor.AdjustedCharge > 0.2)
                {
                    m_startedBoosting = true;

                    if (OnBoostStart != null)
                        OnBoostStart.Invoke();
                }
                break;
        }

        switch (Drifting)
        {
            case (true):
                // If jump is released or our speed is less than 25% of the max, end drift
                if ((!m_jumpInput || Core.RigidBD.velocity.magnitude <= Core.Motor.MaxSpeed / 4))
                {
                    m_endedDrifting = true;

                    if (OnDriftEnd != null)
                        OnDriftEnd.Invoke();
                }
                break;
            case (false):
                // If we have jump input, we are holding left/right, and our speed is greater than 25%, start drift
                if (m_jumpInput && !m_fireInput && Mathf.Abs(HorizontalInput) > 0.2f && Core.RigidBD.velocity.magnitude > Core.Motor.MaxSpeed / 4)
                {
                    m_startedDrifting = true;

                    if (OnDriftStart != null)
                        OnDriftStart.Invoke(HorizontalInput > 0);
                }
                break;
        }
        
        switch (Firing)
        {
            case (true):
                // If we have released Fire OR there is Jump input, end firing
                if (!m_fireInput || Boosting)
                {
                    m_endedFiring = true;

                    if (OnFireEnd != null)
                        OnFireEnd.Invoke();
                }
                break;
            case (false):
                // If we have pressed Fire and are not Boosting, start firing
                if (m_fireInput && !Boosting)
                {
                    m_startedFiring = true;

                    if (OnFireStart != null)
                        OnFireStart.Invoke();
                }
                break;
        }

        switch (Charging)
        {
            case (true):
                // If we are firing, boosting, not Drifting or have no charge remaining, end charge
                if (m_fireInput || Boosting || !Drifting)
                {
                    m_endedCharging = true;

                    if (OnChargeEnd != null)
                        OnChargeEnd.Invoke();
                }
                break;
            case (false):
                // If we aren't firing or boosting, but we are drifting and our charge is not full, start charging
                if (!m_fireInput && !Boosting && Drifting)
                {
                    m_startedCharging = true;

                    if (OnChargeStart != null)
                        OnChargeStart.Invoke();
                }
                break;
        }
    }
    #endregion


    // PHOTON / RPC //
    #region
    new public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            if (photonView && photonView.IsMine)
            {
                stream.SendNext(XYInput);
                stream.SendNext(Drifting);
                stream.SendNext(DriftRight);
                stream.SendNext(Boosting);
                stream.SendNext(Charging);
            }
        }
        else
        {
            if (photonView && !photonView.IsMine)
            {
                XYInput = (Vector2)stream.ReceiveNext();
                Drifting = (bool)stream.ReceiveNext();
                DriftRight = (bool)stream.ReceiveNext();
                Boosting = (bool)stream.ReceiveNext();
                Charging = (bool)stream.ReceiveNext();
            }
        }
    }
    #endregion
}
