class_name BuildMode
extends Node2D

# Le mode construction.
#
# --- Trois états, et rien d'autre ---------------------------------------------
#
# `IDLE`      rien de choisi. On clique un meuble pour le prendre en main, ou une
#             ligne du menu pour en sortir un de la réserve.
# `SELECTED`  un meuble est choisi. Ses quatre actions flottent au-dessus de lui:
#             bouger, tourner, ranger, désélectionner. Clic droit désélectionne.
# `MOVING`    le meuble suit le curseur, vert s'il tient, rouge sinon. Clic
#             gauche pose, clic droit annule.
#
# --- Pourquoi cette réécriture -------------------------------------------------
#
# La version d'avant dessinait TOUT avec `draw_rect` dans l'espace du monde, y
# compris son menu. Deux conséquences en 3D:
#
#   - un dessin 2D passe **après** la 3D dans le même viewport: les rectangles se
#     peignaient par-dessus la salle, et rien ne pouvait les éteindre;
#   - le menu se calait sur `get_camera_2d()`, qui n'existe pas sous une caméra
#     3D: il retombait sur une position écrite en dur, dans le monde.
#
# L'interface vit donc sur une couche d'écran, en vrais contrôles. Ce qui
# appartient au sol — le fantôme, les cases valides, les contours — est posé
# dans la scène 3D par `GroundMarks3D`, et garde son dessin 2D pour la branche
# sans pont.
#
# Le mode ne connaît que des `RestaurantElement`: il ne sait pas ce qu'est un
# four ni une table. C'est la salle qui décide où chaque chose a le droit d'être
# (`placement_blocker`) et qui applique le plan une fois posé.

signal layout_changed
signal closed

const GroundMarks3D := preload("res://scripts/core/ground_marks_3d.gd")

const CELL := 32.0
const VALID_COLOR := Color(0.29, 0.85, 0.45, 0.22)
const GHOST_OK := Color(0.29, 0.85, 0.45, 0.45)
const GHOST_BAD := Color(0.95, 0.35, 0.35, 0.45)
const GHOST_EDGE := Color(1.0, 1.0, 1.0, 0.85)
const PICK_EDGE := Color(1.0, 1.0, 1.0, 0.45)
const CHOSEN_EDGE := Color(1.0, 0.82, 0.4, 0.95)
# Les actions flottent un peu au-dessus du meuble, en PIXELS: sous une caméra
# inclinée, « au-dessus » ne veut rien dire dans le monde.
const ACTION_LIFT := 26.0

enum Stage { IDLE, SELECTED, MOVING }

var restaurant: Node
var active := false
var stage := Stage.IDLE
## Le meuble choisi, en `SELECTED` comme en `MOVING`.
var chosen: RestaurantElement
## Où le meuble retourne si on annule. Un déplacement abandonné ne doit jamais
## laisser un four ailleurs qu'à son point de départ.
var origin_position := Vector2.ZERO
var origin_rotation := 0
## Un exemplaire sorti de la réserve n'était pas posé: l'annuler doit l'y
## renvoyer, et non le laisser tomber au sol. Les deux annulations se
# ressemblent, mais elles ne rendent pas la même chose.
var came_from_reserve := false
var cursor := Vector2.ZERO
var last_blocker := ""

var _layer: CanvasLayer
var _palette: VBoxContainer
var _actions: HBoxContainer
var _hint: Label
var _done: Button
var _marks: Node3D
var _valid_cells: Array[Vector2] = []

func setup(owner_restaurant: Node) -> void:
	restaurant = owner_restaurant
	z_index = 40
	visible = false
	set_process(false)
	set_process_unhandled_input(false)
	_build_interface()

func open() -> void:
	if active or restaurant == null or not is_instance_valid(restaurant):
		return
	active = true
	visible = true
	_enter_idle()
	set_process(true)
	set_process_unhandled_input(true)
	_layer.visible = true
	_refresh_palette()
	queue_redraw()

func close() -> void:
	if not active:
		return
	# Un mode qu'on quitte en portant un meuble le repose d'où il vient: sortir
	# ne doit pas être une façon de perdre un four.
	_cancel_move()
	active = false
	visible = false
	stage = Stage.IDLE
	chosen = null
	_layer.visible = false
	_release_marks()
	set_process(false)
	set_process_unhandled_input(false)
	queue_redraw()
	closed.emit()

# --- L'interface, à l'écran ----------------------------------------------------

func _build_interface() -> void:
	_layer = CanvasLayer.new()
	_layer.name = "BuildInterface"
	_layer.layer = 60
	_layer.visible = false
	add_child(_layer)

	var left := VBoxContainer.new()
	left.name = "Palette"
	left.set_anchors_preset(Control.PRESET_TOP_LEFT)
	left.position = Vector2(18, 96)
	left.custom_minimum_size = Vector2(252, 0)
	left.add_theme_constant_override("separation", 4)
	_layer.add_child(left)

	var title := Label.new()
	title.text = Loc.ui("ui.build.palette")
	left.add_child(title)
	_palette = VBoxContainer.new()
	_palette.add_theme_constant_override("separation", 3)
	left.add_child(_palette)

	_done = Button.new()
	_done.text = Loc.ui("ui.build.done")
	_done.custom_minimum_size = Vector2(252, 38)
	_done.pressed.connect(close)
	left.add_child(_done)

	_hint = Label.new()
	_hint.name = "Blocker"
	_hint.add_theme_color_override("font_color", Color("ff9c9c"))
	_hint.custom_minimum_size = Vector2(252, 0)
	_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	left.add_child(_hint)

	# Les actions du meuble choisi. Elles vivent a l'ecran et suivent sa position
	# projetee: c'est la seule facon d'etre « au-dessus du meuble » sous une
	# camera inclinee.
	_actions = HBoxContainer.new()
	_actions.name = "Actions"
	_actions.add_theme_constant_override("separation", 4)
	_actions.visible = false
	_layer.add_child(_actions)
	for entry: Array in [
			["ui.build.move", _begin_move], ["ui.build.rotate", _rotate_chosen],
			["ui.build.store", _store_chosen], ["ui.build.deselect", _enter_idle]]:
		var button := Button.new()
		button.text = Loc.ui(str(entry[0]))
		button.pressed.connect(entry[1] as Callable)
		_actions.add_child(button)

func _refresh_palette() -> void:
	# Retire de l'arbre tout de suite, libere ensuite: `queue_free` seul laisse
	# l'ancienne ligne comptee jusqu'a la fin de l'image, et le menu mentirait
	# sur ce qu'il contient.
	for child: Node in _palette.get_children():
		_palette.remove_child(child)
		child.queue_free()
	for source_id: String in restaurant.placeable_sources():
		var left_over := int(restaurant.remaining_count(source_id))
		var button := Button.new()
		button.text = "%s   %d/%d" % [DataCatalog.restaurant_element_name(source_id),
			restaurant.placed_count(source_id), restaurant.resolved_max_count(source_id)]
		button.alignment = HORIZONTAL_ALIGNMENT_LEFT
		button.custom_minimum_size = Vector2(252, 30)
		button.disabled = left_over <= 0
		# Prendre une ligne du menu ouvre le MEME mode bouger: un meuble qui sort
		# de la reserve se place comme un meuble qu'on deplace.
		button.pressed.connect(_take_from_reserve.bind(source_id))
		_palette.add_child(button)

# Où poser la barre d'actions: juste au-dessus du meuble, en pixels d'écran.
func _place_action_bar() -> void:
	if chosen == null or not is_instance_valid(chosen):
		_actions.visible = false
		return
	var box := chosen.footprint()
	var top := Vector2(box.get_center().x, box.position.y)
	var screen := Bridge3D.plan_to_screen(top) if Render3D.active \
		else get_viewport().get_canvas_transform() * top
	if screen == Vector2.INF:
		_actions.visible = false
		return
	_actions.visible = true
	var size := _actions.get_combined_minimum_size()
	_actions.position = screen - Vector2(size.x * 0.5, size.y + ACTION_LIFT)

# --- Le déroulé ----------------------------------------------------------------

func _process(_delta: float) -> void:
	if not active:
		return
	cursor = _pointer_plan()
	if stage == Stage.MOVING and chosen != null:
		last_blocker = str(restaurant.placement_blocker(chosen, _snapped(cursor)))
	_hint.text = last_blocker
	_actions.visible = stage == Stage.SELECTED
	if stage == Stage.SELECTED:
		_place_action_bar()
	_refresh_marks()
	queue_redraw()

# Le curseur, résolu sur le SOL. En 3D la caméra est inclinée: viser le plan du
# héros ferait atterrir chaque meuble derrière le curseur.
func _pointer_plan() -> Vector2:
	if Render3D.active:
		var plan := Bridge3D.ground_plan_position()
		if plan != Vector2.INF:
			return plan
	return get_global_mouse_position()

# La grille cale les meubles les uns sur les autres: sans elle, deux postes
# voisins finissent décalés de trois pixels et la cuisine a l'air de travers.
func _snapped(world: Vector2) -> Vector2:
	var wanted := Vector2(roundf(world.x / CELL) * CELL, roundf(world.y / CELL) * CELL)
	if chosen != null and chosen.is_anchored():
		# Un élément ancré ne quitte pas son mur: seul le glissement compte.
		wanted.y = chosen.position.y
	return wanted

func _enter_idle() -> void:
	_cancel_move()
	stage = Stage.IDLE
	chosen = null
	last_blocker = ""
	_valid_cells.clear()
	_actions.visible = false
	queue_redraw()

func _select(element: RestaurantElement) -> void:
	if element == null or not element.movable:
		return
	stage = Stage.SELECTED
	chosen = element
	last_blocker = ""
	queue_redraw()

func _begin_move() -> void:
	if chosen == null:
		return
	stage = Stage.MOVING
	origin_position = chosen.position
	origin_rotation = chosen.rotation_steps
	came_from_reserve = false
	_rebuild_valid_cells()

# Sortir un exemplaire de la réserve: la fiche existe déjà, elle change d'état.
# Elle n'est posée qu'au clic suivant, et jamais si on annule.
func _take_from_reserve(source_id: String) -> void:
	if stage == Stage.MOVING:
		return
	var spare: RestaurantElement = restaurant.spare_element(source_id)
	if spare == null:
		return
	chosen = spare
	origin_position = spare.position
	origin_rotation = spare.rotation_steps
	came_from_reserve = true
	chosen.placed = false
	chosen.position = restaurant.first_free_position(chosen)
	stage = Stage.MOVING
	_rebuild_valid_cells()

func _place_chosen() -> void:
	if chosen == null:
		return
	var wanted := _snapped(cursor)
	if not str(restaurant.placement_blocker(chosen, wanted)).is_empty():
		return
	chosen.position = wanted
	chosen.placed = true
	# Reposer volontairement annule le retrait: le meuble redevient un meuble
	# ordinaire, que les quantités peuvent de nouveau gouverner.
	chosen.dismissed = false
	came_from_reserve = false
	restaurant.apply_layout()
	restaurant.save_layout()
	layout_changed.emit()
	# On reste sur le meuble qu'on vient de poser: le geste suivant est presque
	# toujours de le tourner ou de le reprendre.
	stage = Stage.SELECTED
	_valid_cells.clear()
	_refresh_palette()
	queue_redraw()

# Annuler un déplacement rend le meuble à sa place; annuler une sortie de
# réserve l'y renvoie. Les deux se ressemblent, mais ils ne rendent pas la
# même chose — et confondre les deux ferait tomber un four au milieu du sol.
func _cancel_move() -> void:
	if stage != Stage.MOVING or chosen == null:
		return
	chosen.position = origin_position
	chosen.rotation_steps = origin_rotation
	# Aujourd'hui tous les meubles sont gratuits, et c'est
	# `_sync_placement_with_limits()` qui decide alors de leur pose: cette ligne
	# ne mord donc pas encore. Elle est ecrite pour le jour ou un meuble se
	# paiera — l'annuler ne doit pas revenir a l'obtenir pour rien.
	chosen.placed = not came_from_reserve
	restaurant.apply_layout()
	stage = Stage.SELECTED if not came_from_reserve else Stage.IDLE
	if came_from_reserve:
		chosen = null
	came_from_reserve = false
	_valid_cells.clear()
	_refresh_palette()
	queue_redraw()

# Tourner peut échouer: le meuble tourné n'entre pas forcément là où il est.
# On le dit au lieu de ne rien faire.
func _rotate_chosen() -> void:
	if chosen == null:
		return
	var before := chosen.rotation_steps
	chosen.rotate_step()
	var target := chosen.position if stage != Stage.MOVING else _snapped(cursor)
	var blocker := str(restaurant.placement_blocker(chosen, target))
	if not blocker.is_empty():
		chosen.rotation_steps = before
		last_blocker = blocker
		return
	last_blocker = ""
	if stage != Stage.MOVING:
		restaurant.apply_layout()
		restaurant.save_layout()
		layout_changed.emit()
	else:
		_rebuild_valid_cells()
	queue_redraw()

# Ranger rend le meuble au menu. Certains meubles en exigent d'autres: ranger
# l'étagère désactiverait la brigade sans un mot, alors on nomme ce qui tombe
# avec.
func _store_chosen() -> void:
	if chosen == null:
		return
	var broken := _dependants_of(chosen.source_id)
	if not broken.is_empty():
		last_blocker = Loc.ui("ui.build.store_breaks") % ", ".join(broken)
	restaurant.remove_element(chosen)
	_enter_idle()
	_refresh_palette()
	layout_changed.emit()

func _dependants_of(source_id: String) -> Array[String]:
	var broken: Array[String] = []
	for element: RestaurantElement in restaurant.movable_elements():
		if element.source_id == source_id or not element.placed:
			continue
		var definition: Dictionary = DataCatalog.RESTAURANT_ELEMENTS.get(element.source_id, {})
		for raw_requirement: Variant in definition.get("placement_requires", []):
			if str(raw_requirement) == source_id:
				broken.append(DataCatalog.restaurant_element_name(element.source_id))
				break
	return broken

# --- Ce que le joueur fait -----------------------------------------------------

func _unhandled_input(event: InputEvent) -> void:
	if not active:
		return
	if event is InputEventKey and event.pressed and not event.echo:
		var key := event as InputEventKey
		# `R` tourne aussi PENDANT le déplacement: sans cela il faudrait poser,
		# resélectionner, tourner — et la rotation peut ne plus tenir là où on
		# vient de poser.
		if key.keycode == KEY_R:
			_rotate_chosen()
			get_viewport().set_input_as_handled()
			return
		if key.keycode == KEY_ESCAPE:
			# Échap défait un cran à la fois: le déplacement, puis la sélection,
			# puis le mode.
			if stage == Stage.MOVING:
				_cancel_move()
			elif stage == Stage.SELECTED:
				_enter_idle()
			else:
				close()
			get_viewport().set_input_as_handled()
			return
	if not event is InputEventMouseButton or not event.is_pressed():
		return
	var click := event as InputEventMouseButton
	if click.button_index == MOUSE_BUTTON_RIGHT:
		# Clic droit: annule le déplacement, sinon désélectionne. Les mains
		# vides, il ne fait rien — fermer le mode sur un clic égaré serait brutal.
		if stage == Stage.MOVING:
			_cancel_move()
		elif stage == Stage.SELECTED:
			_enter_idle()
		get_viewport().set_input_as_handled()
		return
	if click.button_index != MOUSE_BUTTON_LEFT:
		return
	if stage == Stage.MOVING:
		_place_chosen()
	else:
		_select(restaurant.element_at(_pointer_plan()))
	get_viewport().set_input_as_handled()

# --- Les marques au sol --------------------------------------------------------

func _rebuild_valid_cells() -> void:
	_valid_cells.clear()
	if chosen == null:
		return
	var area: Rect2 = restaurant.placement_area(chosen)
	var x := floorf(area.position.x / CELL) * CELL
	while x <= area.end.x:
		var y := floorf(area.position.y / CELL) * CELL
		while y <= area.end.y:
			var candidate := _snapped(Vector2(x, y))
			if str(restaurant.placement_blocker(chosen, candidate)).is_empty():
				_valid_cells.append(candidate)
			y += CELL
		x += CELL

func _marks_node() -> Node3D:
	if _marks != null and is_instance_valid(_marks):
		return _marks
	var root := Bridge3D.world_root()
	if root == null:
		return null
	_marks = GroundMarks3D.new()
	_marks.name = "BuildMarks"
	root.add_child(_marks)
	return _marks

func _release_marks() -> void:
	if _marks != null and is_instance_valid(_marks):
		_marks.queue_free()
	_marks = null

func _refresh_marks() -> void:
	if not Render3D.active:
		_release_marks()
		return
	var marks := _marks_node()
	if marks == null:
		return
	marks.show_cells("%s:%d:%d" % [chosen.source_id if chosen != null else "",
		chosen.rotation_steps if chosen != null else 0, _valid_cells.size()],
		_valid_cells, CELL, VALID_COLOR)
	if stage == Stage.MOVING and chosen != null:
		marks.show_ghost(chosen.footprint_at(_snapped(cursor)),
			GHOST_OK if last_blocker.is_empty() else GHOST_BAD, GHOST_EDGE)
	else:
		marks.hide_ghost()
	var boxes: Array[Rect2] = []
	if stage == Stage.SELECTED and chosen != null:
		boxes.append(chosen.footprint())
		marks.show_outlines(boxes, CHOSEN_EDGE)
	else:
		for element: RestaurantElement in restaurant.movable_elements():
			boxes.append(element.footprint())
		marks.show_outlines(boxes, PICK_EDGE)

# Le dessin 2D reste pour la branche sans pont. En 3D il se tait: un sprite se
# peint APRÈS la scène dans le même viewport, et rien ne l'y rattraperait.
func _draw() -> void:
	if not active or Render3D.active:
		return
	for cell: Vector2 in _valid_cells:
		draw_rect(Rect2(cell - Vector2(CELL, CELL) * 0.5, Vector2(CELL, CELL)), VALID_COLOR)
	if stage == Stage.MOVING and chosen != null:
		var ghost := chosen.footprint_at(_snapped(cursor))
		draw_rect(ghost, GHOST_OK if last_blocker.is_empty() else GHOST_BAD)
		draw_rect(ghost, GHOST_EDGE, false, 2.0)
		return
	if stage == Stage.SELECTED and chosen != null:
		draw_rect(chosen.footprint(), CHOSEN_EDGE, false, 3.0)
		return
	for element: RestaurantElement in restaurant.movable_elements():
		draw_rect(element.footprint(), PICK_EDGE, false, 2.0)
