using Godot;
using PolarBears.PlayerControllerAddon;

public partial class MoveSystem : Node3D
{
    public record MoveSystemParameters(
        CharacterBody3D Parent,
        Gravity Gravity,
        MantleSystem MantleSystem,
        TweenQueueSystem TweenQueueSystem,
        HeadSystem HeadSystem,
        CapsuleCollider CapsuleCollider);

    public record MoveAroundParameters(
        double Delta,
        Vector3 MovementDirection,
        bool IsOnFloor,
        bool IsDead,
        bool IsHeadTouchingCeiling
    );
    
    [Export(PropertyHint.Range, "0,20,0.1,or_greater")]
    public float WalkSpeed             { get; set; } = 5.0f;
    [Export(PropertyHint.Range, "0,20,0.1,or_greater")]
    public float SprintSpeed           { get; set; } = 7.2f;
    [Export(PropertyHint.Range, "0,10,0.1,or_greater")]
    public float CrouchSpeed           { get; set; } = 2.5f;
    [Export(PropertyHint.Range, "0,100,0.1,or_greater")]
    
    public float _currentSpeed;
    private const float DecelerationSpeedFactorFloor = 15.0f;
    private const float DecelerationSpeedFactorAir   = 7.0f;
    
    public float CrouchTransitionSpeed { get; set; } = 20.0f;
    [Export(PropertyHint.Range, "0,5,0.1,or_greater")]
    public float DoubleJumpSpeedFactor { get; set; } = 2f;
    
    private bool _canDoubleJump = true;
    private float _lastFrameWasOnFloor = -Mathf.Inf;
    
    private Gravity _gravity;
    private CharacterBody3D _parent;
    private MantleSystem _mantleSystem;
    private TweenQueueSystem _tweenQueueSystem;
    private CapsuleCollider _capsuleCollider;
    private HeadSystem _headSystem;
    public void Init(MoveSystemParameters parameters)
    {
        _parent = parameters.Parent;
        _gravity = parameters.Gravity;
        _mantleSystem = parameters.MantleSystem;
        _tweenQueueSystem = parameters.TweenQueueSystem;
        _capsuleCollider = parameters.CapsuleCollider;
        _headSystem = parameters.HeadSystem;
        
        _currentSpeed = WalkSpeed;
    }

    public void MoveAround(MoveAroundParameters param)
    {
        var (delta, movementDirection, isOnFloor, isDead, isHeadTouchingCeiling) = param;
        
        var doesCapsuleHaveCrouchingHeight = _capsuleCollider.IsCrouchingHeight();
        var doesCapsuleHaveDefaultHeight = _capsuleCollider.IsDefaultHeight();
        
        // Adding the gravity
        if (!isOnFloor)
        {
            _parent.Velocity = new Vector3(
                x: _parent.Velocity.X,
                y: _parent.Velocity.Y - (_gravity.CalculateGravityForce() * (float)delta),
                z: _parent.Velocity.Z);
        }
		
        if (isOnFloor)
        {
            _lastFrameWasOnFloor = Engine.GetPhysicsFrames();
            _canDoubleJump = true;
        }
        
        // The code below is required to quickly adjust player's position on Y-axis when there's a ceiling on the
        // trajectory of player's jump and player is standing
        if (isHeadTouchingCeiling && doesCapsuleHaveDefaultHeight)
        {
            _parent.Velocity = new Vector3(
                x: _parent.Velocity.X,
                y: _parent.Velocity.Y - 2.0f,
                z: _parent.Velocity.Z);
        }

        if (!isDead)
        {
			
            // Used both for detecting the moment when we enter into crouching mode and the moment when we're already
            // in the crouching mode
            if (Input.IsActionPressed("crouch") ||
                (doesCapsuleHaveCrouchingHeight && isHeadTouchingCeiling))
            {
                _capsuleCollider.Crouch((float)delta, CrouchTransitionSpeed);
                _currentSpeed = CrouchSpeed;
            }
            // Used both for the moment when we exit the crouching mode and for the moment when we just walk
            else
            {
                _capsuleCollider.UndoCrouching((float)delta, CrouchTransitionSpeed);
                _currentSpeed = WalkSpeed;
            }
        }

        // Each component of the boolean statement for sprinting is required
        if (Input.IsActionPressed("sprint") && !isHeadTouchingCeiling && 
            !doesCapsuleHaveCrouchingHeight && !isDead)
        {
            _currentSpeed = SprintSpeed;
        }
		
        // Basis is a 3x4 matrix. It contains information about scaling and rotation of head.
        // By multiplying our Vector3 by this matrix we're doing multiple things:
        // a) We start to operate in global space;
        // b) We're applying to Vector3 the current rotation of "head" object;
        // c) We're applying to Vector3 the current scaling of "head" object;
        Vector3 direction = _headSystem.Transform.Basis * movementDirection;

        if (isDead)
        {
            direction = Vector3.Zero;
        }
        

        if (isOnFloor)
        {
            // Set velocity based on input direction when on the floor
            if (direction.Length() > 0)
            {
                float newX = direction.X * _currentSpeed;
                float newZ = direction.Z * _currentSpeed;

                _parent.Velocity = new Vector3(newX, _parent.Velocity.Y, newZ);
            }
            // If there is no input, smoothly decelerate the character on the floor
            else
            {
                float xDeceleration = Mathf.Lerp(_parent.Velocity.X, direction.X * _currentSpeed,
                    (float)delta * DecelerationSpeedFactorFloor);
                float zDeceleration = Mathf.Lerp(_parent.Velocity.Z, direction.Z * _currentSpeed,
                    (float)delta * DecelerationSpeedFactorFloor);

                _parent.Velocity = new Vector3(xDeceleration, _parent.Velocity.Y, zDeceleration);
            }
        }
        else
        {
            float xDeceleration = Mathf.Lerp(_parent.Velocity.X, direction.X * _currentSpeed,
                (float)delta * DecelerationSpeedFactorAir);
            float zDeceleration = Mathf.Lerp(_parent.Velocity.Z, direction.Z * _currentSpeed,
                (float)delta * DecelerationSpeedFactorAir);

            _parent.Velocity = new Vector3(xDeceleration, _parent.Velocity.Y, zDeceleration);
        }
		
        if (isDead)
        {
            _parent.MoveAndSlide();
        }
    }

    public void Jump(bool isOnFloor)
    {
        var mantleLocationResult = _mantleSystem.FindMantleInFrontOfPlayer();
        if (mantleLocationResult.IsSome(out var mantleLocation))
        {
            var duration = 0.1f * mantleLocation.DistanceTo(_parent.Position);
            _tweenQueueSystem.QueueTween(mantleLocation, duration);
        }
        else if (isOnFloor)
        {
            _parent.Velocity = new Vector3(
                x: _parent.Velocity.X,
                y: _gravity.CalculateJumpForce(),
                z: _parent.Velocity.Z);
        }
        else if (_canDoubleJump)
        {
            _canDoubleJump = false;
            _parent.Velocity = new Vector3(
                x: _parent.Velocity.X,
                y: _gravity.CalculateJumpForce() * DoubleJumpSpeedFactor,
                z: _parent.Velocity.Z);
        }
        
    }
}
