class_name VisualCatalog
extends RefCounted

# Répond à une seule question: « quelle ressource pour cette entité dans cet
# état ». Rien d'autre — ni chargement, ni affichage, ni scène. Le porteur de
# visuel viendra plus tard et s'appuiera là-dessus.
#
# La règle est celle du projet, appliquée aux images: **zéro ou vide veut dire
# demande au-dessus**. Un état sans visuel emprunte celui de son parent, et le
# dernier repli est la forme procédurale — donc le jeu reste jouable sans le
# moindre fichier d'art.

const MAX_FALLBACK_DEPTH := 8

static func states_for(collection: String) -> Array:
	return DataCatalog.VISUAL_STATES.get(collection, [])

static func has_state(collection: String, state: String) -> bool:
	return states_for(collection).has(state)

# Chaîne complète d'un état, du plus précis au plus général. Rendue telle quelle
# pour être vérifiable: c'est la règle de repli qu'on veut pouvoir lire.
static func fallback_chain(collection: String, state: String) -> Array[String]:
	var chain: Array[String] = []
	var current := state
	while not current.is_empty() and chain.size() < MAX_FALLBACK_DEPTH:
		if not has_state(collection, current) or chain.has(current):
			break
		chain.append(current)
		current = str(DataCatalog.VISUAL_FALLBACKS.get(current, ""))
	return chain

static func definition(visual_id: String) -> Dictionary:
	return DataCatalog.VISUALS.get(visual_id, {})

# Un visuel déclaré dont le fichier manque n'est pas utilisable. Il ne bloque
# pas pour autant: la chaîne continue, et à la fin il reste la forme
# procédurale. Une référence morte dégrade, elle ne casse jamais.
static func is_available(visual_id: String) -> bool:
	var entry := definition(visual_id)
	if entry.is_empty():
		return false
	var path := str(entry.get("path", ""))
	return not path.is_empty() and ResourceLoader.exists(path)

# Carte `état → identifiant` déclarée par une entité.
static func entity_visuals(collection: String, entity_id: String) -> Dictionary:
	var data: Dictionary = AddressCatalog.collection_data(collection)
	var entity: Dictionary = data.get(entity_id, {})
	return entity.get("visuals", {})

# Identifiant du visuel à employer, en suivant la chaîne. Rend "" quand rien
# n'est utilisable: l'appelant dessine alors sa forme, comme aujourd'hui.
static func resolve(collection: String, entity_id: String, state: String, override: Dictionary = {}) -> String:
	var declared: Dictionary = override.get("visuals", {}) if not override.is_empty() else entity_visuals(collection, entity_id)
	for candidate: String in fallback_chain(collection, state):
		var visual_id := str(declared.get(candidate, ""))
		if not visual_id.is_empty() and is_available(visual_id):
			return visual_id
	return ""

static func resolve_definition(collection: String, entity_id: String, state: String) -> Dictionary:
	return definition(resolve(collection, entity_id, state))

# --- Contrôles d'écriture ------------------------------------------------------

# États déclarés par une entité qui ne mènent à rien d'utilisable. C'est ce que
# l'outil affichera pour signaler une référence morte, sans rien empêcher.
static func broken_states(collection: String, entity_id: String) -> Array[String]:
	var result: Array[String] = []
	var declared := entity_visuals(collection, entity_id)
	for state: String in declared:
		if not is_available(str(declared[state])):
			result.append(state)
	return result

static func missing_files() -> Array[String]:
	var result: Array[String] = []
	for visual_id: String in DataCatalog.VISUALS:
		if not is_available(visual_id):
			result.append(visual_id)
	return result
