class_name DebugPanel
extends CanvasLayer

# Le panneau de débogage, sous F3.
#
# Il montre ce qu'un contrôle headless ne peut pas montrer — ce que le clavier
# envoie réellement à la fenêtre, la cadence, les jets de butin — et il offre
# quelques gestes qui sautent le trajet normal du jeu: éprouver une branche de
# l'arbre d'upgrades demandait sinon de jouer la partie jusqu'à pouvoir se la
# payer.
#
# Pourquoi un fichier à part plutôt que dans le pont 3D, qui l'affiche: le pont
# ne décide d'aucune règle et n'écrit jamais dans l'état du jeu — un contrôle
# veille sur cet invariant. Donner des rondelles est un geste de jeu. Le pont
# montre, ce panneau agit.
#
# C'est un menu maître (13/10/2026): ouvert, il fige la partie — une scène
# d'histoire, la carte ou une boutique comprises —, prend toutes les touches et
# tous les clics, et rend tout tel quel en se fermant (F3 ou Retour). Échap reste
# à la pause, qui s'ouvre par-dessus comme partout. Seule la caméra reste vivante:
# les réglages de vue se voient sur la scène figée derrière le voile.

## Ce qu'un bouton donne, quand il donne des rondelles. Assez pour éprouver
## n'importe quelle branche de l'arbre sans jouer la partie.
const COIN_GRANT := 1000000
## Les dégâts fixes que le bouton fait défiler; zéro rend les dégâts normaux.
const DAMAGE_STEPS := [0.0, 25.0, 50.0, 100.0, 500.0, 1000.0]
## Les réglages de vue à l'essai: clé, libellé, pas, minimum, maximum. La
## distance d'une caméra orthographique ne change que le cadrage: c'est le zoom.
const VIEW_STEPS := [
	["tilt", "Inclinaison (X)", 2.0, 10.0, 89.0],
	["yaw", "Rotation (Y)", 5.0, -180.0, 180.0],
	["roll", "Roulis (Z)", 1.0, -30.0, 30.0],
	["zoom", "Distance (zoom)", 0.05, 0.3, 3.0],
]
## Zéro garde l'orthographique; au-delà, la perspective et son angle de champ.
const FOV_STEPS := [0.0, 10.0, 20.0, 30.0, 45.0, 60.0, 75.0]
const VIEW_COLUMN_X := 1440.0

var _label: Label
var _buttons: Array[Button] = []
var _view_title: Label
var _view_values: Dictionary = {}
const ProgressionMenu := preload("res://scripts/core/debug_progression_menu.gd")
## Au-dessus de tout ce qui se pose à l'écran (la course du départ et le
## générique sont à 90): seule la pause passe devant, par la pile des interfaces.
const LAYER := 120
## Le voile: la partie figée reste visible derrière (la caméra s'y règle), et il
## avale les clics qui ne tombent pas sur le menu.
const VEIL_COLOR := Color(0.0, 0.0, 0.0, 0.35)

var is_open := false
var _was_paused := false
## Les interfaces figées (une scène, la carte) et leur mode d'avant.
var _frozen: Dictionary = {}
var _rig_mode := -1
var _veil: ColorRect

func _ready() -> void:
	layer = LAYER
	process_mode = Node.PROCESS_MODE_ALWAYS
	visible = false
	_veil = ColorRect.new()
	_veil.name = "Veil"
	_veil.color = VEIL_COLOR
	_veil.mouse_filter = Control.MOUSE_FILTER_STOP
	_veil.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	add_child(_veil)
	_label = Label.new()
	_label.position = Vector2(20, 100 + 38 * _actions().size())
	_label.add_theme_font_size_override("font_size", 18)
	_label.add_theme_color_override("font_color", Color("9dfc9d"))
	add_child(_label)
	_build_buttons()
	_build_view_controls()
	var progression := ProgressionMenu.new()
	progression.name = "Progression"
	progression.position = Vector2(570, 100)
	progression.size = Vector2(840, 800)
	add_child(progression)

func _process(_delta: float) -> void:
	if visible:
		_refresh_view_labels()

# --- Le menu maître ----------------------------------------------------------------

func toggle() -> void:
	if is_open:
		close()
	else:
		open()

func open() -> void:
	if is_open:
		return
	is_open = true
	_was_paused = get_tree().paused
	get_tree().paused = true
	# Ce qui tourne encore sous la pause (une scène, la carte) se fige aussi.
	_frozen.clear()
	for node: Node in Stage.open_interfaces():
		if node != self and is_instance_valid(node) and node.process_mode != Node.PROCESS_MODE_DISABLED:
			_frozen[node] = node.process_mode
			node.process_mode = Node.PROCESS_MODE_DISABLED
	# La caméra seule reste vivante: un réglage de vue se voit aussitôt.
	var rig := _rig()
	_rig_mode = -1
	if rig != null:
		_rig_mode = rig.process_mode
		rig.process_mode = Node.PROCESS_MODE_ALWAYS
	layer = Stage.layer_above_interfaces(LAYER, self)
	Stage.push_interface(self)
	visible = true

func close() -> void:
	if not is_open:
		return
	is_open = false
	visible = false
	Stage.pop_interface(self)
	for node: Variant in _frozen:
		if is_instance_valid(node):
			(node as Node).process_mode = _frozen[node]
	_frozen.clear()
	var rig := _rig()
	if rig != null and _rig_mode >= 0:
		rig.process_mode = _rig_mode
	get_tree().paused = _was_paused

## Ouvert, il prend toutes les touches: Retour le ferme, Échap (la pause) passe, le
## reste s'arrête ici. F3 se lit dans l'état du clavier, par le pont.
func _input(event: InputEvent) -> void:
	if not is_open:
		return
	if not Stage.accepts_input(self):
		return
	if Stage.is_back(event):
		close()
		get_viewport().set_input_as_handled()
		return
	# Les clics vont au menu; le voile avale ceux qui tombent à côté.
	if event is InputEventMouse or event.is_action("pause"):
		return
	get_viewport().set_input_as_handled()

## Les gestes, en colonne au-dessus du texte.
##
## `focus_mode` reste à NONE: un bouton qui garde le focus après un clic avale
## les touches de déplacement, et le héros reste planté sans qu'on comprenne
## pourquoi.
func _build_buttons() -> void:
	var column := VBoxContainer.new()
	column.position = Vector2(20, 100)
	column.add_theme_constant_override("separation", 6)
	add_child(column)
	for entry: Array in _actions():
		var button := Button.new()
		button.focus_mode = Control.FOCUS_NONE
		button.pressed.connect(entry[1])
		button.pressed.connect(_refresh_labels)
		column.add_child(button)
		_buttons.append(button)
	_refresh_labels()

# Un réglage dit son état sur son bouton: un clic se lit sans chercher.
func _refresh_labels() -> void:
	var entries := _actions()
	for index: int in mini(entries.size(), _buttons.size()):
		var text: Variant = entries[index][0]
		_buttons[index].text = str(text.call()) if text is Callable else str(text)

## Ce que le panneau sait faire. Une liste plutôt qu'une suite d'appels: en
## ajouter un demande une ligne, et le contrôle les parcourt tous.
func _actions() -> Array:
	return [
		["+%s rondelles" % thousands(COIN_GRANT), grant_coins],
		[func() -> String: return "Invincible: %s" % ("OUI" if GameState.debug_invincible else "non"), toggle_invincible],
		[func() -> String: return "Stamina infinie: %s" % ("OUI" if GameState.debug_infinite_stamina else "non"), toggle_infinite_stamina],
		[func() -> String: return "Dégâts du joueur: %s" % (str(int(GameState.debug_player_damage)) if GameState.debug_player_damage > 0.0 else "normaux"), cycle_player_damage],
		[func() -> String: return "Patience client: %s" % ("B, dans la bulle" if InfoBadge3D.patience_in_bubble else "A, barre dessous"), toggle_patience_in_bubble],
	]

func toggle_invincible() -> void:
	GameState.debug_invincible = not GameState.debug_invincible

func toggle_infinite_stamina() -> void:
	GameState.debug_infinite_stamina = not GameState.debug_infinite_stamina

## Essai A/B de la bulle de commande: un réglage d'affichage, pas de jeu, il
## vit donc sur le badge et non dans GameState.
func toggle_patience_in_bubble() -> void:
	InfoBadge3D.patience_in_bubble = not InfoBadge3D.patience_in_bubble

## Normaux, 25, 50, 100, 500, 1000, puis normaux à nouveau.
func cycle_player_damage() -> void:
	var at := DAMAGE_STEPS.find(GameState.debug_player_damage)
	GameState.debug_player_damage = float(DAMAGE_STEPS[(at + 1) % DAMAGE_STEPS.size()])

func grant_coins() -> void:
	GameState.add_coins(COIN_GRANT)
	GameState.toast_requested.emit("Débogage: +%s rondelles" % thousands(COIN_GRANT))

# --- La vue de la caméra --------------------------------------------------------
#
# L'auteur cherche l'angle juste en jouant, puis recopie les valeurs pour qu'on
# les fixe. Le réglage vaut pour la scène affichée — restaurant ou donjon — et
# se garde d'une scène à l'autre, jamais dans la sauvegarde.

func _build_view_controls() -> void:
	var column := VBoxContainer.new()
	column.name = "CameraView"
	column.position = Vector2(VIEW_COLUMN_X, 100)
	column.add_theme_constant_override("separation", 6)
	add_child(column)
	_view_title = Label.new()
	_view_title.add_theme_font_size_override("font_size", 18)
	_view_title.add_theme_color_override("font_color", Color("9dfc9d"))
	column.add_child(_view_title)
	var rows: Array = VIEW_STEPS.map(func(entry: Array) -> String: return str(entry[0]))
	rows.append("fov")
	for key: String in rows:
		var row := HBoxContainer.new()
		row.add_theme_constant_override("separation", 6)
		column.add_child(row)
		row.add_child(_view_button("−", func() -> void: step_view(_rig(), key, -1)))
		var value := Label.new()
		value.custom_minimum_size = Vector2(300, 0)
		value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
		value.add_theme_font_size_override("font_size", 18)
		row.add_child(value)
		row.add_child(_view_button("+", func() -> void: step_view(_rig(), key, 1)))
		_view_values[key] = value
	column.add_child(_view_button("Réinitialiser cette vue", func() -> void: reset_view(_rig())))
	column.add_child(_view_button("Copier les valeurs", copy_view))
	_refresh_view_labels()

## La caméra montrée. Par le nœud et non par le nom de l'autoload: le pont
## précharge ce fichier, le nommer ici ferait une dépendance circulaire.
func _rig() -> Camera3DRig:
	var bridge := get_node_or_null("/root/Bridge3D")
	return bridge.call("camera_rig") as Camera3DRig if bridge != null else null

func _view_button(text: String, action: Callable) -> Button:
	var button := Button.new()
	button.text = text
	button.focus_mode = Control.FOCUS_NONE
	button.custom_minimum_size = Vector2(44, 0)
	button.pressed.connect(action)
	button.pressed.connect(_refresh_view_labels)
	return button

func _refresh_view_labels() -> void:
	if _view_title == null or not is_inside_tree():
		return
	var rig := _rig()
	_view_title.text = "Caméra — %s" % (view_label(rig.view_profile) if rig != null else "aucune vue 3D")
	for key: String in _view_values:
		(_view_values[key] as Label).text = view_line(rig, key) if rig != null else "—"

## Avance d'un pas le réglage `key` de la vue affichée, dans la vue de SA scène.
static func step_view(rig: Camera3DRig, key: String, direction: int) -> void:
	if rig == null:
		return
	var trial: Dictionary = Camera3DRig.trial_views.get(rig.view_profile, {})
	if key == "fov":
		var at := 0
		for index: int in FOV_STEPS.size():
			if absf(float(FOV_STEPS[index]) - rig.view_value("fov")) < absf(float(FOV_STEPS[at]) - rig.view_value("fov")):
				at = index
		trial["fov"] = float(FOV_STEPS[clampi(at + direction, 0, FOV_STEPS.size() - 1)])
	else:
		for entry: Array in VIEW_STEPS:
			if entry[0] != key:
				continue
			var step := float(entry[2])
			var moved := snappedf(rig.view_value(key) + step * direction, step)
			trial[key] = wrapf(moved, -180.0, 180.0) if key == "yaw" else clampf(moved, float(entry[3]), float(entry[4]))
	Camera3DRig.trial_views[rig.view_profile] = trial
	rig.apply_view()

static func reset_view(rig: Camera3DRig) -> void:
	if rig == null:
		return
	Camera3DRig.trial_views[rig.view_profile] = {}
	rig.apply_view()

static func view_label(profile: String) -> String:
	return "donjon" if profile == "dungeon" else "restaurant"

static func view_line(rig: Camera3DRig, key: String) -> String:
	if key == "fov":
		var angle := rig.view_value("fov")
		return "Perspective: FOV %d°" % roundi(angle) if angle > 0.0 else "Orthographique"
	for entry: Array in VIEW_STEPS:
		if entry[0] == key:
			return "%s: %s" % [entry[1], ("×%.2f" % rig.view_value(key)) if key == "zoom" else ("%d°" % roundi(rig.view_value(key)))]
	return key

## Une ligne à recopier telle quelle dans le suivi: ce sont les valeurs à fixer.
static func describe_view(rig: Camera3DRig) -> String:
	var parts: Array[String] = []
	for entry: Array in VIEW_STEPS:
		parts.append(view_line(rig, str(entry[0])))
	parts.append(view_line(rig, "fov"))
	return "Vue %s — %s" % [view_label(rig.view_profile), ", ".join(parts)]

func copy_view() -> void:
	var rig := _rig()
	if rig == null:
		return
	var text := describe_view(rig)
	DisplayServer.clipboard_set(text)
	GameState.toast_requested.emit("Débogage: vue copiée — %s" % text)

func show_lines(lines: Array) -> void:
	if _label != null and is_instance_valid(_label):
		_label.text = "\n".join(lines)

## Un nombre lisible d'un coup d'œil: 1 000 000 plutôt que 1000000.
static func thousands(amount: int) -> String:
	var digits := str(absi(amount))
	var grouped := ""
	for index: int in digits.length():
		if index > 0 and (digits.length() - index) % 3 == 0:
			grouped += " "
		grouped += digits[index]
	return ("-" if amount < 0 else "") + grouped
