using UnityEngine;

public class Suspension : MonoBehaviour
{
    #region Variables
    
    [Header("References")]
    [SerializeField] private Rigidbody _rb;
    [SerializeField] private Transform _wheel;

    [Header("Details")] 
    [SerializeField] private float _offset = 0.1f;
    [SerializeField] private float _maximumTravel = 0.5f;
    [SerializeField] private float _maxForce = 20000f;
    [SerializeField] private float _dampingForce = 500f;
    
    [Curve(0f, 0f, 1f, 1f, true)]
    [SerializeField] private AnimationCurve _forceCurve;

    [HideInInspector] public bool isGrounded;
    [HideInInspector] public float force;
    
    private float _highestPoint;
    private float _lowestPoint;
    private float _lastUpdateProgression;
    private float _wheelRadius;
    
    #endregion

    #region Unity functions
    
    private void Start()
    {
        CalculateTravel();
        CalculateWheelRadius();
    }

    private void Update()
    {
        ClampWheel();
    }

    private void FixedUpdate()
    {
        CalculateSuspension();
    }
    
    #endregion

    #region Wheel
    
    /// <summary>
    /// Calculates the radius of the attached wheel
    /// </summary>
    private void CalculateWheelRadius()
    {
        _wheelRadius = _wheel.GetComponent<Renderer>().bounds.size.y;
    }
    
    /// <summary>
    /// Update the position of the wheel to the ground
    /// </summary>
    private void UpdateWheelPosition(float progression)
    {
        var wheelPosition = _highestPoint - _maximumTravel * (1 - progression) + _wheelRadius / 2;
        _wheel.localPosition = new Vector3(_wheel.localPosition.x, wheelPosition, _wheel.localPosition.z);
    }

    /// <summary>
    /// Clamp the wheel within the suspension travel
    /// </summary>
    private void ClampWheel()
    {
        var clamp = Mathf.Clamp(_wheel.localPosition.y, _lowestPoint, _highestPoint);
        _wheel.localPosition = new Vector3(_wheel.localPosition.x, clamp, _wheel.localPosition.z);
    }

    #endregion
    
    #region Suspension
    
    /// <summary>
    /// Calculates the top and bottom point of the suspension
    /// </summary>
    private void CalculateTravel()
    {
        _highestPoint = _wheel.localPosition.y + _offset;
        _lowestPoint = _highestPoint - _maximumTravel;
    }
    
    /// <summary>
    /// Calculates and applies all forces of the suspension
    /// </summary>
    private void CalculateSuspension()
    {
        var (progression, hasContact) = CalculateProgression();
        
        if (hasContact)
        {
            force = CalculateForce(progression);
            ApplyForce(force);
            isGrounded = true;
        }
        else
        {
            force = 0;
            isGrounded = false;
        }
        
        UpdateWheelPosition(progression);
    }

    /// <summary>
    /// Calculate the progression of the spring
    /// </summary>
    /// <returns> The progression </returns>
    private (float, bool) CalculateProgression()
    {
        var suspensionWorldPosition = _rb.transform.TransformPoint(new Vector3(_wheel.localPosition.x, _highestPoint, _wheel.localPosition.z));
        var raycastLength = _maximumTravel;
        
        if (Physics.Raycast(suspensionWorldPosition, -_rb.transform.up, out var hit, raycastLength))
        {
            var progression = Vector3.Distance(suspensionWorldPosition, hit.point);
            return (1 - progression / raycastLength, true);
        }
        
        return (0, false);
    }

    /// <summary>
    /// Calculates the force that the spring pushes
    /// </summary>
    /// <param name="progression"> The progression within the spring </param>
    /// <returns> The force it pushes with </returns>
    private float CalculateForce(float progression)
    {
        var forceFactor = _forceCurve.Evaluate(progression);
        var velocity = CalculateVelocity(progression);

        return forceFactor * _maxForce + velocity * _dampingForce;
    }

    /// <summary>
    /// Calculate the velocity of the tyre within the suspension
    /// </summary>
    /// <param name="progression"> The progression of the spring </param>
    /// <returns> The velocity </returns>
    private float CalculateVelocity(float progression)
    {
        var velocity = (progression - _lastUpdateProgression) / Time.fixedDeltaTime;
        _lastUpdateProgression = progression;
        return velocity;
    }

    /// <summary>
    /// Applies the forces to the car
    /// </summary>
    /// <param name="force"> The force that should be applied </param>
    private void ApplyForce(float force)
    {
        var localSuspensionPoint = new Vector3(_wheel.localPosition.x, _highestPoint, _wheel.localPosition.z);
        var forcePoint = _rb.transform.TransformPoint(localSuspensionPoint);
        _rb.AddForceAtPosition(_rb.transform.up * force, forcePoint);
    }
    
    #endregion
}