extends CharacterBody3D

# TODO: Add descriptions for each value

@export_category("Character")
@export var base_speed : float = 10.0
@export var sprint_speed : float = 6.0
@export var crouch_speed : float = 6.0

@export var acceleration : float = 10.0
@export var jump_velocity : float = 6.0
@export var mouse_sensitivity : float = 0.1

@export var walljump_cooldown : float = 0.75
@export var wallbounce_multiplier : float = 0.3

@export var aircontrol_multiplier : float = 0.5
@export var aircontrol_delay : float = 0.5

@export var coyotetime : float = 0.3
var coyotechance : bool = false

@export var dash_cooldown : float = 2.0
@export var dash_speedmultiplier : float = 3.0
@export var dash_distance : float = 5.0
var dash_spent : bool = false
var dash_active : bool = false
var dash_startpos : Vector3 = Vector3(0, 0, 0)

@export var physics_push_force : float = 1.0

var holding_object : bool = false
var held_object : RigidBody3D
@export var object_hurl_force : float = 40.0
@export var object_grab_distance : float = 6.0


@export_group("Nodes")
@export var HEAD : Node3D
@export var CAMERA : Camera3D
@export var CAMERA_ANIMATION : AnimationPlayer

@export_group("Controls")
# We are using UI controls because they are built into Godot Engine so they can be used right away
@export var JUMP : String = "move_jump"
@export var LEFT : String = "move_left"
@export var RIGHT : String = "move_right"
@export var FORWARD : String = "move_forward"
@export var BACKWARD : String = "move_backward"
@export var PAUSE : String = "ui_cancel"
@export var CROUCH : String = "move_crouch"
@export var DASH : String = "move_dash"
@export var TELEK : String = "action_telekinesis"

@export var SPRINT : String

@export_group("Feature Settings")
@export var immobile : bool = false
@export var jumping_enabled : bool = true
@export var in_air_momentum : bool = true
@export var motion_smoothing : bool = true
@export var sprint_enabled : bool = false
@export var crouch_enabled : bool = true
@export_enum("Hold to Crouch", "Toggle Crouch") var crouch_mode : int = 0
@export_enum("Hold to Sprint", "Toggle Sprint") var sprint_mode : int = 0
@export var dynamic_fov : bool = false
@export var continuous_jumping : bool = false
@export var view_bobbing : bool = false

# Member variables
var speed : float = base_speed
var is_crouching : bool = false
var is_sprinting : bool = false
var walljump_ready : bool = true

# Get the gravity from the project settings to be synced with RigidBody nodes
var gravity : float = ProjectSettings.get_setting("physics/3d/default_gravity") # Don't set this as a const, see the gravity section in _physics_process


func _ready():
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
	$Head/Camera/Ray.target_position.z = 0 - object_grab_distance
	$Head/TelekinesisPoint.position.z = 0 - object_grab_distance + 2

func _physics_process(delta):
	
	# Add some debug data
	$UserInterface/DebugPanel.add_property("Movement Speed", speed, 1)
	$UserInterface/DebugPanel.add_property("Velocity", get_real_velocity(), 2)
	
	# Gravity
	#gravity = ProjectSettings.get_setting("physics/3d/default_gravity") # If the gravity changes during your game, uncomment this code
	if not is_on_floor():
		velocity.y -= gravity * delta
	
	handle_jumping()
	
	var input_dir = Vector2.ZERO
	if !immobile:
		input_dir = Input.get_vector(LEFT, RIGHT, FORWARD, BACKWARD)
	
	handle_movement(delta, input_dir)
	
	handle_telekinesis()
	
	# toggle_crouch()
	toggle_sprint(input_dir)
	if Input.is_action_just_pressed(DASH) and !dash_spent:
		dash_spent = true
		dash_startpos = position
		dash_active = true
		$dash_timer.start(dash_cooldown)
		$aircontrol_timer.stop()
		
	if dash_active and dash_startpos.distance_to(position) > dash_distance:
		dash_active = false
		
	if dash_active and input_dir.x == 0 and input_dir.y == 0:
		dash_active = false
	
	if dash_active:
		speed = base_speed * dash_speedmultiplier
	elif is_crouching:
		speed = crouch_speed
	elif is_sprinting:
		speed = sprint_speed
	else:
		speed = base_speed
	
	
	if view_bobbing:
		headbob_animation(input_dir)
		
		
	for i in get_slide_collision_count():
		var slaminto = get_slide_collision(i)
		if slaminto.get_collider() is RigidBody3D:
			slaminto.get_collider().apply_central_impulse(-slaminto.get_normal() * physics_push_force)


func handle_jumping():
	if jumping_enabled:
		if !coyotechance and !is_on_floor():
			$coyote_timer.start(coyotetime)
			coyotechance = true
		
		if continuous_jumping:
			if Input.is_action_pressed(JUMP) and is_on_floor():
				velocity.y += jump_velocity
		else:
			if Input.is_action_just_pressed(JUMP):
				if is_on_floor() or (coyotechance and !$coyote_timer.is_stopped()):
					velocity.y += jump_velocity
					$aircontrol_timer.start(aircontrol_delay)
		
		if walljump_ready and Input.is_action_just_pressed(JUMP) and !is_on_floor() and is_on_wall():
			velocity.x = velocity.x + ((position.bounce(get_slide_collision(0).get_normal()).x) * wallbounce_multiplier)
			velocity.z = velocity.z + ((position.bounce(get_slide_collision(0).get_normal()).z) * wallbounce_multiplier)
			velocity.y += jump_velocity
			walljump_ready = false
			$walljump_timer.start(walljump_cooldown)
			$aircontrol_timer.start(aircontrol_delay)
			print("walljump timer started")

func handle_movement(delta, input_dir):
	
	var direction = input_dir.rotated(-HEAD.rotation.y)
	direction = Vector3(direction.x, 0, direction.y)
	
	move_and_slide()
	
	if in_air_momentum:
		if is_on_floor(): # Don't lerp y movement
			if motion_smoothing:
				velocity.x = lerp(velocity.x, direction.x * speed, acceleration * delta)
				velocity.z = lerp(velocity.z, direction.z * speed, acceleration * delta)
			else:
				velocity.x = direction.x * speed
				velocity.z = direction.z * speed
		else:
			if direction.x != 0 and direction.z != 0 and $aircontrol_timer.is_stopped():
				if motion_smoothing:
					velocity.x = lerp(velocity.x, direction.x * speed, acceleration * delta)
					velocity.z = lerp(velocity.z, direction.z * speed, acceleration * delta)
	else:
		if motion_smoothing:
			velocity.x = lerp(velocity.x, direction.x * speed, acceleration * delta)
			velocity.z = lerp(velocity.z, direction.z * speed, acceleration * delta)
		else:
			velocity.x = direction.x * speed
			velocity.z = direction.z * speed


func _process(delta):
	
	$UserInterface/DebugPanel.add_property("FPS", 1.0/delta, 0)
	
	if Input.is_action_just_pressed(PAUSE):
		if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
			Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
		elif Input.mouse_mode == Input.MOUSE_MODE_VISIBLE:
			Input.mouse_mode = Input.MOUSE_MODE_CAPTURED


func _unhandled_input(event):
	if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
		HEAD.rotation_degrees.y -= event.relative.x * mouse_sensitivity
		HEAD.rotation_degrees.x -= event.relative.y * mouse_sensitivity
		HEAD.rotation.x = clamp(HEAD.rotation.x, deg_to_rad(-90), deg_to_rad(90))


"""func toggle_crouch():
	if crouch_enabled:
		if crouch_mode == 0:
			is_crouching = Input.is_action_pressed(CROUCH)
		elif crouch_mode == 1:
			if Input.is_action_just_pressed(CROUCH):
				is_crouching = !is_crouching
	
		# Replace with your own crouch animation code
		if is_crouching:
			$Collision.scale.y = lerp($Collision.scale.y, 0.75, 0.2)
		else:
			$Collision.scale.y = lerp($Collision.scale.y, 1.0, 0.2)"""


func toggle_sprint(moving):
	if sprint_enabled:
		if sprint_mode == 0:
			if !is_crouching: # Crouching takes priority over sprinting
				is_sprinting = Input.is_action_pressed(SPRINT)
			else:
				is_sprinting = false # Fix a bug where if you are sprinting and then crouch then let go of the sprinting button you keep sprinting
		elif sprint_mode == 1:
			if Input.is_action_just_pressed(SPRINT):
				if !is_crouching:
					is_sprinting = !is_sprinting
				else:
					is_sprinting = false
	
		if dynamic_fov:
			if is_sprinting and moving:
				CAMERA.fov = lerp(CAMERA.fov, 85.0, 0.3)
			else:
				CAMERA.fov = lerp(CAMERA.fov, 75.0, 0.3)


func headbob_animation(moving):
	if moving and is_on_floor():
		CAMERA_ANIMATION.play("headbob")
		CAMERA_ANIMATION.speed_scale = speed / base_speed
	else:
		CAMERA_ANIMATION.play("RESET")
		
func handle_telekinesis():
	if holding_object:
		held_object.global_position = held_object.global_position.lerp($Head/TelekinesisPoint.global_position, 0.3)
	
	if Input.is_action_just_pressed(TELEK):
		if !holding_object:
			var object = $Head/Camera/Ray.get_collider()
			if object != null:
				holding_object = true
				held_object = object
				held_object.gravity_scale = 0.0
		else:
			held_object.apply_central_impulse($Head/Camera/Ray.global_position.direction_to(held_object.global_position) * Vector3(object_hurl_force, object_hurl_force, object_hurl_force))
			holding_object = false
			held_object.gravity_scale = 1.0

"""func ray_check(camera):
	var space_state = get_world_3d().direct_space_state
	var screencenter = get_viewport().get_visible_rect().get_center()
	var from = camera.project_ray_origin(screencenter)
	var to = from + camera.project_ray_normal(screencenter) * 1024
	var query = PhysicsRayQueryParameters3D.create(from, to)
	print(str(space_state.intersect_ray(query)))
	return space_state.intersect_ray(query)"""

func _on_walljump_timer_timeout():
	walljump_ready = true
	$walljump_timer.stop()


func _on_aircontrol_timer_timeout():
	$aircontrol_timer.stop()


func _on_coyote_timer_timeout():
	$coyote_timer.stop()


func _on_dash_timer_timeout():
	dash_spent = false
	$dash_timer.stop()
	pass # Replace with function body.
