class_name GrunkBeast extends CharacterBody3D
## Grunk beast controller

#region Constants
const STALKING_SOUND_LIMIT := 25.0
#endregion

#region Exported Properties
@export var base_speed := 60.0
@export var pursuit_speed := 180.0

@export var debug_destroy: bool:
	set(value):
		queue_free()
#endregion

#region Member Variables
var gravity: Vector3 = (
	ProjectSettings.get_setting("physics/3d/default_gravity")
	* ProjectSettings.get_setting("physics/3d/default_gravity_vector")
)

var pathfinding := true

var traversing_link := false

@onready var model: BeastModel = %SlutchBeast

@onready var nav_agent: NavigationAgent3D = %NavAgent
@onready var nav_probe: NavigationAgent3D = %NavProbe
@onready var stalking_timer: Timer = %StalkingTimer

@onready var blackboard: Blackboard = %Blackboard

#endregion

#region Character Controller


func is_pursuing() -> bool:
	return blackboard.has_value("pursuit_target")


func is_stalking() -> bool:
	return false  # TODO


func get_speed() -> float:
	if is_pursuing():
		return pursuit_speed
	return base_speed


func path_shorter_than(target: Vector3, limit: float) -> bool:
	var length := 0.0
	var last_pos := global_position

	nav_probe.target_position = target

	# Fail early if the target is unreachable.
	# NOTE: this call also forces a navigation path refresh! Do not remove!
	if not nav_probe.is_target_reachable():
		return false

	var path := nav_probe.get_current_navigation_path().slice(
		nav_probe.get_current_navigation_path_index()
	)
	if not path:
		# Shouldn't be possible, but if it is it would cause problems if we didn't fail here
		print_debug("Target is reachable but has no path (tell the developer!)")
		return false

	# Integrate along path
	for waypoint: Vector3 in path:
		length += last_pos.distance_to(waypoint)
		if length > limit:
			return false
		last_pos = waypoint

	return true


func _physics_process(delta: float) -> void:
	var motion := Vector3.ZERO

	if pathfinding and not nav_agent.is_navigation_finished():
		var path_pos := nav_agent.get_next_path_position()
		var relative_pos := path_pos - global_position
		motion = relative_pos.normalized() * get_speed() * delta

	velocity.x = motion.x
	velocity.z = motion.z

	if not is_on_floor():
		velocity += gravity * delta

	if motion:
		model.set_target_rotation(atan2(motion.x, motion.z))

	move_and_slide()


func on_sound_detected(source: Vector3) -> void:
	# Check that the source isn't too far away, e.g. a sound from another room
	if path_shorter_than(source, STALKING_SOUND_LIMIT):
		blackboard.set_value("stalking_target", source)
		stalking_timer.start()


#endregion


func _on_link_reached(_details: Dictionary) -> void:
	traversing_link = true


func _on_waypoint_reached(_details: Dictionary) -> void:
	traversing_link = false
