class_name InfoBadge3D
extends Node3D

# Orders and dialogue use projected UI cards. Health and furniture status retain
# lightweight billboards. All values come from the authoritative 2D actors.
const BAR_WIDTH := 44.0
const BAR_HEIGHT := 4.0
const TEXT_WORLD_HEIGHT := 7.0
const TEXT_LINE_GAP := 3.2
const FONT_SIZE := 64
const NAME_WORLD_HEIGHT := 5.5
const NAME_ON_WORLD := Color(0.99, 0.97, 0.92)

var _callout: PanelContainer
var _callout_layer: CanvasLayer
var _callout_active := false

var _name: Label3D
var _label: Label3D
var _track: MeshInstance3D
var _track_mesh: QuadMesh
var _fill: MeshInstance3D
var _fill_mesh: QuadMesh

func _ready() -> void:
	_callout_layer = CanvasLayer.new()
	_callout_layer.layer = 3
	add_child(_callout_layer)
	_callout = preload("res://scripts/core/actor_callout.gd").new()
	_callout_layer.add_child(_callout)
	_label = Label3D.new()
	_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
	_label.no_depth_test = true
	_label.fixed_size = false
	_label.font_size = FONT_SIZE
	_label.pixel_size = TEXT_WORLD_HEIGHT / float(FONT_SIZE)
	# `line_spacing` se compte dans l'échelle de la police, pas du monde.
	_label.line_spacing = TEXT_LINE_GAP / (TEXT_WORLD_HEIGHT / float(FONT_SIZE))
	_label.outline_size = 0
	_label.modulate = Color("fffdf7")
	_label.outline_size = 16
	_label.outline_modulate = Color("292137")
	add_child(_label)

	# Le nom flotte au-dessus de la bulle, donc au-dessus du décor: il lui faut
	# un texte clair **et** un contour foncé, sinon il disparaît sur un sol pâle.
	# Le contour se règle en points de police, pas en unités du monde: à cette
	# taille de texte, l'ancienne valeur noyait les lettres.
	_name = Label3D.new()
	_name.billboard = BaseMaterial3D.BILLBOARD_ENABLED
	_name.no_depth_test = true
	_name.font_size = FONT_SIZE
	_name.pixel_size = NAME_WORLD_HEIGHT / float(FONT_SIZE)
	_name.outline_size = 22
	_name.modulate = NAME_ON_WORLD
	_name.outline_modulate = Color(0.05, 0.03, 0.08, 0.95)
	add_child(_name)

	_track_mesh = QuadMesh.new()
	_track_mesh.size = Vector2(BAR_WIDTH, BAR_HEIGHT)
	_track = _bar(_track_mesh, Color(0.20, 0.16, 0.24))
	add_child(_track)
	_fill_mesh = QuadMesh.new()
	_fill_mesh.size = Vector2(BAR_WIDTH, BAR_HEIGHT)
	_fill = _bar(_fill_mesh, Color("4ade80"))
	add_child(_fill)
	hide_all()

func _bar(mesh: QuadMesh, tint: Color) -> MeshInstance3D:
	var node := MeshInstance3D.new()
	node.mesh = mesh
	var material := StandardMaterial3D.new()
	material.albedo_color = tint
	material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
	material.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED
	material.no_depth_test = true
	material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
	material.render_priority = 2
	node.material_override = material
	return node

func hide_all() -> void:
	_callout_active = false
	if _callout != null:
		_callout.hide()
	_label.visible = false
	_track.visible = false
	_fill.visible = false

# Ce que le badge montre réellement, pour qui veut le vérifier. Lire `visible`
# sur le badge ne dirait rien: le noeud reste affiché, ce sont ses parties qui
# s'allument.
func shows_bar() -> bool:
	return (_callout_active and _callout.progress.visible) or (_track != null and _track.visible)

func shows_customer_bubble() -> bool:
	return _callout_active

func displayed_text() -> String:
	if _callout_active:
		return "\n".join([_callout.speech.text, _callout.order.text]).strip_edges()
	return _label.text if _label != null and _label.visible else ""

# Le nom vit sa vie: il reste affiché même quand l'entité n'a rien à dire.
func show_name(text: String, elevation: float) -> void:
	position.y = elevation
	_name.visible = not text.is_empty()
	_name.text = text
	_name.position = Vector3(0.0, NAME_WORLD_HEIGHT, 0.0)

# `text` peut être vide, `ratio` négatif: chaque moitié se montre pour elle-même.
func show_info(text: String, ratio: float, tint: Color, elevation: float) -> void:
	_callout_active = false
	_callout.hide()
	position.y = elevation
	# Le texte est partagé avec la bulle, qui sait le grossir: on le rend ici à sa
	# taille ordinaire, sinon une annonce laisserait énormes toutes les suivantes.
	_set_text_height(TEXT_WORLD_HEIGHT)
	_label.visible = not text.is_empty()
	_label.text = text
	_track.visible = ratio >= 0.0
	_fill.visible = ratio >= 0.0
	_label.position = Vector3(0.0, TEXT_WORLD_HEIGHT if ratio >= 0.0 else 0.0, 0.0)
	_name.position.y = (TEXT_WORLD_HEIGHT * 2.0 if not text.is_empty() else 0.0) + NAME_WORLD_HEIGHT
	_name.modulate = NAME_ON_WORLD
	_name.outline_size = 22
	if ratio < 0.0:
		return
	_set_bar_ratio(ratio, tint)

# La barre se remplit **depuis la gauche**. Le remplissage et son fond restent au
# même endroit et ne bougent jamais: c'est la **géométrie** du quad qui se décale
# via `center_offset`. Déplacer le noeud, comme avant, dissociait les deux — un
# matériau en panneau publicitaire pivote autour de l'origine de son noeud, donc
# deux origines différentes tournaient l'une par rapport à l'autre.
func _set_bar_ratio(ratio: float, tint: Color) -> void:
	var clamped := clampf(ratio, 0.0, 1.0)
	_fill.visible = clamped > 0.0
	var filled := maxf(0.001, BAR_WIDTH * clamped)
	_fill_mesh.size = Vector2(filled, BAR_HEIGHT)
	_fill_mesh.center_offset = Vector3(-BAR_WIDTH * 0.5 + filled * 0.5, 0.0, 0.02)
	var material: StandardMaterial3D = _fill.material_override
	material.albedo_color = tint

# One projected card owns speech, order and progress.
func show_customer_info(speech: String, order: String, ratio: float,
		tint: Color, elevation: float, progress_caption: String = "") -> void:
	hide_all()
	position.y = elevation
	_name.hide()
	_callout_active = not (speech.is_empty() and order.is_empty() and ratio < 0)
	_callout.present(_name.text, speech, order, ratio, tint, progress_caption)
	_project_callout()

func _project_callout() -> void:
	if _callout == null:
		return
	var camera := get_viewport().get_camera_3d()
	_callout.visible = _callout_active and is_visible_in_tree() and camera != null
	if not _callout.visible:
		return
	if camera.is_position_behind(global_position):
		_callout.hide()
		return
	_callout.size.y = _callout.get_combined_minimum_size().y
	var anchor := camera.unproject_position(global_position)
	var bounds := get_viewport().get_visible_rect().size
	if not Rect2(Vector2.ZERO, bounds).has_point(anchor):
		_callout.hide()
		return
	_callout.position = Vector2(clampf(anchor.x - _callout.size.x * 0.5, 8, maxf(8, bounds.x - _callout.size.x - 8)),
		maxf(100, anchor.y - _callout.size.y - 10))
	_callout.anchor_screen = anchor

func _set_text_height(world_height: float) -> void:
	_label.pixel_size = world_height / float(FONT_SIZE)

# Resolve nearby cards once after every actor has been projected this frame.
static func arrange(badges: Array[InfoBadge3D]) -> void:
	var placed: Array[Rect2] = []
	badges.sort_custom(func(a: InfoBadge3D, b: InfoBadge3D) -> bool:
		return a._callout.position.y < b._callout.position.y if not is_equal_approx(a._callout.position.y, b._callout.position.y) else a.get_instance_id() < b.get_instance_id())
	for badge: InfoBadge3D in badges:
		var card := badge._callout
		if not card.visible:
			continue
		var rect := Rect2(card.position, card.size)
		for attempt: int in badges.size():
			var collision := false
			for occupied: Rect2 in placed:
				if rect.grow(6).intersects(occupied):
					rect.position.y = occupied.position.y - rect.size.y - 14
					if rect.position.y < 100:
						rect.position.y = occupied.end.y + 14
						rect.position.x = minf(occupied.end.x + 12, badge.get_viewport().get_visible_rect().size.x - rect.size.x - 8)
					collision = true
					break
			if not collision:
				break
		card.position = rect.position
		card.queue_redraw()
		placed.append(rect)
