using Godot;
using Movementtests.player_controller.Scripts;
using RustyOptions;

namespace Movementtests.systems;

public partial class MoveSystem : Node3D
{
    public enum JumpTypes
    {
        SimpleJump,
        DoubleJump,
        JumpFromDash,
        JumpFromWall
    }
    
    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,
        bool isHanging,
        bool isWallHugging
    );
    
    [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;
    [Export(PropertyHint.Range, "0,10,0.1,or_greater")]
    public float AccelerationSpeedFactorFloor = 5.0f;
    [Export(PropertyHint.Range, "0,10,0.1,or_greater")]
    public float DecelerationSpeedFactorFloor = 5.0f;
    [Export(PropertyHint.Range, "0,10,0.1,or_greater")]
    public float DecelerationSpeedFactorAir   = 1.0f;
    [Export(PropertyHint.Range, "0,1,0.01,or_greater")]
    public float ApexHoldTime = 0.0f;
    private float _timeLeftAtApex = 0.0f;
    private bool _wasGoingUpLastFrame = false;
    
    public float CrouchTransitionSpeed { get; set; } = 20.0f;
    [Export(PropertyHint.Range, "0,5,0.1,or_greater")]
    public float WallHugGravityReducingFactor { get; set; } = 0.1f;
    
    
    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;
    }

    private bool IsGoingUp()
    {
        return _parent.Velocity.Y > 0;
    }

    public void MoveAround(MoveAroundParameters param)
    {
        var (delta, movementDirection, isOnFloor, isDead, isHeadTouchingCeiling, isHanging, isWallHugging) = param;
        
        var doesCapsuleHaveCrouchingHeight = _capsuleCollider.IsCrouchingHeight();
        var doesCapsuleHaveDefaultHeight = _capsuleCollider.IsDefaultHeight();

        if (IsGoingUp() || isOnFloor)
        {
            _wasGoingUpLastFrame = true;
            _timeLeftAtApex = ApexHoldTime;
        }

        if (isHanging)
        {
            _parent.Velocity = Vector3.Zero;
            _parent.MoveAndSlide();
            return;
        }
        if (isWallHugging)
        {
            _parent.Velocity = new Vector3(
                x: _parent.Velocity.X,
                y: _parent.Velocity.Y - _gravity.CalculateGravityForce() * (float)delta * WallHugGravityReducingFactor,
                z: _parent.Velocity.Z);
            return;
        }
        
        // Adding the gravity
        if (!isOnFloor)
        {
            if (!IsGoingUp() && _wasGoingUpLastFrame && _timeLeftAtApex > 0)
            {
                _parent.Velocity = new Vector3(
                    x: _parent.Velocity.X,
                    y: 0,
                    z: _parent.Velocity.Z);
                _timeLeftAtApex -= (float) delta;
            }
            else
            {
                _parent.Velocity = new Vector3(
                    x: _parent.Velocity.X,
                    y: _parent.Velocity.Y - (_gravity.CalculateGravityForce() * (float)delta),
                    z: _parent.Velocity.Z);
            }
        }
        
        // 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;
        }

        var accelerationFloorFactor = direction.Length() > 0 ? AccelerationSpeedFactorFloor : DecelerationSpeedFactorFloor;
        var accelerationFactor = isOnFloor ? accelerationFloorFactor : DecelerationSpeedFactorAir;
        
        float xAcceleration = Mathf.Lerp(_parent.Velocity.X, direction.X * _currentSpeed,
            (float)delta * accelerationFactor);
        float zAcceleration = Mathf.Lerp(_parent.Velocity.Z, direction.Z * _currentSpeed,
            (float)delta * accelerationFactor);
        _parent.Velocity = new Vector3(xAcceleration, _parent.Velocity.Y, zAcceleration);
		
        if (isDead)
        {
            _parent.MoveAndSlide();
        }
    }

    public void Jump(JumpTypes jumpType, Vector3? jumpDirection = null, float boost = 1.0f)
    {
        var effectiveJumpDirection = jumpDirection ?? Vector3.Up;
        var jumpForce = 0.0f;
        switch (jumpType)
        {
            case JumpTypes.DoubleJump:
                jumpForce = _gravity.CalculateDoubleJumpForce();
                break;
            case JumpTypes.SimpleJump:
                jumpForce = _gravity.CalculateJumpForce();
                break;
            case JumpTypes.JumpFromDash:
                jumpForce = _gravity.CalculateJumpFromDashForce();
                break;
            case JumpTypes.JumpFromWall:
                jumpForce = _gravity.CalculateJumpFromWallForce();
                break;
            default:
                jumpForce = _gravity.CalculateJumpForce();
                break;
        }
        
        var currentHorizontalVelocity = new Vector3(_parent.Velocity.X, 0, _parent.Velocity.Z);
        var jumpVelocity = jumpForce * effectiveJumpDirection * boost;
        _parent.Velocity = currentHorizontalVelocity + jumpVelocity;
    }

    public bool CanMantle()
    {
        var mantleLocationResult = _mantleSystem.FindMantleInFrontOfPlayer();
        return mantleLocationResult.IsSome(out _);
    }

    public Option<Tween> Mantle()
    {
        var mantleLocationResult = _mantleSystem.FindMantleInFrontOfPlayer();
        if (mantleLocationResult.IsSome(out var mantleLocation))
        {
            var duration = 0.1f * mantleLocation.DistanceTo(_parent.Position);
            var tween = _tweenQueueSystem.TweenToLocation(new TweenQueueSystem.TweenInputs(mantleLocation, duration));
            return tween.Some();
        }
        return Option<Tween>.None;
    }
}