extends Node

# Le régisseur: qui tient le plateau.
#
# Plusieurs séquences veulent l'écran entier — le bilan de fin de plongée, le
# menu de pause, l'annonce d'une recette apprise. Rien ne les empêchait de se
# lever en même temps: les points de réflexion commençaient, la ronde se
# terminait, et les deux se superposaient en arrêtant puis relâchant le jeu
# chacune de son côté.
#
# Le plateau n'appartient donc qu'à une séquence à la fois. Le régisseur ne sait
# rien de ce qui s'y joue: il dit seulement si c'est libre, et prévient qui doit
# s'effacer. Aucune décision de jeu ne passe par ici — c'est de la mise en
# scène, et rien d'autre.
#
# Les séquences ne se valent pas: chacune demande le plateau avec un rang, et le
# plus haut l'emporte. À rang égal ou moindre, on attend son tour.
#
#   CÉRÉMONIE — les cinq premières recettes d'une partie. C'est ce que le joueur
#               ne doit surtout pas manquer: un débutant qui rate l'ouverture de
#               son livre de recettes ne comprend pas ce qui vient de se passer.
#               Elle passe donc devant le bilan d'une ronde, qui l'attendra.
#   BILAN     — la fin d'une plongée. Le joueur vient de la finir, ça se dit
#               tout de suite — sauf si une première recette parle en même temps.
#   MENU      — la pause. Rien d'urgent, et elle peut se rouvrir.
#   BULLE     — une recette de plus, passé la cinquième. Une bulle de passage,
#               qui laisse la place à tout le reste.
const PRIORITY_CEREMONY := 20
const PRIORITY_RESULT := 10
const PRIORITY_MENU := 5
const PRIORITY_NOTICE := 1

# Combien de recettes ont droit au grand jeu. Le tableau des durées vit dans
# `Player.CEREMONY`; un contrôle vérifie que les deux ne divergent pas.
const CEREMONIAL_RECIPES := 5

static func priority_for_rank(rank: int) -> int:
	return PRIORITY_CEREMONY if rank >= 0 and rank < CEREMONIAL_RECIPES else PRIORITY_NOTICE

signal released(previous: String)
signal preempted(previous: String)

var _owner := ""
var _priority := 0
# Une séquence peut **réserver** le plateau avant d'y monter. Entre l'instant où
# une recette s'apprend et celui où le héros commence à y penser il s'écoule une
# image — et c'est exactement là que le bilan de la ronde se levait. La
# réservation tient la place pendant ce battement.
var _reserved := 0
var _scene: Node = null

# --- Les interfaces -----------------------------------------------------------
# Le plateau dit qui joue une séquence; les interfaces, elles, s'empilent : la
# pause sur le four, la carte sur la pause, une question sur l'arbre. Seule
# celle du dessus reçoit les touches (règles du mapping, 10/10) — sans quoi
# Retour fermerait le four caché sous la pause.
#
# Une interface s'inscrit en s'ouvrant et se retire en se fermant. Cachée ou
# sortie de l'arbre, elle ne compte plus même si elle a oublié de se retirer :
# une pile périmée bloquerait toutes les touches du jeu. Tableau non typé : une
# entrée libérée s'y lit sans planter.
var _interfaces: Array = []
var _interface_closed_frame := -1

# --- Les maintiens ------------------------------------------------------------
# Un maintien ne traverse pas une transition : un bouton enfoncé pendant
# qu'une interface tenait l'écran, ou d'une scène à l'autre, ne compte plus
# avant d'avoir été relâché. Garder le clic en fermant le four ne relance pas
# une cuisson; garder ZR en sortant d'un menu ne repart pas en attaque.
const GESTURES := ["action", "attack", "interact", "dive_hold"]
var _awaiting_release := {}
var _gesture_scene: Node = null

# --- L'appareil ---------------------------------------------------------------
# Le dernier appareil touché choisit les invites (mapping des touches, étape
# 5) : "keyboard" (clavier et souris) ou "pad", et la famille de la manette.
const InputPrompts := preload("res://scripts/core/input_prompts.gd")
signal input_device_changed(device: String)
var input_device := "keyboard"
var pad_family := "xbox"
var pad_known := true

func _ready() -> void:
	# Le plateau doit vivre pendant les arrêts: c'est justement quand le jeu est
	# figé qu'une séquence l'occupe.
	process_mode = Node.PROCESS_MODE_ALWAYS

func _input(event: InputEvent) -> void:
	_track_device(event)
	if not interface_active() and not get_tree().paused:
		return
	for action: String in GESTURES:
		if InputMap.has_action(action) and event.is_action_pressed(action):
			_awaiting_release[action] = true

func _process(_delta: float) -> void:
	_watch_gestures()
	# Un changement de scène emporte les fenêtres avec lui. Sans ce garde-fou, un
	# bilan disparu garderait le plateau pour toujours et plus rien ne pourrait
	# s'y jouer.
	if _owner.is_empty():
		return
	var tree := get_tree()
	var current: Node = null if tree == null else tree.current_scene
	if current != _scene:
		clear()

func is_busy() -> bool:
	return not _owner.is_empty()

func priority() -> int:
	return _priority

func reserved_priority() -> int:
	return _reserved

# Retenir la place pour une séquence qui va venir. Toujours la plus haute des
# réservations en cours: deux annonces en attente ne s'annulent pas.
func reserve(wanted: int) -> void:
	_reserved = maxi(_reserved, wanted)

func set_reservation(wanted: int) -> void:
	_reserved = maxi(0, wanted)

func held_by() -> String:
	return _owner

func holds(owner_name: String) -> bool:
	return _owner == owner_name and not _owner.is_empty()

# Demander le plateau. On l'obtient s'il est libre et que personne de plus
# pressé ne l'a réservé, ou si l'on passe devant celui qui l'occupe — auquel cas
# il est prévenu et se retire.
func claim(owner_name: String, wanted: int = PRIORITY_NOTICE) -> bool:
	if owner_name.is_empty():
		return false
	if holds(owner_name):
		return true
	if is_busy():
		if wanted <= _priority:
			return false
		var previous := _owner
		_take(owner_name, wanted)
		preempted.emit(previous)
		return true
	if wanted < _reserved:
		return false
	_take(owner_name, wanted)
	return true

func release(owner_name: String) -> void:
	if _owner != owner_name or _owner.is_empty():
		return
	clear()

func clear() -> void:
	if _owner.is_empty():
		return
	var previous := _owner
	_owner = ""
	_priority = 0
	_scene = null
	released.emit(previous)

func _take(owner_name: String, wanted: int) -> void:
	_owner = owner_name
	_priority = wanted
	# Monter sur le plateau consomme la réservation qu'on y avait posée.
	if wanted >= _reserved:
		_reserved = 0
	var tree := get_tree()
	_scene = null if tree == null else tree.current_scene

func _track_device(event: InputEvent) -> void:
	var wanted := ""
	if event is InputEventJoypadButton:
		wanted = "pad"
	elif event is InputEventJoypadMotion and absf((event as InputEventJoypadMotion).axis_value) > 0.5:
		wanted = "pad"
	elif event is InputEventKey or event is InputEventMouseButton:
		wanted = "keyboard"
	elif event is InputEventMouseMotion and (event as InputEventMouseMotion).relative.length() > 4.0:
		wanted = "keyboard"
	if wanted.is_empty():
		return
	var family := pad_family
	var known := pad_known
	if wanted == "pad":
		var joy_name := Input.get_joy_name(event.device)
		family = InputPrompts.family_of(joy_name)
		known = InputPrompts.is_known_pad(joy_name)
	if wanted == input_device and family == pad_family and known == pad_known:
		return
	input_device = wanted
	pad_family = family
	pad_known = known
	input_device_changed.emit(input_device)

func push_interface(node: Node) -> void:
	if node == null:
		return
	_interfaces.erase(node)
	_interfaces.append(node)

func pop_interface(node: Node) -> void:
	if _interfaces.has(node):
		_interfaces.erase(node)
		_interface_closed_frame = Engine.get_process_frames()

## Les interfaces ouvertes, de la plus ancienne à celle du dessus. Le menu de
## débogage les fige le temps qu'il tient l'écran.
func open_interfaces() -> Array[Node]:
	var found: Array[Node] = []
	for entry: Variant in _interfaces:
		if _interface_alive(entry):
			found.append(entry)
	return found

func top_interface() -> Node:
	for index: int in range(_interfaces.size() - 1, -1, -1):
		var entry: Variant = _interfaces[index]
		if _interface_alive(entry):
			return entry
		_interfaces.remove_at(index)
	return null

## Vrai si `node` peut lire les touches : rien n'est ouvert, ou il est au-dessus.
func accepts_input(node: Node) -> bool:
	var top := top_interface()
	return top == null or top == node

## Une interface est ouverte, ou vient de se fermer sur cet appui même : le menu
## qui se ferme sur Retour lit l'événement avant ceux qui écoutent derrière lui.
func interface_active() -> bool:
	return top_interface() != null or _interface_closed_frame == Engine.get_process_frames()

## Le calque qui passe devant toutes les interfaces ouvertes : la pause sur la
## carte, la carte ouverte depuis la pause.
func layer_above_interfaces(minimum: int, exclude: Node = null) -> int:
	var highest := minimum - 1
	for entry: Variant in _interfaces:
		if not _interface_alive(entry) or entry == exclude:
			continue
		var holder: Node = entry
		while holder != null and not holder is CanvasLayer:
			holder = holder.get_parent()
		if holder != null:
			highest = maxi(highest, (holder as CanvasLayer).layer)
	return maxi(minimum, highest + 1)

## Retour : B, le clic droit ou Retour arrière (règles du mapping). Pas pendant
## qu'on tape dans un champ, où Retour arrière efface une lettre.
func is_back(event: InputEvent) -> bool:
	if not event.is_action_pressed("ui_cancel"):
		return false
	if event is InputEventKey:
		var focus := get_viewport().gui_get_focus_owner()
		if focus is LineEdit or focus is TextEdit:
			return false
	return true

func gesture_held(action: String) -> bool:
	return Input.is_action_pressed(action) and not _awaiting_release.has(action)

func gesture_just_pressed(action: String) -> bool:
	return Input.is_action_just_pressed(action) and not _awaiting_release.has(action)

func _interface_alive(entry: Variant) -> bool:
	if not is_instance_valid(entry):
		return false
	var node: Node = entry
	if not node.is_inside_tree():
		return false
	if node is CanvasItem:
		return (node as CanvasItem).is_visible_in_tree()
	if node is CanvasLayer:
		return (node as CanvasLayer).visible
	return true

func _watch_gestures() -> void:
	var tree := get_tree()
	var scene: Node = null if tree == null else tree.current_scene
	if scene != _gesture_scene:
		_gesture_scene = scene
		for action: String in GESTURES:
			if InputMap.has_action(action) and Input.is_action_pressed(action):
				_awaiting_release[action] = true
	for action: String in _awaiting_release.keys():
		if not InputMap.has_action(action) or not Input.is_action_pressed(action):
			_awaiting_release.erase(action)
