using System;
using Godot;
using GodotStateCharts;
using Movementtests.systems;
using Movementtests.player_controller.Scripts;

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 Gravity          Gravity;
	public HealthSystem     HealthSystem;
	public MoveSystem       MoveSystem;
	public TweenQueueSystem TweenQueueSystem;
	public Node3D           WeaponRoot;
	public WeaponSystem     WeaponSystem;
	public WallHugSystem    WallHugSystem;

	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 bool _isAiming;
	private bool _dashCanceled;

	private Timer _coyoteTimer;
	private Timer _timeScaleAimInAirTimer;
	private Timer _timeAfterDashingTimer;
	[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
	public float TimeScaleAimInAir { get; set; } = 0.2f;
	[Export(PropertyHint.Range, "1,5,0.1,or_greater")]
	public float MaxJumpBoostAfterDashing { get; set; } = 2f;

	private StateChart       _playerState;
	// Actions state
	private StateChartState  _weaponInHand;
	private StateChartState  _aiming;
	private StateChartState  _dashing;
	private StateChartState  _weaponThrown;
	private StateChartState  _actionHanging;
	// Movement state
	private StateChartState  _grounded;
	private StateChartState  _mantling;
	private StateChartState  _movHanging;
	private StateChartState  _wallHugging;
	private StateChartState  _airborne;
	private StateChartState  _coyoteEnabled;
	private StateChartState  _jump;
	private StateChartState  _jumpFromWall;
	private StateChartState  _doubleJumpEnabled;
	private StateChartState  _doubleJump;
	private StateChartState  _falling;

	public override void _Ready()
	{
		///////////////////////////
		// Getting components /////
		///////////////////////////
		
		// General use stuff
		TweenQueueSystem = GetNode<TweenQueueSystem>("TweenQueueSystem");
		// 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");
		Gravity = GetNode<Gravity>("Gravity");
		MoveSystem = GetNode<MoveSystem>("MoveSystem");
		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);
		}
		
		// RPG Stuff
		Stamina = GetNode<Stamina>("Stamina");
		HealthSystem = GetNode<HealthSystem>("HealthSystem");
		
		// State management
		_playerState = StateChart.Of(GetNode("StateChart"));
		// Actions states
		_weaponInHand = StateChartState.Of(GetNode("StateChart/Root/Actions/WeaponInHand"));
		_aiming = StateChartState.Of(GetNode("StateChart/Root/Actions/Aiming"));
		_dashing = StateChartState.Of(GetNode("StateChart/Root/Actions/Dashing"));
		_weaponThrown = StateChartState.Of(GetNode("StateChart/Root/Actions/WeaponThrown"));
		_actionHanging = StateChartState.Of(GetNode("StateChart/Root/Actions/Hanging"));
		// Movement states
		_grounded = StateChartState.Of(GetNode("StateChart/Root/Movement/Grounded"));
		_mantling = StateChartState.Of(GetNode("StateChart/Root/Movement/Mantling"));
		_movHanging = StateChartState.Of(GetNode("StateChart/Root/Movement/Hanging"));
		_airborne = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne"));
		_wallHugging = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/WallHugging"));
		_coyoteEnabled = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/CoyoteEnabled"));
		_jump = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/Jump"));
		_jumpFromWall = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/JumpFromWall"));
		_doubleJumpEnabled = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/DoubleJumpEnabled"));
		_doubleJump = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/DoubleJump"));
		_falling = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/Falling"));
		// State timers
		_coyoteTimer = GetNode<Timer>("CoyoteTime");
		_timeScaleAimInAirTimer = GetNode<Timer>("TimeScaleAimInAir");
		_timeAfterDashingTimer = GetNode<Timer>("TimeAfterDashing");
		
		///////////////////////////
		// 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
		float gravitySetting = (float)ProjectSettings.GetSetting("physics/3d/default_gravity");
		Gravity.Init(gravitySetting);
		MantleSystem.Init(HeadSystem);
		var moveSystemParams = new MoveSystem.MoveSystemParameters(this, Gravity, MantleSystem, TweenQueueSystem,
			HeadSystem, CapsuleCollider);
		MoveSystem.Init(moveSystemParams);
		StairsSystem.Init(stairsBelowRayCast3D, stairsAheadRayCast3D, cameraSmooth);
		DashSystem.Init(HeadSystem, camera, TweenQueueSystem);
		WeaponSystem.Init(HeadSystem, camera);
		WallHugSystem.Init();
		
		// RPG Stuff
		HealthSystem.HealthSystemInitParams healthSystemParams = new HealthSystem.HealthSystemInitParams()
		{
			Gravity = Gravity,
			Parent = this,
			Camera = camera,
			Head =  HeadSystem,
			VignetteRect = vignetteRect,
			DistortionRect = distortionRect,
			BlurRect = blurRect,
		};
		HealthSystem.Init(healthSystemParams);
		Stamina.SetSpeeds(MoveSystem.WalkSpeed, MoveSystem.SprintSpeed);
		
		///////////////////////////
		// Signal setup ///////////
		///////////////////////////

		DashSystem.DashEnded += OnDashEnded;

		_weaponInHand.StateProcessing += HandleWeaponInHand;
		_aiming.StateProcessing += HandleAiming;
		_aiming.StateEntered += OnAimingEntered;
		_aiming.StateExited += ResetTimeScale;
		
		_grounded.StatePhysicsProcessing += HandleGrounded;
		_airborne.StatePhysicsProcessing += HandleAirborne;
		_wallHugging.StatePhysicsProcessing += HandleWallHugging;
		
		_coyoteEnabled.StateEntered += StartCoyoteTime;
		_coyoteTimer.Timeout += CoyoteExpired;
		_timeScaleAimInAirTimer.Timeout += ResetTimeScale;
		_jump.StateEntered += Jump;
		_jumpFromWall.StateEntered += JumpFromWall;
		_doubleJump.StateEntered += DoubleJump;
		_mantling.StateEntered += Mantle;
		
		_dashing.StateEntered += OnDashStarted;
		_weaponThrown.StateEntered += OnWeaponThrown;
	}
	
	///////////////////////////
	// 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 OnInputAimReleased()
	{
		_playerState.SendEvent("aim_released");
	}
	public void OnInputAimCanceled()
	{
		_playerState.SendEvent("aim_canceled");
		DashSystem.CancelDash();
	}
	public void OnInputHitPressed()
	{
		_playerState.SendEvent("hit_pressed");
	}
	public void OnInputJumpPressed()
	{
		if (MoveSystem.CanMantle())
		{
			_playerState.SendEvent("mantle");
			return;
		}
		
		_playerState.SendEvent("jump");
	}
	public void OnInputDropPressed()
	{
		_playerState.SendEvent("drop");
	}
	
	///////////////////////////
	// Stateful logic /////////
	///////////////////////////

	// Jumping
	public void StartCoyoteTime()
	{
		_coyoteTimer.Start();
	}
	public void CoyoteExpired()
	{
		_playerState.SendEvent("coyote_expired");
	}
	public void Jump()
	{
		_playerState.SendEvent("to_double_jump");
		PerformJump(false);
	}
	public void JumpFromWall()
	{
		_playerState.SendEvent("to_double_jump");
		var wallNormal = WallHugSystem.GetWallNormal();
		if (wallNormal.IsSome(out var normal))
			PerformJump(false, normal);
		PerformJump(false);
	}
	public void DoubleJump()
	{
		_playerState.SendEvent("to_falling");
		PerformJump(true);
	}
	private void PerformJump(bool isDoubleJump, Vector3? jumpDirection = null)
	{
		var effectiveJumpDirection = jumpDirection ?? Vector3.Up;
		var jumpVector = (effectiveJumpDirection.Normalized() + Vector3.Up).Normalized();

		var proportionOfTimeGone = _timeAfterDashingTimer.TimeLeft / _timeAfterDashingTimer.WaitTime;
		GD.Print(proportionOfTimeGone);
		var actualBoost = 1 + MaxJumpBoostAfterDashing * proportionOfTimeGone;
		_timeAfterDashingTimer.Stop();
		
		bool doesCapsuleHaveCrouchingHeight = CapsuleCollider.IsCrouchingHeight();
		bool isPlayerDead = HealthSystem.IsDead();
		if (!doesCapsuleHaveCrouchingHeight && !isPlayerDead)
			MoveSystem.Jump(isDoubleJump, jumpVector, (float) actualBoost);
	}
	
	// Mantling
	public void Mantle()
	{
		var optionTween = MoveSystem.Mantle();
		if (optionTween.IsSome(out var tween))
			tween.Finished += MantleFinished;
	}
	public void MantleFinished()
	{
		_playerState.SendEvent("to_grounded");
	}
	
	// Dashing and weapon throwing
	public void OnDashStarted()
	{
		_timeAfterDashingTimer.Start();
		if (WeaponSystem.FlyingState.Active)
		{
			DashSystem.DashResolve = new DashResolveRecord(false, WeaponSystem.GlobalPosition, Vector3.Zero);
		}
		else if (WeaponSystem.PlantedState.Active)
		{
			// Should we try to resolve with mantle?
			DashSystem.DashResolve = new DashResolveRecord(false, WeaponSystem.PlayerDashLocation, Vector3.Zero);
		}
		_dashDirection = (DashSystem.DashResolve.DashLocation - GlobalPosition).Normalized();
		DashSystem.Dash();
	}
	public void OnDashEnded()
	{
		// Regular dash
		if (WeaponSystem.InHandState.Active)
		{
			_playerState.SendEvent("dash_ended");
			return;
		}
		
		// Store the weapon state before resetting it
		var isPlantedOnWall = WeaponSystem.PlantedState.Active 
		                      && (Math.Abs(WeaponSystem.GlobalRotation.X) + Math.Abs(WeaponSystem.GlobalRotation.Z) < 0.3);
		var isFlying = WeaponSystem.FlyingState.Active;
		
		// Get the weapon back
		GetTree().GetRoot().RemoveChild(WeaponRoot);
		AddChild(WeaponRoot);
		WeaponRoot.SetGlobalPosition(GlobalPosition);
		WeaponSystem.ResetWeapon();
		
		if (isFlying)
		{
			var vel = _dashDirection * DashSystem.PostDashSpeed;
			SetVelocity(vel);
			_playerState.SendEvent("dash_ended");
			return;  // In case states aren't exclusives
		}
		
		if (isPlantedOnWall)
		{
			_playerState.SendEvent("dash_to_planted");
			return;  // In case states aren't exclusives
		}
		
		// Weapon planted anywhere else
		_playerState.SendEvent("dash_ended");
	}
	public void OnWeaponThrown()
	{
		RemoveChild(WeaponRoot);
		GetTree().GetRoot().AddChild(WeaponRoot);
		WeaponRoot.SetGlobalPosition(GlobalPosition);
		
		var (hasHit, location, collisionPoint, collisionNormal) = DashSystem.DashComputation;
		var (endWithMantle, dashLocation, mantleLocation) = DashSystem.DashResolve;

		DashSystem.CancelDash();
		WeaponSystem.ThrowWeapon(location, hasHit, collisionPoint, collisionNormal);
	}
	public void OnAimingEntered()
	{
		if (!isOnFloorCustom())
			ReduceTimeScaleWhileAiming();
	}
	
	// Regular processes
	public void HandleWeaponInHand(float delta)
	{
		RotateWeaponWithPlayer();
	}
	public void HandleAiming(float delta)
	{
		RotateWeaponWithPlayer();
		DashSystem.PrepareDash();
		
		if (isOnFloorCustom())
			ResetTimeScale();
	}

	// Physics processes
	public void HandleGrounded(float delta)
	{
		if (!isOnFloorCustom())
			_playerState.SendEvent("start_falling");
	}
	public void HandleAirborne(float delta)
	{
		if (isOnFloorCustom())
			_playerState.SendEvent("grounded");
		if (WallHugSystem.IsWallHugging() && Velocity.Y < 0)
			_playerState.SendEvent("wall_hug");
	}
	public void HandleWallHugging(float delta)
	{
		if (!WallHugSystem.IsWallHugging())
			_playerState.SendEvent("start_falling");
	}

	///////////////////////////
	// Stateless logic ////////
	///////////////////////////
	private void LookAround()
	{
		Vector2 inputLookDir = new Vector2(_inputRotateY, _inputRotateFloorplane);
		HeadSystem.LookAround(inputLookDir);
	}

	private void MoveAround(double delta)
	{
		var moveAroundParams = new MoveSystem.MoveAroundParameters(
			delta, 
			_inputMove, 
			isOnFloorCustom(), 
			HealthSystem.IsDead(),
			IsHeadTouchingCeiling(),
			_actionHanging.Active,
			_wallHugging.Active);
		MoveSystem.MoveAround(moveAroundParams);
	}

	private void HandleStairs(float delta)
	{

		StairsSystem.UpStairsCheckParams upStairsCheckParams = new StairsSystem.UpStairsCheckParams
		{
			IsOnFloorCustom = isOnFloorCustom(),
			IsCapsuleHeightLessThanNormal = CapsuleCollider.IsCapsuleHeightLessThanNormal(),
			CurrentSpeedGreaterThanWalkSpeed = MoveSystem._currentSpeed > MoveSystem.WalkSpeed,
			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(),  // 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 = MoveSystem._currentSpeed > MoveSystem.WalkSpeed,
			BetweenCrouchingAndNormalHeight  = CapsuleCollider.IsBetweenCrouchingAndNormalHeight(),
			Delta = (float)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 = (float)delta,
			SprintSpeed = MoveSystem.SprintSpeed,
			Velocity = Velocity
		};
		FieldOfView.PerformFovAdjustment(fovParams);
	}

	///////////////////////////
	// Helpers ////////////////
	///////////////////////////
	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)
	{
		TweenQueueSystem.ProcessTweens();
		
		LookAround();
		MoveAround(delta);
		CameraModifications((float) delta);
		HandleStairs((float) delta);
	}

}
