extends Node
# This script is added as an Autoload named 'GameState'

# ==========================================
# SIGNALS
# ==========================================
signal currency_changed(currency_id: StringName, new_amount: BigNumber)
signal generator_state_changed(generator_id: StringName, state: Dictionary)
signal generator_buff_level_changed(generator_id: StringName, buff_id: StringName, new_level: int)
signal generator_buff_unlocked_changed(generator_id: StringName, buff_id: StringName, unlocked: bool)
signal buff_level_changed(buff_id: StringName, new_level: int)
signal buff_unlocked_changed(buff_id: StringName, unlocked: bool)
signal goal_completed(goal_id: StringName)
signal goal_progress_changed(goal_id: StringName, progress: float)

# ==========================================
# STATE VARIABLES
# ==========================================
var _current_currency: Dictionary = {}
var _total_currency_acquired: Dictionary = {}
var _all_time_currency_acquired: Dictionary = {}

var generator_states: Dictionary = {}
var generator_buff_levels: Dictionary = {}
var generator_buff_unlocked: Dictionary = {}
var _external_save_sections: Dictionary = {}

var _buff_definitions: Dictionary = {}
var _buff_levels: Dictionary = {}
var _buff_unlocked: Dictionary = {}
var _buff_active: Dictionary = {}

var _goal_definitions: Dictionary = {}
var _goal_completed: Dictionary = {}

var last_save_time: int = 0 # Unix timestamp

# ==========================================
# Save / Load
# ==========================================
static var file_name: String = "user://idle_save.json"
const SAVE_FORMAT_VERSION_KEY: String = "save_format_version"
const CURRENT_SAVE_FORMAT_VERSION: int = 3
const GENERATOR_OWNED_KEY: String = "owned"
const GENERATOR_PURCHASED_COUNT_KEY: String = "purchased_count"
const GENERATOR_UNLOCKED_KEY: String = "unlocked"
const GENERATOR_AVAILABLE_KEY: String = "available"
const GENERATOR_STATES_KEY: String = "generator_states"
const GENERATOR_BUFF_LEVELS_KEY: String = "generator_buff_levels"
const GENERATOR_BUFF_UNLOCKED_KEY: String = "generator_buff_unlocked"
const BUFF_DEFINITIONS_KEY: String = "buff_definitions"
const BUFF_LEVELS_KEY: String = "buff_levels"
const BUFF_UNLOCKED_KEY: String = "buff_unlocked"
const BUFF_ACTIVE_KEY: String = "buff_active"
const GOALS_KEY: String = "goals"
const GOAL_COMPLETED_KEY: String = "completed"
const CURRENCIES_KEY: String = "currencies"
const CURRENT_KEY: String = "current"
const TOTAL_KEY: String = "total"
const ALL_TIME_KEY: String = "all_time"
const LAST_SAVE_TIME_KEY: String = "last_save_time"

func _ready() -> void:
	_initialize_currency_maps()
	_initialize_goals()
	load_game()

# ==========================================
# STATE MODIFIERS
# ==========================================
#func add_gold(amount: BigNumber) -> void:
	#add_currency_by_id(GOLD_CURRENCY_ID, amount)
#
#func spend_gold(cost: BigNumber) -> bool:
	#return spend_currency_by_id(GOLD_CURRENCY_ID, cost)
#
#func add_gems(amount: BigNumber) -> void:
	#add_currency_by_id(GEMS_CURRENCY_ID, amount)
#
#func spend_gems(cost: BigNumber) -> bool:
	#return spend_currency_by_id(GEMS_CURRENCY_ID, cost)

func get_known_currencies() -> Array[Currency]:
	return CurrencyDatabase.get_known_currencies()

func get_currency_id(currency: Resource) -> StringName:
	return CurrencyDatabase.get_currency_id(currency)

func parse_currency_id(raw_currency: Variant) -> StringName:
	return CurrencyDatabase.parse_currency_id(raw_currency)

func is_known_currency_id(currency_id: StringName) -> bool:
	return CurrencyDatabase.is_known_currency_id(currency_id)

func get_currency_resource(currency_id: StringName) -> Resource:
	return CurrencyDatabase.get_currency_resource(currency_id)

func get_currency_name(currency_id: StringName) -> String:
	return CurrencyDatabase.get_currency_name(currency_id)

func get_currency_icon(currency_id: StringName) -> Texture2D:
	return CurrencyDatabase.get_currency_icon(currency_id)

func add_currency(currency: Resource, amount: BigNumber) -> void:
	var currency_id: StringName = get_currency_id(currency)
	if currency_id == &"":
		push_warning("Attempted to add currency with an invalid Currency resource.")
		return

	add_currency_by_id(currency_id, amount)

func spend_currency(currency: Resource, cost: BigNumber) -> bool:
	var currency_id: StringName = get_currency_id(currency)
	if currency_id == &"":
		push_warning("Attempted to spend currency with an invalid Currency resource.")
		return false

	return spend_currency_by_id(currency_id, cost)

func add_currency_by_id(currency_id: StringName, amount: BigNumber) -> void:
	var normalized_currency_id: StringName = _normalize_currency_id(currency_id)
	if normalized_currency_id == &"":
		return

	if amount.mantissa > 0.0:
		_get_total_currency_ref(normalized_currency_id).add_in_place(amount)
		_get_all_time_currency_ref(normalized_currency_id).add_in_place(amount)

	var current: BigNumber = _get_current_currency_ref(normalized_currency_id)
	current.add_in_place(amount)
	currency_changed.emit(normalized_currency_id, current)

func spend_currency_by_id(currency_id: StringName, cost: BigNumber) -> bool:
	var normalized_currency_id: StringName = _normalize_currency_id(currency_id)
	if normalized_currency_id == &"":
		return false

	var current: BigNumber = _get_current_currency_ref(normalized_currency_id)
	if current.is_greater_than(cost) or current.is_equal_to(cost):
		var negative_cost: BigNumber = BigNumber.new(-cost.mantissa, cost.exponent)
		current.add_in_place(negative_cost)
		currency_changed.emit(normalized_currency_id, current)
		return true
	return false

func get_currency_amount(currency: Resource) -> BigNumber:
	return get_currency_amount_by_id(get_currency_id(currency))

func get_currency_amount_by_id(currency_id: StringName) -> BigNumber:
	return _get_current_currency_ref(currency_id)

func get_total_currency_acquired(currency: Resource) -> BigNumber:
	return get_total_currency_acquired_by_id(get_currency_id(currency))

func get_total_currency_acquired_by_id(currency_id: StringName) -> BigNumber:
	return _get_total_currency_ref(currency_id)

func get_all_time_currency_acquired(currency: Resource) -> BigNumber:
	return get_all_time_currency_acquired_by_id(get_currency_id(currency))

func get_all_time_currency_acquired_by_id(currency_id: StringName) -> BigNumber:
	return _get_all_time_currency_ref(currency_id)

func register_generator(generator_id: StringName, unlocked: bool = false, available: bool = false, owned: int = 0) -> void:
	var key: String = String(generator_id)
	if key.is_empty():
		return
	var default_owned: int = _to_non_negative_int(owned, 0)

	if generator_states.has(key):
		var existing_raw: Variant = generator_states.get(key)
		if existing_raw is Dictionary:
			generator_states[key] = _sanitize_generator_state(existing_raw, unlocked, available, default_owned)
			return

	var new_state: Dictionary = _default_generator_state(unlocked, available, default_owned)
	generator_states[key] = new_state
	generator_state_changed.emit(StringName(key), new_state.duplicate(true))

func get_generator_state(generator_id: StringName) -> Dictionary:
	return _get_generator_state(generator_id).duplicate(true)

func get_generator_owned(generator_id: StringName) -> int:
	var state: Dictionary = _get_generator_state(generator_id)
	return int(state.get(GENERATOR_OWNED_KEY, 0))

func set_generator_owned(generator_id: StringName, value: int) -> void:
	var state: Dictionary = _get_generator_state(generator_id)
	var next_owned: int = maxi(value, 0)
	if int(state.get(GENERATOR_OWNED_KEY, 0)) == next_owned:
		return

	state[GENERATOR_OWNED_KEY] = next_owned
	state[GENERATOR_PURCHASED_COUNT_KEY] = maxi(int(state.get(GENERATOR_PURCHASED_COUNT_KEY, 0)), next_owned)
	_set_generator_state(generator_id, state)

func get_generator_purchased_count(generator_id: StringName) -> int:
	var state: Dictionary = _get_generator_state(generator_id)
	return int(state.get(GENERATOR_PURCHASED_COUNT_KEY, 0))

func set_generator_purchased_count(generator_id: StringName, value: int) -> void:
	var state: Dictionary = _get_generator_state(generator_id)
	var min_purchased: int = int(state.get(GENERATOR_OWNED_KEY, 0))
	var next_purchased: int = maxi(value, min_purchased)
	if int(state.get(GENERATOR_PURCHASED_COUNT_KEY, 0)) == next_purchased:
		return

	state[GENERATOR_PURCHASED_COUNT_KEY] = next_purchased
	_set_generator_state(generator_id, state)

func is_generator_unlocked(generator_id: StringName) -> bool:
	var state: Dictionary = _get_generator_state(generator_id)
	return bool(state.get(GENERATOR_UNLOCKED_KEY, false))

func set_generator_unlocked(generator_id: StringName, value: bool) -> void:
	var state: Dictionary = _get_generator_state(generator_id)
	if bool(state.get(GENERATOR_UNLOCKED_KEY, false)) == value:
		return

	state[GENERATOR_UNLOCKED_KEY] = value
	_set_generator_state(generator_id, state)

func is_generator_available(generator_id: StringName) -> bool:
	var state: Dictionary = _get_generator_state(generator_id)
	return bool(state.get(GENERATOR_AVAILABLE_KEY, false))

func set_generator_available(generator_id: StringName, value: bool) -> void:
	var state: Dictionary = _get_generator_state(generator_id)
	if bool(state.get(GENERATOR_AVAILABLE_KEY, false)) == value:
		return

	state[GENERATOR_AVAILABLE_KEY] = value
	_set_generator_state(generator_id, state)

func register_generator_buff(generator_id: StringName, buff_id: StringName, initial_level: int = 0) -> void:
	var generator_key: String = String(generator_id)
	var buff_key: String = String(buff_id)
	if generator_key.is_empty() or buff_key.is_empty():
		return

	var levels: Dictionary = _get_generator_buff_levels_ref(generator_id)
	if levels.has(buff_key):
		var existing_level: int = _to_non_negative_int(levels.get(buff_key, initial_level), initial_level)
		if int(levels.get(buff_key, initial_level)) != existing_level:
			levels[buff_key] = existing_level
			generator_buff_levels[generator_key] = levels
			generator_buff_level_changed.emit(StringName(generator_key), StringName(buff_key), existing_level)
		return

	var sanitized_level: int = _to_non_negative_int(initial_level, 0)
	levels[buff_key] = sanitized_level
	generator_buff_levels[generator_key] = levels
	generator_buff_level_changed.emit(StringName(generator_key), StringName(buff_key), sanitized_level)

func register_generator_buff_unlocked(generator_id: StringName, buff_id: StringName, initially_unlocked: bool = false) -> void:
	var generator_key: String = String(generator_id)
	var buff_key: String = String(buff_id)
	if generator_key.is_empty() or buff_key.is_empty():
		return

	var unlocked_map: Dictionary = _get_generator_buff_unlocked_ref(generator_id)
	if unlocked_map.has(buff_key):
		var existing_unlocked: bool = _to_bool(unlocked_map.get(buff_key, initially_unlocked), initially_unlocked)
		if bool(unlocked_map.get(buff_key, initially_unlocked)) != existing_unlocked:
			unlocked_map[buff_key] = existing_unlocked
			generator_buff_unlocked[generator_key] = unlocked_map
			generator_buff_unlocked_changed.emit(StringName(generator_key), StringName(buff_key), existing_unlocked)
		return

	var sanitized_unlocked: bool = _to_bool(initially_unlocked, false)
	unlocked_map[buff_key] = sanitized_unlocked
	generator_buff_unlocked[generator_key] = unlocked_map
	generator_buff_unlocked_changed.emit(StringName(generator_key), StringName(buff_key), sanitized_unlocked)

func get_generator_buff_level(generator_id: StringName, buff_id: StringName) -> int:
	var buff_key: String = String(buff_id)
	if buff_key.is_empty():
		return 0

	var levels: Dictionary = _get_generator_buff_levels_ref(generator_id)
	return _to_non_negative_int(levels.get(buff_key, 0), 0)

func set_generator_buff_level(generator_id: StringName, buff_id: StringName, value: int) -> void:
	var generator_key: String = String(generator_id)
	var buff_key: String = String(buff_id)
	if generator_key.is_empty() or buff_key.is_empty():
		return

	var levels: Dictionary = _get_generator_buff_levels_ref(generator_id)
	var next_level: int = _to_non_negative_int(value, 0)
	if int(levels.get(buff_key, -1)) == next_level:
		return

	levels[buff_key] = next_level
	generator_buff_levels[generator_key] = levels
	generator_buff_level_changed.emit(StringName(generator_key), StringName(buff_key), next_level)

func is_generator_buff_unlocked(generator_id: StringName, buff_id: StringName) -> bool:
	var buff_key: String = String(buff_id)
	if buff_key.is_empty():
		return false

	var unlocked_map: Dictionary = _get_generator_buff_unlocked_ref(generator_id)
	return _to_bool(unlocked_map.get(buff_key, false), false)

func set_generator_buff_unlocked(generator_id: StringName, buff_id: StringName, value: bool) -> void:
	var generator_key: String = String(generator_id)
	var buff_key: String = String(buff_id)
	if generator_key.is_empty() or buff_key.is_empty():
		return

	var unlocked_map: Dictionary = _get_generator_buff_unlocked_ref(generator_id)
	var next_unlocked: bool = _to_bool(value, false)
	if _to_bool(unlocked_map.get(buff_key, false), false) == next_unlocked:
		return

	unlocked_map[buff_key] = next_unlocked
	generator_buff_unlocked[generator_key] = unlocked_map
	generator_buff_unlocked_changed.emit(StringName(generator_key), StringName(buff_key), next_unlocked)

func get_generator_buff_unlocked(generator_id: StringName) -> Dictionary:
	return _get_generator_buff_unlocked_ref(generator_id).duplicate(true)

func get_generator_buff_levels(generator_id: StringName) -> Dictionary:
	return _get_generator_buff_levels_ref(generator_id).duplicate(true)

func register_buff(buff: GeneratorBuffData) -> void:
	if buff == null:
		return
	var buff_id: StringName = buff.id
	if buff_id == &"":
		push_warning("Attempted to register buff with invalid id")
		return
	
	_buff_definitions[buff_id] = buff
	if not _buff_levels.has(buff_id):
		_buff_levels[buff_id] = 0
	if not _buff_unlocked.has(buff_id):
		_buff_unlocked[buff_id] = false
	if not _buff_active.has(buff_id):
		_buff_active[buff_id] = false

func get_buff(buff_id: StringName) -> GeneratorBuffData:
	return _buff_definitions.get(buff_id, null)

func get_all_buffs() -> Array[GeneratorBuffData]:
	var result: Array[GeneratorBuffData] = []
	for buff in _buff_definitions.values():
		if buff is GeneratorBuffData:
			result.append(buff)
	return result

func get_buff_level(buff_id: StringName) -> int:
	return _buff_levels.get(buff_id, 0)

func set_buff_level(buff_id: StringName, level: int) -> void:
	if level < 0:
		level = 0
	
	var current_level: int = _buff_levels.get(buff_id, 0)
	if current_level == level:
		return
	
	_buff_levels[buff_id] = level
	buff_level_changed.emit(buff_id, level)

func is_buff_unlocked(buff_id: StringName) -> bool:
	return _buff_unlocked.get(buff_id, false)

func set_buff_unlocked(buff_id: StringName, unlocked: bool) -> void:
	if bool(_buff_unlocked.get(buff_id, false)) == unlocked:
		return
	
	_buff_unlocked[buff_id] = unlocked
	buff_unlocked_changed.emit(buff_id, unlocked)
	
	if unlocked and not _buff_active.get(buff_id, false):
		_buff_active[buff_id] = true

func is_buff_active(buff_id: StringName) -> bool:
	return _buff_active.get(buff_id, false)

func get_generators_targeted_by(buff_id: StringName) -> Array[StringName]:
	var buff: GeneratorBuffData = get_buff(buff_id)
	if buff == null:
		return []
	return buff.get_target_generators()

func get_buffs_for_generator(generator_id: StringName) -> Array[GeneratorBuffData]:
	var result: Array[GeneratorBuffData] = []
	for buff in _buff_definitions.values():
		if buff is GeneratorBuffData and buff.targets_generator(generator_id):
			result.append(buff)
	return result

func get_effective_multiplier(generator_id: StringName, kind: int) -> float:
	var multiplier: float = 1.0
	for buff in get_buffs_for_generator(generator_id):
		if buff == null:
			continue
		if not is_buff_active(buff.id):
			continue
		if buff.kind != kind:
			continue
		
		var level: int = get_buff_level(buff.id)
		if level <= 0:
			continue
		
		var buff_multiplier: float = buff.get_effect_multiplier(level)
		multiplier *= buff_multiplier
	
	return multiplier

func _try_unlock_all_buffs_from_goals() -> void:
	for buff in get_all_buffs():
		if buff == null:
			continue
		_try_unlock_buff_from_goal(buff)

func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void:
	if buff == null:
		return
	if not buff.has_unlock_goal():
		return
	if is_buff_unlocked(buff.id):
		return
	var goal_id: StringName = buff.unlock_goal.id
	if not goal_id.is_empty() and is_goal_completed(goal_id):
		set_buff_unlocked(buff.id, true)

func set_external_save_data(section_key: StringName, payload: Dictionary) -> void:
	var key: String = String(section_key).strip_edges()
	if key.is_empty():
		return

	_external_save_sections[key] = payload.duplicate(true)

func get_external_save_data(section_key: StringName) -> Dictionary:
	var key: String = String(section_key).strip_edges()
	if key.is_empty():
		return {}

	var raw_payload: Variant = _external_save_sections.get(key, {})
	if raw_payload is Dictionary:
		return raw_payload.duplicate(true)

	return {}

func reset_for_prestige(reset_total_currency: bool = false, emit_currency_signals: bool = true) -> void:
	var currency_ids: Array[StringName] = _collect_currency_ids_for_save()
	for currency_id in currency_ids:
		_set_current_currency(currency_id, BigNumber.from_float(0.0))
		if reset_total_currency:
			_set_total_currency(currency_id, BigNumber.from_float(0.0))

	generator_states.clear()
	generator_buff_levels.clear()
	generator_buff_unlocked.clear()
	_goal_completed.clear()
	for goal_id in _goal_definitions.keys():
		_goal_completed[goal_id] = false

	if not emit_currency_signals:
		return

	for currency_id in currency_ids:
		currency_changed.emit(currency_id, _get_current_currency_ref(currency_id))

func emit_currency_changed_for_all() -> void:
	for currency_id in _collect_currency_ids_for_save():
		currency_changed.emit(currency_id, _get_current_currency_ref(currency_id))

# ==========================================
# PERSISTENCE (Save/Load)
# ==========================================
func save_game() -> void:
	var save_data = {
		SAVE_FORMAT_VERSION_KEY: CURRENT_SAVE_FORMAT_VERSION,
		CURRENCIES_KEY: _serialize_currency_balances(),
		GENERATOR_STATES_KEY: _serialize_generator_states(),
		BUFF_LEVELS_KEY: _serialize_buff_levels(),
		BUFF_UNLOCKED_KEY: _serialize_buff_unlocked(),
		BUFF_ACTIVE_KEY: _serialize_buff_active(),
		GOALS_KEY: _serialize_goals(),
		LAST_SAVE_TIME_KEY: Time.get_unix_time_from_system()
	}

	for section_key in _external_save_sections.keys():
		var key: String = String(section_key)
		if key.is_empty():
			continue

		var payload: Variant = _external_save_sections.get(section_key)
		if not (payload is Dictionary):
			continue

		save_data[key] = payload

	var file: FileAccess = FileAccess.open(file_name, FileAccess.WRITE)
	if file:
		file.store_string(JSON.stringify(save_data))

func load_game() -> void:
	if not FileAccess.file_exists(file_name):
		return

	var file: FileAccess = FileAccess.open(file_name, FileAccess.READ)
	if file:
		var parsed: Variant = JSON.parse_string(file.get_as_text())
		if parsed is Dictionary:
			var parsed_data: Dictionary = parsed
			
			var version: int = int(parsed_data.get(SAVE_FORMAT_VERSION_KEY, 1))
			if version < 2:
				push_error("Save file version %d too old. Please start fresh." % version)
				return
			
			if version < 3:
				_migrate_goals_from_version_2(parsed_data)
			
			_external_save_sections.clear()
			if parsed_data.has(CURRENCIES_KEY):
				_load_currency_balances(parsed_data.get(CURRENCIES_KEY, {}))

			generator_states = _deserialize_generator_states(parsed_data.get(GENERATOR_STATES_KEY, {}))
			_deserialize_buff_states(parsed_data)
			_deserialize_goals(parsed_data.get(GOALS_KEY, {}))
			last_save_time = parsed_data.get(LAST_SAVE_TIME_KEY, Time.get_unix_time_from_system())
			for save_key_variant in parsed_data.keys():
				var save_key: String = String(save_key_variant)
				if save_key.is_empty():
					continue
				if save_key in [SAVE_FORMAT_VERSION_KEY, CURRENCIES_KEY, GENERATOR_STATES_KEY, BUFF_LEVELS_KEY, BUFF_UNLOCKED_KEY, BUFF_ACTIVE_KEY, GOALS_KEY, LAST_SAVE_TIME_KEY]:
					continue

				var section_payload: Variant = parsed_data.get(save_key_variant)
				if section_payload is Dictionary:
					_external_save_sections[save_key] = section_payload.duplicate(true)

func _load_currency_balances(raw_currencies: Variant) -> void:
	if not (raw_currencies is Dictionary):
		return

	var currencies: Dictionary = raw_currencies
	for raw_currency_id in currencies.keys():
		var currency_id: StringName = _normalize_currency_id(StringName(String(raw_currency_id)))
		if currency_id == &"":
			continue

		var raw_entry: Variant = currencies.get(raw_currency_id)
		if not (raw_entry is Dictionary):
			continue

		var entry: Dictionary = raw_entry
		var current_amount: BigNumber = BigNumber.deserialize(entry.get(CURRENT_KEY, {}))
		var total_amount: BigNumber = _deserialize_total_currency(entry.get(TOTAL_KEY, null), current_amount)
		var all_time_amount: BigNumber = _deserialize_all_time_currency(entry.get(ALL_TIME_KEY, null), total_amount)
		_set_current_currency(currency_id, current_amount)
		_set_total_currency(currency_id, total_amount)
		_set_all_time_currency(currency_id, all_time_amount)

func _deserialize_total_currency(raw_total: Variant, current_amount: BigNumber) -> BigNumber:
	var minimum_total: BigNumber = _copy_big_number(current_amount)
	if minimum_total.mantissa < 0.0:
		minimum_total = BigNumber.from_float(0.0)

	if not (raw_total is Dictionary):
		return minimum_total

	var parsed_total: BigNumber = BigNumber.deserialize(raw_total)
	if parsed_total.mantissa < 0.0:
		return minimum_total
	if parsed_total.is_less_than(minimum_total):
		return minimum_total
	return parsed_total

func _deserialize_all_time_currency(raw_all_time: Variant, total_amount: BigNumber) -> BigNumber:
	var minimum_all_time: BigNumber = _copy_big_number(total_amount)
	if minimum_all_time.mantissa < 0.0:
		minimum_all_time = BigNumber.from_float(0.0)

	if not (raw_all_time is Dictionary):
		return minimum_all_time

	var parsed_all_time: BigNumber = BigNumber.deserialize(raw_all_time)
	if parsed_all_time.mantissa < 0.0:
		return minimum_all_time
	if parsed_all_time.is_less_than(minimum_all_time):
		return minimum_all_time
	return parsed_all_time

func _copy_big_number(value: BigNumber) -> BigNumber:
	return BigNumber.new(value.mantissa, value.exponent)

func _serialize_currency_balances() -> Dictionary:
	var result: Dictionary = {}

	for currency_id in _collect_currency_ids_for_save():
		var key: String = String(currency_id)
		if key.is_empty():
			continue

		result[key] = {
			CURRENT_KEY: _get_current_currency_ref(currency_id).serialize(),
			TOTAL_KEY: _get_total_currency_ref(currency_id).serialize(),
			ALL_TIME_KEY: _get_all_time_currency_ref(currency_id).serialize(),
		}

	return result

func _collect_currency_ids_for_save() -> Array[StringName]:
	var ids: Array[StringName] = []

	for raw_id in _current_currency.keys():
		var normalized_currency_id: StringName = _normalize_currency_id(StringName(String(raw_id)))
		if normalized_currency_id == &"" or ids.has(normalized_currency_id):
			continue
		ids.append(normalized_currency_id)

	for raw_id in _all_time_currency_acquired.keys():
		var normalized_currency_id: StringName = _normalize_currency_id(StringName(String(raw_id)))
		if normalized_currency_id == &"" or ids.has(normalized_currency_id):
			continue
		ids.append(normalized_currency_id)

	for known_currency_id in CurrencyDatabase.get_known_currency_ids():
		var normalized_currency_id: StringName = _normalize_currency_id(known_currency_id)
		if normalized_currency_id == &"" or ids.has(normalized_currency_id):
			continue
		ids.append(normalized_currency_id)

	return ids

func _initialize_currency_maps() -> void:
	for currency_id in CurrencyDatabase.get_known_currency_ids():
		if currency_id == &"":
			continue
		_set_current_currency(currency_id, BigNumber.from_float(0.0))
		_set_total_currency(currency_id, BigNumber.from_float(0.0))
		_set_all_time_currency(currency_id, BigNumber.from_float(0.0))

func _normalize_currency_id(currency_id: StringName) -> StringName:
	var normalized: String = String(currency_id).to_lower().strip_edges()
	if normalized.is_empty():
		return &""
	return StringName(normalized)

func _get_current_currency_ref(currency_id: StringName) -> BigNumber:
	var normalized_currency_id: StringName = _normalize_currency_id(currency_id)
	if normalized_currency_id == &"":
		return BigNumber.from_float(0.0)

	var value: Variant = _current_currency.get(normalized_currency_id)
	if value is BigNumber:
		return value

	var fallback: BigNumber = BigNumber.from_float(0.0)
	_current_currency[normalized_currency_id] = fallback
	return fallback

func _get_total_currency_ref(currency_id: StringName) -> BigNumber:
	var normalized_currency_id: StringName = _normalize_currency_id(currency_id)
	if normalized_currency_id == &"":
		return BigNumber.from_float(0.0)

	var value: Variant = _total_currency_acquired.get(normalized_currency_id)
	if value is BigNumber:
		return value

	var fallback: BigNumber = BigNumber.from_float(0.0)
	_total_currency_acquired[normalized_currency_id] = fallback
	return fallback

func _get_all_time_currency_ref(currency_id: StringName) -> BigNumber:
	var normalized_currency_id: StringName = _normalize_currency_id(currency_id)
	if normalized_currency_id == &"":
		return BigNumber.from_float(0.0)

	var value: Variant = _all_time_currency_acquired.get(normalized_currency_id)
	if value is BigNumber:
		return value

	var fallback: BigNumber = BigNumber.from_float(0.0)
	_all_time_currency_acquired[normalized_currency_id] = fallback
	return fallback

func _set_current_currency(currency_id: StringName, value: BigNumber) -> void:
	var normalized_currency_id: StringName = _normalize_currency_id(currency_id)
	if normalized_currency_id == &"":
		return
	_current_currency[normalized_currency_id] = _copy_big_number(value)

func _set_total_currency(currency_id: StringName, value: BigNumber) -> void:
	var normalized_currency_id: StringName = _normalize_currency_id(currency_id)
	if normalized_currency_id == &"":
		return
	_total_currency_acquired[normalized_currency_id] = _copy_big_number(value)

func _set_all_time_currency(currency_id: StringName, value: BigNumber) -> void:
	var normalized_currency_id: StringName = _normalize_currency_id(currency_id)
	if normalized_currency_id == &"":
		return
	_all_time_currency_acquired[normalized_currency_id] = _copy_big_number(value)

func _default_generator_state(unlocked: bool, available: bool, owned: int = 0) -> Dictionary:
	var owned_value: int = _to_non_negative_int(owned, 0)
	return {
		GENERATOR_OWNED_KEY: owned_value,
		GENERATOR_PURCHASED_COUNT_KEY: owned_value,
		GENERATOR_UNLOCKED_KEY: unlocked,
		GENERATOR_AVAILABLE_KEY: available,
	}

func _get_generator_state(generator_id: StringName) -> Dictionary:
	var key: String = String(generator_id)
	if key.is_empty():
		return _default_generator_state(false, false, 0)

	if not generator_states.has(key):
		register_generator(generator_id)

	var existing_raw: Variant = generator_states.get(key, _default_generator_state(false, false, 0))
	if existing_raw is Dictionary:
		var state: Dictionary = existing_raw
		var current_unlocked: bool = state.get(GENERATOR_UNLOCKED_KEY, false)
		var current_available: bool = state.get(GENERATOR_AVAILABLE_KEY, false)
		var current_owned: int = state.get(GENERATOR_OWNED_KEY, 0)
		var sanitized: Dictionary = _sanitize_generator_state(state, current_unlocked, current_available, current_owned)
		generator_states[key] = sanitized
		return sanitized

	var fallback: Dictionary = _default_generator_state(false, false, 0)
	generator_states[key] = fallback
	return fallback

func _set_generator_state(generator_id: StringName, state: Dictionary) -> void:
	var key: String = String(generator_id)
	if key.is_empty():
		return

	var current_unlocked: bool = state.get(GENERATOR_UNLOCKED_KEY, false)
	var current_available: bool = state.get(GENERATOR_AVAILABLE_KEY, false)
	var current_owned: int = state.get(GENERATOR_OWNED_KEY, 0)
	var sanitized: Dictionary = _sanitize_generator_state(state, current_unlocked, current_available, current_owned)
	generator_states[key] = sanitized
	generator_state_changed.emit(StringName(key), sanitized.duplicate(true))

func _get_generator_buff_levels_ref(generator_id: StringName) -> Dictionary:
	var key: String = String(generator_id)
	if key.is_empty():
		return {}

	var existing_raw: Variant = generator_buff_levels.get(key, {})
	if existing_raw is Dictionary:
		var sanitized: Dictionary = _sanitize_generator_buff_levels(existing_raw)
		generator_buff_levels[key] = sanitized
		return sanitized

	var fallback: Dictionary = {}
	generator_buff_levels[key] = fallback
	return fallback

func _get_generator_buff_unlocked_ref(generator_id: StringName) -> Dictionary:
	var key: String = String(generator_id)
	if key.is_empty():
		return {}

	var existing_raw: Variant = generator_buff_unlocked.get(key, {})
	if existing_raw is Dictionary:
		var sanitized: Dictionary = _sanitize_generator_buff_unlocked(existing_raw)
		generator_buff_unlocked[key] = sanitized
		return sanitized

	var fallback: Dictionary = {}
	generator_buff_unlocked[key] = fallback
	return fallback

func _sanitize_generator_buff_levels(raw_levels: Dictionary) -> Dictionary:
	var sanitized_levels: Dictionary = {}
	for buff_key_variant in raw_levels.keys():
		var buff_key: String = String(buff_key_variant)
		if buff_key.is_empty():
			continue

		sanitized_levels[buff_key] = _to_non_negative_int(raw_levels.get(buff_key_variant, 0), 0)

	return sanitized_levels

func _sanitize_generator_buff_unlocked(raw_unlocked: Dictionary) -> Dictionary:
	var sanitized_unlocked: Dictionary = {}
	for buff_key_variant in raw_unlocked.keys():
		var buff_key: String = String(buff_key_variant)
		if buff_key.is_empty():
			continue

		# Save format only stores true values, but keep parser tolerant.
		var is_unlocked: bool = _to_bool(raw_unlocked.get(buff_key_variant, false), false)
		if not is_unlocked:
			continue

		sanitized_unlocked[buff_key] = true

	return sanitized_unlocked

func _serialize_generator_states() -> Dictionary:
	var result: Dictionary = {}
	for key_variant in generator_states.keys():
		var key: String = String(key_variant)
		if key.is_empty():
			continue

		var value: Variant = generator_states.get(key_variant)
		if value is Dictionary:
			result[key] = _serialize_generator_state_entry(value)
	return result

func _deserialize_generator_states(raw_value: Variant) -> Dictionary:
	var result: Dictionary = {}
	if not (raw_value is Dictionary):
		return result

	for key_variant in raw_value.keys():
		var key: String = String(key_variant)
		if key.is_empty():
			continue

		var entry: Variant = raw_value.get(key_variant)
		if entry is Dictionary:
			result[key] = _deserialize_generator_state_entry(entry)
	return result

func _serialize_generator_buff_levels() -> Dictionary:
	var result: Dictionary = {}
	for generator_key_variant in generator_buff_levels.keys():
		var generator_key: String = String(generator_key_variant)
		if generator_key.is_empty():
			continue

		var raw_levels: Variant = generator_buff_levels.get(generator_key_variant)
		if raw_levels is Dictionary:
			result[generator_key] = _sanitize_generator_buff_levels(raw_levels)

	return result

func _deserialize_generator_buff_levels(raw_value: Variant) -> Dictionary:
	var result: Dictionary = {}
	if not (raw_value is Dictionary):
		return result

	for generator_key_variant in raw_value.keys():
		var generator_key: String = String(generator_key_variant)
		if generator_key.is_empty():
			continue

		var raw_levels: Variant = raw_value.get(generator_key_variant)
		if raw_levels is Dictionary:
			result[generator_key] = _sanitize_generator_buff_levels(raw_levels)

	return result

func _serialize_generator_buff_unlocked() -> Dictionary:
	var result: Dictionary = {}
	for generator_key_variant in generator_buff_unlocked.keys():
		var generator_key: String = String(generator_key_variant)
		if generator_key.is_empty():
			continue

		var raw_unlocked: Variant = generator_buff_unlocked.get(generator_key_variant)
		if not (raw_unlocked is Dictionary):
			continue

		var sanitized_unlocked: Dictionary = _sanitize_generator_buff_unlocked(raw_unlocked)
		if sanitized_unlocked.is_empty():
			continue

		result[generator_key] = sanitized_unlocked

	return result

func _deserialize_generator_buff_unlocked(raw_value: Variant) -> Dictionary:
	var result: Dictionary = {}
	if not (raw_value is Dictionary):
		return result

	for generator_key_variant in raw_value.keys():
		var generator_key: String = String(generator_key_variant)
		if generator_key.is_empty():
			continue

		var raw_unlocked: Variant = raw_value.get(generator_key_variant)
		if not (raw_unlocked is Dictionary):
			continue

		var sanitized_unlocked: Dictionary = _sanitize_generator_buff_unlocked(raw_unlocked)
		if sanitized_unlocked.is_empty():
			continue

		result[generator_key] = sanitized_unlocked

	return result

func _serialize_generator_state_entry(raw_state: Dictionary) -> Dictionary:
	var owned_value: int = _to_non_negative_int(raw_state.get(GENERATOR_OWNED_KEY, 0), 0)
	var purchased_value: int = _to_non_negative_int(raw_state.get(GENERATOR_PURCHASED_COUNT_KEY, owned_value), owned_value)

	var serialized_state: Dictionary = {
		GENERATOR_OWNED_KEY: owned_value,
		GENERATOR_PURCHASED_COUNT_KEY: maxi(purchased_value, owned_value),
	}

	if raw_state.has(GENERATOR_UNLOCKED_KEY) and raw_state.get(GENERATOR_UNLOCKED_KEY) is bool:
		serialized_state[GENERATOR_UNLOCKED_KEY] = raw_state.get(GENERATOR_UNLOCKED_KEY)
	if raw_state.has(GENERATOR_AVAILABLE_KEY) and raw_state.get(GENERATOR_AVAILABLE_KEY) is bool:
		serialized_state[GENERATOR_AVAILABLE_KEY] = raw_state.get(GENERATOR_AVAILABLE_KEY)

	return serialized_state

func _deserialize_generator_state_entry(raw_state: Dictionary) -> Dictionary:
	var owned_value: int = _to_non_negative_int(raw_state.get(GENERATOR_OWNED_KEY, 0), 0)
	var purchased_value: int = _to_non_negative_int(raw_state.get(GENERATOR_PURCHASED_COUNT_KEY, owned_value), owned_value)

	var parsed_state: Dictionary = {
		GENERATOR_OWNED_KEY: owned_value,
		GENERATOR_PURCHASED_COUNT_KEY: maxi(purchased_value, owned_value),
	}

	if raw_state.has(GENERATOR_UNLOCKED_KEY) and raw_state.get(GENERATOR_UNLOCKED_KEY) is bool:
		parsed_state[GENERATOR_UNLOCKED_KEY] = raw_state.get(GENERATOR_UNLOCKED_KEY)
	if raw_state.has(GENERATOR_AVAILABLE_KEY) and raw_state.get(GENERATOR_AVAILABLE_KEY) is bool:
		parsed_state[GENERATOR_AVAILABLE_KEY] = raw_state.get(GENERATOR_AVAILABLE_KEY)

	return parsed_state

func _sanitize_generator_state(raw_state: Dictionary, default_unlocked: bool, default_available: bool, default_owned: int = 0) -> Dictionary:
	var normalized_default_owned: int = _to_non_negative_int(default_owned, 0)
	var owned_value: int = _to_non_negative_int(raw_state.get(GENERATOR_OWNED_KEY, normalized_default_owned), normalized_default_owned)
	var purchased_value: int = _to_non_negative_int(raw_state.get(GENERATOR_PURCHASED_COUNT_KEY, owned_value), owned_value)

	return {
		GENERATOR_OWNED_KEY: owned_value,
		GENERATOR_PURCHASED_COUNT_KEY: maxi(purchased_value, owned_value),
		GENERATOR_UNLOCKED_KEY: _to_bool(raw_state.get(GENERATOR_UNLOCKED_KEY, default_unlocked), default_unlocked),
		GENERATOR_AVAILABLE_KEY: _to_bool(raw_state.get(GENERATOR_AVAILABLE_KEY, default_available), default_available),
	}

func _to_non_negative_int(value: Variant, fallback: int = 0) -> int:
	if value is int:
		return maxi(value, 0)
	if value is float:
		return maxi(int(value), 0)
	return maxi(fallback, 0)

func _to_bool(value: Variant, fallback: bool) -> bool:
	if value is bool:
		return value
	return fallback

func _serialize_buff_levels() -> Dictionary:
	var result: Dictionary = {}
	for buff_id in _buff_levels.keys():
		result[buff_id] = _buff_levels[buff_id]
	return result

func _serialize_buff_unlocked() -> Dictionary:
	var result: Dictionary = {}
	for buff_id in _buff_unlocked.keys():
		if _buff_unlocked[buff_id]:
			result[buff_id] = true
	return result

func _serialize_buff_active() -> Dictionary:
	var result: Dictionary = {}
	for buff_id in _buff_active.keys():
		if _buff_active[buff_id]:
			result[buff_id] = true
	return result

func _deserialize_buff_states(parsed_data: Dictionary) -> void:
	_buff_levels.clear()
	_buff_unlocked.clear()
	_buff_active.clear()
	
	if parsed_data.has(BUFF_LEVELS_KEY):
		var raw_levels: Variant = parsed_data.get(BUFF_LEVELS_KEY)
		if raw_levels is Dictionary:
			for key_variant in raw_levels.keys():
				var key: String = String(key_variant)
				if not key.is_empty():
					var value: Variant = raw_levels.get(key_variant)
					if value is int:
						_buff_levels[key] = value

	if parsed_data.has(BUFF_UNLOCKED_KEY):
		var raw_unlocked: Variant = parsed_data.get(BUFF_UNLOCKED_KEY)
		if raw_unlocked is Dictionary:
			for key_variant in raw_unlocked.keys():
				var key: String = String(key_variant)
				if not key.is_empty():
					_buff_unlocked[key] = true

	if parsed_data.has(BUFF_ACTIVE_KEY):
		var raw_active: Variant = parsed_data.get(BUFF_ACTIVE_KEY)
		if raw_active is Dictionary:
			for key_variant in raw_active.keys():
				var key: String = String(key_variant)
				if not key.is_empty():
					_buff_active[key] = true

# ==========================================
# GOALS
# ==========================================
func _initialize_goals() -> void:
	_goal_definitions.clear()
	_goal_completed.clear()
	
	var goal_paths: PackedStringArray = _discover_goal_paths()
	for goal_path in goal_paths:
		var goal_resource: Resource = load(goal_path)
		if goal_resource == null:
			push_warning("Failed to load goal resource at '%s'." % goal_path)
			continue
		if not (goal_resource is GoalData):
			continue
		
		var goal: GoalData = goal_resource
		if not goal.has_id():
			push_warning("Skipping goal with missing id: %s" % goal_path)
			continue
		if _goal_definitions.has(goal.id):
			push_warning("Duplicate goal id '%s' found at %s. Keeping first occurrence." % [String(goal.id), goal_path])
			continue
		
		_goal_definitions[goal.id] = goal
		_goal_completed[goal.id] = false

func _discover_goal_paths() -> PackedStringArray:
	var result: PackedStringArray = []
	const GOAL_DIRECTORY_PATH: String = "res://docs/gyms/goals"
	const GOAL_RESOURCE_EXTENSIONS: PackedStringArray = ["tres", "res"]
	
	var directory: DirAccess = DirAccess.open(GOAL_DIRECTORY_PATH)
	if directory == null:
		push_warning("Unable to open goals directory: %s" % GOAL_DIRECTORY_PATH)
		return result
	
	directory.list_dir_begin()
	var entry_name: String = directory.get_next()
	while not entry_name.is_empty():
		if directory.current_is_dir() or entry_name.begins_with("."):
			entry_name = directory.get_next()
			continue
		
		var extension: String = entry_name.get_extension().to_lower()
		if GOAL_RESOURCE_EXTENSIONS.has(extension):
			result.append("%s/%s" % [GOAL_DIRECTORY_PATH, entry_name])
		
		entry_name = directory.get_next()
	directory.list_dir_end()
	
	result.sort()
	return result

func register_goal(goal: GoalData) -> void:
	if goal == null:
		return
	var goal_id: StringName = goal.id
	if goal_id == &"":
		push_warning("Attempted to register goal with invalid id")
		return
	if _goal_definitions.has(goal_id):
		return
	_goal_definitions[goal_id] = goal
	_goal_completed[goal_id] = false

func get_goal(goal_id: StringName) -> GoalData:
	return _goal_definitions.get(goal_id, null)

func get_all_goals() -> Array[GoalData]:
	var result: Array[GoalData] = []
	for goal in _goal_definitions.values():
		if goal is GoalData:
			result.append(goal)
	return result

func is_goal_completed(goal_id: StringName) -> bool:
	return _goal_completed.get(goal_id, false)

func get_goal_progress(goal_id: StringName) -> float:
	var goal: GoalData = get_goal(goal_id)
	if goal == null:
		return 0.0
	return goal.get_progress()

func evaluate_all_goals() -> void:
	for goal_id in _goal_completed.keys():
		if not _goal_completed[goal_id]:
			_try_complete_goal(goal_id)
	
	_try_unlock_all_buffs_from_goals()

func _try_complete_goal(goal_id: StringName) -> void:
	var goal: GoalData = _goal_definitions.get(goal_id, null)
	if goal == null:
		return
	if _goal_completed[goal_id]:
		return
	if goal.is_met():
		if goal.unlock_behavior == GoalData.UnlockBehavior.MANUAL:
			return
		_goal_completed[goal_id] = true
		goal_completed.emit(goal_id)

func _complete_goal_manually(goal_id: StringName) -> void:
	var goal: GoalData = _goal_definitions.get(goal_id, null)
	if goal == null:
		return
	if _goal_completed.get(goal_id, false):
		return
	if not goal.is_met():
		return
	_goal_completed[goal_id] = true
	goal_completed.emit(goal_id)

func _serialize_goals() -> Dictionary:
	var result: Dictionary = {}
	var completed_goals: Array[StringName] = []
	for goal_id in _goal_completed.keys():
		if _goal_completed[goal_id]:
			completed_goals.append(goal_id)
	result[GOAL_COMPLETED_KEY] = completed_goals
	return result

func _deserialize_goals(raw_value: Variant) -> void:
	_goal_completed.clear()
	
	if not (raw_value is Dictionary):
		return
	
	var raw_completed: Variant = raw_value.get(GOAL_COMPLETED_KEY, [])
	if raw_completed is Array:
		for goal_id_variant in raw_completed:
			var goal_id: String = String(goal_id_variant)
			if not goal_id.is_empty():
				_goal_completed[goal_id] = true
	
	for goal_id in _goal_definitions.keys():
		if not _goal_completed.has(goal_id):
			_goal_completed[goal_id] = false

func _migrate_goals_from_version_2(parsed_data: Dictionary) -> void:
	_goal_completed.clear()
	
	var generator_states_data: Dictionary = parsed_data.get(GENERATOR_STATES_KEY, {})
	for gen_id_variant in generator_states_data.keys():
		var gen_id: String = String(gen_id_variant)
		if gen_id.is_empty():
			continue
		
		var state: Dictionary = generator_states_data.get(gen_id_variant, {})
		if not state.get(GENERATOR_UNLOCKED_KEY, false):
			continue
		
		var generator_data: CurrencyGeneratorData = _load_generator_data_from_id(gen_id)
		if generator_data == null or not generator_data.has_unlock_goal():
			continue
		
		var goal_id: StringName = generator_data.get_unlock_goal_id()
		if not goal_id.is_empty():
			_goal_completed[goal_id] = true
	
	for goal_id in _goal_definitions.keys():
		if not _goal_completed.has(goal_id):
			_goal_completed[goal_id] = false

func _load_generator_data_from_id(generator_id: String) -> CurrencyGeneratorData:
	const GENERATORS_DIRECTORY_PATH: String = "res://docs/gyms/generators"
	const GENERATOR_RESOURCE_EXTENSIONS: PackedStringArray = ["tres", "res"]
	
	var directory: DirAccess = DirAccess.open(GENERATORS_DIRECTORY_PATH)
	if directory == null:
		return null
	
	directory.list_dir_begin()
	var entry_name: String = directory.get_next()
	while not entry_name.is_empty():
		if directory.current_is_dir() or entry_name.begins_with("."):
			entry_name = directory.get_next()
			continue
		
		var extension: String = entry_name.get_extension().to_lower()
		if GENERATOR_RESOURCE_EXTENSIONS.has(extension):
			var generator_path: String = "%s/%s" % [GENERATORS_DIRECTORY_PATH, entry_name]
			var generator_resource: Resource = load(generator_path)
			if generator_resource is CurrencyGeneratorData:
				var gen_data: CurrencyGeneratorData = generator_resource
				if String(gen_data.id) == generator_id:
					directory.list_dir_end()
					return gen_data
		
		entry_name = directory.get_next()
	directory.list_dir_end()
	
	return null
