extends CanvasLayer
class_name TeamSelect

# Movement speed factor by role, shared across both factions so e.g. every
# faction's Fighter moves at the same speed as every other faction's.
const SPEED_BY_ROLE := {
	"Fighter": 1.3,
	"Gunner": 1.15,
	"Tank": 0.7,
}

const RACES := [
	{
		"id": 1, "name": "INNER SPHERE NAVY", "sub": "Gunmetal-gray navy of Earth, Mars, and Mercury — disciplined, frontal, built to hold a line",
		"color": Color(0.80, 0.32, 0.32),
		"ships": [
			{
				"name": "PATRIOT", "role": "Fighter",
				"desc": "A single, hard-hitting high-caliber explosive shell. Standard-issue and dependable — a clean, punishing hit if you land it.",
				"path": "res://assets/images/ships/isn/patriot.png", "scale": 0.504, "speed_factor": SPEED_BY_ROLE.Fighter,
				"sound_path": "res://assets/sound/ships/isn/isn_1.wav",
				"flame_path": "res://assets/images/ships/isn/patriot_flame.png",
			},
			{
				"name": "BARRAGE", "role": "Gunner",
				"desc": "A relentless quad-barrel spray of fast, low-damage light bullets. Built for sustained suppressing fire rather than any one killing blow.",
				"path": "res://assets/images/ships/isn/barrage.png", "scale": 0.516, "speed_factor": SPEED_BY_ROLE.Gunner,
				"sound_path": "res://assets/sound/ships/isn/isn_1.wav",
				"flame_path": "res://assets/images/ships/isn/barrage_flame.png",
			},
			{
				"name": "BEHEMOTH", "role": "Tank",
				"desc": "Heavy tracking torpedoes plus massive physical deck-guns. The navy's flagship-killer — slow to turn, but nothing stands in its way for long.",
				"path": "res://assets/images/ships/isn/behemoth.png", "scale": 0.331, "speed_factor": SPEED_BY_ROLE.Tank,
				"sound_path": "res://assets/sound/ships/isn/isn_1.wav",
				"flame_path": "res://assets/images/ships/isn/behemoth_flame.png",
			},
		],
	},
	{
		"id": 2, "name": "OUTER RIM COLLECTIVE", "sub": "Gritty, jury-rigged mining rebels — kinetic weapons scavenged and welded onto anything that flies",
		"color": Color(0.82, 0.52, 0.24),
		"ships": [
			{
				"name": "RAIL-JACK", "role": "Fighter",
				"desc": "A single, heavy magnetic railgun slug. Repurposed mining hardware — one slug, but it hits like a dropped asteroid.",
				"path": "res://assets/images/ships/orc/rail_jack.png", "scale": 0.492, "speed_factor": SPEED_BY_ROLE.Fighter,
				"sound_path": "res://assets/sound/ships/orc/orc_1.wav",
				"flame_path": "res://assets/images/ships/orc/rail_jack_flame.png",
			},
			{
				"name": "SCRAP-SPITTER", "role": "Gunner",
				"desc": "A rotary scrap-cannon spraying rapid, low-damage metal shrapnel. Loud, dirty, and built to keep the pressure on.",
				"path": "res://assets/images/ships/orc/scrap_spitter.png", "scale": 0.462, "speed_factor": SPEED_BY_ROLE.Gunner,
				"sound_path": "res://assets/sound/ships/orc/orc_1.wav",
				"flame_path": "res://assets/images/ships/orc/scrap_spitter_flame.png",
			},
			{
				"name": "IRON-CLAD", "role": "Tank",
				"desc": "Unguided explosive rockets plus continuous vulcan machine-gun fire. Bolted-together and brutal — built to grind a fight down, not finesse it.",
				"path": "res://assets/images/ships/orc/iron_clad.png", "scale": 0.495, "speed_factor": SPEED_BY_ROLE.Tank,
				"sound_path": "res://assets/sound/ships/orc/orc_1.wav",
				"flame_path": "res://assets/images/ships/orc/iron_clad_flame.png",
			},
		],
	},
]

# Row layout constants — ROW_X1/ROW_X2_MARGIN are left/right margins (not
# absolute x-coordinates), so rows stretch to fill the actual window width;
# stretch mode is disabled (project.godot), so nothing scales automatically.
const ROW_X1        := 120.0
const ROW_X2_MARGIN := 120.0
const ROW_H     := 115.0
const ROW_GAP   := 6.0
const ROW_START := 175.0
const ROW_IMG   := 95.0

var _root: Control
var _content: Control
var _loading_lbl: Label
var _offered: Array = []
var _chosen_race: Dictionary = {}

# False until the player's first-ever pick (pre-spawn, mandatory — no cancel
# possible). True afterward, once reopen() below can show this screen again
# for a live swap, at which point ESC should be able to back out of it
# without forcing a re-pick.
var _reopenable: bool = false

# race_id -> {btn, count_lbl, hint_lbl, roster_lbl, color}, populated by
# _show_race_selection() and cleared the moment that screen isn't showing —
# _refresh_race_boxes() no-ops while empty, so a loadout_updated/player_removed
# signal arriving on the ship-selection screen (or while closed) is harmless.
var _race_boxes: Dictionary = {}


func _ready() -> void:
	layer = 20
	_build_root()
	GameConfig.team_select_focused = true

	# Keeps the race-selection screen's live counts/rosters/TEAM FULL state
	# in sync as other players join, leave, or swap — both here (pre-spawn)
	# and when reopened mid-match from the pause menu.
	PlayerRegistry.loadout_updated.connect(_on_registry_changed)
	PlayerRegistry.player_removed.connect(_on_registry_player_removed)

	# The 2 offered races must be the same for every player in the match, not
	# independently randomized per client — the server decides once (see
	# World.decide_offered_races()) and every client either asks for it
	# (real remote peers) or reads it directly (the server's own instance,
	# no round-trip needed since it's the same process).
	var world := get_node("/root/World")
	if multiplayer.is_server():
		offer_races(world.decide_offered_races())
	else:
		world.request_offered_races.rpc_id(1)


func _input(event: InputEvent) -> void:
	if _reopenable and _root.visible and event.is_action_pressed("toggle_pause"):
		_close()
		get_viewport().set_input_as_handled()


# Called by PauseMenu's SELECT TEAM button so an already-playing player can
# swap race/ship mid-match. Reuses the match's already-decided _offered pair
# instead of re-requesting it — the server decides that once for everyone
# (World.decide_offered_races()) and it never changes mid-match.
func reopen() -> void:
	_root.visible = true
	GameConfig.team_select_focused = true
	_show_race_selection()


func _close() -> void:
	_root.visible = false
	GameConfig.team_select_focused = false
	_race_boxes.clear()


func _on_registry_changed(_peer_id: int) -> void:
	_refresh_race_boxes()


func _on_registry_player_removed(_peer_id: int, _info: Dictionary) -> void:
	_refresh_race_boxes()


# Recomputes each offered race's human headcount/roster and whether it's
# blocked as "TEAM FULL". Bots don't count — they're padding, not players
# choosing a side, so they'd distort the balance check for no reason. Only
# ever blocks the strictly larger side, and never a race the local player is
# already on (re-picking your own current race shouldn't lock you out of it).
func _refresh_race_boxes() -> void:
	if _race_boxes.is_empty():
		return

	var counts: Dictionary = {}
	var rosters: Dictionary = {}
	for race in _offered:
		counts[race.id] = 0
		rosters[race.id] = []

	for peer_id in PlayerRegistry.players:
		if peer_id < 0:
			continue
		var info: Dictionary = PlayerRegistry.players[peer_id]
		var race_id: int = info.get("race", 0)
		if not counts.has(race_id):
			continue
		counts[race_id] += 1
		rosters[race_id].append(String(info.get("name", "?")))

	var my_race: int = PlayerRegistry.get_info(PlayerRegistry.get_local_id()).get("race", 0)
	var diff: int = counts[_offered[0].id] - counts[_offered[1].id]
	const MAX_ROSTER_SHOWN := 14

	for race in _offered:
		var box: Dictionary = _race_boxes[race.id]
		var count: int = counts[race.id]
		box.count_lbl.text = "%d PLAYER%s" % [count, "" if count == 1 else "S"]

		var names: Array = rosters[race.id]
		if names.is_empty():
			box.roster_lbl.text = "—"
		elif names.size() > MAX_ROSTER_SHOWN:
			box.roster_lbl.text = "\n".join(names.slice(0, MAX_ROSTER_SHOWN)) + \
				"\n+%d more" % (names.size() - MAX_ROSTER_SHOWN)
		else:
			box.roster_lbl.text = "\n".join(names)

		# Only one side can ever be "larger" at a time with exactly 2 offered
		# races, so there's always at least one joinable side.
		# Explicit `bool` types below (not `:=`): `race` comes from the
		# untyped `_offered` Array, so `race.id == ...` resolves to Variant,
		# and this project treats inferring a var's type from Variant as an
		# error (same class of issue as the clamp() gotcha elsewhere).
		var is_larger: bool = (race.id == _offered[0].id and diff > 2) or \
			(race.id == _offered[1].id and diff < -2)
		var is_full: bool = is_larger and race.id != my_race
		box.btn.disabled = is_full
		box.hint_lbl.text = "TEAM FULL" if is_full else "CLICK TO SELECT"
		var col: Color = box.color
		box.hint_lbl.add_theme_color_override("font_color",
			Color(0.95, 0.35, 0.35) if is_full else Color(col.r * 0.5, col.g * 0.5, col.b * 0.5))


# Called once this client knows which 2 races (by id) the match is offering.
func offer_races(race_ids: Array) -> void:
	if _loading_lbl:
		_loading_lbl.queue_free()
		_loading_lbl = null
	_offered = race_ids.map(func(id): return _race_by_id(id))
	_show_race_selection()


func _race_by_id(id: int) -> Dictionary:
	for race in RACES:
		if race.id == id:
			return race
	return RACES[0]


func _build_root() -> void:
	_root = Control.new()
	_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	add_child(_root)

	var overlay := ColorRect.new()
	overlay.color = Color(0.0, 0.0, 0.0, 0.65)
	overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	overlay.mouse_filter = Control.MOUSE_FILTER_STOP
	_root.add_child(overlay)

	_loading_lbl = _label(_root, "LOADING MATCH...", 20, Color(0.5, 0.6, 0.75),
		0, 380, 0, 420, HORIZONTAL_ALIGNMENT_CENTER, 0.0, 1.0)


func _swap_content() -> Control:
	if _content:
		_content.queue_free()
	_content = Control.new()
	_content.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	_content.mouse_filter = Control.MOUSE_FILTER_IGNORE
	_root.add_child(_content)
	return _content


# ── Race selection ───────────────────────────────────────────────────────────

func _show_race_selection() -> void:
	var c := _swap_content()

	_label(c, "CHOOSE YOUR FACTION", 32, Color(0.42, 0.87, 1.0),
		0, 105, 0, 160, HORIZONTAL_ALIGNMENT_CENTER, 0.0, 1.0)
	_label(c, "This match: %s  vs  %s" % [_offered[0].name, _offered[1].name],
		13, Color(0.45, 0.58, 0.72),
		0, 170, 0, 200, HORIZONTAL_ALIGNMENT_CENTER, 0.0, 1.0)

	# Both race buttons are a fixed 440px wide with a 100px gap, centered on
	# the actual screen width via anchor 0.5 — not a hardcoded 1280px design
	# width, since stretch mode is disabled (see project.godot).
	const BTN_W := 440.0
	const HALF_GAP := 50.0

	_race_boxes.clear()
	_build_race_box(c, _offered[0], -(BTN_W + HALF_GAP), -HALF_GAP)
	_build_race_box(c, _offered[1], HALF_GAP, BTN_W + HALF_GAP)
	_refresh_race_boxes()


# Builds one race's button + the player roster below it, and registers both
# in _race_boxes under the race's id so _refresh_race_boxes() can update them
# live as PlayerRegistry changes.
func _build_race_box(c: Control, race: Dictionary, x1: float, x2: float) -> void:
	var built := _make_race_btn(race)
	_place(built.btn, x1, 225, x2, 540, 0.5, 0.5)
	built.btn.pressed.connect(_on_race_chosen.bind(race))
	c.add_child(built.btn)

	var roster_lbl := _label(c, "", 12, Color(0.55, 0.65, 0.78),
		x1, 550, x2, 760, HORIZONTAL_ALIGNMENT_CENTER, 0.5, 0.5)
	roster_lbl.vertical_alignment = VERTICAL_ALIGNMENT_TOP
	roster_lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART

	_race_boxes[race.id] = {
		"btn": built.btn, "count_lbl": built.count_lbl, "hint_lbl": built.hint_lbl,
		"roster_lbl": roster_lbl, "color": race.color,
	}


func _on_race_chosen(race: Dictionary) -> void:
	_chosen_race = race
	_race_boxes.clear()
	_show_ship_selection()


# ── Ship selection ───────────────────────────────────────────────────────────

func _show_ship_selection() -> void:
	var c := _swap_content()

	_label(c, "SELECT YOUR SHIP", 30, Color(0.42, 0.87, 1.0),
		0, 85, 0, 130, HORIZONTAL_ALIGNMENT_CENTER, 0.0, 1.0)
	_label(c, "Playing as  —  " + _chosen_race.name, 14, _chosen_race.color,
		0, 135, 0, 165, HORIZONTAL_ALIGNMENT_CENTER, 0.0, 1.0)

	var ships: Array = _chosen_race.ships
	for i in ships.size():
		var y := ROW_START + i * (ROW_H + ROW_GAP)
		var btn := _make_ship_row(ships[i], _chosen_race.color)
		# ROW_X1 margin from the left edge, ROW_X2_MARGIN margin from the
		# actual right edge (anchor_right=1.0) — the row stretches to fill
		# whatever width is available instead of stopping at a hardcoded
		# 1280px design width.
		_place(btn, ROW_X1, y, -ROW_X2_MARGIN, y + ROW_H, 0.0, 1.0)
		btn.pressed.connect(_on_ship_chosen.bind(ships[i]))
		c.add_child(btn)


func _on_ship_chosen(ship: Dictionary) -> void:
	PlayerRegistry.submit_local_loadout(
		GameConfig.player_name, _chosen_race.id, ship.path, ship.scale, ship.speed_factor, ship.role,
		ship.get("flame_path", ""), ship.get("sound_path", "")
	)
	# Only the mandatory pre-spawn pick should end the menu music — a
	# mid-match reopen (pause menu SELECT TEAM) leaves it alone since it's
	# already stopped by this point (see _reopenable).
	if not _reopenable:
		MusicManager.stop_menu_music()
	_reopenable = true
	_close()


# ── Button builders ──────────────────────────────────────────────────────────

func _make_race_btn(race: Dictionary) -> Dictionary:
	var btn := Button.new()
	btn.text = ""
	var col: Color = race.color

	var sn := _flat(Color(col.r * 0.14, col.g * 0.14, col.b * 0.14, 0.96), 12)
	sn.set_border_width_all(2)
	sn.border_color = Color(col.r * 0.38, col.g * 0.38, col.b * 0.38)
	var sh := _flat(Color(col.r * 0.24, col.g * 0.24, col.b * 0.24, 0.96), 12)
	sh.set_border_width_all(2)
	sh.border_color = col
	var sd := _flat(Color(0.08, 0.08, 0.09, 0.9), 12)
	sd.set_border_width_all(2)
	sd.border_color = Color(0.4, 0.15, 0.15)
	btn.add_theme_stylebox_override("normal", sn)
	btn.add_theme_stylebox_override("hover", sh)
	btn.add_theme_stylebox_override("pressed", sh)
	btn.add_theme_stylebox_override("disabled", sd)

	var vb := VBoxContainer.new()
	vb.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	vb.offset_left = 20; vb.offset_top = 24; vb.offset_right = -20; vb.offset_bottom = -24
	vb.alignment = BoxContainer.ALIGNMENT_CENTER
	vb.add_theme_constant_override("separation", 16)
	vb.mouse_filter = Control.MOUSE_FILTER_IGNORE
	btn.add_child(vb)

	var name_lbl := Label.new()
	name_lbl.text = race.name
	name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	name_lbl.add_theme_font_size_override("font_size", 28)
	name_lbl.add_theme_color_override("font_color", col)
	name_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
	vb.add_child(name_lbl)

	var sep := HSeparator.new()
	sep.add_theme_color_override("color", Color(col.r * 0.4, col.g * 0.4, col.b * 0.4))
	sep.mouse_filter = Control.MOUSE_FILTER_IGNORE
	vb.add_child(sep)

	var sub_lbl := Label.new()
	sub_lbl.text = race.sub
	sub_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	sub_lbl.add_theme_font_size_override("font_size", 15)
	sub_lbl.add_theme_color_override("font_color", Color(0.60, 0.70, 0.82))
	sub_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
	vb.add_child(sub_lbl)

	var count_lbl := Label.new()
	count_lbl.text = "0 PLAYERS"
	count_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	count_lbl.add_theme_font_size_override("font_size", 16)
	count_lbl.add_theme_color_override("font_color", col)
	count_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
	vb.add_child(count_lbl)

	var hint := Label.new()
	hint.text = "CLICK TO SELECT"
	hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	hint.add_theme_font_size_override("font_size", 11)
	hint.add_theme_color_override("font_color", Color(col.r * 0.5, col.g * 0.5, col.b * 0.5))
	hint.mouse_filter = Control.MOUSE_FILTER_IGNORE
	vb.add_child(hint)

	return {"btn": btn, "count_lbl": count_lbl, "hint_lbl": hint}


func _make_ship_row(ship: Dictionary, accent: Color) -> Button:
	var btn := Button.new()
	btn.text = ""
	btn.clip_contents = true

	var sn := _flat(Color(0.06, 0.09, 0.17, 0.95), 8)
	sn.set_border_width_all(1)
	sn.border_color = Color(0.18, 0.28, 0.48, 0.6)
	var sh := _flat(Color(0.10, 0.16, 0.28, 0.97), 8)
	sh.set_border_width_all(2)
	sh.border_color = accent
	btn.add_theme_stylebox_override("normal", sn)
	btn.add_theme_stylebox_override("hover", sh)
	btn.add_theme_stylebox_override("pressed", sh)

	var hb := HBoxContainer.new()
	hb.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	hb.offset_left = 10; hb.offset_top = 8; hb.offset_right = -16; hb.offset_bottom = -8
	hb.add_theme_constant_override("separation", 16)
	hb.mouse_filter = Control.MOUSE_FILTER_IGNORE
	btn.add_child(hb)

	var img_box := Control.new()
	img_box.custom_minimum_size = Vector2(ROW_IMG, ROW_IMG)
	img_box.size_flags_vertical = Control.SIZE_SHRINK_CENTER
	img_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
	hb.add_child(img_box)

	var tex := TextureRect.new()
	tex.texture = load(ship.path)
	tex.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	tex.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
	tex.mouse_filter = Control.MOUSE_FILTER_IGNORE
	img_box.add_child(tex)

	var vb := VBoxContainer.new()
	vb.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	vb.size_flags_vertical = Control.SIZE_EXPAND_FILL
	vb.alignment = BoxContainer.ALIGNMENT_CENTER
	vb.add_theme_constant_override("separation", 3)
	vb.mouse_filter = Control.MOUSE_FILTER_IGNORE
	hb.add_child(vb)

	var header := HBoxContainer.new()
	header.add_theme_constant_override("separation", 10)
	header.mouse_filter = Control.MOUSE_FILTER_IGNORE
	vb.add_child(header)

	var name_lbl := Label.new()
	name_lbl.text = ship.name
	name_lbl.add_theme_font_size_override("font_size", 18)
	name_lbl.add_theme_color_override("font_color", Color(0.92, 0.95, 1.0))
	name_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
	header.add_child(name_lbl)

	var role_lbl := Label.new()
	role_lbl.text = ship.role.to_upper()
	role_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
	role_lbl.add_theme_font_size_override("font_size", 13)
	role_lbl.add_theme_color_override("font_color", accent)
	role_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
	header.add_child(role_lbl)

	var desc_lbl := Label.new()
	desc_lbl.text = ship.desc
	desc_lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	desc_lbl.clip_text = true
	desc_lbl.add_theme_font_size_override("font_size", 12)
	desc_lbl.add_theme_color_override("font_color", Color(0.62, 0.70, 0.80))
	desc_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
	vb.add_child(desc_lbl)

	return btn


# ── Helpers ──────────────────────────────────────────────────────────────────

func _label(parent: Control, text: String, font_size: int, color: Color,
		x1: float, y1: float, x2: float, y2: float,
		align: HorizontalAlignment = HORIZONTAL_ALIGNMENT_LEFT,
		anchor_left: float = 0.0, anchor_right: float = 0.0) -> Label:
	var lbl := Label.new()
	lbl.text = text
	lbl.horizontal_alignment = align
	lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
	lbl.add_theme_font_size_override("font_size", font_size)
	lbl.add_theme_color_override("font_color", color)
	_place(lbl, x1, y1, x2, y2, anchor_left, anchor_right)
	parent.add_child(lbl)
	return lbl


# anchor_left/anchor_right default to 0.0 (pixels measured from the left
# edge), matching the design-resolution layout this screen was originally
# built at. Pass 1.0 for anchor_right to make x2 a margin from the actual
# right edge instead (stretches with window width), or 0.5 for both to
# center a fixed-size element regardless of window width — see call sites
# below for which each element needs (stretch mode is disabled, so nothing
# scales automatically the way it used to under canvas_items stretch).
#
# Anchors and offsets are set as plain property assignments, NOT via
# sequential set_anchor_and_offset() calls — that method's default
# push_opposite_anchor=true drags the opposite side's anchor/offset along
# whenever the two calls momentarily disagree (e.g. left set to 0.5 while
# right is still its 0.0 default), corrupting layout for any non-0.0 anchor.
# Found via main_menu.gd's play buttons rendering flush against the left
# edge instead of centered.
func _place(node: Control, x1: float, y1: float, x2: float, y2: float,
		anchor_left: float = 0.0, anchor_right: float = 0.0) -> void:
	node.anchor_left = anchor_left
	node.anchor_right = anchor_right
	node.anchor_top = 0.0
	node.anchor_bottom = 0.0
	node.offset_left = x1
	node.offset_top = y1
	node.offset_right = x2
	node.offset_bottom = y2


func _flat(col: Color, radius: int = 0) -> StyleBoxFlat:
	var s := StyleBoxFlat.new()
	s.bg_color = col
	s.set_corner_radius_all(radius)
	return s
