class_name PlayerMovement
extends Node

@export var player: CharacterBody3D
@export var head: Node3D

#Most of the default values are based primarily on minecraft, but the
#movement code does not replicate how movement in minecraft works

@export_category("Movement")
@export_subgroup("Basic")
@export var hard_speed_limit: float = 4.317
@export var toggle_sprint: bool = false
@export var limit_sprint_to_forward: bool = true
@export var sprint_boost: float = 1.3
@export var acceleration: float = 0.9
@export var friction_percentage: float = 0.15
@export var air_drag: float = 0.03
@export_range(0.0, 90.0) var max_look_down_angle: float = 90.0
@export_range(0.0, 90.0) var max_look_up_angle: float = 90.0
@export_range(0.0, 1.0) var air_control: float = 0.2
@export var climb_speed: float = 3.0

@export_subgroup("Jump")
@export var enable_jumping: bool = false
@export var min_height: float = 1.25
@export var max_height: float = 1.25
@export var time_to_peak: float = 0.3
@export var time_to_floor: float = 0.3

@export_subgroup("Crouch")
@export var enable_crouching: bool = false
@export var standing_collider: CollisionShape3D
@export var crouching_collider: CollisionShape3D
@export var crouch_shapecast: ShapeCast3D

@export_category("Look")
@export_range(1.0, 100.0) var sensitivity: float = 28.0
@export var invert_y: bool = false
@export var invert_x: bool = false

@onready var jump_velocity: float = (2 * max_height) / time_to_peak
@onready var jump_gravity: float = (-2 * max_height) / pow(time_to_peak, 2)
@onready var fall_gravity: float = (-2 * max_height) / pow(time_to_floor, 2)
@onready var time_to_min_peak: float = (clampf(min_height, 0.0, max_height) / max_height) * time_to_peak
@onready var min_jump_gravity: float = (-2 * clampf(min_height, 0.0, max_height)) / pow(time_to_min_peak, 2)

var ragdoll: bool = false
var paused: bool = false
var zoom_factor: float = 1.0
var input_vector: Vector2 = Vector2.ZERO
var look_vector: Vector2 = Vector2.ZERO
var joystick_deadzone: float = 0.12
var can_sprint: bool = true
var sprint_zoom_factor: float = 0.08
var sprinting: bool = false
var jump_held: bool = false
var is_in_climb_zone: bool = false
var hold_climb: bool = false
var ragdoll_grace_period_length: float = 0.5
var ragdoll_grace_period_elapsed: float = 0.0
var crouching: bool = false
var prev_crouch_height: float = 0.0
var new_crouch_height: float = 0.0
var default_camera_height: float = 0.0
var change_crouch: bool = false
@export var weapon_holder: WeaponMovement

var head_angle: float = 0.0
var look_sens: float :
	set(_value):
		return
	get:
		return sensitivity / 40000.0


func _ready() -> void:
	default_camera_height = head.position.y


func get_gravity() -> float:
	if jump_held:
		return jump_gravity if player.velocity.y > 0.0 else fall_gravity
	return min_jump_gravity if player.velocity.y > 0.0 else fall_gravity


func apply_ragdoll(vector: Vector3) -> void:
	ragdoll = true
	player.velocity = vector
	print(vector)


func _physics_process(delta: float) -> void:
	if !is_multiplayer_authority():
		return
	if ragdoll:
		player.velocity += Vector3.DOWN * 9.81 * delta
		ragdoll_grace_period_elapsed += delta
		if player.is_on_floor() and ragdoll_grace_period_elapsed >= ragdoll_grace_period_length:
			ragdoll_grace_period_elapsed = 0.0
			ragdoll = false
		player.move_and_slide()
		return
	var result_vector: Vector2 = input_vector * acceleration
	if !player.is_on_floor():
		result_vector *= air_control
	var velocity: Vector2 = Vector2(player.velocity.x, player.velocity.z)
	var down_velocity: float = player.velocity.y
	var movement: Vector3 = ((player.transform.basis.z * result_vector.y) + (player.transform.basis.x * result_vector.x))
	if player.is_on_floor():
		velocity = velocity.limit_length(velocity.length() * (1.0 - friction_percentage))
	velocity = velocity.limit_length(velocity.length() * (1.0 - air_drag))
	velocity += Vector2(movement.x, movement.z)
	velocity = velocity.limit_length(hard_speed_limit * sprint_boost if sprinting else hard_speed_limit)
	if !is_in_climb_zone:
		if !player.is_on_floor():
			player.velocity = Vector3(velocity.x, down_velocity + (get_gravity() * delta), velocity.y)
		else:
			player.velocity = Vector3(velocity.x, down_velocity, velocity.y)
	else:
		if jump_held:
			player.velocity = Vector3(velocity.x, climb_speed, velocity.y)
		elif hold_climb:
			player.velocity = Vector3(velocity.x, 0.0, velocity.y)
		else:
			player.velocity = Vector3(velocity.x, -climb_speed, velocity.y)
	if change_crouch:
		if !crouching:
			standing_collider.disabled = true
			crouching_collider.disabled = false
			crouching = true
		else:
			standing_collider.disabled = false
			crouching_collider.disabled = true
			crouching = false
		change_crouch = false
	player.move_and_slide()


func _process(_delta: float) -> void:
	if !is_multiplayer_authority():
		return
	if paused:
		input_vector = Vector2.ZERO
		return
	can_sprint = true
	input_vector = Input.get_vector("Move Left", "Move Right", "Move Forward", "Move Backward")
	if limit_sprint_to_forward and input_vector.y >= 0:
		can_sprint = false
	if toggle_sprint:
		if Input.is_action_just_pressed("Sprint"):
			sprinting = !sprinting
	else:
		sprinting = Input.is_action_pressed("Sprint")
	if !can_sprint:
		sprinting = false
	if enable_jumping:
		if Input.is_action_just_pressed("Jump") and player.is_on_floor() and !is_in_climb_zone:
			player.velocity.y = jump_velocity
			jump_held = true
	if Input.is_action_just_pressed("Jump") and is_in_climb_zone:
		jump_held = true
	if Input.is_action_just_released("Jump"):
		jump_held = false
	if enable_crouching:
		if Input.is_action_just_pressed("Crouch"):
			if !crouching:
				prev_crouch_height = 0.0
				new_crouch_height = default_camera_height
				change_crouch = true
			elif !crouch_shapecast.is_colliding():
				tween_head_height(default_camera_height + 0.2)
				change_crouch = true
	if crouching:
		crouch()
	if Input.is_action_just_pressed("Hold Climbing") and is_in_climb_zone:
		hold_climb = true
	if Input.is_action_just_released("Hold Climbing") and is_in_climb_zone:
		hold_climb = false
	look_vector.x = Input.get_joy_axis(0, JOY_AXIS_RIGHT_X)
	look_vector.y = Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y)
	if look_vector.length_squared() <= joystick_deadzone:
		look_vector = Vector2(0.0, 0.0)
	player.rotation.y -= look_vector.x * 0.02
	head_angle -= look_vector.y * 0.02
	head_angle = clamp(head_angle, deg_to_rad(-max_look_down_angle), deg_to_rad(max_look_up_angle))
	head.rotation.x = head_angle


func enable_climbing() -> void:
	is_in_climb_zone = true


func disable_climbing() -> void:
	is_in_climb_zone = false
	hold_climb = false


func crouch() -> void:
	var lowest_crouch_point: float = default_camera_height
	for collider: Dictionary in crouch_shapecast.collision_result:
		var point: Vector3 = collider["point"]
		if crouch_shapecast.global_position.distance_to(point) + 0.6 < lowest_crouch_point:
			lowest_crouch_point = crouch_shapecast.global_position.distance_to(point) + 0.6
	if !crouch_shapecast.is_colliding():
		lowest_crouch_point = default_camera_height
	if abs(lowest_crouch_point - new_crouch_height) > 0.05:
		new_crouch_height = lowest_crouch_point
	if abs(new_crouch_height - prev_crouch_height) > 0.05:
		tween_head_height(new_crouch_height)
		prev_crouch_height = new_crouch_height


func tween_head_height(height: float) -> void:
	var tween: Tween = create_tween()
	tween.set_ease(Tween.EASE_IN)
	tween.set_trans(Tween.TRANS_QUAD)
	tween.tween_property(head, "position", Vector3(0.0, height - 0.2, 0.0), 0.1)


func _unhandled_input(event: InputEvent) -> void:
	if !is_multiplayer_authority():
		return
	if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
		player.rotation.y -= (event.relative.x * get_viewport().get_final_transform().x.x) * (look_sens / zoom_factor) * (-1 if invert_x else 1)
		head_angle -= (event.relative.y * get_viewport().get_final_transform().y.y) * (look_sens / zoom_factor) * (-1 if invert_y else 1)
		head_angle = clamp(head_angle, deg_to_rad(-max_look_down_angle), deg_to_rad(max_look_up_angle))
		head.rotation.x = head_angle
		if weapon_holder:
			weapon_holder.mouse_input = event.relative
	elif event is InputEventJoypadMotion:
		if event.axis == JOY_AXIS_RIGHT_X:
			look_vector.x = event.axis_value
		elif event.axis == JOY_AXIS_RIGHT_Y:
			look_vector.y = event.axis_value
