class_name UpgradeTreeView
extends Control

# La fenêtre sur l'arbre. Le plan des noeuds est plus grand que l'écran, et il
# n'y avait ni déplacement ni zoom: une branche entière pouvait rester hors de
# vue, injoignable à la souris. L'outil de gestion, lui, laissait tout balayer.
#
# Cette fenêtre ne connaît que la géométrie. Elle ne sait pas ce qu'est un noeud
# d'upgrade, et c'est ce qui lui permet de rester à côté du canevas plutôt que
# dedans.

const MIN_ZOOM := 0.4
const MAX_ZOOM := 1.8
const ZOOM_STEP := 1.12
# En deçà, un clic reste un clic. Sans ce seuil, le moindre tremblement de main
# transformerait un achat en déplacement — et l'inverse.
const DRAG_THRESHOLD := 6.0
# Marge à l'intérieur de laquelle un noeud suivi doit rester. On ne recadre que
# lorsqu'il en sort: recentrer à chaque pas ferait sauter tout l'arbre.
const FOLLOW_MARGIN := 180.0

@onready var canvas: UpgradeTreeCanvas = %TreeCanvas

var view_offset := Vector2.ZERO
var view_zoom := 1.0
var dragging := false
var drag_moved := false
var drag_origin := Vector2.ZERO

func _ready() -> void:
	resized.connect(_fit_canvas)
	call_deferred("_fit_canvas")

# Le canevas garde la taille de la fenêtre: le placement des noeuds se mesure
# dessus, et le déplacement se fait par la position, pas par les ancres.
func _fit_canvas() -> void:
	if canvas == null or not is_instance_valid(canvas):
		return
	canvas.size = size
	_apply_view()

func _apply_view() -> void:
	if canvas == null or not is_instance_valid(canvas):
		return
	canvas.scale = Vector2(view_zoom, view_zoom)
	canvas.position = view_offset

func pan(delta: Vector2) -> void:
	view_offset += delta
	_apply_view()

# Zoom centré sur un point de la fenêtre: ce qui est sous le curseur y reste.
# Sans ça, zoomer déporterait ce qu'on est en train de regarder.
func zoom_at(point: Vector2, factor: float) -> void:
	var target := clampf(view_zoom * factor, MIN_ZOOM, MAX_ZOOM)
	if is_equal_approx(target, view_zoom):
		return
	var world := (point - view_offset) / view_zoom
	view_zoom = target
	view_offset = point - world * view_zoom
	_apply_view()

func center_on(world_point: Vector2) -> void:
	view_offset = size * 0.5 - world_point * view_zoom
	_apply_view()

# Garde un point du plan dans la fenêtre. C'est ce qui fait suivre la vue quand
# on navigue au clavier: les touches de déplacement déplacent donc bien la vue,
# sans cesser de désigner un noeud.
func keep_visible(world_point: Vector2) -> void:
	var screen := world_point * view_zoom + view_offset
	var wanted := view_offset
	wanted.x += maxf(0.0, FOLLOW_MARGIN - screen.x) - maxf(0.0, screen.x - (size.x - FOLLOW_MARGIN))
	wanted.y += maxf(0.0, FOLLOW_MARGIN - screen.y) - maxf(0.0, screen.y - (size.y - FOLLOW_MARGIN))
	if wanted.is_equal_approx(view_offset):
		return
	view_offset = wanted
	_apply_view()

func _gui_input(event: InputEvent) -> void:
	if event is InputEventMouseButton:
		var button := event as InputEventMouseButton
		if button.button_index == MOUSE_BUTTON_WHEEL_UP and button.pressed:
			zoom_at(button.position, ZOOM_STEP)
			accept_event()
			return
		if button.button_index == MOUSE_BUTTON_WHEEL_DOWN and button.pressed:
			zoom_at(button.position, 1.0 / ZOOM_STEP)
			accept_event()
			return
		# Un noeud consomme son propre clic: ce qui arrive ici part donc du vide,
		# et peut devenir un déplacement sans jamais voler un achat.
		if [MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_RIGHT].has(button.button_index):
			dragging = button.pressed
			drag_moved = false
			drag_origin = button.position
			accept_event()
		return
	if event is InputEventMouseMotion and dragging:
		var motion := event as InputEventMouseMotion
		if not drag_moved and motion.position.distance_to(drag_origin) < DRAG_THRESHOLD:
			return
		drag_moved = true
		pan(motion.relative)
		accept_event()
