extends CharacterBody3D

# --- Exported Properties ---
@export var swim_speed: float = 8.0
@export var boost_multiplier: float = 2.5
@export var mouse_sensitivity: float = 0.002
@export var max_oxygen: float = 100.0
@export var oxygen_drain: float = 0.8
@export var max_health: float = 100.0
@export var max_hunger: float = 100.0
@export var hunger_drain: float = 0.3
@export var water_ceiling_y: float = 60.0

# --- Signals ---
signal block_break_requested(hit_position: Vector3, block_normal: Vector3)
signal block_place_requested(hit_position: Vector3, block_normal: Vector3)
signal hotbar_scroll(direction: int)
signal echolocation_triggered(position: Vector3, radius: float)
signal stats_changed(oxygen: float, health: float, hunger: float)
signal player_died()

# --- Internal State ---
var oxygen: float
var health: float
var hunger: float
var is_boosting: bool = false
var is_echolocating: bool = false

var _yaw: float = 0.0
var _pitch: float = 0.0
var _time: float = 0.0
var _is_dead: bool = false

@onready var _camera_pivot: Node3D = $CameraPivot
@onready var _spring_arm: SpringArm3D = $CameraPivot/SpringArm
@onready var _camera: Camera3D = $CameraPivot/SpringArm/Camera
@onready var _dolphin_body: Node3D = $DolphinBody
@onready var _echo_pulse: Node3D = $EcholocationPulse


func _ready() -> void:
	oxygen = max_oxygen
	health = max_health
	hunger = max_hunger
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	_setup_input_actions()
	_emit_stats()


func _setup_input_actions() -> void:
	var actions: Dictionary = {
		"move_forward": KEY_W,
		"move_backward": KEY_S,
		"strafe_left": KEY_A,
		"strafe_right": KEY_D,
		"ascend": KEY_SPACE,
		"descend": KEY_SHIFT,
		"boost": KEY_CTRL,
		"echolocate": KEY_E,
		"toggle_inventory": KEY_TAB,
	}
	for action_name: String in actions:
		if not InputMap.has_action(action_name):
			InputMap.add_action(action_name)
			var ev := InputEventKey.new()
			ev.keycode = actions[action_name]
			InputMap.action_add_event(action_name, ev)

	var mouse_actions: Dictionary = {
		"break_block": MOUSE_BUTTON_LEFT,
		"place_block": MOUSE_BUTTON_RIGHT,
	}
	for action_name: String in mouse_actions:
		if not InputMap.has_action(action_name):
			InputMap.add_action(action_name)
			var ev := InputEventMouseButton.new()
			ev.button_index = mouse_actions[action_name]
			InputMap.action_add_event(action_name, ev)

func _input(event: InputEvent) -> void:
	if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
		var motion := event as InputEventMouseMotion
		_yaw -= motion.relative.x * mouse_sensitivity
		_pitch -= motion.relative.y * mouse_sensitivity
		_pitch = clamp(_pitch, -PI * 0.45, PI * 0.45)
		rotation.y = _yaw
		_camera_pivot.rotation.x = _pitch

	if event is InputEventMouseButton:
		var mb := event as InputEventMouseButton
		if mb.pressed:
			if mb.button_index == MOUSE_BUTTON_WHEEL_UP:
				hotbar_scroll.emit(1)
			elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN:
				hotbar_scroll.emit(-1)

	if event.is_action_pressed("break_block"):
		_do_raycast_break()
	if event.is_action_pressed("place_block"):
		_do_raycast_place()
	if event.is_action_pressed("echolocate"):
		_trigger_echolocation()


func _physics_process(delta: float) -> void:
	if _is_dead:
		return

	_time += delta
	_update_stats(delta)
	_update_movement(delta)
	_animate_body()

	if position.y > water_ceiling_y:
		position.y = water_ceiling_y
		velocity.y = min(velocity.y, 0.0)


func _update_movement(delta: float) -> void:
	is_boosting = Input.is_action_pressed("boost")
	var speed: float = swim_speed * (boost_multiplier if is_boosting else 1.0)

	var forward: Vector3 = -global_transform.basis.z
	var right: Vector3 = global_transform.basis.x

	# Recompute forward/right using camera pitch for intuitive swim direction
	var cam_basis: Basis = _camera_pivot.global_transform.basis
	var swim_forward: Vector3 = -cam_basis.z
	var swim_right: Vector3 = cam_basis.x

	var input_dir: Vector3 = Vector3.ZERO
	if Input.is_action_pressed("move_forward"):
		input_dir += swim_forward
	if Input.is_action_pressed("move_backward"):
		input_dir -= swim_forward
	if Input.is_action_pressed("strafe_right"):
		input_dir += swim_right
	if Input.is_action_pressed("strafe_left"):
		input_dir -= swim_right
	if Input.is_action_pressed("ascend"):
		input_dir += Vector3.UP
	if Input.is_action_pressed("descend"):
		input_dir += Vector3.DOWN

	if input_dir.length_squared() > 0.0:
		input_dir = input_dir.normalized()

	var target_velocity: Vector3 = input_dir * speed
	velocity = velocity.lerp(target_velocity, 4.0 * delta)

	move_and_slide()


func _update_stats(delta: float) -> void:
	var changed: bool = false

	# Hunger drain
	hunger = max(0.0, hunger - hunger_drain * delta)
	changed = true

	# Oxygen
	if position.y < 55.0:
		oxygen = max(0.0, oxygen - oxygen_drain * delta)
	else:
		oxygen = min(max_oxygen, oxygen + 20.0 * delta)

	# Health drain from oxygen/hunger
	if oxygen <= 0.0:
		health = max(0.0, health - 5.0 * delta)
		changed = true
	if hunger <= 0.0:
		health = max(0.0, health - 1.0 * delta)
		changed = true

	if changed:
		_emit_stats()

	if health <= 0.0 and not _is_dead:
		_die()


func _emit_stats() -> void:
	stats_changed.emit(oxygen, health, hunger)


func _die() -> void:
	_is_dead = true
	player_died.emit()
	# Respawn after short delay
	await get_tree().create_timer(2.0).timeout
	_respawn()


func _respawn() -> void:
	position = Vector3(0.0, 50.0, 0.0)
	velocity = Vector3.ZERO
	oxygen = max_oxygen
	health = max_health
	hunger = max_hunger
	_is_dead = false
	_emit_stats()


func _animate_body() -> void:
	if not is_instance_valid(_dolphin_body):
		return
	# Body bob
	_dolphin_body.position.y = sin(_time * 2.5) * 0.05
	# Tail wag via last child if present
	var child_count: int = _dolphin_body.get_child_count()
	if child_count > 1:
		var tail_node: Node3D = _dolphin_body.get_child(child_count - 1) as Node3D
		if tail_node != null:
			tail_node.rotation.y = sin(_time * 3.0) * 0.3


func _do_raycast_break() -> void:
	var result: Dictionary = _raycast(5.0)
	if not result.is_empty():
		block_break_requested.emit(result["position"], result["normal"])


func _do_raycast_place() -> void:
	var result: Dictionary = _raycast(5.0)
	if not result.is_empty():
		block_place_requested.emit(result["position"], result["normal"])


func _raycast(reach: float) -> Dictionary:
	if not is_instance_valid(_camera):
		return {}
	var space_state: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
	var cam_pos: Vector3 = _camera.global_position
	var cam_fwd: Vector3 = -_camera.global_transform.basis.z
	var query := PhysicsRayQueryParameters3D.create(cam_pos, cam_pos + cam_fwd * reach)
	query.exclude = [self]
	var result: Dictionary = space_state.intersect_ray(query)
	return result


func take_damage(amount: float) -> void:
	health = max(0.0, health - amount)
	stats_changed.emit(oxygen, health, hunger)
	if health <= 0.0:
		player_died.emit()


func _trigger_echolocation() -> void:
	if is_echolocating:
		return
	is_echolocating = true
	echolocation_triggered.emit(global_position, 20.0)
	if is_instance_valid(_echo_pulse):
		_echo_pulse.trigger()
	await get_tree().create_timer(1.2).timeout
	is_echolocating = false
