class_name Registry
extends RefCounted

# Reads any value of the game through a single address. Writing is deliberately
# narrower than reading: only stored cells accept a write, and computed values
# have a resolver instead of a setter.
#
# Phase 1 wires reading and the legacy translation. Live counters have no storage
# yet, so they answer with their default until phase 2 gives them a store.

const LIVE_DEFAULT := 0

static func read(path: String, fallback: Variant = null) -> Variant:
	var error := AddressCatalog.path_error(path)
	if not error.is_empty():
		push_warning("Registry: adresse illisible '%s' — %s" % [path, error])
		return fallback
	var parts := AddressCatalog.split_path(path)
	if AddressCatalog.SCALAR_ROOTS.has(parts[0]):
		return _read_scalar(parts[0], parts[1], fallback)
	return _read_entity(parts[0], parts[1], parts[2], fallback)

static func _read_scalar(root: String, field: String, fallback: Variant) -> Variant:
	match root:
		"fact":
			return GameState.get_fact_value(field)
		"stat":
			return GameState.resolve_game_variable(field)
		"player":
			if field == "coins":
				return GameState.coins
		"run", "meta":
			return GameState.get_scoped_value(root, field, fallback)
	return fallback

static func _read_entity(collection: String, id: String, field: String, fallback: Variant) -> Variant:
	match AddressCatalog.field_nature(collection, field):
		AddressCatalog.Nature.COMPUTED:
			return _read_computed(collection, id, field, fallback)
		AddressCatalog.Nature.LIVE:
			return GameState.get_entity_counter(collection, id, field, LIVE_DEFAULT)
		AddressCatalog.Nature.AUTHORED:
			var definition: Dictionary = AddressCatalog.collection_data(collection).get(id, {})
			return definition.get(field, fallback)
	return fallback

static func _read_computed(collection: String, id: String, field: String, fallback: Variant) -> Variant:
	match "%s.%s" % [collection, field]:
		"ingredients.owned":
			return GameState.get_ingredient_amount(id)
		"recipes.can_cook":
			return GameState.can_cook(id)
		"upgrade_nodes.purchased":
			return GameState.has_upgrade(id)
		"upgrade_nodes.state":
			return GameState.get_upgrade_state(id)
	return fallback

static func write(path: String, value: Variant) -> bool:
	if not AddressCatalog.is_writable(path):
		push_warning("Registry: adresse non inscriptible '%s'" % path)
		return false
	var parts := AddressCatalog.split_path(path)
	if AddressCatalog.SCALAR_ROOTS.has(parts[0]):
		return _write_scalar(parts[0], parts[1], value)
	return GameState.set_entity_counter(parts[0], parts[1], parts[2], value)

static func _write_scalar(root: String, field: String, value: Variant) -> bool:
	match root:
		"fact":
			GameState.set_fact_value(field, value)
			return true
		"player":
			if field == "coins":
				GameState.add_coins(int(value) - GameState.coins)
				return true
		"run", "meta":
			GameState.set_scoped_value(root, field, value)
			return true
	return false

# --- Legacy compatibility -------------------------------------------------
#
# Conditions authored before the address space used {source, id}. They are
# translated on the fly so no data file has to change during this phase.

static func legacy_path(source: String, id: String) -> String:
	match source:
		"fact": return "fact.%s" % id
		"variable": return "stat.%s" % id
		"upgrade": return "upgrade_nodes.%s.purchased" % id
	return ""

# Accepts both the legacy shape and the new {path} shape.
static func condition_path(condition: Dictionary) -> String:
	var path := str(condition.get("path", ""))
	if not path.is_empty():
		return path
	return legacy_path(str(condition.get("source", "")), str(condition.get("id", "")))
