# The visitor's running tally, kept on a clipboard tucked in the bottom-left
# corner. The board itself is the glb model, framed head-on in a little 3D
# viewport; the two lists are laid over its paper as flat pixel text so they stay
# crisp. Names too long for the sheet are trimmed with an ellipsis, and when the
# two lists together overrun the sheet the oldest drop off behind a "...".
#
# Click the board and it swells to the middle of the screen at a readable size,
# every name spelled out in full; click it again, press Esc, or simply walk off
# and it folds back to the corner.
#
# The model is thin along X and faces +X, so the camera sits out in front on the
# X axis looking back. The paper's own rectangle — read straight off the mesh —
# maps to the screen rectangle the labels live in, so the text always lands on
# the white and never on the board or under the clip.
extends Control

const MODEL := "res://assets/divers/clipboard.glb"
const FONT := "res://assets/divers/PixelatedPusab.ttf"

# The corner widget's pixel size.
const VIEW := Vector2i(240, 320)
# Orthographic frame: how much world height it spans, and where its centre sits.
# The board runs Y -0.356..0.154, so this holds it with a little air top and bottom.
const CAM_SIZE := 0.55
const CAM_CENTRE_Y := -0.10
const CAM_FRONT_X := 0.6

# The paper's usable rectangle, in the model's own units. Y stops short of the
# clip at the top (the clip covers the sheet above ~0.074) and of the sheet's own
# edges; Z is inset from the paper edge so text keeps a margin.
const PAPER_TOP_Y := 0.055
const PAPER_BOTTOM_Y := -0.30
const PAPER_HALF_Z := 0.15

const INK := Color(0.16, 0.16, 0.20)
const HEADER_SIZE := 16
const ITEM_SIZE := 12
# The names are indented a touch past their heading, the way a list sits under
# its title on a written sheet.
const ITEM_INDENT := 12
# How tall the enlarged board stands, as a share of the screen height.
const ENLARGED_HEIGHT_RATIO := 0.86

var _font: FontFile
var _museum: Museum
# The corner face and the enlarged face share a builder; each keeps its own sheet
# and the two list boxes on it.
var _small := {}
var _enlarged := {}
var _overlay: CanvasLayer

func _ready() -> void:
	custom_minimum_size = VIEW
	size = VIEW
	# The board takes clicks (to open the enlarged view); the rest of it lets them
	# through to the game underneath.
	mouse_filter = Control.MOUSE_FILTER_STOP
	_font = load(FONT)
	_small = _build_face(self, VIEW, 1.0)

# One clipboard face: the board in a transparent 3D viewport, and the two lists
# laid over its paper. `scale` grows the type with the board.
func _build_face(root: Control, view: Vector2i, scale: float) -> Dictionary:
	var container := SubViewportContainer.new()
	container.stretch = true
	container.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	container.mouse_filter = Control.MOUSE_FILTER_IGNORE
	root.add_child(container)

	var world := World3D.new()
	var env := Environment.new()
	env.background_mode = Environment.BG_CLEAR_COLOR
	env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
	env.ambient_light_color = Color.WHITE
	env.ambient_light_energy = 1.3
	world.environment = env

	var viewport := SubViewport.new()
	viewport.size = view
	viewport.transparent_bg = true
	# A world of its own, handed over rather than copied: own_world_3d, flipped on
	# a viewport that is not in the tree yet, has no world to copy and complains
	# about a null scenario. Given the world first, it enters the tree already
	# knowing where it draws.
	viewport.world_3d = world
	viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
	container.add_child(viewport)

	viewport.add_child((load(MODEL) as PackedScene).instantiate())

	var key := DirectionalLight3D.new()
	key.light_energy = 0.6
	key.rotation = Vector3(deg_to_rad(-35), deg_to_rad(70), 0)
	viewport.add_child(key)

	var cam := Camera3D.new()
	cam.projection = Camera3D.PROJECTION_ORTHOGONAL
	cam.size = CAM_SIZE
	cam.position = Vector3(CAM_FRONT_X, CAM_CENTRE_Y, 0.0)
	# Look back along -X with +Y up — a quarter turn about Y. Set directly rather
	# than look_at, which needs the node in the tree. This is the frame the paper
	# mapping assumes, and it puts world +Z to screen-left.
	cam.rotation = Vector3(0.0, PI * 0.5, 0.0)
	viewport.add_child(cam)

	var top_left := _paper_to_screen(PAPER_TOP_Y, PAPER_HALF_Z, view)
	var bottom_right := _paper_to_screen(PAPER_BOTTOM_Y, -PAPER_HALF_Z, view)

	var sheet := VBoxContainer.new()
	sheet.position = top_left
	sheet.size = bottom_right - top_left
	sheet.add_theme_constant_override("separation", int(2 * scale))
	sheet.clip_contents = true
	sheet.mouse_filter = Control.MOUSE_FILTER_IGNORE
	root.add_child(sheet)

	return {"sheet": sheet, "scale": scale}

# Read the marks off the museum and lay both faces out afresh.
func refresh(museum: Museum, _current: int = 0) -> void:
	_museum = museum
	if not _small.is_empty():
		_lay_out(_small, false)
	if not _enlarged.is_empty() and _overlay.visible:
		_lay_out(_enlarged, true)

# Build one face's sheet from the marks. The corner face keeps a fixed size and
# drops its oldest names behind a "..." when the two lists overrun; the enlarged
# face shrinks its type just enough to spell every name out in full on the paper.
func _lay_out(face: Dictionary, full: bool) -> void:
	if _museum == null:
		return
	var fakes: Array[String] = []
	var originals: Array[String] = []
	for room: Dictionary in _museum.rooms:
		if room["vestibule"] or room["piece"].is_empty():
			continue
		var name: String = room["piece"].get("title_native", "")
		match room["mark"]:
			"fake":
				fakes.append(name)
			"original":
				originals.append(name)

	var sheet: VBoxContainer = face["sheet"]
	for child in sheet.get_children():
		child.queue_free()

	if full:
		# Pick the largest type that fits every name on the paper, both ways.
		var size := _fit_item_size(fakes + originals, sheet.size)
		_section(sheet, "Fakes:", fakes, fakes.size(), size, true)
		_gap(sheet, size)
		_section(sheet, "Originals:", originals, originals.size(), size, true)
		return

	# Share the sheet's lines between the two lists so the fuller one gives up its
	# oldest first.
	var budget := _line_budget(sheet)
	var fake_lines: int = fakes.size()
	var orig_lines: int = originals.size()
	while fake_lines + orig_lines > budget:
		if fake_lines >= orig_lines and fake_lines > 0:
			fake_lines -= 1
		elif orig_lines > 0:
			orig_lines -= 1
		else:
			break
	_section(sheet, "Fakes:", fakes, fake_lines, ITEM_SIZE, false)
	_gap(sheet, ITEM_SIZE)
	_section(sheet, "Originals:", originals, orig_lines, ITEM_SIZE, false)

# A heading and its list of names, indented under it. `lines` caps the corner
# face; enlarged, every name is shown.
func _section(sheet: VBoxContainer, heading: String, names: Array[String], lines: int, size: int, full: bool) -> void:
	sheet.add_child(_header(heading, size))
	var margin := MarginContainer.new()
	margin.add_theme_constant_override("margin_left", ITEM_INDENT)
	sheet.add_child(margin)
	var box := VBoxContainer.new()
	box.add_theme_constant_override("separation", 0)
	margin.add_child(box)
	if lines <= 0:
		return
	var shown := names
	if not full and names.size() > lines:
		box.add_child(_item("...", size, false))
		shown = names.slice(names.size() - (lines - 1))
	for name: String in shown:
		box.add_child(_item(name, size, full))

func _gap(sheet: VBoxContainer, size: int) -> void:
	var gap := Control.new()
	gap.custom_minimum_size = Vector2(0, size * 0.7)
	sheet.add_child(gap)

# The name lines a fixed-size sheet can hold below its two headings.
func _line_budget(sheet: Control) -> int:
	var item_h := _font.get_height(ITEM_SIZE) + 2.0
	var header_h := _font.get_height(HEADER_SIZE)
	var free := sheet.size.y - header_h * 2.0 - ITEM_SIZE * 0.7 - 6.0
	return maxi(2, int(free / item_h))

# The largest item type that fits both headings, both name lists and the gap on a
# sheet of this size, with the longest name still inside the width. Heading type
# keeps its usual ratio over the item type.
func _fit_item_size(names: Array[String], sheet_size: Vector2) -> int:
	var ratio := float(HEADER_SIZE) / float(ITEM_SIZE)
	for size in range(HEADER_SIZE + 8, 6, -1):
		var header := int(size * ratio)
		var tall := _font.get_height(header) * 2.0 + size * 0.7 \
			+ (_font.get_height(size) + 2.0) * names.size()
		if tall > sheet_size.y:
			continue
		var widest := 0.0
		for name: String in names:
			widest = maxf(widest, _font.get_string_size(name, HORIZONTAL_ALIGNMENT_LEFT, -1, size).x)
		if widest > sheet_size.x - ITEM_INDENT:
			continue
		return size
	return 7

func _header(text: String, size: int) -> Label:
	var label := Label.new()
	label.text = text
	label.add_theme_font_override("font", _font)
	label.add_theme_font_size_override("font_size", int(size * float(HEADER_SIZE) / float(ITEM_SIZE)))
	label.add_theme_color_override("font_color", INK)
	return label

# One line of a list: the piece's own-language title. On the corner sheet it is
# trimmed with an ellipsis if it would run off; enlarged, it is shown whole (the
# type was already sized so it fits).
func _item(text: String, size: int, full: bool) -> Label:
	var label := Label.new()
	label.text = text
	label.add_theme_font_override("font", _font)
	label.add_theme_font_size_override("font_size", size)
	label.add_theme_color_override("font_color", INK)
	label.autowrap_mode = TextServer.AUTOWRAP_OFF
	if not full:
		label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
		label.clip_text = true
	return label

# A point on the paper (its own Y up, Z across) to a pixel in a face of the given
# size. The camera looks along -X with +Y up, which puts world +Z to screen-left.
func _paper_to_screen(world_y: float, world_z: float, view: Vector2i) -> Vector2:
	var aspect := float(view.x) / float(view.y)
	var half_w := CAM_SIZE * aspect * 0.5
	var top := CAM_CENTRE_Y + CAM_SIZE * 0.5
	var sx := (half_w - world_z) / (CAM_SIZE * aspect) * float(view.x)
	var sy := (top - world_y) / CAM_SIZE * float(view.y)
	return Vector2(sx, sy)

# --- The enlarged view -----------------------------------------------------
#
# Opened by a click on the board, or by the clipboard key (run.gd) — the corner of
# the screen is not the only way in. Esc, a walk order or another press closes it.


func _gui_input(event: InputEvent) -> void:
	if event is InputEventMouseButton and event.pressed \
			and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT:
		toggle_enlarged()
		accept_event()

# Esc folds the enlarged board away (and is swallowed so it does not also open the
# menu); so does any walk order — a movement key, or a click, which the overlay
# itself catches.
func _input(event: InputEvent) -> void:
	if _overlay == null or not _overlay.visible:
		return
	if event.is_action_pressed("cancel"):
		_close_enlarged()
		get_viewport().set_input_as_handled()
	elif event.is_action_pressed("move_left") or event.is_action_pressed("move_right") \
			or event.is_action_pressed("move_up") or event.is_action_pressed("move_down"):
		_close_enlarged()

func toggle_enlarged() -> void:
	if _overlay == null:
		_build_enlarged()
	if _overlay.visible:
		_close_enlarged()
	else:
		_overlay.visible = true
		_lay_out(_enlarged, true)

func _close_enlarged() -> void:
	if _overlay != null:
		_overlay.visible = false

# Built the first time the board is clicked: a dim backdrop that catches the click
# to close, and the same clipboard face grown to the middle of the screen.
func _build_enlarged() -> void:
	_overlay = CanvasLayer.new()
	_overlay.layer = 20
	_overlay.visible = false
	add_child(_overlay)

	var screen := get_viewport().get_visible_rect().size
	var backdrop := ColorRect.new()
	backdrop.color = Color(0, 0, 0, 0.35)
	backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
	backdrop.gui_input.connect(func(event: InputEvent) -> void:
		if event is InputEventMouseButton and event.pressed:
			_close_enlarged())
	_overlay.add_child(backdrop)

	var height := screen.y * ENLARGED_HEIGHT_RATIO
	var view := Vector2i(int(height * float(VIEW.x) / float(VIEW.y)), int(height))
	var board := Control.new()
	board.size = view
	board.position = (screen - Vector2(view)) * 0.5
	board.mouse_filter = Control.MOUSE_FILTER_IGNORE
	backdrop.add_child(board)

	_enlarged = _build_face(board, view, float(view.y) / float(VIEW.y))
