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

/// <summary>
/// The Racer Motor keeps track of all movement/physics behaviors.
/// It is also in charge of holding Charge data from drifts.
/// </summary>

public class RacerMotor : RacerCoreFeature, IBoostable
{
    [Header("Kart Motor Attributes")]
    #region
    [Tooltip("The weight of the kart. Higher numbers will cause the kart to fall faster.")]
    [Range(2f, 4f)]
    public float Weight;

    [Tooltip("The rate at which a kart accelerates to max speed.")]
    [Range(30f, 50)]
    public float Acceleration;

    [Tooltip("The rate at which a kart can turn.")]
    [Range(0.5f, 1.5f)]
    public float Handling;

    [Tooltip("The amount of control you have over your drift.")]
    [Range(0.5f, 1.5f)]
    public float DriftControl;
    #endregion
    
    [Header("Kart Adjustment Values")]
    #region
    [Tooltip("An adjusted X input value derived from the actual X input and our drift status.")]
    [ReadOnly]
    public float AdjustedHorizontalInput;

    [Tooltip("The speed at which the kart will attempt to stabilize to a mid-air position.")]
    [Range(0.1f, 5f)]
    public float VerticalSpeed;

    [Tooltip("The amount of time in seconds taken to reset back to the last stable position.")]
    [ReadOnly]
    public float TimeToLastStablePosition;
    #endregion
    
    [Header("Charging Data")]
    #region
    [Tooltip("A modifier to augment charge time.")]
    [Range(0.5f, 1.5f)]
    public float ChargeUpRate;

    [Tooltip("The rate at which a boost uses up charge.")]
    [Range(0.5f, 1.5f)]
    public float BoostDepletionRate;

    [Tooltip("Holds the current unscaled charge level of the kart. The Y value holds the maximum value.")]
    [ReadOnly]
    public Vector2 RawCharge = new Vector2(0, 5);

    [Tooltip("Holds the current scaled charge level of the kart. The Y value holds the maximum value.")]
    [ReadOnly]
    public Vector2 ScaledCharge = new Vector2(0, 3);

    public float AdjustedCharge { get { return ScaledCharge.y * (RawCharge.x / RawCharge.y); } }
    public float ChargePerc { get { return AdjustedCharge / ScaledCharge.y; } }
    public int ChargeLevel { get { return Mathf.FloorToInt(AdjustedCharge); } }
    public bool Charging { get { return co_charge != null; } }
    #endregion
    
    [Header("Boost Parameters")]
    #region
    [Tooltip("Overrides the default boost behavior inherited from the controller for external calls.")]
    [ReadOnly]
    public bool BoostOverride;

    [Tooltip("Multiplies the kart's acceleration while boosting")]
    [Range(1.5f, 10f)]
    public float BoostMultiplier;
    #endregion
    
    [Header("Floor Detection")]
    #region
    [Tooltip("Keeps track of whether or not the player is falling. While falling, turning and accelerating are disabled.")]
    [ReadOnly]
    public bool Grounded;

    [Tooltip("An empty transform used to perform ground-detection raycasts with local rotations.")]
    public Transform Checker;
    private List<Vector3> CheckOffsets;

    [Tooltip("The distance from the center of the kart that the raycasts will originate from.")]
    [Range(0f, 1f)]
    public float CheckRadius;

    [Tooltip("The maximum distance of raycasts.")]
    [Range(0, 2f)]
    public float CheckRange;

    [Tooltip("The maximum angle of a surface that will be counted as 'stable' compared to our current floor angle.")]
    [Range(15, 90f)]
    public float MaxLegalAngle;

    [Tooltip("A smoothing factor that controls how quickly the floor-checker can rotate to match the angle of 'stable' ground.")]
    [Range(0.1f, 10)]
    public float CheckerRotationSpeed;

    [Tooltip("Raycast source transforms")]
    public List<RaycastHit> FloorHits;

    //[Tooltip("A stable position derived from the average of all floor detection hit locations.")]
    //[ReadOnly]
    //public Vector3 StablePosition = new Vector3();

    [Tooltip("A vector that stores the direction pointing away from our current 'stable' ground.")]
    [ReadOnly]
    [SerializeField] Vector3 m_floorNormal = new Vector3();
    public Vector3 FloorNormal { get { return m_floorNormal; } }
    #endregion
    
    [Header("Physics")]
    #region
    [Tooltip("Toggles gravity for this object.")]
    public bool GravityEnabled;

    [Tooltip("The strength of gravity on this object.")]
    [Range(0.1f, 20)]
    public float GravityStrength;

    [Tooltip("Toggles whether or not the rigidbody's velocity is clamped to the MaxSpeed variable.")]
    public bool ClampMaxSpeed;

    [ReadOnly]
    public float CurrentSpeed;

    [Tooltip("The maximum velocity of the kart's rigidbody.")]
    [Range(10, 30)]
    public float MaxSpeed;
    #endregion
    
    [Header("Persistent Values")]
    #region
    [Tooltip("Time elapsed since the kart was last on the ground.")]
    [ReadOnly]
    public float TimeSinceGrounded = 0;

    [Tooltip("Time remaining in external boost conditions.")]
    [ReadOnly]
    [SerializeField] float BoostTimeRemaining = 0;

    [Tooltip("The last valid position found by the floor checker.")]
    [ReadOnly]
    public Vector3 LastValidPosition = new Vector3();

    [Tooltip("The rotation of the kart at the time of the last recorded valid position.")]
    [ReadOnly]
    public Quaternion LastValidRotation = new Quaternion();

    [Tooltip("Rotational value of the kart's Y axis")]
    [ReadOnly]
    public float Yaw;

    [Tooltip("The height that the kart will try to maintain as it moves across a surface")]
    [ReadOnly]
    public float DriveHeight;

    [Tooltip("The last valid surface normal that the kart drove across.")]
    [SerializeField] Vector3 m_lastNormal = new Vector3();
    public Vector3 LastNormal { get { return m_lastNormal; } }

    [SerializeField] [ReadOnly] bool m_boostHeld = false;
    #endregion

    [Header("Debug")]
    [SerializeField] DebugChannelSO m_debugChannel;

    // PRIVATE VALUES //
    #region
    float m_defaultAcceleration;
    float m_defaultDrag;
    #endregion

    // COROUTINES //
    #region
    private IEnumerator co_charge, co_suppCharge, co_boost, co_fall, co_externalBoost, co_moveToPosition;
    #endregion

    // DELEGATES //
    #region
    public delegate void OnStartMoveAction();
    public OnStartMoveAction OnStartMove;

    public delegate void OnEndMoveAction();
    public OnEndMoveAction OnEndMove;

    public delegate void OnLandAction();
    public OnLandAction OnLand;

    public delegate void OnFallAction();
    public OnFallAction OnFall;

    public delegate void OnHitWallAction();
    public OnHitWallAction OnHitWall;

    public delegate void OnHitHazardAction();
    public OnHitHazardAction OnHitHazard;

    public delegate void OnChargeUpdateAction(float percentage);
    public OnChargeUpdateAction OnChargeUpdate;

    public delegate void OnGroundHitInfoUpdateAction(List<RaycastHit> _hits);
    public OnGroundHitInfoUpdateAction OnGroundHitInfoUpdate;

    public delegate void OnForcedMoveStartAction();
    public OnForcedMoveStartAction OnForcedMoveStart;

    public delegate void OnForcedMoveEndAction();
    public OnForcedMoveEndAction OnForcedMoveEnd;
    #endregion

    // DEFAULT METHODS //
    #region
    new void OnEnable()
    {
        base.OnEnable();
        
        OnFall += StartFall;
        OnLand += Land;

        Core.Animator.OnSpinStart += SlowOnHit;
        Core.Animator.OnSpinEnd += RestoreSpeed;

        Core.Controller.OnChargeStart += StartCharge;
        Core.Controller.OnChargeEnd += EndCharge;

        Core.Controller.OnBoostStart += StartBoost;
        Core.Controller.OnBoostEnd += EndBoost;

        Core.Controller.OnReset += ResetPosition;
    }
    new void OnDisable()
    {
        base.OnEnable();

        OnFall -= StartFall;
        OnLand -= Land;

        Core.Animator.OnSpinStart -= SlowOnHit;
        Core.Animator.OnSpinEnd -= RestoreSpeed;

        Core.Controller.OnChargeStart -= StartCharge;
        Core.Controller.OnChargeEnd -= EndCharge;

        Core.Controller.OnBoostStart -= StartBoost;
        Core.Controller.OnBoostEnd -= EndBoost;
        
        Core.Controller.OnReset -= ResetPosition;
    }
    void Start()
    {
        // By default, our floor normal is the default kart up.
        m_floorNormal = transform.up;
        m_defaultAcceleration = Acceleration;
        m_defaultDrag = Core.RigidBD.drag;
    }
    void FixedUpdate()
    {
        // UPDATE FLOOR HITS
        #region
        FloorHits = GetFloorHits();
        #endregion

        // CHECK IF GROUNDED
        #region
        CheckIfGrounded(FloorHits);
        #endregion

        // HANDLE MOVEMENT BEHAVIORS
        #region
        HandleMovement();
        #endregion
    }
    void OnDrawGizmos()
    {
        if (!m_debugChannel || !m_debugChannel.DebugGizmosEnabled)
            return;
        
        CheckOffsets = new List<Vector3>
        {
            Vector3.zero,
            Checker.forward,
            -Checker.forward,
            Checker.right,
            -Checker.right,
            Checker.forward - Checker.right,
            Checker.forward + Checker.right,
            -Checker.forward - Checker.right,
            -Checker.forward + Checker.right
        };

        RaycastHit hit;

        foreach (Vector3 offset in CheckOffsets)
        {
            if (Physics.Raycast(Checker.position + (offset.normalized * CheckRadius), -Checker.up, out hit, Core.SphereCol.radius + CheckRange) && Vector3.Angle(hit.normal, Checker.up) <= MaxLegalAngle)
            {
                Gizmos.color = Color.blue;
                Gizmos.DrawSphere(hit.point, 0.1f);
                Gizmos.color = Color.green;
                Gizmos.DrawRay(Checker.position + (offset.normalized * CheckRadius), -Checker.up * (hit.distance - 0.1f));
            }
            else
            {
                Gizmos.color = Color.red;
                Gizmos.DrawRay(Checker.position + (offset.normalized * CheckRadius), -Checker.up * (Core.SphereCol.radius + CheckRange));
            }
        }

        Gizmos.color = Color.magenta;
        Gizmos.DrawWireSphere(LastValidPosition, 0.5f);

        Gizmos.color = Color.cyan;
        Gizmos.DrawRay(transform.position, m_floorNormal);
    }
    #endregion

    // GRAVITY METHODS //
    #region
    public void EnableGravity()
    {
        GravityEnabled = true;
    }
    public void DisableGravity(float _duration = 0)
    {
        GravityEnabled = false;
    }
    #endregion

    // FALLING METHODS //
    #region
    void StartFall()
    {
        Grounded = false;

        if (co_fall != null)
        {
            StopCoroutine(co_fall);
            co_fall = null;
        }

        co_fall = Fall();
        StartCoroutine(co_fall);
    }
    void Land()
    {
        Grounded = true;

        if (co_fall == null)
            return;
        
        StopCoroutine(co_fall);
        co_fall = null;
    }
    IEnumerator Fall()
    {
        TimeSinceGrounded = 0;

        while (TimeSinceGrounded >= 3)
        {
            TimeSinceGrounded += Time.deltaTime;
            
            yield return null;
        }

        ResetToPosition(LastValidPosition);

        co_fall = null;

        yield return null;
    }
    #endregion

    // CHARGING METHODS //
    #region
    void StartCharge()
    {
        if (m_debugChannel)
            m_debugChannel.Raise(this, "Started Charging");

        if (co_charge != null)
        {
            StopCoroutine(co_charge);
            co_charge = null;
        }

        co_charge = Charge();
        StartCoroutine(co_charge);
    }
    void EndCharge()
    {
        if (co_charge == null)
            return;

        if (m_debugChannel)
            m_debugChannel.Raise(this, "Stopped Charging");

        StopCoroutine(co_charge);
        co_charge = null;
    }
    IEnumerator Charge()
    {
        while (ChargePerc < 1)
        {
            //Debug.Log("Still charging...");

            RawCharge.x = Mathf.Clamp(RawCharge.x + ChargeUpRate * Mathf.Abs(Core.Controller.XYInput.x) * Time.deltaTime, 0, RawCharge.y);

            ScaledCharge.x = AdjustedCharge;

            if (OnChargeUpdate != null)
                OnChargeUpdate.Invoke(ChargePerc);

            yield return null;
        }

        if (OnChargeUpdate != null)
            OnChargeUpdate.Invoke(1);

        co_charge = null;

        yield return null;
    }
    public void StartSupplementalCharge()
    {
        if (m_debugChannel)
            m_debugChannel.Raise(this, "Started Supplemental Charging");

        if (co_suppCharge != null)
        {
            StopCoroutine(co_suppCharge);
            co_suppCharge = null;
        }

        co_suppCharge = SupplementalCharge();
        StartCoroutine(co_suppCharge);
    }
    void EndSupplementalCharge()
    {
        if (co_suppCharge == null)
            return;

        if (m_debugChannel)
            m_debugChannel.Raise(this, "Stopped Supplemental Charging");

        StopCoroutine(co_suppCharge);
        co_suppCharge = null;
    }
    IEnumerator SupplementalCharge()
    {
        for (float t = 3; t > 0; t -= Time.deltaTime)
        {
            RawCharge.x = Mathf.Clamp(RawCharge.x + Time.deltaTime, 0, RawCharge.y);

            ScaledCharge.x = AdjustedCharge;

            if (OnChargeUpdate != null)
                OnChargeUpdate.Invoke(ChargePerc);

            yield return null;
        }

        co_suppCharge = null;

        yield return null;
    }
    #endregion

    // BOOSTING METHODS //
    #region
    void StartBoost()
    {
        if (m_debugChannel)
            m_debugChannel.Raise(this, "Started Boosting");

        m_boostHeld = true;

        if (co_boost != null)
        {
            StopCoroutine(co_boost);
            co_boost = null;
        }

        co_boost = Boost();
        StartCoroutine(co_boost);
    }
    void EndBoost()
    {
        if (co_boost == null)
            return;

        m_boostHeld = false;

        if (m_debugChannel)
            m_debugChannel.Raise(this, "Stopped Boosting");

        StopCoroutine(co_boost);
        co_boost = null;

        if (OnChargeUpdate != null)
            OnChargeUpdate.Invoke(ChargePerc);
    }
    IEnumerator Boost()
    {
        while (m_boostHeld)
        {
            RawCharge.x = Mathf.Clamp(RawCharge.x - BoostDepletionRate * Time.deltaTime, 0, RawCharge.y);

            ScaledCharge.x = AdjustedCharge;

            if (m_debugChannel)
                m_debugChannel.Raise(this, "Charge at " + ChargePerc.ToString() + " percent.");

            if (OnChargeUpdate != null)
                OnChargeUpdate.Invoke(ChargePerc);

            yield return null;
        }

        co_boost = null;

        yield break;
    }
    #endregion

    // FLOOR CHECK METHODS //
    #region
    /// <summary>
    /// Get our floor hit results from the checker
    /// </summary>
    /// <returns></returns>
    List<RaycastHit> GetFloorHits()
    {
        // Create a field for our hit results
        RaycastHit hit;

        // Create a list for multiple hits
        List<RaycastHit> hits = new List<RaycastHit>();

        // Create a list of normalized offsets to check
        // Directly below, front, back, right, left, front-left, front-right, back-left, and back-right
        CheckOffsets = new List<Vector3>
        {
            Vector3.zero,
            Checker.forward,
            -Checker.forward,
            Checker.right,
            -Checker.right,
            (Checker.forward - Checker.right).normalized,
            (Checker.forward + Checker.right).normalized,
            (-Checker.forward - Checker.right).normalized,
            (-Checker.forward + Checker.right).normalized
        };

        // Start with no valid hits
        int validHits = 0;

        // For each offset...
        foreach (Vector3 _offset in CheckOffsets)
        {
            // Raycast downwards from the Checker's position plus the offset scaled by the checking radius
            // at a distance equal to the radius of the collider plus an additional range modifier
            if (Physics.Raycast(Checker.position + (_offset * CheckRadius), -Checker.up, out hit, Core.SphereCol.radius + CheckRange))
                if (Vector3.Angle(hit.normal, m_floorNormal) < MaxLegalAngle)
                {
                    hits.Add(hit);
                    validHits++;
                    continue;
                }

            hits.Add(new RaycastHit());
        }

        //if (enableDebugMessages) { Debug.Log("[Racer Motor] " + validHits + " hits!"); }

        return hits;
    }
    /// <summary>
    /// Return whether or not we are grounded based on our floor hits
    /// </summary>
    /// <param name="hits"></param>
    /// <returns></returns>
    public void CheckIfGrounded(List<RaycastHit> hits)
    {
        int hitCounter = 0;

        for (int i = 0; i < hits.Count; i++)
        {
            if (hits[i].collider)
            {
                hitCounter++;
            }
        }

        if (!Grounded && hitCounter > 4)
            if (OnLand != null)
                OnLand.Invoke();

        if (Grounded && hitCounter <= 4)
            if (OnFall != null)
                OnFall.Invoke();

        if (Grounded)
        {
            // Update our most recent "up" derived from our track
            m_floorNormal = GetAverageNormal(hits);

            if (CheckIfOnTrack())
            {
                // Update our most recent valid position and store it for later
                LastValidPosition = GetStablePosition(hits);

                // Update our most recent valid rotation and store it for later
                LastValidRotation = transform.rotation;
            }
        }

    }
    /// <summary>
    /// Gets a stable position based on an average of floor hits
    /// </summary>
    /// <param name="hits"></param>
    /// <returns></returns>
    Vector3 GetStablePosition(List<RaycastHit> hits)
    {
        Vector3 stablePosition = new Vector3();

        int validPoints = 0;

        foreach (RaycastHit hit in hits)
        {
            if (hit.collider)
            {
                stablePosition += hit.point;
                validPoints++;
            }
        }

        if (validPoints == 0) { return LastValidPosition; }

        Vector3 finalPosition = (stablePosition / validPoints) + (m_floorNormal * Core.SphereCol.radius);

        return finalPosition;
    }
    /// <summary>
    /// Gets an average of all normals from our floor hits
    /// </summary>
    /// <param name="hits"></param>
    /// <returns></returns>
    Vector3 GetAverageNormal(List<RaycastHit> hits)
    {
        Vector3 averagedNormal = new Vector3();

        int validHits = 0;

        foreach (RaycastHit hit in hits)
        {
            if (hit.collider)
            {
                averagedNormal += hit.normal;
                validHits++;
            }
        }

        if (validHits == 0) { return LastNormal; }

        return (averagedNormal / validHits).normalized;
    }
    #endregion

    // MOVEMENT METHODS //
    #region
    /// <summary>
    /// Handles physics-based mvoement based on floor checks and states
    /// </summary>
    void HandleMovement()
    {
        if (Grounded)
        {
            if (!Core.Controller.Disabled)
                ApplyMovement();

            AdjustToGround(FloorHits);
        }
        else
        {
            // Gravity direction is determined by the last 
            if (m_floorNormal == Vector3.zero)
            {
                m_floorNormal = Vector3.up;
            }

            if (GravityEnabled)
            {
                ApplyGravity();
            }
        }

        if (ClampMaxSpeed && Core.RigidBD.velocity.magnitude > MaxSpeed)
        {
            Core.RigidBD.velocity = Vector3.ClampMagnitude(Core.RigidBD.velocity, MaxSpeed);
        }
    }
    /// <summary>
    /// Adjusts the collider (not visual kart) to the ground based on persistent and new data
    /// </summary>
    /// <param name="hits"></param>
    void AdjustToGround(List<RaycastHit> hits)
    {
        // Create a target rotation based on the rotation to get from up to the floor angle combined with our rotation
        Quaternion targetRotation = Quaternion.FromToRotation(transform.up, m_floorNormal) * transform.rotation;

        // Set our rotation to the new target rotation
        transform.rotation = targetRotation;

        float YRotation = Core.Controller.Disabled ? 0 : Core.Controller.XYInput.x * (Handling * 100);

        // Apply horizontal rotation according to our horizontal input axis
        transform.Rotate(0, YRotation * Time.fixedDeltaTime, 0, Space.Self);

        // Store the current floor normal
        m_lastNormal = m_floorNormal;
        
        DriveHeight = Mathf.Lerp(DriveHeight, Core.SphereCol.radius - hits[0].distance, Time.fixedDeltaTime * VerticalSpeed);

        transform.localPosition += m_floorNormal * DriveHeight;
    }
    /// <summary>
    /// Applies force to the kart's rigidbody
    /// </summary>
    void ApplyMovement()
    {
        // Derive our forward factor. Boosting overrides forward input.
        float _forwardFactor = Core.Controller.Boosting || BoostOverride ? 1 : Core.Controller.VerticalInput;

        // Derive our acceleration factor. Boosting applies an additional multiplier.
        float _accelFactor = Core.Controller.Boosting || BoostOverride ? Acceleration * BoostMultiplier : Acceleration;

        Core.RigidBD.AddForce(transform.forward * _forwardFactor * _accelFactor, ForceMode.Acceleration);

        CurrentSpeed = Core.RigidBD.velocity.magnitude;
    }
    /// <summary>
    /// Applies a custom gravitational force to the kart's rigidbody
    /// </summary>
    void ApplyGravity()
    {
        Vector3 GravityVector = -m_floorNormal * (Weight * GravityStrength);
        Core.RigidBD.AddForce(GravityVector, ForceMode.Acceleration);
    }
    #endregion

    // FORCE BOOST CALL //
    #region
    /// <summary>
    /// An external boost call that overrides the controller's boost behavior
    /// </summary>
    /// <param name="forward"></param>
    /// <param name="force"></param>
    /// <param name="dir"></param>
    public void CallBoost(float _duration = 0)
    {
        if (BoostTimeRemaining > _duration)
            return;

        if (BoostTimeRemaining != 0 && _duration == 0)
            return;

        if (co_externalBoost == null)
        {
            co_externalBoost = ExternalBoost();
            StartCoroutine(co_externalBoost);
        }
    }
    IEnumerator ExternalBoost()
    {
        BoostOverride = true;

        while (BoostTimeRemaining > 0)
        {
            BoostTimeRemaining -= Time.deltaTime;

            yield return null;
        }

        BoostOverride = false;

        co_externalBoost = null;

        yield return null;
    }
    #endregion

    // RESET POSITION METHODS //
    #region
    bool CheckIfOnTrack()
    {
        return Physics.Raycast(transform.position, Vector3.down, 1, LayerMask.GetMask("Track"));
    }
    void ResetPosition()
    {
        transform.position = LastValidPosition;
        transform.rotation = LastValidRotation;
    }
    public void ResetToPosition(Vector3 _destination)
    {
        if (co_moveToPosition != null)
        {
            StopCoroutine(co_moveToPosition);
            co_moveToPosition = null;
        }

        co_moveToPosition = MoveToPosition(_destination);
        StartCoroutine(co_moveToPosition);
    }
    void EndResetPosition()
    {
        if (co_moveToPosition == null)
            return;

        StopCoroutine(co_moveToPosition);
        co_moveToPosition = null;
    }
    IEnumerator MoveToPosition(Vector3 _destination)
    {
        if (OnForcedMoveStart != null)
            OnForcedMoveStart.Invoke();

        DisableGravity();

        float timer = 0;
        Vector3 StartPos = transform.position;

        while (timer < TimeToLastStablePosition)
        {
            transform.position = Vector3.Lerp(StartPos, _destination, timer / TimeToLastStablePosition);

            timer += Time.deltaTime;

            yield return null;
        }

        transform.position = _destination;

        EnableGravity();

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

        yield return null;
    }
    #endregion

    // HAZARD HIT METHODS //
    #region
    public void Hit()
    {
        if (OnHitHazard != null)
            OnHitHazard.Invoke();
    }
    void SlowOnHit()
    {
        // Sets acceleration down to a crawl
        Core.Motor.Acceleration = m_defaultAcceleration * 0.25f;

        // Increases the drag on the kart to slow it down
        Core.RigidBD.drag = 5f;
    }
    void RestoreSpeed()
    {
        // Set the acceleration factor and drag back to default
        Acceleration = m_defaultAcceleration;
        Core.RigidBD.drag = m_defaultDrag;
    }
    #endregion

    // PHOTON METHODS //
    #region
    new public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        throw new System.NotImplementedException();
    }
    #endregion
}
