class_name CurrencyGenerator
extends Node2D

## Handles generator economy behavior:
## - automatic production cycles
## - manual/hover click rewards with cooldown
## - purchasing and max-buy
## - generator state access through the GameState autoload

## Emitted after a successful purchase.
signal purchase_completed(amount: int, total_owned: int, total_purchased: int, total_cost: BigNumber)
## Emitted when a purchase is attempted but cannot be paid.
signal purchase_failed(requested_amount: int, required_cost: BigNumber, available_currency: BigNumber)
## Emitted when one or more automatic production cycles complete.
signal production_tick(amount: BigNumber, cycle_count: int, total_owned: int)
## Emitted after a successful buff purchase.
signal buff_purchased(buff_id: StringName, new_level: int, cost: BigNumber, cost_currency_id: StringName)
## Emitted when a buff purchase cannot be paid.
signal buff_purchase_failed(buff_id: StringName, required_cost: BigNumber, cost_currency_id: StringName)
## Emitted when this generator unlock goal transitions to completed.
signal goal_achieved(generator_id: StringName, goal_id: StringName)

## Sentinel exponent used when cost math overflows float range.
const HUGE_COST_EXPONENT: int = 1000000

## Currency target updated by this generator.
@export var currency: Resource
## Data resource containing balancing values and defaults (required).
@export var data: CurrencyGeneratorData
## Enables per-cycle automatic production when true.
@export var is_automatic: bool = true
## If true, `_on_pressed` buys one generator instead of granting click currency.
@export var press_buys_generator: bool = true
## If true, hovering grants click reward every cooldown without pressing.
@export var grants_click_while_hovering: bool = false
## Extra multiplier applied to automatic production output.
@export var run_multiplier: float = 1.0

## Reference to generator UI
@export var info_generator_container: GeneratorPanel

## True while the pointer is inside the generator Area2D.
var _mouse_entered: bool = false
## Time left until next click/hover grant is allowed.
var _remaining_click_cooldown_seconds: float = 0.0

## Number of currently owned generators.
## Backed by GameState via this generator's resolved id.
var owned: int:
	get:
		_ensure_registered()
		return GameState.get_generator_owned(_generator_id)
	set(value):
		_ensure_registered()
		GameState.set_generator_owned(_generator_id, value)

## Lifetime purchased generators (does not decrease if ownership drops).
var purchased_count: int:
	get:
		_ensure_registered()
		return GameState.get_generator_purchased_count(_generator_id)
	set(value):
		_ensure_registered()
		GameState.set_generator_purchased_count(_generator_id, value)

## Whether this generator is unlocked for the player.
var is_unlocked: bool:
	get:
		_ensure_registered()
		return GameState.is_generator_unlocked(_generator_id)
	set(value):
		_ensure_registered()
		GameState.set_generator_unlocked(_generator_id, value)

## Whether this unlocked generator is currently visible/usable to the player.
var is_available: bool:
	get:
		_ensure_registered()
		return GameState.is_generator_available(_generator_id)
	set(value):
		_ensure_registered()
		GameState.set_generator_available(_generator_id, value)

## Accumulated elapsed time toward the next automatic production cycle.
var cycle_progress_seconds: float = 0.0
## Stable id used to store/read generator state in GameState.
var _generator_id: StringName = &""
## Prevents duplicate state registration in GameState.
var _is_registered: bool = false
## Prevents re-entrant registration while GameState emits synchronous callbacks.
var _is_registering: bool = false

## Resolves and registers this generator state before gameplay updates.
func _ready() -> void:
	assert(currency != null, "Currency cannot be null")
	assert(data != null, "Data cannot be null")
	
	_generator_id = _resolve_generator_id()
	cycle_progress_seconds = 0.0
	
	GameState.currency_changed.connect(_on_currency_changed)
	GameState.generator_state_changed.connect(_on_generated_state_changed)
	GameState.goal_completed.connect(_on_goal_completed)
	
	_ensure_registered()

## Updates click cooldown/click grants and runs automatic production cycles.
func _process(delta: float) -> void:
	_remaining_click_cooldown_seconds = maxf(_remaining_click_cooldown_seconds - maxf(delta, 0.0), 0.0)
	_try_handle_mouse_click()

	if not is_automatic:
		return
	if not is_available_to_player():
		return
	if data == null or owned <= 0:
		return

	var cycle_seconds: float = maxf(data.initial_time, 0.0001)
	cycle_progress_seconds += maxf(delta, 0.0)
	if cycle_progress_seconds < cycle_seconds:
		return

	var completed_cycles: int = floori(cycle_progress_seconds / cycle_seconds)
	cycle_progress_seconds -= cycle_seconds * float(completed_cycles)
	_grant_cycle_income(completed_cycles)

## Adds automatic production from completed cycles and emits `production_tick`.
func _grant_cycle_income(cycle_count: int) -> void:
	if data == null or cycle_count <= 0:
		return

	var effective_run_multiplier: float = get_effective_auto_run_multiplier()
	var per_cycle: float = data.production_per_cycle(owned, effective_run_multiplier, purchased_count)
	if per_cycle <= 0.0:
		return

	var produced: BigNumber = BigNumber.from_float(per_cycle).multiply(BigNumber.from_float(float(cycle_count)))
	_add_currency(produced)
	production_tick.emit(produced, cycle_count, owned)

## Convenience helper for the next single-unit purchase cost.
func get_next_cost() -> BigNumber:
	return get_cost_for_amount(1)

## Returns cumulative cost to buy `amount` units from current ownership.
func get_cost_for_amount(amount: int = 1) -> BigNumber:
	if data == null:
		return BigNumber.from_float(0.0)

	var safe_amount: int = maxi(amount, 1)
	var raw_cost: float = data.cost_for_amount(owned, safe_amount)
	return _float_to_big_number(raw_cost)

## Checks whether enough target currency is available for `amount` units.
func can_buy(amount: int = 1) -> bool:
	if not is_available_to_player():
		return false

	var purchase_currency_id: StringName = get_purchase_currency_id()
	if purchase_currency_id == &"":
		return false

	var cost: BigNumber = get_cost_for_amount(amount)
	var current: BigNumber = _get_currency_amount_by_id(purchase_currency_id)
	return current.is_greater_than(cost) or current.is_equal_to(cost)

## Attempts to buy `amount` units and emits purchase success/failure signals.
func buy(amount: int = 1) -> bool:
	if data == null:
		return false
	if not is_available_to_player():
		return false
	var purchase_currency_id: StringName = get_purchase_currency_id()
	if purchase_currency_id == &"":
		return false

	var safe_amount: int = maxi(amount, 1)
	var total_cost: BigNumber = get_cost_for_amount(safe_amount)
	if not _spend_currency_by_id(purchase_currency_id, total_cost):
		purchase_failed.emit(safe_amount, total_cost, _get_currency_amount_by_id(purchase_currency_id))
		return false

	owned += safe_amount
	purchased_count += safe_amount
	purchase_completed.emit(safe_amount, owned, purchased_count, total_cost)
	return true

## Computes and buys the maximum affordable units with current currency.
func buy_max() -> int:
	if data == null:
		return 0
	if not is_available_to_player():
		return 0
	var purchase_currency_id: StringName = get_purchase_currency_id()
	if purchase_currency_id == &"":
		return 0

	var available_currency: float = _big_number_to_float(_get_currency_amount_by_id(purchase_currency_id))
	var estimated_max: int = data.max_affordable(owned, available_currency)
	if estimated_max <= 0:
		return 0

	# Keep max-buy accurate by validating affordability with BigNumber comparisons.
	var low: int = 1
	var high: int = estimated_max
	var best: int = 0
	while low <= high:
		var mid: int = floori((float(low) + float(high)) * 0.5)
		if can_buy(mid):
			best = mid
			low = mid + 1
		else:
			high = mid - 1

	if best <= 0:
		return 0
	return best if buy(best) else 0

func get_buffs() -> Array[GeneratorBuffData]:
	if data == null:
		return []
	return GameState.get_buffs_for_generator(_generator_id)

func get_buff_level(buff_id: StringName) -> int:
	_ensure_registered()
	return GameState.get_buff_level(buff_id)

func is_buff_unlocked(buff_id: StringName) -> bool:
	_ensure_registered()
	return GameState.is_buff_unlocked(buff_id)

func get_buff_cost(buff_id: StringName) -> BigNumber:
	var buff: GeneratorBuffData = GameState.get_buff(buff_id)
	if buff == null:
		return BigNumber.from_float(0.0)
	if not is_buff_unlocked(buff.id):
		return BigNumber.from_float(0.0)

	var level: int = get_buff_level(buff.id)
	return buff.get_cost_for_level(level)

func get_buff_cost_currency_id(buff_id: StringName) -> StringName:
	var buff: GeneratorBuffData = GameState.get_buff(buff_id)
	if buff == null:
		return &""
	return _resolve_buff_cost_currency_id(buff)

func can_buy_buff(buff_id: StringName) -> bool:
	if not is_available_to_player():
		return false

	var buff: GeneratorBuffData = GameState.get_buff(buff_id)
	if buff == null:
		return false

	if not is_buff_unlocked(buff.id):
		return false

	var current_level: int = get_buff_level(buff.id)
	if not buff.can_purchase_next_level(current_level):
		return false

	var cost_currency_id: StringName = _resolve_buff_cost_currency_id(buff)
	if cost_currency_id == &"":
		return false

	var cost: BigNumber = buff.get_cost_for_level(current_level)
	var current_amount: BigNumber = GameState.get_currency_amount_by_id(cost_currency_id)
	return current_amount.is_greater_than(cost) or current_amount.is_equal_to(cost)

func buy_buff(buff_id: StringName) -> bool:
	if not is_available_to_player():
		return false

	var buff: GeneratorBuffData = GameState.get_buff(buff_id)
	if buff == null:
		return false

	if not GameState.is_buff_unlocked(buff.id):
		return false

	var current_level: int = GameState.get_buff_level(buff.id)
	if not buff.can_purchase_next_level(current_level):
		return false

	var cost: BigNumber = buff.get_cost_for_level(current_level)
	var cost_currency_id: StringName = _resolve_buff_cost_currency_id(buff)
	if cost_currency_id == &"":
		return false

	if not _spend_currency_by_id(cost_currency_id, cost):
		buff_purchase_failed.emit(buff.id, cost, cost_currency_id)
		return false

	var next_level: int = current_level + 1
	GameState.set_buff_level(buff.id, next_level)
	_apply_resource_purchase_buff(buff, next_level)
	buff_purchased.emit(buff.id, next_level, cost, cost_currency_id)
	return true

func is_buff_maxed(buff_id: StringName) -> bool:
	var buff: GeneratorBuffData = GameState.get_buff(buff_id)
	if buff == null:
		return true
	if not GameState.is_buff_unlocked(buff.id):
		return false

	return not buff.can_purchase_next_level(GameState.get_buff_level(buff.id))

func get_automatic_production_multiplier() -> float:
	return _get_multiplier_for_buff_kind(GeneratorBuffData.BuffKind.AUTO_PRODUCTION_MULTIPLIER)

func get_effective_auto_run_multiplier() -> float:
	return run_multiplier * get_automatic_production_multiplier() * _get_prestige_multiplier()

func reset_runtime_state_for_prestige() -> void:
	_is_registered = false
	_is_registering = false
	cycle_progress_seconds = 0.0
	_remaining_click_cooldown_seconds = 0.0
	_ensure_registered()
	visible = is_available_to_player()

func get_manual_click_multiplier() -> float:
	return _get_multiplier_for_buff_kind(GeneratorBuffData.BuffKind.MANUAL_CLICK_MULTIPLIER)

## Handles external "pressed" interaction: buy if configured, otherwise click-grant.
func _on_pressed() -> void:
	if not is_available_to_player():
		return

	if press_buys_generator:
		buy(1)
		return

	_try_grant_click_currency()

## Adds currency directly through the configured currency target.
func grant_currency(amount: BigNumber) -> void:
	_add_currency(amount)

## Processes pointer interaction while hovering (hold click or hover-only mode).
func _try_handle_mouse_click() -> void:
	if not _mouse_entered:
		return
	if not is_available_to_player():
		return
	if not (_grants_click_while_hovering() or Input.is_action_pressed("click")):
		return

	_try_grant_click_currency()

## Grants one click/hover reward if cooldown has elapsed.
func _try_grant_click_currency() -> void:
	if data == null:
		return
	if _remaining_click_cooldown_seconds > 0.0:
		return

	_add_currency(_get_click_value())
	_remaining_click_cooldown_seconds = _get_click_cooldown_seconds()

## Resolves click reward from data.
func _get_click_value() -> BigNumber:
	if data == null:
		return BigNumber.from_float(0.0)

	var click_multiplier: float = get_manual_click_multiplier()
	if click_multiplier <= 0.0:
		return BigNumber.from_float(0.0)

	var base_click: BigNumber = BigNumber.new(data.click_mantissa, data.click_exponent)
	return base_click.multiply(BigNumber.from_float(click_multiplier))

## Resolves click cooldown from data.
func _get_click_cooldown_seconds() -> float:
	if data == null:
		return 0.0
	return maxf(data.click_cooldown_seconds, 0.0)

## Resolves hover-only click-grant mode from node configuration.
func _grants_click_while_hovering() -> bool:
	return grants_click_while_hovering

func _get_prestige_multiplier() -> float:
	var manager: PrestigeManager = get_node_or_null("/root/PrestigeManager")
	if manager == null:
		return 1.0
	if not manager.get_total_multiplier():
		return 1.0

	var raw_multiplier: Variant = manager.get_total_multiplier()
	if raw_multiplier is float:
		return maxf(raw_multiplier, 0.0)
	if raw_multiplier is int:
		return maxf(float(raw_multiplier), 0.0)

	return 1.0

## Returns this generator's resolved state id.
func get_generator_id() -> StringName:
	_ensure_registered()
	return _generator_id

## Returns the configured currency id.
func get_currency_id() -> StringName:
	return GameState.get_currency_id(currency)

func get_purchase_currency_id() -> StringName:
	var purchase_currency: Currency = _resolve_purchase_currency()
	if purchase_currency == null:
		return &""

	return GameState.get_currency_id(purchase_currency)

## True when the generator is both unlocked and available.
func is_available_to_player() -> bool:
	return is_unlocked and is_available

## Routes positive/negative currency deltas to the configured currency bucket.
func _add_currency(amount: BigNumber) -> void:
	if currency == null:
		push_error("CurrencyGenerator '%s' cannot add currency: currency resource is null." % String(name))
		return

	GameState.add_currency(currency, amount)

func _add_currency_by_id(currency_id: StringName, amount: BigNumber) -> void:
	if currency_id == &"":
		return

	GameState.add_currency_by_id(currency_id, amount)

## Attempts to spend target currency; returns false when insufficient.
func _spend_currency(cost: BigNumber) -> bool:
	if currency == null:
		push_error("CurrencyGenerator '%s' cannot spend currency: currency resource is null." % String(name))
		return false

	return GameState.spend_currency(currency, cost)

func _spend_currency_by_id(currency_id: StringName, cost: BigNumber) -> bool:
	if currency_id == &"":
		return false

	return GameState.spend_currency_by_id(currency_id, cost)

## Returns the current amount for the configured currency type.
func _get_currency_amount() -> BigNumber:
	if currency == null:
		return BigNumber.from_float(0.0)
	return GameState.get_currency_amount(currency)

func _get_currency_amount_by_id(currency_id: StringName) -> BigNumber:
	if currency_id == &"":
		return BigNumber.from_float(0.0)

	return GameState.get_currency_amount_by_id(currency_id)

## Safely converts finite float values into BigNumber costs.
func _float_to_big_number(value: float) -> BigNumber:
	if value <= 0.0:
		return BigNumber.from_float(0.0)
	if not is_finite(value):
		return BigNumber.new(1.0, HUGE_COST_EXPONENT)
	return BigNumber.from_float(value)

## Converts BigNumber to float for approximate estimate math.
func _big_number_to_float(value: BigNumber) -> float:
	if value.mantissa <= 0.0:
		return 0.0
	if value.exponent > 308:
		return INF
	if value.exponent < -308:
		return 0.0
	return value.mantissa * pow(10.0, float(value.exponent))

func _resolve_buff_cost_currency_id(buff: GeneratorBuffData) -> StringName:
	if buff == null:
		return &""

	var cost_currency: Resource = buff.cost_currency if buff.cost_currency != null else currency
	if cost_currency == null:
		return &""

	return GameState.get_currency_id(cost_currency)

func _resolve_purchase_currency() -> Currency:
	if data == null:
		return null

	return data.purchase_currency

func _resolve_buff_target_currency_id(buff: GeneratorBuffData) -> StringName:
	if buff == null:
		return &""

	var target_currency: Resource = buff.resource_target_currency if buff.resource_target_currency != null else currency
	if target_currency == null:
		return &""

	return GameState.get_currency_id(target_currency)

func _get_multiplier_for_buff_kind(kind: GeneratorBuffData.BuffKind) -> float:
	return GameState.get_effective_multiplier(_generator_id, kind)

func _apply_resource_purchase_buff(buff: GeneratorBuffData, level: int) -> void:
	if buff == null:
		return
	if buff.kind != GeneratorBuffData.BuffKind.RESOURCE_PURCHASE:
		return

	var target_currency_id: StringName = _resolve_buff_target_currency_id(buff)
	if target_currency_id == &"":
		return

	var purchased_amount: BigNumber = buff.get_resource_purchase_amount_for_level(level)
	if purchased_amount.mantissa <= 0.0:
		return

	_add_currency_by_id(target_currency_id, purchased_amount)

## Builds a stable state id from data id, node name, or fallback string.
func _resolve_generator_id() -> StringName:
	if data != null:
		var data_id: String = String(data.id)
		if not data_id.is_empty():
			return StringName(data_id)

	if not String(name).is_empty():
		return StringName(String(name))

	return &"generator"

## Ensures this generator has a state entry in GameState.
func _ensure_registered() -> void:
	if _is_registered or _is_registering:
		return

	if _generator_id == &"":
		_generator_id = _resolve_generator_id()
	if _generator_id == &"":
		return

	_is_registering = true
	GameState.register_generator(
		_generator_id,
		_default_unlocked_state(),
		_default_available_state(),
		_default_owned_state()
	)
	_register_buffs()
	_is_registered = true
	_is_registering = false

func _register_buffs() -> void:
	for buff in get_buffs():
		if buff == null:
			continue

		var buff_id_text: String = String(buff.id).strip_edges()
		if buff_id_text.is_empty():
			continue

		var buff_id: StringName = StringName(buff_id_text)
		GameState.register_buff(buff)
		
		if not GameState.is_buff_unlocked(buff_id):
			var persisted_level: int = GameState.get_buff_level(buff_id)
			var starts_unlocked: bool = buff.unlocked or persisted_level > 0
			GameState.set_buff_unlocked(buff_id, starts_unlocked)

		_try_unlock_buff_from_goal(buff)

func _try_unlock_buff_from_goal(buff: GeneratorBuffData) -> void:
	GameState._try_unlock_buff_from_goal(buff)

func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void:
	GameState.evaluate_all_goals()

func _on_goal_completed(goal_id: StringName) -> void:
	if data == null or not data.has_unlock_goal():
		return
	if data.get_unlock_goal_id() != goal_id:
		return
	if is_available_to_player():
		return

	GameState.set_generator_unlocked(_generator_id, true)
	GameState.set_generator_available(_generator_id, true)
	goal_achieved.emit(_generator_id, goal_id)
	_on_generated_state_changed(_generator_id, GameState._get_generator_state(_generator_id))

## Default unlocked state loaded from generator data.
func _default_unlocked_state() -> bool:
	if data == null:
		return false
	return data.starts_unlocked

## Default available state loaded from generator data.
func _default_available_state() -> bool:
	if data == null:
		return false
	return data.starts_available

func _default_owned_state() -> int:
	if data == null:
		return 0
	return maxi(data.initial_owned, 0)

## Area2D callback: pointer entered generator interaction region.
func _on_area_2d_mouse_entered() -> void:
	_mouse_entered = true
	info_generator_container.visible = true

## Area2D callback: pointer exited generator interaction region.
func _on_area_2d_mouse_exited() -> void:
	_mouse_entered = false
	info_generator_container.visible = false


func _on_generated_state_changed(generator_id: StringName, _state: Dictionary) -> void:
	if generator_id == _generator_id:
		visible = is_available_to_player()
		if info_generator_container != null:
			info_generator_container._refresh_generator_ui()
