<!--
BattleViewer — orchestrates the radial battle scene, the playback
controls, and the accessibility text log for one BattleReport. Owns
the playback state (`frameIndex`, `playing`, `speed`, `logOpen`).
Layout reorganisation (latest iteration):

- The header carries the planet title, the back-navigation links and
  the frame counter so the scene captures the full viewer width and
  height beneath them.
- A drag-seek slider sits between the scene and the controls; the
  user can scrub the playback timeline at any speed.
- The text log collapses behind a toggle in the controls bar so a
  user who wants the biggest scene possible can hide it entirely.

The component is logically isolated: feed it any `BattleReport`
matching `pkg/model/report/battle.go` and it plays back.
-->
<script lang="ts">
	import { i18n } from "$lib/i18n/index.svelte";
	import type { BattleReport } from "../../api/battle-fetch";
	import type { ShipClassLookup } from "./mass";

	import BattleScene from "./battle-scene.svelte";
	import PlaybackControls, {
		type PlaybackSpeed,
	} from "./playback-controls.svelte";
	import { buildFrames } from "./timeline";

	let {
		report,
		shipClassLookup,
		onBackToMap,
		onBackToReport,
	}: {
		report: BattleReport;
		shipClassLookup?: ShipClassLookup;
		onBackToMap?: () => void;
		onBackToReport?: () => void;
	} = $props();

	const frames = $derived(buildFrames(report));
	let frameIndex = $state(0);
	let playing = $state(false);
	let speed = $state<PlaybackSpeed>(1);
	let logOpen = $state(true);
	let shotVisible = $state(true);
	let logEl = $state<HTMLOListElement | null>(null);

	const rawFrame = $derived(frames[Math.min(frameIndex, frames.length - 1)]);

	// displayFrame freezes the layout at the penultimate frame's
	// state once the protocol's last action eliminates a race, so
	// the surviving cluster does not suddenly reflow onto the
	// planet ring on the very last shot. The frame counter still
	// advances to the final shot and `lastAction` still drives the
	// killing line + flash; only `remaining` and `activeRaceIds`
	// (the layout-determining state) freeze.
	const displayFrame = $derived.by(() => {
		const last = frames.length - 1;
		if (
			frameIndex === last &&
			last >= 1 &&
			frames[last].activeRaceIds.length < frames[last - 1].activeRaceIds.length
		) {
			const prev = frames[last - 1];
			const cur = frames[last];
			return {
				shotIndex: cur.shotIndex,
				lastAction: cur.lastAction,
				remaining: prev.remaining,
				activeRaceIds: prev.activeRaceIds,
			};
		}
		return rawFrame;
	});

	// One tick per frame: blink the shot line off during the last
	// 10 % of the frame's interval, then advance. Effect re-arms
	// whenever frameIndex / playing / speed changes; previous
	// timers clean up through the return.
	$effect(() => {
		void frameIndex;
		void speed;
		shotVisible = true;
		if (!playing) return;
		const intervalMs = 400 / speed;
		const blinkOff = setTimeout(() => {
			shotVisible = false;
		}, intervalMs * 0.9);
		const advance = setTimeout(() => {
			if (frameIndex >= frames.length - 1) {
				playing = false;
				return;
			}
			frameIndex = frameIndex + 1;
		}, intervalMs);
		return () => {
			clearTimeout(blinkOff);
			clearTimeout(advance);
		};
	});

	// Auto-scroll the visible log row into view so the highlight
	// keeps up with the timeline on long battles.
	$effect(() => {
		void displayFrame.shotIndex;
		if (!logOpen || logEl === null) return;
		const current = logEl.querySelector(
			'li[data-current="true"]',
		) as HTMLElement | null;
		if (current !== null) {
			current.scrollIntoView({ block: "nearest", behavior: "smooth" });
		}
	});

	function seekToShot(actionIndex: number) {
		playing = false;
		frameIndex = Math.max(
			0,
			Math.min(frames.length - 1, actionIndex + 1),
		);
	}

	function onScrub(event: Event) {
		const target = event.currentTarget as HTMLInputElement;
		const value = Number(target.value);
		if (!Number.isFinite(value)) return;
		playing = false;
		frameIndex = Math.max(0, Math.min(frames.length - 1, Math.trunc(value)));
	}

	function describeAction(index: number): string {
		const action = report.protocol[index];
		const attackerGroup = report.ships[String(action.sa)];
		const defenderGroup = report.ships[String(action.sd)];
		const attackerRace = attackerGroup?.race ?? `race ${action.a}`;
		const attackerClass = attackerGroup?.className ?? `class ${action.sa}`;
		const defenderRace = defenderGroup?.race ?? `race ${action.d}`;
		const defenderClass = defenderGroup?.className ?? `class ${action.sd}`;
		const key = action.x
			? "game.battle.log.destroyed"
			: "game.battle.log.shielded";
		return i18n.t(key, {
			attacker_race: attackerRace,
			attacker_class: attackerClass,
			defender_race: defenderRace,
			defender_class: defenderClass,
		});
	}
</script>

<div class="viewer" data-testid="battle-viewer">
	<header class="header">
		<div class="back-row">
			{#if onBackToMap}
				<button
					type="button"
					class="back-btn"
					onclick={onBackToMap}
					data-testid="battle-back-to-map"
				>{i18n.t("game.battle.back_to_map")}</button>
			{/if}
			{#if onBackToReport}
				<button
					type="button"
					class="back-btn"
					onclick={onBackToReport}
					data-testid="battle-back-to-report"
				>{i18n.t("game.battle.back_to_report")}</button>
			{/if}
		</div>
		<h2 data-testid="battle-viewer-title">
			{i18n.t("game.battle.header_title", {
				planet_name: report.planetName,
				planet_number: String(report.planet),
			})}
		</h2>
		<span class="progress" data-testid="battle-frame-index">
			{displayFrame.shotIndex} / {report.protocol.length}
		</span>
	</header>

	<div class="scene">
		<BattleScene {report} frame={displayFrame} {shipClassLookup} {shotVisible} />
	</div>

	<input
		class="scrubber"
		type="range"
		min="0"
		max={Math.max(0, frames.length - 1)}
		step="1"
		value={frameIndex}
		oninput={onScrub}
		aria-label={i18n.t("game.battle.controls.scrub")}
		data-testid="battle-scrubber"
	/>

	<PlaybackControls
		bind:playing
		bind:frameIndex
		bind:speed
		bind:logOpen
		frameCount={frames.length}
	/>

	{#if logOpen}
		<section
			class="log"
			aria-label={i18n.t("game.battle.accessibility.protocol_heading")}
		>
			<h3>{i18n.t("game.battle.accessibility.protocol_heading")}</h3>
			<ol bind:this={logEl} data-testid="battle-protocol-log">
				{#each report.protocol as _action, i (i)}
					<li
						data-testid="battle-protocol-log-item"
						data-current={i + 1 === displayFrame.shotIndex ? "true" : "false"}
					>
						<button
							type="button"
							class="log-row-btn"
							onclick={() => seekToShot(i)}
						>{describeAction(i)}</button>
					</li>
				{/each}
			</ol>
		</section>
	{/if}
</div>

<style>
	.viewer {
		display: flex;
		flex-direction: column;
		gap: 0.5rem;
		width: 100%;
		flex: 1 1 auto;
		min-height: 0;
		margin: 0 auto;
		padding: 0.75rem 1rem;
		color: #d6dcf2;
		font-family: inherit;
		box-sizing: border-box;
	}
	.header {
		display: flex;
		align-items: center;
		gap: 0.75rem;
		flex: 0 0 auto;
	}
	.header h2 {
		flex: 1 1 auto;
		margin: 0;
		font-size: 1.05rem;
		text-transform: uppercase;
		letter-spacing: 0.04em;
		text-align: center;
	}
	.back-row {
		display: flex;
		gap: 0.3rem;
		flex: 0 0 auto;
	}
	.back-btn {
		appearance: none;
		background: #1f2748;
		color: #d6dcf2;
		border: 1px solid #2c3568;
		padding: 0.3rem 0.6rem;
		border-radius: 3px;
		cursor: pointer;
		font-size: 0.8rem;
	}
	.back-btn:hover {
		background: #2a3463;
	}
	.progress {
		color: #93a0d0;
		font-variant-numeric: tabular-nums;
		font-size: 0.85rem;
		flex: 0 0 auto;
		min-width: 5rem;
		text-align: right;
	}
	.scene {
		background: #0a0d1a;
		border: 1px solid #1e264a;
		border-radius: 4px;
		overflow: hidden;
		flex: 1 1 auto;
		min-height: 0;
	}
	.scrubber {
		width: 100%;
		margin: 0;
		flex: 0 0 auto;
		accent-color: #6d7bb5;
	}
	.log {
		flex: 0 1 auto;
		min-height: 4rem;
		max-height: 30vh;
		overflow-y: auto;
		display: flex;
		flex-direction: column;
	}
	.log h3 {
		margin: 0 0 0.3rem;
		color: #93a0d0;
		font-size: 0.75rem;
		text-transform: uppercase;
		letter-spacing: 0.05em;
		flex: 0 0 auto;
	}
	.log ol {
		list-style: none;
		margin: 0;
		padding: 0;
		font-size: 0.85rem;
		overflow-y: auto;
		color: #c6cdf0;
		flex: 1 1 auto;
		min-height: 0;
	}
	.log li {
		border-bottom: 1px solid #1c2240;
	}
	.log-row-btn {
		display: block;
		width: 100%;
		text-align: left;
		padding: 0.15rem 0.4rem;
		background: transparent;
		border: 0;
		color: inherit;
		font: inherit;
		cursor: pointer;
	}
	.log-row-btn:hover,
	.log-row-btn:focus-visible {
		background: #131a36;
	}
	.log li[data-current="true"] .log-row-btn {
		color: #ffe27a;
		font-weight: 600;
		background: #1a2240;
	}
</style>
