using System;
using Godot;
using GodotStateCharts;
using Movementtests.addons.godot_state_charts.csharp;
using Movementtests.systems;
using Movementtests.player_controller.Scripts;
using RustyOptions;

public enum JumpTypes
{
	SimpleJump,
	DoubleJump,
	JumpFromDash,
	JumpFromWall
}

public partial class PlayerController : CharacterBody3D
{
	// User API to important child nodes.
	public HeadSystem       HeadSystem;
	public Bobbing          Bobbing;
	public FieldOfView      FieldOfView;
	public Stamina          Stamina;
	public StairsSystem     StairsSystem;
	public MantleSystem     MantleSystem;
	public DashSystem       DashSystem;
	public CapsuleCollider  CapsuleCollider;
	public HealthSystem     HealthSystem;
	public TweenQueueSystem TweenQueueSystem;
	public Node3D           WeaponRoot;
	public WeaponSystem     WeaponSystem;
	public WallHugSystem    WallHugSystem;
	public PlayerUi		    PlayerUi;
	public TextureRect      DashIndicator;
	public ColorRect        PowerCooldownIndicator;

	private bool _movementEnabled = true;

	private bool _shouldMantle;
	private Vector3 _mantleLocation = Vector3.Zero;
	private Vector3 _dashDirection = Vector3.Zero;

	private float _lastFrameWasOnFloor = -Mathf.Inf;

	private const int NUM_OF_HEAD_COLLISION_DETECTORS = 4;
	private RayCast3D[] _headCollisionDetectors;
	
	private Vector3 _inputMove = Vector3.Zero;
	private float _inputRotateY;
	private float _inputRotateFloorplane;
	
	private int _framesSinceJumpAtApex = 0;

	// Timers
	private Timer _timeScaleAimInAirTimer;
	private Timer _simpleDashCooldownTimer;
	private Timer _powerCooldownTimer;
	
	[ExportCategory("Movement")]
	[ExportGroup("Ground")]
	[Export(PropertyHint.Range, "0,20,0.1,or_greater")]
	public float WalkSpeed             { get; set; } = 7.0f;
	[Export(PropertyHint.Range, "0,10,0.1,or_greater")]
	public float AccelerationFloor                   = 5.0f;
	[Export(PropertyHint.Range, "0,10,0.1,or_greater")]
	public float DecelerationFloor                   = 5.0f;
	[ExportGroup("Air")]
	[Export(PropertyHint.Range, "0,10,0.1,or_greater")]
	public float AccelerationAir                     = 3.0f;
	[Export(PropertyHint.Range, "0,10,0.01,or_greater")]
	public float DecelerationAir                     = 1.0f;
	[Export(PropertyHint.Range, "0,10,0.01,or_greater")]
	public float Weight        { get; set; } = 3.0f;
	[ExportGroup("Mantle")]
	[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
	public float MantleTime { get; set; } = 0.1f;
	
	// Jump
	[ExportGroup("Jump")] 
	[Export(PropertyHint.Range, "0,1,0.01")]
	public float CoyoteTime { get; set; } = 0.2f;
	
	// Simple jump
	[ExportSubgroup("Simple jump")]
	[Export(PropertyHint.Range, "0,100,1,or_greater")]
	public float SimpleJumpStartVelocity { get; set; } = 3.0f;
	[Export(PropertyHint.Range, "0,10,1,or_greater")]
	public int SimpleJumpHangTimeInFrames { get; set; } = 5;
	[Export(PropertyHint.Range, "1,10,0.1,or_greater")]
	public float SimpleJumpGravityLesseningFactor { get; set; } = 3f;
	
	
	// Double jump
	[ExportSubgroup("Double jump")]
	[Export(PropertyHint.Range, "0,100,1,or_greater")]
	public float DoubleJumpStartVelocity { get; set; } = 10.0f;
	[Export(PropertyHint.Range, "0,10,1,or_greater")]
	public int DoubleJumpHangTimeInFrames { get; set; } = 5;
	[Export(PropertyHint.Range, "1,10,0.1,or_greater")]
	public float DoubleJumpGravityLesseningFactor { get; set; } = 3f;
	
	
	// Mega jump
	[ExportSubgroup("Mega jump")]
	[Export(PropertyHint.Range, "0,100,1,or_greater")]
	public float MegaJumpStartVelocity { get; set; } = 10.0f;
	[Export(PropertyHint.Range, "0,10,1,or_greater")]
	public int MegaJumpHangTimeInFrames { get; set; } = 5;
	[Export(PropertyHint.Range, "1,10,0.1,or_greater")]
	public float MegaJumpGravityLesseningFactor { get; set; } = 3f;
	
	// Wall jump
	[ExportSubgroup("Wall jump")]
	[Export(PropertyHint.Range, "0,100,1,or_greater")]
	public float WallJumpStartVelocity { get; set; } = 10.0f;
	[Export(PropertyHint.Range, "0,100,1,or_greater")]
	public float WallMegajumpStartVelocity { get; set; } = 20.0f;
	
	// Dash
	[ExportGroup("Dash")]
	[Export(PropertyHint.Range, "0,5,1,or_greater")]
	public int MaxNumberOfEmpoweredActions { get; set; } = 1;
	// Simple dash
	[ExportSubgroup("Simple")]
	[Export(PropertyHint.Range, "0,50,0.1")]
	public float SimpleDashStrength { get; set; } = 10f;
	// Powered Dash
	[ExportSubgroup("Powered")]
	[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
	public float PoweredDashTime { get; set; } = 0.3f;
	[Export(PropertyHint.Range, "0,100,0.1")]
	public float PoweredDashStrength { get; set; } = 10f;
	// Aimed Dash
	[ExportSubgroup("Special")]
	[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
	public float AimedDashTime { get; set; } = 0.1f;
	[Export(PropertyHint.Range, "0,100,1,or_greater")]
	public float PostDashSpeed { get; set; } = 100f;
	[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
	public float TimeScaleAimInAir { get; set; } = 0.05f;
	
	// Wall hug
	[ExportGroup("Wall hug")]
	[Export(PropertyHint.Range, "0,50,0.1,or_greater")]
	public float WallHugGravityLesseningFactor { get; set; } = 2f;
	[Export(PropertyHint.Range, "0.1,50,0.1,or_greater")]
	public float WallHugDownwardMaxSpeed { get; set; } = 2f;
	[Export(PropertyHint.Range, "0.1,10,0.1,or_greater")]
	public float WallHugHorizontalDeceleration { get; set; } = 5f;
	
	private float _targetSpeed;
	private float _gravity;
	
	private int _empoweredActionsLeft;
	public int EmpoweredActionsLeft
	{
		get => _empoweredActionsLeft;
		set
		{
			_empoweredActionsLeft = value;
			PlayerUi.SetNumberOfDashesLeft(value);
		}
	}

	private bool _isWallJumpAvailable = true;
	private bool _canDash = true;
	private bool _shouldMantleOnDashEnded = false;
	
	private StateChart       _playerState;
	
	private StateChartState  _weaponInHand;
	private StateChartState  _aiming;
	private StateChartState  _weaponThrown;
	private StateChartState  _actionHanging;
	private StateChartState  _empowerOn;
	private StateChartState  _empowerOff;
	private StateChartState  _powerExpired;
	private StateChartState  _powerRecharging;
	private StateChartState  _powerFull;
	
	private StateChartState  _grounded;
	private StateChartState  _mantling;
	private StateChartState  _movHanging;
	private StateChartState  _airborne;
	private StateChartState  _coyoteEnabled;
	private StateChartState  _simpleJump;
	private StateChartState  _doubleJump;
	private StateChartState  _megaJump;
	private StateChartState  _simpleDash;
	private StateChartState  _poweredDash;
	private StateChartState  _aimedDash;
	private StateChartState  _doubleJumpEnabled;
	private StateChartState  _onWall;
	private StateChartState  _onWallHugCanceled;
	private StateChartState  _onWallHugging;
	private StateChartState  _onWallHanging;
	private StateChartState  _falling;
	private StateChartState  _to_weapon_dash;

	private Transition       _onJumpFromWall;
	private Transition       _onMegajumpFromWall;

	private float            _playerHeight;
	private float            _playerRadius;

	public override void _Ready()
	{
		///////////////////////////
		// Getting components /////
		///////////////////////////
		
		// General use stuff
		TweenQueueSystem = GetNode<TweenQueueSystem>("TweenQueueSystem");
		PlayerUi = GetNode<PlayerUi>("UI");
		// DashIndicator = GetNode<TextureRect>("%DashIndicator");
		PowerCooldownIndicator = GetNode<ColorRect>("%DashCooldownIndicator");
		PowerCooldownIndicator.Visible = false;
		EmpoweredActionsLeft = MaxNumberOfEmpoweredActions;
		_targetSpeed = WalkSpeed;
		
		// Node3D mapNode = GetTree().Root.FindChild("Map", true, false) as Node3D;
		
		// Camera stuff
		HeadSystem = GetNode<HeadSystem>("HeadSystem");
		Bobbing = GetNode<Bobbing>("Bobbing");
		FieldOfView = GetNode<FieldOfView>("FieldOfView");
		Camera3D camera = GetNode<Camera3D>("HeadSystem/CameraSmooth/Camera3D");
		Node3D cameraSmooth = GetNode<Node3D>("HeadSystem/CameraSmooth");
		ColorRect vignetteRect = GetNode<ColorRect>(
			"HeadSystem/CameraSmooth/Camera3D/CLVignette(Layer_1)/HealthVignetteRect");
		ColorRect distortionRect = GetNode<ColorRect>(
			"HeadSystem/CameraSmooth/Camera3D/CLDistortion(Layer_2)/HealthDistortionRect");
		ColorRect blurRect = GetNode<ColorRect>("HeadSystem/CameraSmooth/Camera3D/CLBlur(Layer_2)/BlurRect");
		
		// Movement stuff
		WeaponRoot = GetNode<Node3D>("WeaponRoot");
		WeaponSystem = GetNode<WeaponSystem>("WeaponRoot/WeaponSystem");
		MantleSystem = GetNode<MantleSystem>("MantleSystem");
		CapsuleCollider = GetNode<CapsuleCollider>("CapsuleCollider");
		DashSystem = GetNode<DashSystem>("DashSystem");
		StairsSystem = GetNode<StairsSystem>("StairsSystem");
		WallHugSystem = GetNode<WallHugSystem>("WallHugSystem");
		RayCast3D stairsBelowRayCast3D = GetNode<RayCast3D>("StairsBelowRayCast3D");
		RayCast3D stairsAheadRayCast3D = GetNode<RayCast3D>("StairsAheadRayCast3D");
		_headCollisionDetectors = new RayCast3D[NUM_OF_HEAD_COLLISION_DETECTORS];
		for (int i = 0; i < NUM_OF_HEAD_COLLISION_DETECTORS; i++)
		{
			_headCollisionDetectors[i] = GetNode<RayCast3D>(
				"HeadCollisionDetectors/HeadCollisionDetector" + i);
		}
		
		var playerShape = CapsuleCollider.GetShape() as CapsuleShape3D;
		_playerHeight = playerShape!.Height;
		_playerRadius = playerShape.Radius;
		
		// RPG Stuff
		Stamina = GetNode<Stamina>("Stamina");
		HealthSystem = GetNode<HealthSystem>("HealthSystem");
		
		// State management
		_playerState = StateChart.Of(GetNode("StateChart"));
		
		_weaponInHand = StateChartState.Of(GetNode("StateChart/Root/WeaponState/InHand"));
		_weaponThrown = StateChartState.Of(GetNode("StateChart/Root/WeaponState/Flying"));
		_aiming = StateChartState.Of(GetNode("StateChart/Root/Aim/On"));
		_simpleDash = StateChartState.Of(GetNode("StateChart/Root/Movement/Dashing/Dash"));
		_poweredDash = StateChartState.Of(GetNode("StateChart/Root/Movement/Dashing/PoweredDash"));
		_aimedDash = StateChartState.Of(GetNode("StateChart/Root/Movement/Dashing/AimedDash"));
		// _actionHanging = StateChartState.Of(GetNode("StateChart/Root/Actions/Hanging"));
		_empowerOn = StateChartState.Of(GetNode("StateChart/Root/Empower/On"));
		_empowerOff = StateChartState.Of(GetNode("StateChart/Root/Empower/Off"));
		_powerExpired = StateChartState.Of(GetNode("StateChart/Root/PowerReserve/Expired"));
		_powerRecharging = StateChartState.Of(GetNode("StateChart/Root/PowerReserve/AtLeastOneCharge"));
		_powerFull = StateChartState.Of(GetNode("StateChart/Root/PowerReserve/Full"));
		
		_grounded = StateChartState.Of(GetNode("StateChart/Root/Movement/Grounded"));
		_mantling = StateChartState.Of(GetNode("StateChart/Root/Movement/Mantling"));
		_movHanging = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall/Hanging"));
		_airborne = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne"));
		_coyoteEnabled = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/CoyoteEnabled"));
		_simpleJump = StateChartState.Of(GetNode("StateChart/Root/Movement/Jump/SimpleJump"));
		_doubleJump = StateChartState.Of(GetNode("StateChart/Root/Movement/Jump/DoubleJump"));
		_megaJump = StateChartState.Of(GetNode("StateChart/Root/Movement/Jump/MegaJump"));
		_doubleJumpEnabled = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/DoubleJumpEnabled"));
		_falling = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/Falling"));
		_to_weapon_dash = StateChartState.Of(GetNode("StateChart/Root/Movement/Dashing/ToWeaponDash"));
		_onWall = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall"));
		_onJumpFromWall = Transition.Of(GetNode("StateChart/Root/Movement/OnWall/OnJump"));
		_onMegajumpFromWall = Transition.Of(GetNode("StateChart/Root/Movement/OnWall/OnMegajump"));
		_onWallHugging = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall/Hugging"));
		_onWallHugCanceled = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall/HugCanceled"));
		_onWallHanging = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall/Hanging"));
		// State timers
		_powerCooldownTimer = GetNode<Timer>("PowerCooldown");
		_timeScaleAimInAirTimer = GetNode<Timer>("TimeScaleAimInAir");
		_simpleDashCooldownTimer = GetNode<Timer>("DashCooldown");
		
		///////////////////////////
		// Initialize components //
		///////////////////////////
		
		// General use stuff
		TweenQueueSystem.Init(this);

		// Camera stuff
		HeadSystem.Init();
		Bobbing.Init(camera);
		FieldOfView.Init(camera);
		
		// Movement stuff
		// Getting universal setting from GODOT editor to be in sync
		_gravity = (float)ProjectSettings.GetSetting("physics/3d/default_gravity");
		MantleSystem.Init(HeadSystem);
		StairsSystem.Init(stairsBelowRayCast3D, stairsAheadRayCast3D, cameraSmooth);
		DashSystem.Init(HeadSystem, camera, TweenQueueSystem);
		WeaponSystem.Init(HeadSystem, camera);
		WallHugSystem.Init();
		
		// RPG Stuff
		HealthSystem.HealthSystemInitParams healthSystemParams = new HealthSystem.HealthSystemInitParams()
		{
			Parent = this,
			Camera = camera,
			Head =  HeadSystem,
			VignetteRect = vignetteRect,
			DistortionRect = distortionRect,
			BlurRect = blurRect,
		};
		HealthSystem.Init(healthSystemParams);
		Stamina.SetSpeeds(WalkSpeed, WalkSpeed);

		EmpoweredActionsLeft = MaxNumberOfEmpoweredActions;
		
		///////////////////////////
		// Signal setup ///////////
		///////////////////////////
		_weaponInHand.StateProcessing += HandleWeaponInHand;
		_aiming.StatePhysicsProcessing += HandleAiming;
		_aiming.StateEntered += OnAimingEntered;
		_aiming.StateExited += ResetTimeScale;
		_aiming.StateExited += OnAimingExited;

		_grounded.StateEntered += OnGrounded;
		_grounded.StatePhysicsProcessing += HandleGrounded;
		_airborne.StatePhysicsProcessing += HandleAirborne;
		
		_coyoteEnabled.StateEntered += StartCoyoteTime;
		_timeScaleAimInAirTimer.Timeout += ResetTimeScale;
		
		// _weaponThrown.StateEntered += OnWeaponThrown;

		_powerFull.StateEntered += StopPowerCooldown;
		_powerFull.StateExited += StartPowerCooldown;
		_powerRecharging.StateEntered += StartPowerCooldown;
		_powerCooldownTimer.Timeout += PowerCooldownExpired;
		_powerRecharging.StateProcessing += PowerRecharging;
		_powerExpired.StateProcessing += PowerRecharging;

		_simpleJump.StateEntered += OnSimpleJumpStarted;
		_simpleJump.StatePhysicsProcessing += HandleSimpleJump;
		
		_doubleJump.StateEntered += OnDoubleJumpStarted;
		_doubleJump.StatePhysicsProcessing += HandleDoubleJump;
		
		_megaJump.StateEntered += OnMegaJumpStarted;
		_megaJump.StatePhysicsProcessing += HandleMegaJump;

		_simpleDash.StateEntered += OnSimpleDashStarted;
		_simpleDash.StatePhysicsProcessing += HandleSimpleDash;

		_poweredDash.StateEntered += OnPoweredDashStarted;
		_poweredDash.StatePhysicsProcessing += HandlePoweredDash;
		_poweredDash.StateExited += OnPoweredDashFinished;
		
		_aimedDash.StateEntered += OnAimedDashStarted;
		_aimedDash.StateExited += OnAimedDashFinished;

		_simpleDashCooldownTimer.Timeout += DashCooldownTimeout;
		
		_onWallHugCanceled.StatePhysicsProcessing += HandleAirborne;
		_onWallHugging.StatePhysicsProcessing += HandleWallHugging;
		_onWallHanging.StatePhysicsProcessing += HandleWallHanging;

		_onJumpFromWall.Taken += OnJumpFromWall;
		_onMegajumpFromWall.Taken += OnMegajumpFromWall;
	}
	
	private bool _canDashAirborne = true;
	
	public void OnWallDetected()
	{
		FinishPoweredDash();
	}
	
	public void OnGrounded()
	{
		_isWallJumpAvailable = true;
		_canDashAirborne = true;
		
		if (_simpleDashCooldownTimer.IsStopped())
			_simpleDashCooldownTimer.Start();
	}

	public void DashCooldownTimeout()
	{
		_canDash = true;
	}
	
	public void HandleGrounded(float delta)
	{
		MoveOnGround(delta);
		if (!isOnFloorCustom())
			_playerState.SendEvent("start_falling");
	}
	public void HandleAirborne(float delta)
	{
		MoveInAir(delta);
		if (isOnFloorCustom())
			_playerState.SendEvent("grounded");
		if (WallHugSystem.IsWallHugging() && Velocity.Y < 0)
			_playerState.SendEvent("wall_hug");
	}
	public void HandleWallHugging(float delta)
	{
		WallHug(delta);
		if (isOnFloorCustom())
			_playerState.SendEvent("grounded");
		if (!WallHugSystem.IsWallHugging())
			_playerState.SendEvent("start_falling");
	}
	public void HandleWallHanging(float delta)
	{
		WallHang(delta);
	}
	
	// Jump
	public void OnInputJumpStarted()
	{
		if (CanMantle())
		{
			MantleToLocation(MantleSystem.FindMantleInFrontOfPlayer().Unwrap());
			return;
		}

		if (_empowerOn.Active && CanPerformEmpoweredAction())
		{
			_playerState.SendEvent("megajump");
			return;
		}
		_playerState.SendEvent("jump");
	}

	public void OnInputJumpOngoing()
	{
	}

	public void OnInputJumpEnded()
	{
		_playerState.SendEvent("jump_ended");
	}

	public void OnJumpStarted(float verticalVelocity)
	{
		_framesSinceJumpAtApex = 0;
		SetVerticalVelocity(verticalVelocity);
	}
	public void OnSimpleJumpStarted()
	{
		OnJumpStarted(SimpleJumpStartVelocity);
	}
	public void OnDoubleJumpStarted()
	{
		OnJumpStarted(DoubleJumpStartVelocity);
	}
	public void OnMegaJumpStarted()
	{
		PerformEmpoweredAction();
		OnJumpStarted(MegaJumpStartVelocity);
	}
	
	public void WallHug(float delta)
	{
		var hvel = ComputeHVelocity(delta, WallHugHorizontalDeceleration, WallHugHorizontalDeceleration);
		var vvel = Velocity.Y - (CalculateGravityForce() * delta / WallHugGravityLesseningFactor);
		vvel = Math.Abs(vvel) > WallHugDownwardMaxSpeed ? -WallHugDownwardMaxSpeed : vvel;
		Velocity = hvel + vvel*Vector3.Up;
	}

	public void WallHang(float delta)
	{
		Velocity = Vector3.Zero;
		MoveAndSlide();
	}

	public void ComputeJumpFromWallHSpeed(float jumpStrength)
	{
		// if (!_isWallJumpAvailable)
		// 	return;
		// _isWallJumpAvailable = false;
		// var isLookingTowardsWall = HeadSystem.GetForwardHorizontalVector().Dot(wallNormal) > 0.5;
		// var jumpDirection = isLookingTowardsWall ? Vector3.Up : wallNormal;
		var wallNormal = WallHugSystem.GetWallNormal().UnwrapOr(Vector3.Up);
		var jumpVector = wallNormal * jumpStrength;
		
		SetHorizontalVelocity(new Vector2(jumpVector.X, jumpVector.Z));
	}
	public void OnJumpFromWall()
	{
		ComputeJumpFromWallHSpeed(WallJumpStartVelocity);
	}
	public void OnMegajumpFromWall()
	{
		ComputeJumpFromWallHSpeed(WallMegajumpStartVelocity);
	}

	public Vector3 ComputeHVelocity(float delta, float accelerationFactor, float decelerationFactor, Vector3? direction = null)
	{
		var dir = direction ?? HeadSystem.Transform.Basis * _inputMove;
		
		var acceleration = dir.Length() > 0 ? accelerationFactor : decelerationFactor;
        
		float xAcceleration = Mathf.Lerp(Velocity.X, dir.X * _targetSpeed, delta * acceleration);
		float zAcceleration = Mathf.Lerp(Velocity.Z, dir.Z * _targetSpeed, delta * acceleration);
		return new Vector3(xAcceleration, 0, zAcceleration);
	}
	public Vector3 ComputeHVelocityGround(float delta)
	{
		return ComputeHVelocity(delta, AccelerationFloor, DecelerationFloor);
	}
	public Vector3 ComputeHVelocityAir(float delta)
	{
		return ComputeHVelocity(delta, AccelerationAir, DecelerationAir);
	}

	public void SetVerticalVelocity(float verticalVelocity)
	{
		Velocity = new Vector3(
			x: Velocity.X,
			y: verticalVelocity,
			z: Velocity.Z);
	}
	public void SetHorizontalVelocity(Vector2 velocity)
	{
		Velocity = new Vector3(
			x: velocity.X,
			y: Velocity.Y,
			z: velocity.Y);
	}

	public void HandleJump(float delta, float gravityFactor, int hangFrames)
	{
		// Update horizontal velocity
		var horizontalVelocity = ComputeHVelocityAir(delta);
		Velocity = new Vector3(horizontalVelocity.X, Velocity.Y, horizontalVelocity.Z);
		
		// Hang time at the top of the jump
		if (Velocity.Y <= Mathf.Epsilon)
		{
			_framesSinceJumpAtApex++;
			SetVerticalVelocity(0);
		}
		
		// Cancel gravity on jump apex
		var gravity = CalculateGravityForce() / gravityFactor;
		var isAtApex = _framesSinceJumpAtApex > 0;
		if (isAtApex)
		{
			gravity = 0;
		}
		// Update velocity accordingly
		var newVerticalSpeed = Velocity.Y - gravity * delta;
		SetVerticalVelocity(newVerticalSpeed);
		if (IsHeadTouchingCeiling())
		{
			SetVerticalVelocity(Velocity.Y - 2.0f);
		}
		
		// Move back to Airborne state management when starting to go down again
		if (_framesSinceJumpAtApex > hangFrames)
			_playerState.SendEvent("jump_ended");
	}
	public void HandleSimpleJump(float delta)
	{
		HandleJump(delta, SimpleJumpGravityLesseningFactor, SimpleJumpHangTimeInFrames);
	}
	public void HandleDoubleJump(float delta)
	{
		HandleJump(delta, DoubleJumpGravityLesseningFactor, DoubleJumpHangTimeInFrames);
	}
	public void HandleMegaJump(float delta)
	{
		HandleJump(delta, MegaJumpGravityLesseningFactor, MegaJumpHangTimeInFrames);
	}

	public void PowerRecharging(float delta)
	{
		var progress = (float) (_powerCooldownTimer.TimeLeft / _powerCooldownTimer.WaitTime);
		PowerCooldownIndicator.SetSize(new Vector2(100 * progress, 10));
	}
	public void StartPowerCooldown()
	{
		_powerCooldownTimer.Start();
		PowerCooldownIndicator.Visible = true;
	}
	public void StopPowerCooldown()
	{
		_powerCooldownTimer.Stop();
		PowerCooldownIndicator.Visible = false;
	}
	public void PowerCooldownExpired()
	{
		EmpoweredActionsLeft += 1;
		var eventToSend = EmpoweredActionsLeft == MaxNumberOfEmpoweredActions ? "fully_charged" : "recharge";
		_playerState.SendEvent(eventToSend);
	}
	
	///////////////////////////
	// Input Management ///////
	///////////////////////////
	public void OnInputMove(Vector3 value)
	{
		_inputMove = value;
	}
	public void OnInputRotateY(float value)
	{
		_inputRotateY = value;
	}
	public void OnInputRotateFloorplane(float value)
	{
		_inputRotateFloorplane = value;
	}
	public void OnInputAimPressed()
	{
		_playerState.SendEvent("aim_pressed");
	}
	public void OnInputAimDown()
	{
		_playerState.SendEvent("aim_down");
	}
	public void OnInputAimReleased()
	{
		_playerState.SendEvent("aim_released");
	}
	public void OnInputAimCanceled()
	{
		_playerState.SendEvent("cancel");
		DashSystem.StopPreparingDash();
	}
	public void OnInputHitPressed()
	{
		if (_aiming.Active)
		{
			ThrowWeapon();
		}
	}
	public void OnInputEmpowerDown()
	{
		_playerState.SendEvent("empower_down");
	}
	public void OnInputEmpowerReleased()
	{
		_playerState.SendEvent("empower_released");
	}
	public void OnInputDashPressed()
	{
		if (_aiming.Active && CanPerformEmpoweredAction())
		{
			PerformEmpoweredAction();
			_playerState.SendEvent("aimed_dash");
			_playerState.SendEvent("cancel_aim");
			return;
		}
		if (_empowerOn.Active && CanPerformEmpoweredAction())
		{
			PerformEmpoweredAction();
			_playerState.SendEvent("powered_dash");
			return;
		}

		if (_airborne.Active)
		{
			if (!_canDashAirborne)
				return;
			_canDashAirborne = false;
		}
			
		_playerState.SendEvent("dash");
	}
	public void OnAimingEntered()
	{
		if (!CanPerformEmpoweredAction())
			return;
		
		if (WeaponSystem.FlyingState.Active)
		{
			DashToFlyingWeapon();
			return;
		}

		if (WeaponSystem.PlantedState.Active)
		{
			DashToPlantedWeapon();
			return;
		}
		
		DashSystem.StartPreparingDash();
		if (!isOnFloorCustom())
			ReduceTimeScaleWhileAiming();
	}
	public void HandleAiming(float delta)
	{
		RotateWeaponWithPlayer();
		
		if (CanPerformEmpoweredAction())
			DashSystem.PrepareDash();
	}
	public void OnAimingExited()
	{
		DashSystem.StopPreparingDash();
	}

	public void DashToFlyingWeapon()
	{
		_playerState.SendEvent("cancel_aim");
		_playerState.SendEvent("weapon_dash");
		PerformEmpoweredAction();
	
		DashSystem.ShouldMantle = false;
		_dashDirection = (WeaponSystem.GlobalPosition - GlobalPosition).Normalized();
		
		var dashTween = CreatePositionTween(WeaponSystem.GlobalPosition, AimedDashTime);
		dashTween.Finished += DashToFlyingWeaponTweenEnded;
	}

	public void DashToFlyingWeaponTweenEnded()
	{
		// Get the weapon back
		GetTree().GetRoot().RemoveChild(WeaponRoot);
		AddChild(WeaponRoot);
		WeaponRoot.SetGlobalPosition(GlobalPosition);
		WeaponSystem.ResetWeapon();
		
		var vel = _dashDirection * PostDashSpeed;
		SetVelocity(vel);
		_playerState.SendEvent("dash_finished");
	}

	public void DashToPlantedWeapon()
	{
		_playerState.SendEvent("cancel_aim");
		_playerState.SendEvent("weapon_dash");
		PerformEmpoweredAction();
		
		DashSystem.ShouldMantle = false;
		var dashLocation = WeaponSystem.PlantLocation;
		if (WeaponSystem.IsPlantedInWall())
			dashLocation += WeaponSystem.PlantNormal * _playerRadius;
		if (WeaponSystem.IsPlantedUnderPlatform())
			dashLocation += Vector3.Down * _playerHeight;
		
		var dashTween = CreatePositionTween(dashLocation, AimedDashTime);
		dashTween.Finished += DashToPlantedWeaponTweenEnded;
	}
	
	public void DashToPlantedWeaponTweenEnded()
	{
		// Store the weapon state before resetting it
		var isPlantedOnWall = WeaponSystem.IsPlantedInWall();
		var isPlantedUnderPlatform = WeaponSystem.IsPlantedUnderPlatform();
		var shouldDashToHanging = isPlantedOnWall || isPlantedUnderPlatform;
		
		// Get the weapon back
		GetTree().GetRoot().RemoveChild(WeaponRoot);
		AddChild(WeaponRoot);
		WeaponRoot.SetGlobalPosition(GlobalPosition);
		WeaponSystem.ResetWeapon();

		var resultingEvent = shouldDashToHanging ? "to_planted" : "dash_finished";
		_playerState.SendEvent(resultingEvent);
	}
	
	public void OnAimedDashStarted()
	{
		// Adjusting for player height, where the middle of the capsule should get to the dash location instead of the 
		// feet of the capsule
		var correction = DashSystem.CollisionNormal == Vector3.Down ? _playerHeight : DashSystem.DashCastRadius;
		var correctedLocation = DashSystem.PlannedLocation + Vector3.Down * correction;

		var dashTween = CreatePositionTween(correctedLocation, AimedDashTime);
		// dashTween.TweenMethod(Callable.From<float>(AimedDashTweenOngoing), 0.0f, 1.0f, AimedDashTime);
		dashTween.Finished += AimedDashTweenEnded;

		_shouldMantleOnDashEnded = DashSystem.ShouldMantle;
		_mantleLocation = DashSystem.PlannedMantleLocation;
	}

	Tween CreatePositionTween(Vector3 targetLocation, float tweenTime)
	{
		var tween = GetTree().CreateTween();
		tween.SetParallel();
		tween.SetTrans(Tween.TransitionType.Cubic);
		tween.SetEase(Tween.EaseType.InOut);
		tween.TweenProperty(this, "global_position", targetLocation, tweenTime);

		return tween;
	}
	
	public void AimedDashTweenEnded()
	{
		_playerState.SendEvent("dash_finished");
	}
	
	public void ThrowWeapon()
	{
		_playerState.SendEvent("cancel_aim");
		
		RemoveChild(WeaponRoot);
		GetTree().GetRoot().AddChild(WeaponRoot);
		WeaponRoot.SetGlobalPosition(GlobalPosition);

		var weaponTargetLocation = DashSystem.HasHit ? DashSystem.CollisionPoint : DashSystem.PlannedLocation;
		WeaponSystem.ThrowWeapon(
			weaponTargetLocation,
			DashSystem.HasHit,
			DashSystem.CollisionPoint,
			DashSystem.CollisionNormal);
	}

	public void OnAimedDashFinished()
	{
		if (_shouldMantleOnDashEnded)
			MantleToLocation(_mantleLocation);
	}

	public void OnSimpleDashStarted()
	{
		if (!_canDash)
			return;
		_canDash = false;
		
		var dashStrength = SimpleDashStrength;

		var direction = GetInputGlobalHDirection();
		SetVelocity(direction * dashStrength);
	}

	public void HandleSimpleDash(float delta)
	{
		_playerState.SendEvent("dash_finished");
	}

	public Vector3 GetInputGlobalHDirection()
	{
		var direction = HeadSystem.Transform.Basis * _inputMove;
		return new Vector3(direction.X, 0, direction.Z).Normalized();
	}

	public Vector3 GetInputLocalHDirection()
	{
		var direction = _inputMove;
		return new Vector3(direction.X, 0, direction.Z).Normalized();
	}

	public void OnPoweredDashStarted()
	{
		Velocity = GetInputGlobalHDirection() * PoweredDashStrength;
		GetTree().CreateTimer(PoweredDashTime).Timeout += FinishPoweredDash;
	}

	public void OnPoweredDashFinished()
	{
		// Try mantling but don't know if this is useful
		if (CanMantle())
			MantleToLocation(MantleSystem.FindMantleInFrontOfPlayer().Unwrap());
	}

	public void FinishPoweredDash()
	{
		_playerState.SendEvent("dash_finished");
	}

	public void HandlePoweredDash(float delta)
	{
		var collision = MoveAndCollide(Velocity * delta, maxCollisions: 10);
		if (collision != null)
		{
			FinishPoweredDash();
		}
	}

	public bool CanPerformEmpoweredAction()
	{
		return EmpoweredActionsLeft > 0;
	}

	public void PerformEmpoweredAction()
	{
		_isWallJumpAvailable = true;
		EmpoweredActionsLeft--;
		_playerState.SendEvent(EmpoweredActionsLeft <= 0 ? "expired" : "power_used");
	}

	public void StartCoyoteTime()
	{
		GetTree().CreateTimer(CoyoteTime).Timeout += CoyoteExpired;
	}
	public void CoyoteExpired()
	{
		_playerState.SendEvent("coyote_expired");
	}
	
	// Mantling
	public bool CanMantle()
	{
		var mantleLocationResult = MantleSystem.FindMantleInFrontOfPlayer();
		return mantleLocationResult.IsSome(out _);
	}
	public void MantleToLocation(Vector3 location)
	{
		var mantleTween = CreatePositionTween(location, MantleTime);
		mantleTween.Finished += MantleFinished;
	}
	public void MantleFinished()
	{
		_playerState.SendEvent("grounded");
	}
	
	// Regular processes
	public void HandleWeaponInHand(float delta)
	{
		if (WeaponSystem.InHandState.Active)
			RotateWeaponWithPlayer();
	}
	
	///////////////////////////
	// Stateless logic ////////
	///////////////////////////
	private void LookAround()
	{
		Vector2 inputLookDir = new Vector2(_inputRotateY, _inputRotateFloorplane);
		HeadSystem.LookAround(inputLookDir);
	}
	
	public void MoveOnGround(double delta)
	{
		var horizontalVelocity = ComputeHVelocityGround((float) delta);
		Velocity = new Vector3(horizontalVelocity.X, Velocity.Y, horizontalVelocity.Z);
	}

	public void MoveInAir(double delta)
	{
		var horizontalVelocity = ComputeHVelocityAir((float) delta);
		var verticalVelocity = Velocity.Y - (CalculateGravityForce() * (float)delta);
		Velocity = new Vector3(horizontalVelocity.X, verticalVelocity, horizontalVelocity.Z);
	}
	
	private void HandleStairs(float delta)
	{
		StairsSystem.UpStairsCheckParams upStairsCheckParams = new StairsSystem.UpStairsCheckParams
		{
			IsOnFloorCustom = isOnFloorCustom(),
			IsCapsuleHeightLessThanNormal = CapsuleCollider.IsCapsuleHeightLessThanNormal(),
			CurrentSpeedGreaterThanWalkSpeed = false,
			IsCrouchingHeight = CapsuleCollider.IsCrouchingHeight(),
			Delta = (float)delta,
			FloorMaxAngle = FloorMaxAngle,
			GlobalPositionFromDriver = GlobalPosition,
			Velocity = Velocity,
			GlobalTransformFromDriver = GlobalTransform,
			Rid = GetRid()
		};
		StairsSystem.UpStairsCheckResult upStairsCheckResult = StairsSystem.SnapUpStairsCheck(upStairsCheckParams);

		if (upStairsCheckResult.UpdateRequired)
		{
			upStairsCheckResult.Update(this);
		}
		else
		{
			MoveAndSlide();
		
			StairsSystem.DownStairsCheckParams downStairsCheckParams = new StairsSystem.DownStairsCheckParams
			{
				IsOnFloor = IsOnFloor(),
				IsCrouchingHeight = CapsuleCollider.IsCrouchingHeight(),
				LastFrameWasOnFloor = _lastFrameWasOnFloor,
				CapsuleDefaultHeight = CapsuleCollider.GetDefaultHeight(),
				CurrentCapsuleHeight = CapsuleCollider.GetCurrentHeight(),
				FloorMaxAngle = FloorMaxAngle,
				VelocityY = Velocity.Y,
				GlobalTransformFromDriver = GlobalTransform,
				Rid = GetRid()
			};
		
			StairsSystem.DownStairsCheckResult downStairsCheckResult = StairsSystem.SnapDownStairsCheck(
				downStairsCheckParams);

			if (downStairsCheckResult.UpdateIsRequired)
			{
				downStairsCheckResult.Update(this);
			}
		}
		
		StairsSystem.SlideCameraParams slideCameraParams = new StairsSystem.SlideCameraParams
		{
			IsCapsuleHeightLessThanNormal = CapsuleCollider.IsCapsuleHeightLessThanNormal(),
			CurrentSpeedGreaterThanWalkSpeed = false,
			BetweenCrouchingAndNormalHeight  = CapsuleCollider.IsBetweenCrouchingAndNormalHeight(),
			Delta = delta
		};
		StairsSystem.SlideCameraSmoothBackToOrigin(slideCameraParams);
	}

	private void CameraModifications(float delta)
	{
		Bobbing.CameraBobbingParams cameraBobbingParams = new Bobbing.CameraBobbingParams
		{
			Delta = delta,
			IsOnFloorCustom = isOnFloorCustom(),
			Velocity = Velocity
		};
		Bobbing.PerformCameraBobbing(cameraBobbingParams);

		FieldOfView.FovParameters fovParams = new FieldOfView.FovParameters
		{
			IsCrouchingHeight = CapsuleCollider.IsCrouchingHeight(),
			Delta = delta,
			SprintSpeed = WalkSpeed,
			Velocity = Velocity
		};
		FieldOfView.PerformFovAdjustment(fovParams);
	}

	///////////////////////////
	// Helpers ////////////////
	///////////////////////////
	
	public float CalculateGravityForce() => _gravity * Weight;
	
	public void ReduceTimeScaleWhileAiming()
	{
		Engine.SetTimeScale(TimeScaleAimInAir);
		_timeScaleAimInAirTimer.Start();
	}
	public void ResetTimeScale()
	{
		Engine.SetTimeScale(1);
	}
	
	private bool IsHeadTouchingCeiling()
	{
		for (int i = 0; i < NUM_OF_HEAD_COLLISION_DETECTORS; i++)
		{
			if (_headCollisionDetectors[i].IsColliding())
			{
				return true;
			}
		}

		return false;
	}
	 
	private bool isOnFloorCustom()
	{
		return IsOnFloor() || StairsSystem.WasSnappedToStairsLastFrame();
	}

	public void RotateWeaponWithPlayer()
	{
		WeaponRoot.SetRotation(HeadSystem.Rotation);
	}
	
	///////////////////////////
	// Processes //////////////
	///////////////////////////
	public override void _PhysicsProcess(double delta)
	{
		LookAround();
		CameraModifications((float) delta);
		HandleStairs((float) delta);
	}

}
