using Extensions;
using UnityEngine;
using NaughtyAttributes;

public class PlayerController : MonoBehaviour
{
    [Header("Config")]
    [SerializeField]
    [Expandable]
    private PlayerMovementSettings _movementSettings;

    [Header("References")]
    [SerializeField]
    private PlayerInputHandler _inputHandler;
    
    [SerializeField]
    private CharacterController _controller;
    
    [SerializeField]
    private PlayerPowers _powers;

    [SerializeField]
    private Transform _graphics;

    [SerializeField]
    private Renderer[] _renderers;
    
    [Header( "Debug" )]
    [SerializeField]
    private Color _groundedColor;
    
    [SerializeField]
    private Color _jumpColor;
    
    [SerializeField]
    private Color _boostColor;
    
    [SerializeField]
    private Color _exhaustedColor;

    [ShowNonSerializedField]
    private Vector2 _surfVelocity;
    
    [ShowNonSerializedField]
    private Vector2 _overrideDelta;

    [ShowNonSerializedField]
    private float _lookAngle;
    
    [ShowNonSerializedField]
    private float _yVelocity;

    [ShowNonSerializedField]
    private bool _grounded;

    [ShowNativeProperty]
    private bool Still => Mathf.Approximately(_inputHandler?.InputState?.Move.Value.sqrMagnitude ?? 0f, 0f);

    [ShowNonSerializedField]
    private float _boostTime;
    
    [ShowNonSerializedField]
    private float _fallTime;

    public bool CanJump => _grounded || _fallTime < _movementSettings.CoyoteTime;

    private const float PI = Mathf.PI;
    private const float TAU = Mathf.PI * 2f;
    private const float HALF_PI = Mathf.PI / 2f;

    private enum JumpState
    {
        None,
        Jump,
        Boost,
        Exhausted,
        COUNT,
    }

    [ShowNonSerializedField]
    private JumpState _jumpState;

    private void Start()
    {
    }

    private Color GetDebugColor()
    {
        switch ( _jumpState )
        {
            case JumpState.None:
                return _groundedColor;
            
            case JumpState.Jump:
                return _jumpColor;
            
            case JumpState.Boost:
                return _boostColor;
            
            case JumpState.Exhausted:
                return _exhaustedColor;
        }

        return Color.red;
    }

    private float normaliseAngle( float angle )
    {
        while ( angle > PI )
        {
            angle -= TAU;
        }

        while ( angle < -PI )
        {
            angle += TAU;
        }

        return angle;
    }

    private void Land()
    {
        _powers.Blink.Reset();
        _powers.Boost.Reset();
    }

    private void UpdateLook( float dt )
    {
        if ( _surfVelocity.sqrMagnitude > 0.01f )
        {
            float targetAngle = -Vector2.SignedAngle( Vector2.up, _surfVelocity.normalized ) * Mathf.Deg2Rad;
            float delta = targetAngle - _lookAngle;
            float ls = _movementSettings.LookSpeed * dt;

            _lookAngle += Mathf.Clamp( normaliseAngle(delta), -ls, ls );
            _lookAngle = normaliseAngle( _lookAngle );
        }
    }

    private void UpdateBlink( float dt )
    {
        if ( _powers.Blink.CanConsume && _inputHandler.InputState.Blink.GetDown() )
        {
            _powers.Blink.Consume();
            Vector2 blinkDirection = Vector2.up.Rotate( -_lookAngle );
            _overrideDelta += blinkDirection * _movementSettings.BlinkDistance;
            _surfVelocity += blinkDirection * _movementSettings.BlinkVelocity;
        }
    }

    private void UpdateJump(float dt)
    {
        _fallTime = _grounded ? 0f : (_fallTime + dt);
        
        switch ( _jumpState )
        {
            case JumpState.None:
                if ( CanJump )
                {
                    if ( _inputHandler.InputState.Jump.GetDown() )
                    {
                        _grounded = false;
                        _jumpState = JumpState.Jump;
                        _yVelocity = Mathf.Sqrt( 2f * _movementSettings.JumpHeight * _movementSettings.UpGravity );
                        
                        // always make sure we can blink once per jump
                        _powers.Blink.Reset();
                    }
                }
                else
                {
                    _jumpState = JumpState.Jump;
                }
                break;
            
            case JumpState.Jump:
                if ( _powers.Boost.CanConsume && _inputHandler.InputState.Jump.GetDown( consume: false ) )
                {
                    _powers.Boost.Consume();
                    _boostTime = 0f;
                    _jumpState = JumpState.Boost;
                    if ( _yVelocity < _movementSettings.MinBoostVelocity )
                    {
                        _yVelocity = _movementSettings.MinBoostVelocity;
                    }
                }

                break;

            case JumpState.Boost:
                if ( _inputHandler.InputState.Jump.GetRawValue() )
                {
                    _yVelocity += _movementSettings.BoostForce * dt;
                    _boostTime += dt;
                    if ( _boostTime > _movementSettings.BoostTime )
                    {
                        _jumpState = JumpState.Exhausted;
                    }
                }
                break;

            default:
                break;
        }
    }

    private void UpdateGravity( float dt )
    {
        if ( _grounded )
        {
            _yVelocity = _movementSettings.GroundedYVelocity;
            _jumpState = JumpState.None;
        }
        else
        {
            if ( _yVelocity > 0f && _jumpState == JumpState.Jump )
            {
                _yVelocity -= ( _movementSettings.UpGravity * dt );
            }
            else
            {
                _yVelocity -= ( _movementSettings.DownGravity * dt );
            }
        }
    }

    private void UpdateDrag( float dt )
    {
        Vector2 drag = -_surfVelocity;
        float coefficient
            = _grounded
                ? ( Still
                    ? _movementSettings.StillDragCoefficient
                    : _movementSettings.DragCoefficient )
                : ( _jumpState == JumpState.Boost
                    ? _movementSettings.BoostDragCoefficient
                    : _movementSettings.AirDragCoefficient );
        
        _surfVelocity += drag * (dt * coefficient);
    }

    private void UpdateMovement( float dt )
    {
        Vector2 moveVector = _inputHandler.InputState.Move.Value;
        _surfVelocity += moveVector * (_movementSettings.BaseMovementForce * dt);
    }

    private void UpdateDebug()
    {
        Color debugColor = GetDebugColor();
        for ( int i = 0; i < _renderers.Length; ++i )
        {
            _renderers[ i ].material.color = debugColor;
        }
    }

    private void FixedUpdate()
    {
        float dt = Time.fixedDeltaTime;

        _overrideDelta = Vector2.zero;

        if ( !_grounded && _controller.isGrounded )
        {
            Land();
        }
        
        _grounded = _controller.isGrounded;

        _powers.UpdatePowers( dt, _grounded );
        
        UpdateJump( dt );
        UpdateLook( dt );
        UpdateBlink( dt );
        UpdateGravity( dt );
        UpdateDrag( dt );
        UpdateMovement( dt );
        UpdateDebug();

        _controller.Move( 
            new Vector3(
                _surfVelocity.x * dt + _overrideDelta.x, 
                _yVelocity * dt, 
                _surfVelocity.y * dt + _overrideDelta.y
            ) 
        );
        
        _graphics.rotation = Quaternion.Euler( 0f, _lookAngle * Mathf.Rad2Deg, 0f );
    }
}