class_name CharacterAnimationPresenter
extends Node

## Selects and plays the current sandbox idle or moving animation.
## Animation import, movement simulation and model composition remain external.

const MOVING_ANIMATION_CANDIDATES: Array[String] = ["Run", "Walk"]
const STATIONARY_ANIMATION_CANDIDATES: Array[String] = ["Stand", "Idle"]

## Cross-fade time retained from the current sandbox controller.
@export var animation_blend_time_seconds: float = 0.15

var active_animation_name: String:
	get:
		return _active_animation_name

var has_animation_player: bool:
	get:
		return _animation_player != null and is_instance_valid(_animation_player)

var _animation_player: AnimationPlayer
var _active_animation_name := ""


## Finds the first AnimationPlayer below a newly composed character root.
## Binding null or a root without animations clears the current presentation.
func bind_character_root(character_root: Node) -> bool:
	_animation_player = (
		_find_animation_player(character_root)
		if character_root != null and is_instance_valid(character_root)
		else null
	)
	_active_animation_name = ""
	if _animation_player == null:
		return false
	_prepare_animation_player(_animation_player)
	present_locomotion(false)
	return true


## Presents the current moving/stationary locomotion state.
## Returns true only when a different animation starts playing.
func present_locomotion(is_moving: bool) -> bool:
	if _animation_player == null or not is_instance_valid(_animation_player):
		_animation_player = null
		_active_animation_name = ""
		return false
	var candidates := MOVING_ANIMATION_CANDIDATES if is_moving else STATIONARY_ANIMATION_CANDIDATES
	var selected_animation := _choose_animation(candidates)
	if selected_animation.is_empty() or selected_animation == _active_animation_name:
		return false
	_active_animation_name = selected_animation
	_animation_player.play(selected_animation, animation_blend_time_seconds)
	return true


func _prepare_animation_player(animation_player: AnimationPlayer) -> void:
	animation_player.playback_default_blend_time = animation_blend_time_seconds
	for animation_name in animation_player.get_animation_list():
		var lowercase_name := String(animation_name).to_lower()
		if (
			lowercase_name == "stand"
			or lowercase_name == "idle"
			or lowercase_name == "run"
			or lowercase_name == "walk"
			or lowercase_name.contains("stand")
			or lowercase_name.contains("run")
			or lowercase_name.contains("walk")
		):
			var animation := animation_player.get_animation(animation_name)
			if animation != null:
				animation.loop_mode = Animation.LOOP_LINEAR


func _choose_animation(candidates: Array[String]) -> String:
	for candidate in candidates:
		if _animation_player.has_animation(candidate):
			return candidate
	var animation_names := _animation_player.get_animation_list()
	for candidate in candidates:
		var lowercase_candidate := candidate.to_lower()
		for animation_name in animation_names:
			if String(animation_name).to_lower().contains(lowercase_candidate):
				return String(animation_name)
	return ""


func _find_animation_player(root: Node) -> AnimationPlayer:
	if root is AnimationPlayer:
		return root
	for child in root.get_children():
		var animation_player := _find_animation_player(child)
		if animation_player != null:
			return animation_player
	return null
