class_name Player extends CharacterBody3D

const JUMP_VELOCITY: float = 4.5
const DECAY: float = 8.0

# Stores the x/y direction of the camera
var _look := Vector2.ZERO
# Stores the direction the player moves when attacking
var _attack_direction: Vector3 = Vector3.ZERO

@export var mouse_sensitivity: float = 0.0075
@export var camera_max_pitch: float = 25
@export var camera_min_pitch: float = -75
@export var animation_decay: float = 25.0
@export var attack_move_speed: float = 3.0
@export_category("RPG Stats")
@export var stats: CharacterStats

@onready var horizontal_pivot: Node3D = $HorizontalPivot
@onready var vertical_pivot: Node3D = $HorizontalPivot/VerticalPivot
@onready var rig_pivot: Node3D = $RigPivot
@onready var rig: Node3D = $RigPivot/Rig
@onready var attack_cast: RayCast3D = %AttackCast
@onready var health_component: HealthComponent = $HealthComponent
@onready var collision_shape: CollisionShape3D = $CollisionShape
@onready var area_attack: ShapeCast3D = $RigPivot/AreaAttack
@onready var ui: Control = $UserInterface

func _ready() -> void:
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
	health_component.update_max_health(stats.get_max_hp())
	stats.level_up_notification.connect(
		func(): health_component.update_max_health(stats.get_max_hp())
	)
	stats.update_stats.connect(ui.update_stats_display)
	ui.update_stats_display()

func _physics_process(delta: float) -> void:
	frame_camera_rotation()
	var direction := get_movement_direction()
	rig.update_animation_tree(direction)
	handle_idle_physics_frame(delta, direction)
	handle_slashing_physics_frame(delta)
	handle_overhead_physics_frame()

	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta

	# Handle jump.
	if Input.is_action_just_pressed("move_jump") and is_on_floor():
		velocity.y = JUMP_VELOCITY

	move_and_slide()

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("ui_cancel"):
		Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
	
	if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
		if event is InputEventMouseMotion:
			_look = -event.relative * mouse_sensitivity
	if rig.is_idle():
		if event.is_action_pressed("click"):
			slash_attack()
		if event.is_action_pressed("right_click"):
			heavy_attack()

	if event.is_action_pressed("debug_gain_xp"):
		stats.xp += 10_000

func frame_camera_rotation() -> void:
	horizontal_pivot.rotate_y(_look.x)
	vertical_pivot.rotate_x(_look.y)
	vertical_pivot.rotation.x = clampf(vertical_pivot.rotation.x, deg_to_rad(camera_min_pitch), deg_to_rad(camera_max_pitch))
	_look = Vector2.ZERO

func get_movement_direction() -> Vector3:
	var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
	var input_vector := Vector3(input_dir.x, 0, input_dir.y).normalized()
	var direction := horizontal_pivot.global_transform.basis * input_vector
	return direction

func look_toward_direction(direction: Vector3, delta: float) -> void:
	var target_transform := rig_pivot.global_transform.looking_at(
		rig_pivot.global_position + direction, Vector3.UP, true
	)
	rig_pivot.global_transform = rig_pivot.global_transform.interpolate_with(target_transform, 1.0 - exp(-animation_decay * delta))

func slash_attack() -> void:
	rig.playback.travel("Slash")
	var movement_direction = get_movement_direction()
	if movement_direction.is_zero_approx():
		_attack_direction = rig.global_basis * Vector3(0, 0, 1)
	else:
		_attack_direction = movement_direction
	attack_cast.clear_exceptions()

func heavy_attack() -> void:
	rig.playback.travel("Overhead")
	var movement_direction = get_movement_direction()
	if movement_direction.is_zero_approx():
		_attack_direction = rig.global_basis * Vector3(0, 0, 1)
	else:
		_attack_direction = movement_direction
	attack_cast.clear_exceptions()

func handle_idle_physics_frame(delta: float, direction: Vector3) -> void:
	if !rig.is_idle() and !rig.is_dashing():
		return

	velocity.x = exponential_decay(velocity.x, direction.x * stats.speed.get_modifier(), DECAY, delta)
	velocity.z = exponential_decay(velocity.z, direction.z * stats.speed.get_modifier(), DECAY, delta)

	if direction:
		look_toward_direction(direction, delta)

func exponential_decay(a: float, b: float, decay: float, delta: float) -> float:
	return b + (a - b) * exp(-decay * delta)

func handle_slashing_physics_frame(delta: float) -> void:
	if !rig.is_slashing():
		return

	velocity.x = _attack_direction.x * attack_move_speed
	velocity.z = _attack_direction.z * attack_move_speed
	look_toward_direction(_attack_direction, delta)
	var damage = 10.0 + stats.strength.get_modifier()
	attack_cast.deal_damage(damage, stats.agility.get_modifier())

func handle_overhead_physics_frame() -> void:
	if !rig.is_overhead():
		return
	velocity.x = 0.0
	velocity.z = 0.0

func _on_health_component_defeat() -> void:
	rig.playback.travel("Defeat")
	collision_shape.disabled = true
	set_physics_process(false)

func _on_rig_heavy_attack() -> void:
	var damage = 10.0 + stats.strength.get_modifier()
	area_attack.deal_damage(damage, stats.agility.get_modifier())
