class_name RecipeMenu
extends Control

# Deux usages, un seul panneau. Le four demande « quel plat », la console
# demande « comment tourne le magasin ». Ce qu'ils partagent — la liste, la
# navigation clavier, le focus — n'est pas trivial; ce qui les sépare tient
# dans le mode.
#
# Le panneau ne décide de rien: il émet, le restaurant applique. C'est ce qui
# lui permet de porter l'interrupteur d'ouverture et l'embauche sans rien
# savoir ni du magasin ni de la brigade.

signal recipe_selected(recipe_id: String)
signal menu_closed
signal open_toggle_requested
signal takeout_toggle_requested
signal build_mode_requested
signal hire_requested(employee_id: String)
signal employee_work_requested(employee_id: String)
signal employee_standby_requested(employee_id: String)
signal takeout_recipe_requested(recipe_id: String)
signal weapon_purchase_requested(weapon_id: String)
signal weapon_equip_requested(weapon_id: String, slot: int)
signal equipment_purchase_requested(equipment_id: String)
signal equipment_equip_requested(equipment_id: String, slot: int)
signal recipe_consumable_prepare_requested(recipe_id: String)
signal consumable_equip_requested(consumable_id: String, slot: int)

enum Mode { COOK, CONSOLE }
# Les actions générales vivent dans le bandeau permanent, jamais dans un onglet.
enum Section { MENU, RECIPES, BRIGADE, TAKEOUT, INGREDIENTS, ARSENAL }

const SUFFICIENT_COLOR := Color("6ee7a0")
const MISSING_COLOR := Color("ff7b7b")
const DIMMED := Color(0.62, 0.6, 0.66, 1.0)
const SECTION_COLOR := Color("ffd166")

# Ce qu'une ligne désigne. La liste porte trois natures depuis que la console
# existe, et le clavier doit savoir laquelle il a sous le curseur.
const ROW_RECIPE := "recipe"
const ROW_EMPLOYEE := "employee"
const ROW_ROLE := "role"
const ROW_TAKEOUT_RULE := "takeout_rule"
const ROW_INGREDIENT := "ingredient"
const ROW_WEAPON := "weapon"
const ROW_EQUIPMENT := "equipment"
const ROW_CONSUMABLE := "consumable"

@onready var list: VBoxContainer = %RecipeList
@onready var scroll: ScrollContainer = %RecipeScroll
@onready var close_button: Button = %CloseButton
@onready var dining_toggle: OpenCloseButton = %DiningToggle
@onready var takeout_toggle: OpenCloseButton = %TakeoutToggle
@onready var build_button: Button = %BuildButton
@onready var console_status: Label = %ConsoleStatus
@onready var menu_count_label: Label = %MenuCount
@onready var title_label: Label = %Title
@onready var hint_label: Label = %Hint
@onready var menu_column_label: Label = %ColMenu
@onready var column_header: HBoxContainer = %Header
@onready var icon_slot: CenterContainer = %IconSlot
@onready var tab_row: HBoxContainer = %TabRow
@onready var panel: PanelContainer = $Panel
@onready var backdrop: ColorRect = $Backdrop

var mode: int = Mode.COOK
# Renseignés par le restaurant avant l'ouverture et après chaque bascule. Le
# panneau n'a aucun moyen — ni aucune raison — d'aller les chercher lui-même.
var shop_closed := false
# Le comptoir a son propre rideau, et il n'existe qu'une fois construit: les
# deux drapeaux sont séparés pour que la console ne les confonde jamais.
var takeout_closed := false
var takeout_installed := false
# Le permis de chantier: sans lui, la console n'offre pas le mode construction.
var build_permit := false
var served_per_minute := 0.0
var brigade_role_filter := "server"

# Un bouton par ligne: c'est lui qui prend le focus. Les tableaux parallèles
# disent ce que la ligne désigne, parce qu'une même liste porte maintenant des
# recettes, des employés et l'interrupteur.
var row_buttons: Array[Button] = []
var row_ids: Array[String] = []
var row_kinds: Array[String] = []
var ingredient_amount_labels: Dictionary = {}
# La barre d'onglets vit à part de la liste: elle est horizontale, et la liste
# ne l'est pas. Les côtés y changent d'onglet, le haut et le bas en sortent.
var tab_buttons: Array[Button] = []
var tab_sections: Array[int] = []
var section: int = Section.MENU
var icon: ConsoleIcon

func _ready() -> void:
	close_button.pressed.connect(close)
	dining_toggle.pressed.connect(_press_switch)
	takeout_toggle.pressed.connect(_press_takeout_switch)
	build_button.pressed.connect(_press_build_mode)
	for button: Button in [dining_toggle, takeout_toggle, build_button, close_button]:
		button.mouse_entered.connect(button.grab_focus)
	GameState.state_changed.connect(_on_state_changed)
	build_list()

func open(new_mode: int = Mode.COOK, new_section: int = Section.MENU) -> void:
	mode = new_mode
	section = new_section if _available_sections().has(new_section) else Section.MENU
	_apply_layout()
	visible = true
	build_list()
	call_deferred("_focus_first_available")

func close() -> void:
	if not visible:
		return
	visible = false
	menu_closed.emit()

func _apply_layout() -> void:
	var compact := mode == Mode.COOK
	panel.offset_left = 256.0 if compact else 190.0
	panel.offset_top = 94.0 if compact else 18.0
	panel.offset_right = 896.0 if compact else 962.0
	panel.offset_bottom = 554.0 if compact else 630.0
	scroll.custom_minimum_size.y = 210.0 if compact else 260.0
	backdrop.color.a = 0.28 if compact else 0.72

func _on_state_changed() -> void:
	if visible:
		build_list()

func _input(event: InputEvent) -> void:
	if not visible:
		return
	if event.is_action_pressed("pause") or event.is_action_pressed("ui_cancel"):
		close()
		get_viewport().set_input_as_handled()
		return
	# Left mouse is also mapped to action, but mouse clicks must be handled by
	# the actual hovered Button instead of confirming the currently focused one.
	if event is InputEventMouseButton:
		return
	if event.is_action_pressed("move_up") or event.is_action_pressed("ui_up"):
		_navigate(-1)
		get_viewport().set_input_as_handled()
	elif event.is_action_pressed("move_down") or event.is_action_pressed("ui_down"):
		_navigate(1)
		get_viewport().set_input_as_handled()
	elif event.is_action_pressed("move_left") or event.is_action_pressed("ui_left"):
		_horizontal(-1)
		get_viewport().set_input_as_handled()
	elif event.is_action_pressed("move_right") or event.is_action_pressed("ui_right"):
		_horizontal(1)
		get_viewport().set_input_as_handled()
	elif event.is_action_pressed("action") or event.is_action_pressed("ui_accept"):
		_confirm_focused()
		get_viewport().set_input_as_handled()

func build_list() -> void:
	if list == null:
		return
	if mode == Mode.CONSOLE and not _available_sections().has(section):
		section = Section.MENU
	var focused_index := _focused_index()
	row_buttons.clear()
	row_ids.clear()
	row_kinds.clear()
	ingredient_amount_labels.clear()
	for child: Node in list.get_children():
		list.remove_child(child)
		child.queue_free()
	if mode == Mode.CONSOLE:
		match section:
			Section.MENU:
				for id: String in listed_recipes():
					list.add_child(_build_recipe_row(id))
				# Une carte vide n'est pas une impasse: elle dit qu'il manque des
				# ingrédients, pas que le magasin est cassé.
				if row_buttons.is_empty() and list.get_child_count() == 0:
					list.add_child(_section_label(Loc.ui("ui.console.menu_empty"), true))
			Section.RECIPES:
				for id: String in listed_recipes():
					list.add_child(_build_recipe_row(id))
				if list.get_child_count() == 0:
					list.add_child(_section_label(Loc.ui("ui.console.recipes_empty"), true))
			Section.BRIGADE:
				_build_brigade()
			Section.TAKEOUT:
				_build_takeout()
			Section.INGREDIENTS:
				_build_ingredients()
			Section.ARSENAL:
				_build_arsenal()
			_:
				section = Section.MENU
				for id: String in listed_recipes():
					list.add_child(_build_recipe_row(id))
	else:
		for id: String in listed_recipes():
			list.add_child(_build_recipe_row(id))
		# Une carte vidée n'est pas une impasse: la console reste là pour la
		# regarnir, et le four le dit au lieu d'afficher une liste vide.
		if row_buttons.is_empty():
			list.add_child(_section_label(Loc.ui("ui.menu.empty_cook"), true))
	_refresh_chrome()
	if visible:
		call_deferred("_restore_focus", focused_index)

# Le four ne propose que la carte du jour: cuisiner un plat qu'on n'offre à
# personne n'a plus de sens. La console ne révèle que les recettes débloquées;
# une découverte doit précéder toute décision de les mettre à la carte.
func listed_recipes() -> Array[String]:
	if mode != Mode.CONSOLE:
		# Le four ne propose que ce qui est réellement cuisinable.
		return GameState.offered_recipes()
	match section:
		Section.RECIPES:
			return GameState.unlocked_recipes()
		Section.TAKEOUT:
			return GameState.unlocked_recipes()
		_:
			return GameState.offered_recipes()

const SECTION_KEYS := {
	Section.MENU: "ui.console.tab_menu",
	Section.RECIPES: "ui.console.tab_recipes",
	Section.BRIGADE: "ui.console.tab_brigade",
	Section.TAKEOUT: "ui.console.tab_takeout",
	Section.INGREDIENTS: "ui.console.tab_ingredients",
	Section.ARSENAL: "ui.console.tab_arsenal",
}
const SECTION_TITLES := {
	Section.MENU: "ui.console.section_menu",
	Section.RECIPES: "ui.console.section_recipes",
	Section.BRIGADE: "ui.console.section_brigade",
	Section.TAKEOUT: "ui.console.section_takeout",
	Section.INGREDIENTS: "ui.console.section_ingredients",
	Section.ARSENAL: "ui.console.section_arsenal",
}

func _available_sections() -> Array[int]:
	var available: Array[int] = [Section.MENU, Section.RECIPES]
	if GameState.brigade_is_unlocked():
		available.append(Section.BRIGADE)
	available.append(Section.TAKEOUT)
	available.append(Section.INGREDIENTS)
	available.append(Section.ARSENAL)
	return available

func _refresh_chrome() -> void:
	var console := mode == Mode.CONSOLE
	if title_label != null:
		title_label.text = Loc.ui(SECTION_TITLES[section]) if console else Loc.ui("ui.menu.title_cook")
	if hint_label != null:
		hint_label.visible = not console
		hint_label.text = Loc.ui("ui.menu.hint_cook")
	# Les colonnes ne décrivent que des recettes: ailleurs, elles mentiraient.
	var recipe_rows := (console and (section == Section.MENU or section == Section.TAKEOUT)) or not console
	if menu_column_label != null:
		menu_column_label.visible = console and recipe_rows
		menu_column_label.text = Loc.ui("ui.console.takeout_select") if section == Section.TAKEOUT else Loc.ui("ui.menu.column_menu")
	if column_header != null:
		column_header.visible = recipe_rows
	if menu_count_label != null:
		menu_count_label.visible = console and section == Section.MENU
		menu_count_label.text = Loc.ui("ui.menu.count") % [GameState.offered_recipes().size(), GameState.unlocked_recipes().size()]
	if icon_slot != null:
		icon_slot.visible = console
	if tab_row != null:
		tab_row.visible = console
	_refresh_console_actions()
	if console:
		_build_tabs()
		_refresh_icon()

func _refresh_console_actions() -> void:
	var console := mode == Mode.CONSOLE
	dining_toggle.visible = console
	dining_toggle.setup(
		Loc.ui("ui.console.dining"),
		not shop_closed,
		Loc.ui("ui.console.open_word"),
		Loc.ui("ui.console.close_word")
	)
	dining_toggle.tooltip_text = Loc.ui("ui.console.switch_tooltip")
	takeout_toggle.visible = console and takeout_installed
	takeout_toggle.setup(
		Loc.ui("ui.console.tab_takeout"),
		not takeout_closed,
		Loc.ui("ui.console.open_word"),
		Loc.ui("ui.console.close_word")
	)
	takeout_toggle.tooltip_text = Loc.ui("ui.console.takeout_switch_tooltip")
	build_button.visible = console and build_permit
	build_button.text = Loc.ui("ui.console.build_mode")
	build_button.tooltip_text = Loc.ui("ui.console.build_tooltip")
	console_status.visible = console
	console_status.text = "%s  •  %s" % [
		Loc.ui("ui.console.open_state") % Loc.ui(opening_state_key()),
		Loc.ui("ui.console.rate") % served_per_minute,
	]

# La grosse icône du haut. Elle dit de quoi parle l'onglet avant qu'on lise
# quoi que ce soit — et, sur le magasin, elle dit s'il est ouvert.
func _refresh_icon() -> void:
	if icon_slot == null:
		return
	if icon == null or not is_instance_valid(icon):
		icon = ConsoleIcon.new()
		icon_slot.add_child(icon)
	icon.setup(section)

func _build_tabs() -> void:
	tab_buttons.clear()
	tab_sections.clear()
	if tab_row == null:
		return
	for child: Node in tab_row.get_children():
		tab_row.remove_child(child)
		child.queue_free()
	for index: int in _available_sections():
		var button := Button.new()
		button.custom_minimum_size = Vector2(112, 42)
		button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
		button.toggle_mode = true
		button.button_pressed = index == section
		button.text = Loc.ui(SECTION_KEYS[index])
		button.pressed.connect(_select_section.bind(index))
		button.mouse_entered.connect(button.grab_focus)
		tab_row.add_child(button)
		tab_buttons.append(button)
		tab_sections.append(index)

func _select_section(new_section: int) -> void:
	if section == new_section or not _available_sections().has(new_section):
		return
	section = new_section
	build_list()
	call_deferred("_focus_tab")

func _focus_tab() -> void:
	var tab_index := tab_sections.find(section)
	if tab_index >= 0 and tab_index < tab_buttons.size() and is_instance_valid(tab_buttons[tab_index]):
		tab_buttons[tab_index].grab_focus()

# Une liste n'a pas de navigation horizontale. Sur la barre d'onglets, les côtés
# changent d'onglet; sur une ligne, ils l'actionnent.
func _horizontal(step: int) -> void:
	if mode != Mode.CONSOLE:
		return
	var focused := get_viewport().gui_get_focus_owner() as Button
	var actions := _top_action_buttons()
	if focused != null and actions.has(focused):
		var current_action := actions.find(focused)
		_focus_button(actions[wrapi(current_action + step, 0, actions.size())])
		return
	if focused != null and tab_buttons.has(focused):
		var current_tab := tab_sections.find(section)
		_select_section(tab_sections[wrapi(current_tab + step, 0, tab_sections.size())])
		return
	_confirm_focused()

func _register_row(button: Button, id: String, kind: String) -> void:
	row_buttons.append(button)
	row_ids.append(id)
	row_kinds.append(kind)

func _section_label(text: String, quiet: bool = false) -> Control:
	var label := Label.new()
	label.text = text
	label.add_theme_font_size_override("font_size", 12 if quiet else 14)
	label.add_theme_color_override("font_color", DIMMED if quiet else SECTION_COLOR)
	return label

# Les quatre états, nommés. Un comptoir non construit ne compte pas comme fermé:
# il n'existe pas encore, et « seulement salle ouverte » serait un mensonge.
func opening_state_key() -> String:
	var counter_open := takeout_installed and not takeout_closed
	if not shop_closed and (counter_open or not takeout_installed):
		return "ui.console.state_all_open"
	if not shop_closed:
		return "ui.console.state_dining_only"
	if counter_open:
		return "ui.console.state_takeout_only"
	return "ui.console.state_all_closed"

func _press_takeout_switch() -> void:
	takeout_toggle_requested.emit()

func _press_build_mode() -> void:
	build_mode_requested.emit()

func _build_recipe_row(id: String) -> Control:
	var recipe: Dictionary = DataCatalog.RECIPES[id]
	var unlocked := GameState.recipe_is_unlocked(id)
	var cookable := GameState.can_cook(id)
	var row := HBoxContainer.new()
	row.add_theme_constant_override("separation", 8)
	var focusable: Button = null
	if mode != Mode.CONSOLE:
		# Au four, la ligne est le bouton de cuisson.
		focusable = Button.new()
		focusable.custom_minimum_size = Vector2(168, 44)
		focusable.text = DataCatalog.recipe_name(id)
		focusable.clip_text = true
		focusable.tooltip_text = Loc.ui("ui.menu.cook_tooltip") if cookable else (Loc.ui("ui.menu.missing_tooltip") if unlocked else Loc.ui("ui.menu.locked_tooltip"))
		# A recipe that cannot be cooked is dimmed but stays browsable.
		focusable.modulate = Color.WHITE if cookable else DIMMED
		focusable.disabled = not unlocked
		focusable.pressed.connect(_select.bind(id))
		focusable.mouse_entered.connect(focusable.grab_focus)
		row.add_child(focusable)
	else:
		# À la console, on ne cuisine pas: le nom n'est qu'une étiquette. La carte
		# du jour se lit, le livre de recettes se consulte.
		var name_label := Label.new()
		name_label.custom_minimum_size = Vector2(168, 44)
		name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
		name_label.clip_text = true
		name_label.text = DataCatalog.recipe_name(id)
		name_label.modulate = Color.WHITE if unlocked else DIMMED
		row.add_child(name_label)

	row.add_child(_column("%.1fs" % float(recipe.cook_time), 58, cookable))
	row.add_child(_column("%d c" % int(recipe.sale_price), 54, cookable))
	if mode == Mode.CONSOLE and section == Section.MENU:
		row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	else:
		row.add_child(_ingredient_column(recipe))

	if mode == Mode.CONSOLE:
		focusable = Button.new()
		focusable.custom_minimum_size = Vector2(96, 44)
		if section == Section.TAKEOUT:
			var selected := id == GameState.takeout_recipe
			focusable.text = Loc.ui("ui.console.takeout_selected") if selected else Loc.ui("ui.console.takeout_select")
			focusable.tooltip_text = Loc.ui("ui.console.takeout_recipe_tooltip")
			focusable.pressed.connect(_press_takeout_recipe.bind(id))
			focusable.disabled = not unlocked
		else:
			# Plus rien à cocher: la carte du jour se lit. Le bouton reste, mais
			# il ne fait plus rien — il porte le seul chiffre que le gérant ne
			# peut pas deviner de tête, et il garde la ligne parcourable au
			# clavier et à la manette.
			var portions := GameState.cookable_portions(id)
			focusable.custom_minimum_size = Vector2(112, 44)
			focusable.text = Loc.ui("ui.console.portions_left") % portions
			focusable.tooltip_text = Loc.ui("ui.console.portions_tooltip")
			focusable.modulate = Color.WHITE if portions > 0 else DIMMED
		focusable.mouse_entered.connect(focusable.grab_focus)
		row.add_child(focusable)

	_register_row(focusable, id, ROW_RECIPE)
	return row

# La brigade ne révèle jamais une fiche encore verrouillée. Les postes indiquent
# le nombre de travailleurs actifs; les contrats en stand-by ne les occupent pas.
func _build_brigade() -> void:
	list.add_child(_section_label(Loc.ui("ui.console.section_brigade")))
	var role_tabs := HBoxContainer.new()
	role_tabs.add_theme_constant_override("separation", 10)
	for role: String in DataCatalog.EMPLOYEE_ROLES:
		var role_button := Button.new()
		role_button.custom_minimum_size = Vector2(250, 40)
		role_button.text = ("● " if role == brigade_role_filter else "") + _role_name(role)
		role_button.pressed.connect(_select_brigade_role.bind(role))
		role_button.mouse_entered.connect(role_button.grab_focus)
		role_tabs.add_child(role_button)
		_register_row(role_button, role, ROW_ROLE)
	list.add_child(role_tabs)
	var slots := GameState.employee_slots(brigade_role_filter)
	list.add_child(_section_label(
		Loc.ui("ui.console.slots") % [_role_name(brigade_role_filter), GameState.working_of_role(brigade_role_filter).size(), slots], true))
	var visible_employees := 0
	for id: String in DataCatalog.EMPLOYEES:
		if str(DataCatalog.EMPLOYEES[id].get("role", "")) == brigade_role_filter \
		and (GameState.employee_is_unlocked(id) or GameState.employee_is_hired(id)):
			list.add_child(_build_employee_row(id, brigade_role_filter, slots))
			visible_employees += 1
	if visible_employees <= 0:
		list.add_child(_section_label(Loc.ui("ui.console.no_unlocked_employees"), true))
	if slots <= 0:
		list.add_child(_section_label(Loc.ui("ui.console.no_slots"), true))

func _select_brigade_role(role: String) -> void:
	if not DataCatalog.EMPLOYEE_ROLES.has(role):
		return
	brigade_role_filter = role
	build_list()

func _build_takeout() -> void:
	if not GameState.takeout_is_open():
		list.add_child(_section_label(Loc.ui("ui.console.takeout_not_installed"), true))
		return
	list.add_child(_section_label(Loc.ui("ui.console.takeout_stock") % [GameState.takeout_stock, GameState.takeout_capacity()]))
	var recipe_name := DataCatalog.recipe_name(GameState.takeout_recipe) if DataCatalog.RECIPES.has(GameState.takeout_recipe) else Loc.ui("ui.takeout.no_recipe")
	list.add_child(_section_label(Loc.ui("ui.console.takeout_recipe") % recipe_name, true))
	list.add_child(_section_label(Loc.ui("ui.console.takeout_restock_rule") % [GameState.takeout_restock_threshold_value(), GameState.takeout_restock_target_value()], true))
	list.add_child(_build_takeout_rule_row("threshold"))
	list.add_child(_build_takeout_rule_row("target"))
	for id: String in listed_recipes():
		list.add_child(_build_recipe_row(id))

func _build_arsenal() -> void:
	list.add_child(_section_label("ARME PRINCIPALE", true))
	for weapon_id: String in DataCatalog.WEAPONS:
		list.add_child(_build_weapon_row(weapon_id))
	if GameState.secondary_weapon_slot_count() > 0:
		var secondary_name := DataCatalog.weapon_name(GameState.secondary_weapon_id) \
			if not GameState.secondary_weapon_id.is_empty() else "—"
		list.add_child(_section_label("ACTION SECONDAIRE [F]  %s" % secondary_name, true))
	else:
		list.add_child(_section_label("ACTION SECONDAIRE  — À DÉBLOQUER", true))
	list.add_child(_section_label("ÉQUIPEMENT  %d SLOT(S)" % GameState.equipment_slot_count(), true))
	for equipment_id: String in DataCatalog.EQUIPMENT:
		list.add_child(_build_equipment_row(equipment_id))
	list.add_child(_section_label("PLATS DE COMBAT  %d/%d PORTÉS · %d SLOT(S)" % [
		GameState.prepared_consumable_total(), GameState.consumable_capacity(),
		GameState.consumable_slot_count()], true))
	for recipe_id: String in GameState.unlocked_recipes():
		list.add_child(_build_recipe_consumable_row(recipe_id))

func _build_weapon_row(id: String) -> Control:
	var definition: Dictionary = DataCatalog.WEAPONS[id]
	var owned := GameState.weapon_is_owned(id)
	var row := HBoxContainer.new()
	row.add_theme_constant_override("separation", 8)
	var name_label := _column(DataCatalog.weapon_name(id), 210, owned)
	name_label.tooltip_text = str(definition.get("description", ""))
	row.add_child(name_label)
	var delivery := Loc.ui("ui.console.weapon_projectile") if str(definition.get("delivery", "direct")) == "projectile" \
		else Loc.ui("ui.console.weapon_direct")
	var summary := _column("%s  ×%.2f  ↦%d" % [delivery,
		float(definition.get("damage_multiplier", 1.0)), int(definition.get("piercing", 0))], 190, true)
	row.add_child(summary)
	if not owned:
		var cost := int(definition.get("purchase_cost", 0))
		var buy := Button.new()
		buy.custom_minimum_size = Vector2(190, 42)
		buy.text = Loc.ui("ui.console.buy_cost") % cost
		buy.disabled = GameState.coins < cost
		buy.modulate = Color.WHITE if not buy.disabled else DIMMED
		buy.pressed.connect(_press_weapon_purchase.bind(id))
		buy.mouse_entered.connect(buy.grab_focus)
		row.add_child(buy)
		_register_row(buy, id, ROW_WEAPON)
		return row
	var slot_type := str(definition.get("slot_type", "either"))
	if slot_type != "secondary":
		var button := Button.new()
		button.custom_minimum_size = Vector2(132, 42)
		var equipped := GameState.active_weapon_id() == id
		button.text = ("● " if equipped else "") + "PRINCIPALE"
		button.tooltip_text = Loc.ui("ui.console.weapon_active") if equipped \
			else Loc.ui("ui.console.weapon_equip")
		button.pressed.connect(_press_weapon_equip.bind(id, 0))
		button.mouse_entered.connect(button.grab_focus)
		row.add_child(button)
		_register_row(button, id, ROW_WEAPON)
	if slot_type != "main" and GameState.secondary_weapon_slot_count() > 0:
		var secondary := Button.new()
		secondary.custom_minimum_size = Vector2(148, 42)
		var secondary_equipped := GameState.secondary_weapon_id == id
		secondary.text = ("● " if secondary_equipped else "") + "SECONDAIRE [F]"
		secondary.pressed.connect(_press_weapon_equip.bind(id, 1))
		secondary.mouse_entered.connect(secondary.grab_focus)
		row.add_child(secondary)
		_register_row(secondary, id, ROW_WEAPON)
	return row

func _build_equipment_row(id: String) -> Control:
	var definition: Dictionary = DataCatalog.EQUIPMENT[id]
	var owned := GameState.equipment_is_owned(id)
	var row := HBoxContainer.new()
	row.add_theme_constant_override("separation", 8)
	var label := _column(DataCatalog.equipment_name(id), 210, owned)
	label.tooltip_text = str(definition.get("description", ""))
	row.add_child(label)
	row.add_child(_column(str(definition.get("type", "armor")).to_upper(), 105, true))
	if not owned:
		var buy := Button.new()
		var cost := int(definition.get("purchase_cost", 0))
		buy.custom_minimum_size = Vector2(150, 42)
		buy.text = Loc.ui("ui.console.buy_cost") % cost
		buy.disabled = GameState.coins < cost
		buy.pressed.connect(_press_equipment_purchase.bind(id))
		buy.mouse_entered.connect(buy.grab_focus)
		row.add_child(buy)
		_register_row(buy, id, ROW_EQUIPMENT)
		return row
	if GameState.equipment_slot_count() <= 0:
		row.add_child(_column("SLOT VERROUILLÉ", 180, false))
		return row
	for slot: int in range(GameState.equipment_slot_count()):
		var button := Button.new()
		button.custom_minimum_size = Vector2(76, 42)
		var equipped := slot < GameState.equipped_equipment.size() and GameState.equipped_equipment[slot] == id
		button.text = ("● " if equipped else "") + str(slot + 1)
		button.pressed.connect(_press_equipment_equip.bind(id, slot))
		button.mouse_entered.connect(button.grab_focus)
		row.add_child(button)
		_register_row(button, id, ROW_EQUIPMENT)
	return row

func _build_recipe_consumable_row(id: String) -> Control:
	var definition: Dictionary = DataCatalog.RECIPES[id]
	var combat: Dictionary = definition.get("combat_consumable", {})
	var amount := GameState.get_consumable_amount(id)
	var row := HBoxContainer.new()
	row.add_theme_constant_override("separation", 8)
	var label := _column(DataCatalog.recipe_name(id), 210, amount > 0)
	label.tooltip_text = "Mangé" if str(combat.get("mode", "eat")) == "eat" else "Lancé en zone"
	row.add_child(label)
	row.add_child(_column("×%d" % amount, 58, amount > 0))
	var prepare := Button.new()
	prepare.custom_minimum_size = Vector2(128, 42)
	prepare.text = "CUISINER"
	prepare.disabled = not GameState.can_prepare_recipe_consumable(id)
	prepare.tooltip_text = "Ingrédients manquants ou capacité pleine" if prepare.disabled else "Dépense les ingrédients pour préparer une portion"
	prepare.pressed.connect(_press_recipe_consumable_prepare.bind(id))
	prepare.mouse_entered.connect(prepare.grab_focus)
	row.add_child(prepare)
	_register_row(prepare, id, ROW_CONSUMABLE)
	for slot: int in range(GameState.consumable_slot_count()):
		var button := Button.new()
		button.custom_minimum_size = Vector2(64, 42)
		var equipped := slot < GameState.equipped_consumable_slots.size() \
			and GameState.equipped_consumable_slots[slot] == id
		button.text = ("● " if equipped else "") + str(slot + 1)
		button.disabled = amount <= 0
		button.pressed.connect(_press_consumable_equip.bind(id, slot))
		button.mouse_entered.connect(button.grab_focus)
		row.add_child(button)
		_register_row(button, id, ROW_CONSUMABLE)
	return row

func _press_weapon_purchase(id: String) -> void:
	weapon_purchase_requested.emit(id)

func _press_weapon_equip(id: String, slot: int) -> void:
	weapon_equip_requested.emit(id, slot)

func _press_equipment_purchase(id: String) -> void:
	equipment_purchase_requested.emit(id)

func _press_equipment_equip(id: String, slot: int) -> void:
	equipment_equip_requested.emit(id, slot)

func _press_recipe_consumable_prepare(id: String) -> void:
	recipe_consumable_prepare_requested.emit(id)

func _press_consumable_equip(id: String, slot: int) -> void:
	consumable_equip_requested.emit(id, slot)

func _build_ingredients() -> void:
	var total := 0
	for ingredient_id: String in DataCatalog.INGREDIENTS:
		total += GameState.get_ingredient_amount(ingredient_id)
	list.add_child(_section_label(Loc.ui("ui.console.ingredients_total") % total, true))
	for ingredient_id: String in DataCatalog.INGREDIENTS:
		var row := HBoxContainer.new()
		row.add_theme_constant_override("separation", 12)
		var name_label := Label.new()
		name_label.custom_minimum_size = Vector2(360, 42)
		name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
		name_label.text = DataCatalog.ingredient_name(ingredient_id)
		row.add_child(name_label)
		var amount_label := Label.new()
		amount_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
		amount_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
		amount_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
		amount_label.add_theme_color_override("font_color", SECTION_COLOR)
		amount_label.text = Loc.ui("ui.console.ingredient_amount") % GameState.get_ingredient_amount(ingredient_id)
		row.add_child(amount_label)
		ingredient_amount_labels[ingredient_id] = amount_label
		list.add_child(row)

func _build_takeout_rule_row(kind: String) -> Control:
	var row := HBoxContainer.new()
	row.add_theme_constant_override("separation", 8)
	var threshold := GameState.takeout_restock_threshold_value()
	var target := GameState.takeout_restock_target_value()
	var value := threshold if kind == "threshold" else target
	var label := Label.new()
	label.custom_minimum_size = Vector2(350, 40)
	label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
	label.text = (Loc.ui("ui.console.takeout_threshold") if kind == "threshold" else Loc.ui("ui.console.takeout_target")) % value
	row.add_child(label)
	for delta: int in [-1, 1]:
		var button := Button.new()
		button.custom_minimum_size = Vector2(80, 40)
		button.text = "−" if delta < 0 else "+"
		button.pressed.connect(_adjust_takeout_rule.bind(kind, delta))
		button.mouse_entered.connect(button.grab_focus)
		row.add_child(button)
		_register_row(button, kind, ROW_TAKEOUT_RULE)
	return row

func _adjust_takeout_rule(kind: String, delta: int) -> void:
	var threshold := GameState.takeout_restock_threshold_value()
	var target := GameState.takeout_restock_target_value()
	if kind == "threshold":
		threshold += delta
	else:
		target += delta
	GameState.set_takeout_restock_rule(threshold, target)

func _press_takeout_recipe(id: String) -> void:
	takeout_recipe_requested.emit(id)

func _build_employee_row(id: String, role: String, _slots: int) -> Control:
	var definition: Dictionary = DataCatalog.EMPLOYEES[id]
	var hired := GameState.employee_is_hired(id)
	var working := GameState.employee_is_working(id)
	var cost := int(definition.get("hire_cost", 0))
	var allowed := GameState.employee_is_unlocked(id) or hired
	var affordable := GameState.coins >= cost
	var has_locker := GameState.employee_locker_is_installed()
	var row := HBoxContainer.new()
	row.add_theme_constant_override("separation", 8)

	var name_label := Label.new()
	name_label.custom_minimum_size = Vector2(168, 44)
	name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
	name_label.clip_text = true
	name_label.text = DataCatalog.employee_name(id)
	name_label.modulate = Color.WHITE if allowed else DIMMED
	row.add_child(name_label)
	var state_label: Label
	if working:
		state_label = _column(Loc.ui("ui.console.employee_working"), 112, true)
	elif hired:
		state_label = _column(Loc.ui("ui.console.employee_standby"), 112, false)
		var reason := GameState.employee_standby_reason(id)
		state_label.tooltip_text = Loc.ui("ui.console.standby_reason_%s" % reason)
	else:
		state_label = _column("%d c" % cost, 112, affordable)
	row.add_child(state_label)

	if role == "server":
		var profile := VBoxContainer.new()
		profile.size_flags_horizontal = Control.SIZE_EXPAND_FILL
		profile.add_theme_constant_override("separation", 2)
		profile.add_child(_server_preference_panel(definition, allowed))
		var traits := Label.new()
		traits.text = _skill_summary(definition, 2)
		traits.add_theme_font_size_override("font_size", 10)
		traits.modulate = DIMMED if allowed else DIMMED.darkened(0.25)
		profile.add_child(traits)
		row.add_child(profile)
	else:
		var skills := Label.new()
		skills.size_flags_horizontal = Control.SIZE_EXPAND_FILL
		skills.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
		skills.text = _skill_summary(definition)
		skills.modulate = Color.WHITE if allowed else DIMMED
		row.add_child(skills)

	var button := Button.new()
	button.custom_minimum_size = Vector2(176, 44)
	if working:
		button.text = Loc.ui("ui.console.standby")
		button.tooltip_text = Loc.ui("ui.console.standby_tooltip")
		button.pressed.connect(_press_standby.bind(id))
	elif hired:
		var blocker := GameState.employee_work_blocker(id)
		button.text = Loc.ui("ui.console.work")
		button.tooltip_text = Loc.ui("ui.console.work_tooltip") if blocker.is_empty() else blocker
		button.disabled = not blocker.is_empty()
		button.modulate = Color.WHITE if blocker.is_empty() else DIMMED
		button.pressed.connect(_press_work.bind(id))
	else:
		button.text = Loc.ui("ui.console.hire") if affordable else Loc.ui("ui.console.insufficient_resources")
		button.tooltip_text = _hire_tooltip(has_locker, affordable)
		button.disabled = not (has_locker and affordable)
		button.modulate = Color.WHITE if not button.disabled else DIMMED
		button.pressed.connect(_press_hire.bind(id))
	button.mouse_entered.connect(button.grab_focus)
	row.add_child(button)

	_register_row(button, id, ROW_EMPLOYEE)
	return row

func _hire_tooltip(has_locker: bool, affordable: bool) -> String:
	if not has_locker:
		return Loc.ui("ui.console.hire_no_locker")
	if not affordable:
		return Loc.ui("ui.console.hire_no_coins")
	return Loc.ui("ui.console.hire_tooltip")

func _role_name(role: String) -> String:
	return Loc.ui("ui.role.cook") if role == "cook" else Loc.ui("ui.role.server")

# Four compact gauges are easier to compare at hiring time than another list of
# prose stats. Their common 0–5 scale controls choice only, never performance.
func _server_preference_panel(definition: Dictionary, allowed: bool) -> Control:
	var panel := HBoxContainer.new()
	panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	panel.add_theme_constant_override("separation", 6)
	var values: Dictionary = definition.get("task_preferences", {})
	for preference: String in DataCatalog.SERVER_TASK_PREFERENCES:
		var value := clampf(float(values.get(preference, 1.0)), 0.0, 5.0)
		var gauge := VBoxContainer.new()
		gauge.custom_minimum_size = Vector2(76, 28)
		gauge.add_theme_constant_override("separation", 1)
		var title := Label.new()
		title.text = "%s %.1f" % [Loc.ui("ui.server_preference.%s" % preference), value]
		title.add_theme_font_size_override("font_size", 9)
		title.tooltip_text = title.text
		gauge.add_child(title)
		var bar := ProgressBar.new()
		bar.custom_minimum_size = Vector2(74, 7)
		bar.max_value = 5.0
		bar.value = value
		bar.show_percentage = false
		bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
		bar.modulate = Color.WHITE if allowed else DIMMED
		gauge.add_child(bar)
		panel.add_child(gauge)
	return panel

# Ce qui distingue vraiment un employé: ses meilleures compétences, et son
# affinité de poste s'il en a une — c'est elle qui fait d'un cuisinier un
# épicier, sans qu'aucun champ ne le déclare.
func _skill_summary(definition: Dictionary, skill_limit: int = 3) -> String:
	var parts: Array[String] = []
	for entry: Dictionary in _ranked_pairs(definition.get("skills", {})):
		if parts.size() >= skill_limit:
			break
		parts.append("%s ×%.1f" % [Loc.ui("ui.skill.%s" % entry.key), float(entry.value)])
	var affinities: Array[Dictionary] = _ranked_pairs(definition.get("station_affinity", {}))
	if not affinities.is_empty():
		parts.append("%s ×%.1f" % [Loc.ui("ui.station_kind.%s" % affinities[0].key), float(affinities[0].value)])
	return "   ".join(parts)

func _ranked_pairs(source: Variant) -> Array[Dictionary]:
	var ranked: Array[Dictionary] = []
	if not source is Dictionary:
		return ranked
	for key: String in (source as Dictionary):
		var value := float((source as Dictionary)[key])
		if value > 0.0:
			ranked.append({"key": key, "value": value})
	ranked.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return float(a.value) > float(b.value))
	return ranked

func _column(text: String, width: float, cookable: bool) -> Label:
	var label := Label.new()
	label.custom_minimum_size = Vector2(width, 0)
	label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
	label.text = text
	label.modulate = Color.WHITE if cookable else DIMMED
	return label

# Chaque ingrédient montre toujours possédé/requis. La couleur suffit à dire si
# la quantité couvre la recette; le format ne change donc jamais sous le joueur.
func _ingredient_column(recipe: Dictionary) -> Control:
	var box := HBoxContainer.new()
	box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	box.add_theme_constant_override("separation", 10)
	for ingredient_id: String in recipe.ingredients:
		var required := int(recipe.ingredients[ingredient_id])
		var owned := GameState.get_ingredient_amount(ingredient_id)
		var enough := owned >= required
		var entry := Label.new()
		entry.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
		entry.add_theme_color_override("font_color", SUFFICIENT_COLOR if enough else MISSING_COLOR)
		entry.text = "%s %d/%d" % [DataCatalog.ingredient_name(ingredient_id), owned, required]
		box.add_child(entry)
	return box

func _press_switch() -> void:
	open_toggle_requested.emit()

func _press_hire(id: String) -> void:
	hire_requested.emit(id)

func _press_work(id: String) -> void:
	employee_work_requested.emit(id)

func _press_standby(id: String) -> void:
	employee_standby_requested.emit(id)

func _focused_index() -> int:
	var focused := get_viewport().gui_get_focus_owner() as Button
	return row_buttons.find(focused) if focused != null else -1

func _restore_focus(index: int) -> void:
	if index >= 0 and index < row_buttons.size():
		_focus_button(row_buttons[index])
	else:
		_focus_first_available()

func _focus_first_available() -> void:
	if mode == Mode.CONSOLE:
		var actions := _top_action_buttons()
		if not actions.is_empty():
			_focus_button(actions[0])
			return
	var rows := _available_row_buttons()
	_focus_button(rows[0] if not rows.is_empty() else close_button)

func _navigate(direction: int) -> void:
	var focused := get_viewport().gui_get_focus_owner() as Button
	var actions := _top_action_buttons()
	var rows := _available_row_buttons()
	if focused != null and actions.has(focused):
		if direction > 0:
			if mode == Mode.CONSOLE and not tab_buttons.is_empty():
				_focus_tab()
			else:
				_focus_button(rows[0] if not rows.is_empty() else close_button)
		elif not rows.is_empty():
			_focus_button(rows[-1])
		return
	# Depuis la barre d'onglets: vers le bas on entre dans la liste, vers le
	# haut on revient aux actions permanentes.
	if focused != null and tab_buttons.has(focused):
		if direction > 0 and not rows.is_empty():
			_focus_button(rows[0])
		else:
			_focus_button(actions[0] if not actions.is_empty() else close_button)
		return
	if rows.is_empty():
		_focus_top()
		return
	var index := rows.find(focused)
	if index < 0:
		_focus_first_available()
		return
	var target_index := index + direction
	if target_index < 0:
		_focus_top()
	elif target_index >= rows.size():
		_focus_button(actions[0] if mode == Mode.CONSOLE and not actions.is_empty() else close_button)
	else:
		_focus_button(rows[target_index])

# Ce qui est au-dessus de la liste: la barre d'onglets quand elle existe, le
# bouton de fermeture sinon.
func _focus_top() -> void:
	if mode == Mode.CONSOLE and not tab_buttons.is_empty():
		_focus_tab()
		return
	close_button.grab_focus()

func _focus_button(button: Button) -> void:
	button.grab_focus()
	if row_buttons.has(button):
		scroll.call_deferred("ensure_control_visible", button)

func _top_action_buttons() -> Array[Button]:
	var available: Array[Button] = []
	for button: Button in [dining_toggle, takeout_toggle, build_button, close_button]:
		if is_instance_valid(button) and button.visible and not button.disabled:
			available.append(button)
	return available

func _available_row_buttons() -> Array[Button]:
	var available: Array[Button] = []
	for button: Button in row_buttons:
		if is_instance_valid(button) and not button.disabled:
			available.append(button)
	return available

func _get_navigation_buttons() -> Array[Button]:
	var available := _top_action_buttons()
	available.append_array(tab_buttons)
	available.append_array(_available_row_buttons())
	return available

func _confirm_focused() -> void:
	var focused := get_viewport().gui_get_focus_owner() as Button
	if focused != null and (_top_action_buttons().has(focused) or tab_buttons.has(focused)):
		focused.pressed.emit()
	elif focused != null and row_buttons.has(focused):
		# Une bascule ne s'active pas par `pressed`: c'est `toggled` qui porte le
		# changement, donc le clavier doit inverser l'état lui-même.
		if focused.toggle_mode:
			focused.button_pressed = not focused.button_pressed
		else:
			focused.pressed.emit()
	else:
		_focus_first_available()

func _select(id: String) -> void:
	if not GameState.recipe_is_unlocked(id):
		GameState.toast_requested.emit(Loc.ui("ui.menu.locked_for") % DataCatalog.recipe_name(id))
		return
	if not GameState.can_cook(id):
		GameState.toast_requested.emit(Loc.ui("ui.menu.missing_for") % DataCatalog.recipe_name(id))
		return
	recipe_selected.emit(id)
	close()
