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

# --- Declarative effects --------------------------------------------------
#
# An effect is a flat {op, path, value} record. There are no expressions and no
# nested conditions on purpose: the moment those appear this becomes a scripting
# language, and a poor one. Anything that does not fit stays in GDScript.

const EFFECT_OPS := ["set", "add", "multiply", "min", "max", "toggle"]

static func apply_effect(effect: Dictionary) -> bool:
	var path := str(effect.get("path", ""))
	var op := str(effect.get("op", "add"))
	if not EFFECT_OPS.has(op):
		push_warning("Registry: opération d'effet inconnue '%s'" % op)
		return false
	if not AddressCatalog.is_writable(path):
		push_warning("Registry: effet vers une adresse non inscriptible '%s'" % path)
		return false
	var current: Variant = read(path)
	var value: Variant = effect.get("value", 0)
	var result: Variant = current
	match op:
		"set": result = value
		"toggle": result = not bool(current)
		"add": result = _as_number(current) + _as_number(value)
		"multiply": result = _as_number(current) * _as_number(value)
		"min": result = minf(_as_number(current), _as_number(value))
		"max": result = maxf(_as_number(current), _as_number(value))
	# Integer cells stay integers so counters never drift into floats.
	if current is int and result is float:
		result = int(result)
	return write(path, result)

static func apply_effects(effects: Variant) -> int:
	if not effects is Array:
		return 0
	var applied := 0
	for effect: Variant in effects:
		if effect is Dictionary and apply_effect(effect):
			applied += 1
	return applied

# Runs the hook declared on an entity, if any. This is the single entry point
# the gameplay uses, so a collection without hooks costs one dictionary lookup.
static func run_hook(collection: String, id: String, hook: String) -> int:
	var definition: Dictionary = AddressCatalog.collection_data(collection).get(id, {})
	return apply_effects(definition.get(hook, []))

static func _as_number(value: Variant) -> float:
	if value is bool:
		return 1.0 if value else 0.0
	if value is int or value is float:
		return float(value)
	return 0.0

# --- 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", "")))
