class_name Walker
extends CharacterBody3D


signal focus_required(me: Node3D)
signal got_in(vehicle: SeatedVehicle)
signal got_out
signal can_get_in(possible: bool)
signal chocolate_collected

## How fast the player moves in meters per second.
@export var speed = 14
## Convert an axis from [-1, -1] to rad
@export var turn_to_rad: float = 0.020
## The downward acceleration when in the air, in meters per second squared.
@export var fall_acceleration = 75
## Vertical impulse applied to the character upon jumping in meters per second.
@export var jump_impulse = 20

@export_group("Boarding", "board_")
@export var board_door_access_duration: float = 1.0
@export var board_seat_access_duration: float = 0.5

var target_velocity := Vector3.ZERO
var target_character_direction := Vector3.ZERO # From command

var _vehicle: SeatedVehicle = null
var _can_get_in: bool = false
var _in_transition: bool = false

@onready var _vehicle_range: RayCast3D = $VehicleRange


func _ready() -> void:
	focus_required.emit(self)


func trigger_jump() -> void:
	if is_on_floor():
		target_velocity.y = jump_impulse


func trigger_direction(dir: Vector2) -> void:
	target_character_direction = Vector3(dir.x, 0.0, -dir.y)


## Return true if inside a vehicle
func is_onboard() -> bool:
	return _vehicle != null


## Return true if the player can get in a vehicle
func can_get_in_vehicle() -> bool:
	return !is_onboard() and _get_closest_vehicle() != null


## Give this walker a chocolate bar
func give_chocolate():
	chocolate_collected.emit()


## Return local pocket position where the chocolate is collected
func get_pocket_position() -> Vector3:
	return $ChocolatePocket.get_position()


func _physics_process(delta: float) -> void:
	if is_onboard():
		pass # Nothing
	else:
		_move_with_feet(delta)
		_signal_when_can_get_in()


func _move_with_feet(delta: float):
	# Walk according to the camera angle
	var camera_basis: Basis = get_viewport().get_camera_3d().get_camera_transform().basis
	var target_world_direction: Vector3 = camera_basis * target_character_direction
	target_world_direction.y = 0.0
	var target_walk_velocity = target_world_direction.normalized() * speed * target_character_direction.length()

	target_velocity.x = target_walk_velocity.x
	target_velocity.z = target_walk_velocity.z

	# Face forward
	_look_forward(target_velocity)

	# Gravity
	if not is_on_floor():
		target_velocity.y = target_velocity.y - (fall_acceleration * delta)

	# Moving the Character
	velocity = target_velocity
	move_and_slide()


## Trigger only signal when ability is changed
func _signal_when_can_get_in() -> void:
	var can_get_in_now : bool = can_get_in_vehicle()
	if _can_get_in != can_get_in_now:
		can_get_in.emit(can_get_in_now)
		_can_get_in = can_get_in_now


func _on_dir_changed(dir: Vector2) -> void:
	if is_onboard():
		return

	trigger_direction(dir)


func _on_main_action(pressed: bool) -> void:
	if is_onboard():
		return

	if pressed:
		trigger_jump()


func _on_get_in_action(commander: LocalInput) -> void:
	if is_onboard():
		_get_out_vehicle()
	else:
		_get_in_vehicle(commander)


## Seek the first vehicle in front of the player
## and get in to it
## and take the wheel !
func _get_in_vehicle(commander: LocalInput) -> void:
	if _in_transition:
		return

	var closest_vehicle: SeatedVehicle = _get_closest_vehicle()

	if closest_vehicle == null:
		return # No vehicle to get inside

	_vehicle = closest_vehicle
	reparent(_vehicle)
	add_collision_exception_with(_vehicle)
	_vehicle.become_driven_by(self)
	_vehicle.drive_with(commander)
	_move_to_seat(_vehicle)
	got_in.emit(_vehicle)


## Get out of the current vehicle
func _get_out_vehicle() -> void:
	if _in_transition:
		return
	_init_transition()

	var tween = create_tween()
	tween.tween_property(self, "transform", _vehicle.get_door().get_transform(), board_seat_access_duration)
	tween.tween_callback(_finish_getting_out)


func _finish_getting_out() -> void:
	_end_transition()

	_vehicle.get_out()
	remove_collision_exception_with(_vehicle)
	reparent(_vehicle.get_parent())
	_vehicle = null
	_get_head_up()
	got_out.emit()


## Make the player stand up
func _get_head_up():
	set_rotation(Vector3(0.0, rotation.y, 0.0))


## Make the player look forward
func _look_forward(forward: Vector3) -> void:
	var horizontal_forward: Vector3 = forward.slide(Vector3.UP)
	if horizontal_forward.is_zero_approx():
		return
	else:
		look_at(position + horizontal_forward)


## Return closest vehicle within reach
## or null if there are none
func _get_closest_vehicle() -> SeatedVehicle:
	_vehicle_range.force_raycast_update()

	var object_within_range: Object = _vehicle_range.get_collider()

	if object_within_range is SeatedVehicle:
		return object_within_range as SeatedVehicle

	return null


## Move gently to a free seat from the nearest door
func _move_to_seat(vehicule: SeatedVehicle) -> void:
	_init_transition()

	var tween: Tween = create_tween()

	var door: Node3D = vehicule.get_closest_door(get_position())
	tween.tween_property(self, "position", door.get_position(), board_door_access_duration)
	tween.parallel().tween_property(self, "quaternion", door.get_quaternion(), board_door_access_duration)

	var seat: Node3D = vehicule.get_free_seat()
	tween.tween_property(self, "position", seat.get_position(), board_seat_access_duration)
	tween.parallel().tween_property(self, "quaternion", seat.get_quaternion(), board_seat_access_duration)

	tween.tween_callback(_end_transition)


## Lock getting in / out while still doing it
func _init_transition() -> void:
	_in_transition = true


## Allow walker to get in / out again
func _end_transition() -> void:
	_in_transition = false