<!--
F8-10 ship-groups table. Lists own (`localShipGroups`) and foreign
(`otherShipGroups`) ship groups under a single grid, with checkboxes
for the two owner buckets, a planet dropdown that narrows to groups
touching the chosen planet (destination OR origin), and a ship-class
dropdown. A row click focuses the on-map target through the
transient `SelectionStore.focus` channel:

  - on-planet groups (origin === null && range === null) focus the
    destination planet so the map centres on it and the inspector
    opens its planet card;
  - in-space groups focus the group itself, so the map centres on
    its interpolated position and the inspector shows the group.

`incomingShipGroups` and `unidentifiedShipGroups` are intentionally
out of scope here — the map and inspector already surface those
categories.
-->
<script lang="ts">
	import { getContext } from "svelte";

	import type {
		ReportLocalShipGroup,
		ReportOtherShipGroup,
	} from "../../api/game-state";
	import { activeView } from "$lib/app-nav.svelte";
	import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
	import { planetLabel } from "./report/format";
	import {
		RENDERED_REPORT_CONTEXT_KEY,
		type RenderedReportSource,
	} from "$lib/rendered-report.svelte";
	import {
		SELECTION_CONTEXT_KEY,
		type Selected,
		type SelectionStore,
		type ShipGroupRef,
	} from "$lib/selection.svelte";
	import ViewState from "$lib/ui/view-state.svelte";
	import { formatFloat, formatInt } from "$lib/util/number-format";
	import {
		shipGroupsTableState as persistent,
		type ShipGroupsSortColumn as SortColumn,
	} from "./table-ship-groups-state.svelte";

	type OwnerKind = "own" | "foreign";

	type Row = {
		key: string;
		owner: OwnerKind;
		ref: ShipGroupRef;
		class: string;
		count: number;
		race: string;
		mass: number;
		speed: number;
		destination: number;
		origin: number | null;
		range: number | null;
		state: string;
	};

	const COLUMN_LABELS: Record<SortColumn, TranslationKey> = {
		owner: "game.table.ship_groups.column.owner",
		class: "game.table.ship_groups.column.class",
		count: "game.table.ship_groups.column.count",
		race: "game.table.ship_groups.column.race",
		location: "game.table.ship_groups.column.location",
		mass: "game.table.ship_groups.column.mass",
		speed: "game.table.ship_groups.column.speed",
	};

	const COLUMNS: readonly SortColumn[] = [
		"owner",
		"class",
		"count",
		"race",
		"location",
		"mass",
		"speed",
	];

	const rendered = getContext<RenderedReportSource | undefined>(
		RENDERED_REPORT_CONTEXT_KEY,
	);
	const selection = getContext<SelectionStore | undefined>(SELECTION_CONTEXT_KEY);

	// `persistent` (module-level rune above) drives every filter /
	// sort selection so the user's choices survive table↔map round
	// trips.

	const reportLoaded = $derived(
		rendered?.report !== null && rendered?.report !== undefined,
	);
	const local = $derived<ReportLocalShipGroup[]>(
		rendered?.report?.localShipGroups ?? [],
	);
	const other = $derived<ReportOtherShipGroup[]>(
		rendered?.report?.otherShipGroups ?? [],
	);

	const rows = $derived.by<Row[]>(() => {
		const acc: Row[] = [];
		for (const g of local) {
			acc.push({
				key: `local:${g.id}`,
				owner: "own",
				ref: { variant: "local", id: g.id },
				class: g.class,
				count: g.count,
				race: g.race,
				mass: g.mass,
				speed: g.speed,
				destination: g.destination,
				origin: g.origin,
				range: g.range,
				state: g.state,
			});
		}
		for (let i = 0; i < other.length; i++) {
			const g = other[i]!;
			acc.push({
				key: `other:${i}`,
				owner: "foreign",
				ref: { variant: "other", index: i },
				class: g.class,
				count: g.count,
				race: g.race,
				mass: g.mass,
				speed: g.speed,
				destination: g.destination,
				origin: g.origin,
				range: g.range,
				state: "",
			});
		}
		return acc;
	});

	// rowsByOwner is the slice surviving just the owner checkboxes,
	// reused for the planet / class dropdown options so toggling
	// "foreign" off prunes the choices to only own-group planets and
	// classes (and vice versa).
	const rowsByOwner = $derived.by(() => {
		return rows.filter((r) => {
			if (r.owner === "own" && !persistent.showOwn) return false;
			if (r.owner === "foreign" && !persistent.showForeign) return false;
			return true;
		});
	});

	const classes = $derived.by(() => {
		const set = new Set<string>();
		for (const r of rowsByOwner) if (r.class !== "") set.add(r.class);
		return Array.from(set).sort((a, b) => a.localeCompare(b));
	});

	const planets = $derived.by(() => {
		const set = new Set<number>();
		for (const r of rowsByOwner) {
			set.add(r.destination);
			if (r.origin !== null) set.add(r.origin);
		}
		return Array.from(set).sort((a, b) => a - b);
	});

	const filtered = $derived.by(() => {
		const planet =
			persistent.planetFilter === "" ? null : Number(persistent.planetFilter);
		return rowsByOwner.filter((r) => {
			if (planet !== null && r.destination !== planet && r.origin !== planet) {
				return false;
			}
			if (persistent.classFilter !== "" && r.class !== persistent.classFilter) {
				return false;
			}
			return true;
		});
	});

	const sorted = $derived.by(() => {
		const list = [...filtered];
		const dir = persistent.sortDirection === "asc" ? 1 : -1;
		list.sort((a, b) => compare(a, b, persistent.sortColumn) * dir);
		return list;
	});

	function compare(a: Row, b: Row, column: SortColumn): number {
		switch (column) {
			case "owner":
				return a.owner.localeCompare(b.owner);
			case "class":
				return a.class.localeCompare(b.class);
			case "count":
				return a.count - b.count;
			case "race":
				return a.race.localeCompare(b.race);
			case "location": {
				if (a.destination !== b.destination) return a.destination - b.destination;
				const ao = a.origin ?? Number.MAX_SAFE_INTEGER;
				const bo = b.origin ?? Number.MAX_SAFE_INTEGER;
				return ao - bo;
			}
			case "mass":
				return a.mass - b.mass;
			case "speed":
				return a.speed - b.speed;
		}
	}

	function toggleSort(column: SortColumn): void {
		if (persistent.sortColumn === column) {
			persistent.sortDirection = persistent.sortDirection === "asc" ? "desc" : "asc";
			return;
		}
		persistent.sortColumn = column;
		persistent.sortDirection = "asc";
	}

	function ariaSort(column: SortColumn): "ascending" | "descending" | "none" {
		if (persistent.sortColumn !== column) return "none";
		return persistent.sortDirection === "asc" ? "ascending" : "descending";
	}

	function isInSpace(row: Row): boolean {
		return row.origin !== null && row.range !== null;
	}

	function openOnMap(row: Row): void {
		let target: Selected;
		if (isInSpace(row)) {
			target = { kind: "shipGroup", ref: row.ref };
		} else {
			target = { kind: "planet", id: row.destination };
		}
		selection?.focus(target);
		activeView.select("map");
	}

	function locationLabel(row: Row): string {
		const all = rendered?.report?.planets ?? [];
		if (!isInSpace(row)) {
			return `@ ${planetLabel(row.destination, all)}`;
		}
		const from = planetLabel(row.origin!, all);
		const to = planetLabel(row.destination, all);
		return `${from} → ${to} (${formatFloat(row.range!)})`;
	}

	function ownerLabel(row: Row): string {
		return i18n.t(
			row.owner === "own"
				? "game.table.ship_groups.owner.own"
				: "game.table.ship_groups.owner.foreign",
		);
	}
</script>

<section
	class="active-view"
	data-testid="active-view-table"
	data-entity="ship-groups"
>
	<header>
		<h2>{i18n.t("game.table.ship_groups.title")}</h2>
		<div class="controls">
			<label class="check">
				<input
					type="checkbox"
					data-testid="ship-groups-filter-own"
					bind:checked={persistent.showOwn}
				/>
				<span>{i18n.t("game.table.ship_groups.owner.own")}</span>
			</label>
			<label class="check">
				<input
					type="checkbox"
					data-testid="ship-groups-filter-foreign"
					bind:checked={persistent.showForeign}
				/>
				<span>{i18n.t("game.table.ship_groups.owner.foreign")}</span>
			</label>
			{#if planets.length > 0}
				<label class="dropdown">
					<span>{i18n.t("game.table.ship_groups.filter.planet")}</span>
					<select
						data-testid="ship-groups-filter-planet"
						bind:value={persistent.planetFilter}
					>
						<option value=""
							>{i18n.t("game.table.ship_groups.filter.planet.all")}</option
						>
						{#each planets as n (n)}
							<option value={String(n)}
								>{planetLabel(n, rendered?.report?.planets ?? [])}</option
							>
						{/each}
					</select>
				</label>
			{/if}
			{#if classes.length > 0}
				<label class="dropdown">
					<span>{i18n.t("game.table.ship_groups.filter.class")}</span>
					<select
						data-testid="ship-groups-filter-class"
						bind:value={persistent.classFilter}
					>
						<option value=""
							>{i18n.t("game.table.ship_groups.filter.class.all")}</option
						>
						{#each classes as c (c)}
							<option value={c}>{c}</option>
						{/each}
					</select>
				</label>
			{/if}
		</div>
	</header>

	{#if !reportLoaded}
		<ViewState
			kind="loading"
			testid="ship-groups-loading"
			message={i18n.t("game.table.ship_groups.loading")}
		/>
	{:else if rows.length === 0}
		<ViewState
			kind="empty"
			testid="ship-groups-empty"
			message={i18n.t("game.table.ship_groups.empty")}
		/>
	{:else}
		<table class="grid" data-testid="ship-groups-table">
			<thead>
				<tr>
					{#each COLUMNS as column (column)}
						<th
							aria-sort={ariaSort(column)}
							class:numeric={column === "count" ||
								column === "mass" ||
								column === "speed"}
						>
							<button
								type="button"
								class="sort"
								data-testid="ship-groups-column-{column}"
								onclick={() => toggleSort(column)}
							>
								{i18n.t(COLUMN_LABELS[column])}
								{#if persistent.sortColumn === column}
									<span class="sort-indicator" aria-hidden="true">
										{persistent.sortDirection === "asc" ? "▲" : "▼"}
									</span>
								{/if}
							</button>
						</th>
					{/each}
				</tr>
			</thead>
			<tbody>
				{#each sorted as row (row.key)}
					<tr
						data-testid="ship-groups-row"
						data-key={row.key}
						data-owner={row.owner}
						data-in-space={isInSpace(row)}
						onclick={() => openOnMap(row)}
					>
						<td data-testid="ship-groups-cell-owner">{ownerLabel(row)}</td>
						<td data-testid="ship-groups-cell-class">{row.class || "—"}</td>
						<td class="numeric" data-testid="ship-groups-cell-count">
							{formatInt(row.count)}
						</td>
						<td data-testid="ship-groups-cell-race">{row.race || "—"}</td>
						<td data-testid="ship-groups-cell-location">
							{locationLabel(row)}
						</td>
						<td class="numeric" data-testid="ship-groups-cell-mass">
							{formatFloat(row.mass)}
						</td>
						<td class="numeric" data-testid="ship-groups-cell-speed">
							{formatFloat(row.speed)}
						</td>
					</tr>
				{/each}
			</tbody>
		</table>
	{/if}
</section>

<style>
	.active-view {
		padding: 1.5rem;
		font-family: system-ui, sans-serif;
		display: flex;
		flex-direction: column;
		gap: 1rem;
	}
	header {
		display: flex;
		flex-direction: column;
		gap: 0.5rem;
	}
	header h2 {
		margin: 0;
		font-size: 1.1rem;
	}
	.controls {
		display: flex;
		gap: 0.75rem;
		align-items: center;
		flex-wrap: wrap;
	}
	.check {
		display: inline-flex;
		gap: 0.3rem;
		align-items: center;
		font-size: 0.85rem;
		color: var(--color-text-muted);
		cursor: pointer;
	}
	.check input {
		margin: 0;
	}
	.dropdown {
		display: inline-flex;
		gap: 0.4rem;
		align-items: center;
		font-size: 0.85rem;
		color: var(--color-text-muted);
	}
	.dropdown select {
		font: inherit;
		font-size: 0.85rem;
		padding: 0.15rem 0.3rem;
		background: var(--color-bg);
		color: var(--color-text);
		border: 1px solid var(--color-border);
		border-radius: 3px;
	}
	.grid {
		border-collapse: collapse;
		width: 100%;
		font-variant-numeric: tabular-nums;
	}
	.grid th,
	.grid td {
		padding: 0.4rem 0.6rem;
		text-align: left;
		border-bottom: 1px solid var(--color-border-subtle);
		font-size: 0.85rem;
	}
	.grid th {
		color: var(--color-text-muted);
		text-transform: uppercase;
		letter-spacing: 0.04em;
		font-size: 0.75rem;
	}
	.grid th.numeric,
	.grid td.numeric {
		font-family: var(--font-mono);
		text-align: right;
	}
	.grid th.numeric .sort {
		justify-content: flex-end;
	}
	.grid tbody tr {
		cursor: pointer;
	}
	.grid tbody tr:hover {
		background: var(--color-surface);
	}
	.sort {
		font: inherit;
		font-size: inherit;
		text-transform: inherit;
		letter-spacing: inherit;
		color: inherit;
		background: transparent;
		border: none;
		padding: 0;
		cursor: pointer;
		display: inline-flex;
		gap: 0.3rem;
		align-items: baseline;
	}
	.sort-indicator {
		font-size: 0.7em;
	}
</style>
