using System.Collections.Generic;
using System.Runtime.InteropServices.JavaScript;
using Godot;
using RustyOptions;

namespace PolarBears.PlayerControllerAddon;

public partial class PlayerController : CharacterBody3D
{
	// User API to important child nodes.
	public Node3D           Head;
	public Bobbing          Bobbing;
	public FieldOfView      FieldOfView;
	public Stamina          Stamina;
	public StairsSystem     StairsSystem;
	public MantleSystem     MantleSystem;
	public DashSystem       DashSystem;
	public CapsuleCollider  CapsuleCollider;
	public Gravity          Gravity;
	public HealthSystem     HealthSystem;
	public Mouse            Mouse;

	[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 CrouchTransitionSpeed { get; set; } = 20.0f;

	[Export(PropertyHint.Range, "0,5,0.1,or_greater")]
	public float DoubleJumpSpeedFactor { get; set; } = 2f;
	
	[Export(PropertyHint.Range, "1,50,1,or_greater")]
	public float ControllerSensitivity { get; set; } = 20f;

	private bool _canDoubleJump = true;
	private bool _movementEnabled = true;

	private bool _isTweening = false;
	private record TweenInputs(Vector3 Location, float Duration);

	private Queue<TweenInputs> _tweenInputs = new Queue<TweenInputs>();

	private bool _shouldMantle = false;
	private Vector3 _dashLocation = Vector3.Zero;
	private Vector3 _mantleLocation = Vector3.Zero;

	private float _currentSpeed;

	private const float DecelerationSpeedFactorFloor = 15.0f;
	private const float DecelerationSpeedFactorAir   = 7.0f;

	private float _lastFrameWasOnFloor = -Mathf.Inf;

	private const int NumOfHeadCollisionDetectors = 4;
	private RayCast3D[] _headCollisionDetectors;
	
	private Vector3 _inputMove = Vector3.Zero;
	private float _inputRotateY = 0.0f;
	private float _inputRotateFloorplane = 0.0f;

	private bool _isAiming = false;

	public void OnInputMove(Vector3 value)
	{
		_inputMove = value;
	}
	
	public void OnInputRotateY(float value)
	{
		_inputRotateY = value;
	}
	
	public void OnInputRotateFloorplane(float value)
	{
		_inputRotateFloorplane = value;
	}

	public void OnInputAimPressed()
	{
		_isAiming = true;
		GD.Print("Aim pressed");
	}
	public void OnInputAimReleased()
	{
		_isAiming = false;
		GD.Print("Aim released");
	}
	public void OnInputAimCanceled()
	{
		_isAiming = false;
		GD.Print("Aim canceled");
	}

	

	public override void _Ready()
	{
		_currentSpeed = WalkSpeed;
		
		Head = GetNode<Node3D>("Head");
		
		_headCollisionDetectors = new RayCast3D[NumOfHeadCollisionDetectors];

		for (int i = 0; i < NumOfHeadCollisionDetectors; i++)
		{
			_headCollisionDetectors[i] = GetNode<RayCast3D>(
				"HeadCollisionDetectors/HeadCollisionDetector" + i);
		}

		// Getting dependencies of the components(In godot we manage this from upwards to downwards not vice versa)
		Camera3D camera = GetNode<Camera3D>("Head/CameraSmooth/Camera3D");

		RayCast3D stairsBelowRayCast3D = GetNode<RayCast3D>("StairsBelowRayCast3D");
		RayCast3D stairsAheadRayCast3D = GetNode<RayCast3D>("StairsAheadRayCast3D");

		Node3D cameraSmooth = GetNode<Node3D>("Head/CameraSmooth");

		AnimationPlayer animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
		
		// Getting universal setting from GODOT editor to be in sync
		float gravitySetting = (float)ProjectSettings.GetSetting("physics/3d/default_gravity");

		ColorRect vignetteRect = GetNode<ColorRect>(
			"Head/CameraSmooth/Camera3D/CLVignette(Layer_1)/HealthVignetteRect");
		
		ColorRect distortionRect = GetNode<ColorRect>(
			"Head/CameraSmooth/Camera3D/CLDistortion(Layer_2)/HealthDistortionRect");

		ColorRect blurRect = GetNode<ColorRect>("Head/CameraSmooth/Camera3D/CLBlur(Layer_2)/BlurRect");
		
		Node3D mapNode = GetTree().Root.FindChild("Map", true, false) as Node3D;
		
		// Getting components

		Bobbing = GetNode<Bobbing>("Bobbing");
		Bobbing.Init(camera);

		FieldOfView = GetNode<FieldOfView>("FieldOfView");
		FieldOfView.Init(camera);

		Stamina = GetNode<Stamina>("Stamina");
		Stamina.SetSpeeds(WalkSpeed, SprintSpeed);

		StairsSystem = GetNode<StairsSystem>("StairsSystem");
		StairsSystem.Init(stairsBelowRayCast3D, stairsAheadRayCast3D, cameraSmooth);

		MantleSystem = GetNode<MantleSystem>("MantleSystem");
		MantleSystem.Init(Head);
		
		DashSystem = GetNode<DashSystem>("DashSystem");
		DashSystem.Init(Head, camera);

		CapsuleCollider = GetNode<CapsuleCollider>("CapsuleCollider");

		Gravity = GetNode<Gravity>("Gravity");
		Gravity.Init(gravitySetting);

		HealthSystem = GetNode<HealthSystem>("HealthSystem");
		
		HealthSystem.HealthSystemInitParams healthSystemParams = new HealthSystem.HealthSystemInitParams()
		{
			Gravity = Gravity,
			Parent = this,
			Camera = camera,
			AnimationPlayer = animationPlayer,
			Head =  Head,
			VignetteRect = vignetteRect,
			DistortionRect = distortionRect,
			BlurRect = blurRect,
		};
		
		HealthSystem.Init(healthSystemParams);
		
		Mouse = GetNode<Mouse>("Mouse");
		Mouse.Init(Head, camera, HealthSystem.IsDead);
	}

	private void DisableMovement()
	{
		_movementEnabled = false;
	}
	public void EnableMovement()
	{
		_movementEnabled = true;
	}

	public void EndTween()
	{
		EnableMovement();
		_isTweening = false;
	}

	private void TweenToLocation(TweenInputs inputs)
	{
		var (location, duration) = inputs;
		
		var tween = GetTree().CreateTween();
		var callback = new Callable(this, MethodName.EndTween);
				
		tween.TweenProperty(this, "position", location, duration);
		tween.TweenCallback(callback);
				
		DisableMovement();
		_isTweening = true;
		tween.Play();
	}

	private void QueueTween(TweenInputs inputs)
	{
		_tweenInputs.Enqueue(inputs);
	}

	private void QueueTween(Vector3 location, float duration)
	{
		QueueTween(new TweenInputs(location, duration));
	}

	public override void _PhysicsProcess(double delta)
	{
		if (_tweenInputs.Count > 0 && !_isTweening)
			TweenToLocation(_tweenInputs.Dequeue());
		
		if (Input.IsActionPressed("aim_dash"))
		{
			(_shouldMantle, _dashLocation, _mantleLocation) = DashSystem.PrepareDash();
		}

		if (Input.IsActionJustReleased("aim_dash"))
		{
			DashSystem.Dash();
			QueueTween(_dashLocation, 0.1f);
			if (_shouldMantle)
			{
				QueueTween(_mantleLocation, 0.1f);
			}
		}
		
		var mantleLocationResult = MantleSystem.FindMantleInFrontOfPlayer();
		if (isOnFloorCustom())
		{
			_lastFrameWasOnFloor = Engine.GetPhysicsFrames();
			_canDoubleJump = true;
		}

		// Adding the gravity
		if (!isOnFloorCustom())
		{
			Velocity = new Vector3(
				x: Velocity.X,
				y: Velocity.Y - (Gravity.CalculateGravityForce() * (float)delta),
				z: Velocity.Z);
		}

		bool doesCapsuleHaveCrouchingHeight = CapsuleCollider.IsCrouchingHeight();

		bool isPlayerDead = HealthSystem.IsDead();

		// Handle Jump input
		if (Input.IsActionJustPressed("jump")
		    && !doesCapsuleHaveCrouchingHeight 
		    && !isPlayerDead)
		{
			if (mantleLocationResult.IsSome(out var mantleLocation))
			{
				var duration = 0.1f * mantleLocation.DistanceTo(Position);
				QueueTween(mantleLocation, duration);
			}
			else if (isOnFloorCustom())
			{
				Velocity = new Vector3(
					x: Velocity.X,
					y: Gravity.CalculateJumpForce() * (float)delta,
					z: Velocity.Z);
			}
			else if (!isOnFloorCustom()
			         && _canDoubleJump)
			{
				_canDoubleJump = false;
				Velocity = new Vector3(
					x: Velocity.X,
					y: Gravity.CalculateJumpForce() * (float)delta * DoubleJumpSpeedFactor,
					z: Velocity.Z);
			}
		}
		
		bool isHeadTouchingCeiling = IsHeadTouchingCeiling();
		bool doesCapsuleHaveDefaultHeight = CapsuleCollider.IsDefaultHeight();

		// 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)
		{
			Velocity = new Vector3(
				x: Velocity.X,
				y: Velocity.Y - 2.0f,
				z: Velocity.Z);
		}

		if (!isPlayerDead)
		{
			
			// 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 && !isPlayerDead)
		{
			_currentSpeed = SprintSpeed;
		}
		
		// Vector2 inputLookDir = Input.GetVector("look_left", "look_right", "look_up", "look_down");
		Vector2 inputLookDir = new Vector2(_inputRotateY, _inputRotateFloorplane);
		Mouse.LookAround(-1 * ControllerSensitivity * inputLookDir);
		
		// 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 = Head.Transform.Basis * _inputMove;

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

		if (isOnFloorCustom())
		{
			// Set velocity based on input direction when on the floor
			if (direction.Length() > 0)
			{
				float availableSpeed = Stamina.AccountStamina(delta, _currentSpeed);

				float newX = direction.X * availableSpeed;
				float newZ = direction.Z * availableSpeed;

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

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

			Velocity = new Vector3(xDeceleration, Velocity.Y, zDeceleration);
		}
		
		if (isPlayerDead)
		{
			MoveAndSlide();
			return;
		}
		
		Bobbing.CameraBobbingParams cameraBobbingParams = new Bobbing.CameraBobbingParams
		{
			Delta = (float)delta,
			IsOnFloorCustom = isOnFloorCustom(),
			Velocity = Velocity
		};
		
		Bobbing.PerformCameraBobbing(cameraBobbingParams);

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

		StairsSystem.UpStairsCheckParams upStairsCheckParams = new StairsSystem.UpStairsCheckParams
		{
			IsOnFloorCustom = isOnFloorCustom(),
			IsCapsuleHeightLessThanNormal = CapsuleCollider.IsCapsuleHeightLessThanNormal(),
			CurrentSpeedGreaterThanWalkSpeed = _currentSpeed > WalkSpeed,
			IsCrouchingHeight = CapsuleCollider.IsCrouchingHeight(),
			Delta = (float)delta,
			FloorMaxAngle = FloorMaxAngle,
			GlobalPositionFromDriver = GlobalPosition,
			Velocity = Velocity,
			GlobalTransformFromDriver = GlobalTransform,
			Rid = GetRid()
		};
		
		// TODO: SnapUpStairsCheck influences the ability of player to crouch because of `stepHeightY <= 0.01` part
		// Ideally, it should not. SnapUpStairsCheck and SnapDownStairsCheck should be called, when player is actually
		// on the stairs

		StairsSystem.UpStairsCheckResult upStairsCheckResult = StairsSystem.SnapUpStairsCheck(upStairsCheckParams);

		if (upStairsCheckResult.UpdateRequired)
		{
			upStairsCheckResult.Update(this);
		}
		else
		{
			MoveAndSlide();
		
			StairsSystem.DownStairsCheckParams downStairsCheckParams = new StairsSystem.DownStairsCheckParams
			{
				IsOnFloor = IsOnFloor(),  // TODO: replace on IsOnFloor Custom
				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 = _currentSpeed > WalkSpeed,
			BetweenCrouchingAndNormalHeight  = CapsuleCollider.IsBetweenCrouchingAndNormalHeight(),
			Delta = (float)delta
		};
		
		StairsSystem.SlideCameraSmoothBackToOrigin(slideCameraParams);
	}

	private bool IsHeadTouchingCeiling()
	{
		for (int i = 0; i < NumOfHeadCollisionDetectors; i++)
		{
			if (_headCollisionDetectors[i].IsColliding())
			{
				return true;
			}
		}

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