class_name UpgradeTreeNode
extends Control

signal node_selected(id: String)
signal node_hovered(id: String)
signal node_unhovered(id: String)

var node_id := ""
var definition: Dictionary = {}
var state := "locked"
var icon: Texture2D
## Niveaux possedes sur niveaux totaux quand cette pastille est une coquille.
## Un nœud ordinaire est une coquille d'un seul niveau: `(0,1)` ou `(1,1)`, et
## rien ne s'affiche — ce n'est pas une progression, c'est un achat.
var progress := Vector2i(0, 1)
## Le curseur clavier/manette est ici, et SEULEMENT lui.
##
## Il n'y a rien a « selectionner » sur cet ecran: ce qu'on veut savoir se lit au
## survol, et cliquer achete ou n'achete pas selon la bourse et le deverrouillage.
## Une bague jaune qui restait accrochee au dernier noeud clique n'annoncait donc
## aucun etat: elle disait « celui-ci est choisi » alors que rien ne l'est.
##
## Elle garde un seul role, celui qu'aucune autre marque ne remplit: dire ou est
## le doigt de celui qui joue aux fleches, faute de pointeur a suivre. La souris
## l'efface des qu'elle entre sur un noeud.
var cursor := false

func setup(id: String, data: Dictionary, current_state: String, shell_progress := Vector2i(0, 1)) -> void:
	node_id = id
	progress = shell_progress
	definition = data
	state = current_state
	# Le coefficient de taille agrandit le gabarit, pas seulement le dessin:
	# sans quoi le nœud deborderait de sa zone cliquable.
	var scale_factor := float(_theme_definition().get("size_scale", 1.0))
	custom_minimum_size = Vector2(118, 92) * scale_factor
	size = custom_minimum_size
	mouse_filter = Control.MOUSE_FILTER_STOP
	focus_mode = Control.FOCUS_NONE
	tooltip_text = str(data.get("detailed_text", "")) if state != "mystery" else "Unknown upgrade"
	var image_path := str(data.get("image_path", ""))
	if not image_path.is_empty() and ResourceLoader.exists(image_path):
		icon = load(image_path) as Texture2D
	mouse_entered.connect(_on_hovered)
	mouse_exited.connect(_on_unhovered)
	queue_redraw()

func _gui_input(event: InputEvent) -> void:
	# Un clic dit « achete-le », rien d'autre. Le clavier, lui, passe par l'ecran:
	# c'est lui qui tient le curseur, et un noeud sans focus ne recevrait de
	# toute facon aucune touche.
	if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
		node_selected.emit(node_id)
		accept_event()

func _on_hovered() -> void:
	node_hovered.emit(node_id)
	queue_redraw()

func _on_unhovered() -> void:
	node_unhovered.emit(node_id)

func _draw() -> void:
	var theme_definition := _theme_definition()
	var states: Dictionary = theme_definition.get("states", {})
	var style: Dictionary = states.get(state, states.get("available", {}))
	var background := _as_color(style.get("background_color", Color("334155")))
	var border := _as_color(style.get("border_color", Color("94a3b8")))
	var text_color := _as_color(style.get("text_color", Color.WHITE))
	var border_width := float(style.get("border_width", 4.0))
	var center := size * 0.5
	var shape := str(theme_definition.get("shape", "square"))
	var scale_factor := float(theme_definition.get("size_scale", 1.0))
	var corner := float(theme_definition.get("corner_scale", 0.0))
	var points := _shape_points(shape, center, Vector2(54, 42) * scale_factor)
	if shape != "circle":
		points = _round_corners(points, corner)
	draw_colored_polygon(points, background)
	draw_polyline(PackedVector2Array(Array(points) + [points[0]]), border, border_width, true)
	if cursor:
		var focus_points := _shape_points(shape, center, Vector2(57, 45) * scale_factor)
		draw_polyline(PackedVector2Array(Array(focus_points) + [focus_points[0]]), Color("fde047"), 2.0, true)
	if icon != null and state != "mystery":
		draw_texture_rect(icon, Rect2(center.x - 13, 8, 26, 26), false)
	# Le thème repeint fond, contour et texte quand le nœud est acheté: un
	# préfixe le redirait, en mangeant la largeur du texte compact.
	var text := "?" if state == "mystery" else str(definition.get("node_text", node_id))
	var baseline := center.y - 3.0 if icon == null else center.y + 9.0
	var text_size := int(round(float(style.get("text_size", 13.0))))
	var font := _label_font(style)
	var text_width := size.x - 16.0
	draw_string(font, Vector2(8, baseline), text, HORIZONTAL_ALIGNMENT_CENTER, text_width, text_size, text_color)
	if bool(style.get("text_underline", false)) and not text.is_empty():
		var measured := font.get_string_size(text, HORIZONTAL_ALIGNMENT_CENTER, text_width, text_size)
		var half_width := minf(measured.x, text_width) * 0.5
		var underline_y := baseline + maxf(2.0, float(text_size) * 0.18)
		draw_line(Vector2(center.x - half_width, underline_y), Vector2(center.x + half_width, underline_y), text_color, maxf(1.0, float(text_size) / 11.0))
	var displayed_cost := get_cost_text()
	if not displayed_cost.is_empty():
		draw_string(ThemeDB.fallback_font, Vector2(8, center.y + 25.0), displayed_cost, HORIZONTAL_ALIGNMENT_CENTER, size.x - 16, 11, text_color)
	_draw_levels(center, scale_factor, text_color, border)

## Le compteur d'une coquille: « 2/5 », et autant de crans que de niveaux.
##
## Le chiffre dit ou l'on en est; les crans disent combien de route il reste.
## Sans eux, six niveaux et deux niveaux occupent la meme place a l'ecran, et
## l'arbre perd ce qu'une file de pastilles disait d'elle-meme — la longueur du
## chemin. En mystere on ne montre rien: la coquille n'est pas encore revelee.
func _draw_levels(center: Vector2, scale_factor: float, text_color: Color, border: Color) -> void:
	if progress.y <= 1 or state == "mystery":
		return
	var owned := clampi(progress.x, 0, progress.y)
	draw_string(ThemeDB.fallback_font, Vector2(8, center.y - 22.0 * scale_factor),
		"%d/%d" % [owned, progress.y], HORIZONTAL_ALIGNMENT_CENTER, size.x - 16, 10, text_color)
	var pip := 3.0
	var gap := 3.0
	var span := progress.y * pip * 2.0 + (progress.y - 1) * gap
	var left := center.x - span * 0.5 + pip
	var row := center.y + 36.0 * scale_factor
	for level: int in progress.y:
		var at := Vector2(left + level * (pip * 2.0 + gap), row)
		if level < owned:
			draw_circle(at, pip, text_color)
		else:
			draw_arc(at, pip, 0.0, TAU, 12, border, 1.0, true)

func get_cost_text() -> String:
	if state == "mystery":
		return ""
	if state == "purchased":
		return "OWNED"
	return GameState.upgrade_cost_label(node_id).to_upper()

## Gras et italique sont synthetises depuis la police unique du theme par
## defaut: embolden pour l'un, cisaillement pour l'autre.
func _label_font(style: Dictionary) -> Font:
	var base: Font = ThemeDB.fallback_font
	var bold := bool(style.get("text_bold", false))
	var italic := bool(style.get("text_italic", false))
	if not bold and not italic:
		return base
	var variation := FontVariation.new()
	variation.base_font = base
	if bold:
		variation.variation_embolden = 0.6
	if italic:
		variation.variation_transform = Transform2D(Vector2(1.0, 0.0), Vector2(0.22, 1.0), Vector2.ZERO)
	return variation

## Chaque sommet recule de r le long de ses deux aretes, et une quadratique
## passant par le sommet relie les deux points. r est borne par la moitie de
## l'arete la plus courte: un coefficient de 1 arrondit au maximum sans jamais
## manger la forme.
static func _round_corners(points: PackedVector2Array, coefficient: float) -> PackedVector2Array:
	var amount := clampf(coefficient, 0.0, 1.0)
	if amount <= 0.0 or points.size() < 3:
		return points
	var count := points.size()
	var result := PackedVector2Array()
	for index: int in range(count):
		var current := points[index]
		var to_previous := points[(index - 1 + count) % count] - current
		var to_next := points[(index + 1) % count] - current
		var radius := amount * 0.5 * minf(to_previous.length(), to_next.length())
		if radius <= 0.5:
			result.append(current)
			continue
		var start := current + to_previous.normalized() * radius
		var end := current + to_next.normalized() * radius
		for step: int in range(5):
			var t := float(step) / 4.0
			result.append(start.lerp(current, t).lerp(current.lerp(end, t), t))
	return result

func _theme_definition() -> Dictionary:
	var theme_id := str(definition.get("theme", ""))
	if DataCatalog.UPGRADE_THEMES.has(theme_id):
		return DataCatalog.UPGRADE_THEMES[theme_id]
	return DataCatalog.DEFAULT_UPGRADE_THEMES.values()[0]

func _as_color(value: Variant) -> Color:
	return value if value is Color else Color.from_string(str(value), Color.WHITE)

func _shape_points(shape: String, center: Vector2, half: Vector2) -> PackedVector2Array:
	match shape:
		"circle":
			var circle := PackedVector2Array()
			for index: int in range(32):
				var angle := TAU * float(index) / 32.0
				circle.append(center + Vector2(cos(angle) * half.x, sin(angle) * half.y))
			return circle
		"diamond":
			return PackedVector2Array([center + Vector2(0, -half.y), center + Vector2(half.x, 0), center + Vector2(0, half.y), center + Vector2(-half.x, 0)])
		"hexagon":
			return PackedVector2Array([center + Vector2(-half.x * 0.55, -half.y), center + Vector2(half.x * 0.55, -half.y), center + Vector2(half.x, 0), center + Vector2(half.x * 0.55, half.y), center + Vector2(-half.x * 0.55, half.y), center + Vector2(-half.x, 0)])
		_:
			return PackedVector2Array([center - half, center + Vector2(half.x, -half.y), center + half, center + Vector2(-half.x, half.y)])
