class_name Player
extends CharacterBody2D

# Player States
enum State {
	IDLE,
	RUN,
	JUMP,
	ATTACK_1,
	ATTACK_2,
	ATTACK_3,
	HEAVY_ATTACK,
	DASH,
	DASH_ATTACK,
	DEFEND,
	HURT,
	DEATH,
}

# Player Nodes Variables
@onready var _sprite: AnimatedSprite2D = $AnimatedSprite2D
@onready var _hitbox: HitBox = $HitBox
@onready var _hitbox_collision: CollisionShape2D = $HitBox/CollisionShape2D
@onready var _health: HealthComponent = $HealthComponent
@onready var _hurtbox: HurtBox = $HurtBox

# Collision masks (World = 1, EnemyBody = 8)
const _DEFAULT_COLLISION_MASK: int = 1 | 8
const _DASH_COLLISION_MASK: int = 1

const _DEATH_SCREEN_PATH: String = "res://scenes/components/death_screen.tscn"

# Damage dealt per attack state
const _ATTACK_DAMAGE: Dictionary = {
	State.ATTACK_1: 3,
	State.ATTACK_2: 3,
	State.ATTACK_3: 5,
	State.HEAVY_ATTACK: 8,
	State.DASH_ATTACK: 2,
}

# Hitbox-active frame range per attack state
const _ATTACK_HIT_FRAMES: Dictionary = {
	State.ATTACK_1: Vector2i(2, 4),
	State.ATTACK_2: Vector2i(2, 4),
	State.ATTACK_3: Vector2i(2, 4),
	State.DASH_ATTACK: Vector2i(4, 6),
	State.HEAVY_ATTACK: Vector2i(6, 8),
}

# Player Runtime Variables
var direction: float = 0.0
var facing_direction: float = 1.0
var current_state: State = State.IDLE
var is_attacking: bool = false
var combo_step: int = 0
var combo_timer: float = 0.0
var is_dashing: bool = false
var dash_timer: float = 0.0
var dash_cooldown_timer: float = 0.0
var is_hurt: bool = false
var is_dead: bool = false

# Player Exported Variables
@export_group("Movement")
@export var move_speed: float = 200.0

@export_group("Jump")
@export var jump_velocity: float = -350.0

@export_group("Combat")
@export var attack_cooldown: float = 0.4
@export var heavy_attack_cooldown: float = 0.6
@export var combo_window: float = 0.6
@export var attack_move_speed: float = 0.0

@export_group("Dash")
@export var dash_speed: float = 500.0
@export var dash_duration: float = 0.2
@export var dash_cooldown: float = 0.8

func _ready() -> void:
	add_to_group("player")
	_sprite.animation_finished.connect(_on_animation_finished)
	_health.health_changed.connect(_on_health_changed)
	_health.died.connect(_on_died)
	_hurtbox.hit_received.connect(_on_hurtbox_hit_received)
	if EventBus.has_checkpoint:
		global_position = EventBus.last_checkpoint_position
	_on_health_changed.call_deferred(_health.current_health, _health.max_health)

func _physics_process(delta: float) -> void:
	_apply_gravity(delta)
	_process_dash_input()
	_apply_movement(delta)
	_process_attack_input()
	_update_combo_timer(delta)
	_update_dash_timers(delta)
	_update_state()
	_update_animation()
	_update_hitbox()
	move_and_slide()

func _apply_gravity(delta: float) -> void:
	if is_dashing:
		return
	if not is_on_floor():
		velocity.y += ProjectSettings.get_setting("physics/2d/default_gravity") * delta

func _apply_movement(delta: float) -> void:
	if is_dead or is_hurt:
		velocity.x = 0.0
		return

	if is_dashing:
		velocity.x = facing_direction * dash_speed
		return

	if is_attacking:
		velocity.x = facing_direction * attack_move_speed
		return

	direction = Input.get_axis("move_left", "move_right")
	velocity.x = direction * move_speed
	if direction != 0.0:
		facing_direction = direction

	if is_on_floor() and Input.is_action_just_pressed("jump"):
		velocity.y = jump_velocity

func _process_dash_input() -> void:
	if is_dashing or is_attacking or is_hurt or is_dead or dash_cooldown_timer > 0.0:
		return
	if not Input.is_action_just_pressed("dash"):
		return
	
	is_dashing = true
	dash_timer = dash_duration
	dash_cooldown_timer = dash_cooldown
	velocity.y = 0.0
	current_state = State.DASH
	collision_mask = _DASH_COLLISION_MASK

func _process_attack_input() -> void:
	if is_attacking or is_dashing or is_hurt or is_dead or not Input.is_action_just_pressed("light_attack"):
		return

	if combo_step > 0 and combo_timer > 0.0:
		combo_step += 1
	else:
		combo_step = 1

	is_attacking = true
	combo_timer = 0.0
	current_state = State.values()[State.ATTACK_1 + combo_step - 1]

func _update_combo_timer(delta: float) -> void:
	if combo_timer > 0.0:
		combo_timer -= delta
		if combo_timer <= 0.0:
			combo_step = 0

func _update_dash_timers(delta: float) -> void:
	if is_dashing:
		dash_timer -= delta
		if dash_timer <= 0.0:
			is_dashing = false
			collision_mask = _DEFAULT_COLLISION_MASK

	if dash_cooldown_timer > 0.0:
		dash_cooldown_timer -= delta

func _on_animation_finished() -> void:
	if current_state == State.HURT:
		is_hurt = false
		return

	if not is_attacking:
		return

	is_attacking = false
	if current_state == State.ATTACK_3:
		combo_step = 0
	else:
		combo_timer = combo_window

func _update_state() -> void:
	if is_dead:
		return
	if is_hurt:
		current_state = State.HURT
		return
	if is_attacking:
		return
	if is_dashing:
		current_state = State.DASH
		return

	if not is_on_floor():
		current_state = State.JUMP
	elif direction != 0.0:
		current_state = State.RUN
	else:
		current_state = State.IDLE

func _update_animation() -> void:
	var animation_name: String = State.keys()[current_state].to_lower()
	if _sprite.animation != animation_name:
		_sprite.play(animation_name)
	_sprite.flip_h = facing_direction < 0.0

func _update_hitbox() -> void:
	_hitbox.scale.x = facing_direction
	_hitbox.damage = _ATTACK_DAMAGE.get(current_state, 0)

	var hit_frames: Vector2i = _ATTACK_HIT_FRAMES.get(current_state, Vector2i(-1, -1))
	var is_active_hit_frame: bool = _sprite.frame >= hit_frames.x and _sprite.frame <= hit_frames.y
	_hitbox_collision.disabled = not (is_attacking and is_active_hit_frame)

func _on_hurtbox_hit_received(hitbox: HitBox) -> void:
	_health.take_damage(hitbox.damage)

	is_attacking = false
	if is_dashing:
		is_dashing = false
		collision_mask = _DEFAULT_COLLISION_MASK
	is_hurt = true
	current_state = State.HURT

func _on_health_changed(current_health: int, max_health: int) -> void:
	EventBus.player_health_changed.emit(current_health, max_health)

func _on_died() -> void:
	EventBus.player_died.emit()
	is_dead = true
	_hurtbox.set_deferred("monitoring", false)
	_hitbox_collision.set_deferred("disabled", true)
	_switch_to_death_screen()

func _switch_to_death_screen() -> void:
	EventBus.last_level_path = get_tree().current_scene.scene_file_path
	EventBus.last_death_facing_direction = facing_direction
	SceneManager.call_deferred("change_scene", _DEATH_SCREEN_PATH)
