<script lang="ts">
import type { PanZoomState } from '$lib/person/genealogy/panZoom';

interface RailRow {
	rank: number;
	label: number;
	/** Row centre in SVG user coordinates. */
	centerY: number;
}

interface Props {
	/** The canvas SVG, read for its live screen transform. */
	svg: SVGSVGElement | null;
	rows: RailRow[];
	/** Tracked so chip positions recompute on every pan/zoom. */
	panZoom: PanZoomState;
}

let { svg, rows, panZoom }: Props = $props();

type Chip = { rank: number; label: number; top: number; visible: boolean };
let chips = $state<Chip[]>([]);
let height = $state(0);

// Fallback stacked positions when no screen transform is available (e.g. an
// unsized test container): list the chips top-down rather than hiding them.
const FALLBACK_TOP = 24;
const FALLBACK_GAP = 28;
// Off-screen cull margin (px) so a chip just past an edge isn't popped abruptly.
const CULL_MARGIN = 16;

// Map each generation-row centre from SVG user space to a screen-y via the live
// CTM. This reads whatever transform the browser actually computed, so it is
// independent of `preserveAspectRatio` (no manual letterbox math). Runs in an
// effect — not a $derived — so the CTM is read AFTER the viewBox DOM update is
// flushed. NB: this reads layout (getScreenCTM + getBoundingClientRect) once per
// pan/zoom/inertia frame; fine for a handful of chips, revisit if rows grow large.
$effect(() => {
	// Reading pan/zoom + height registers them as effect dependencies (so it
	// re-runs on pan, zoom and resize — the CTM reflects the parent's viewBox,
	// which Svelte cannot track on its own) and doubles as a NaN guard.
	const inputsFinite = Number.isFinite(panZoom.x + panZoom.y + panZoom.z + height);
	const ctm = svg && inputsFinite ? svg.getScreenCTM() : null;
	const top0 = svg ? svg.getBoundingClientRect().top : 0;
	// Always emit one chip per labelled row so the labels exist regardless of
	// transform availability; the CTM only positions them (fallback: stacked).
	chips = rows.map((row, i) => {
		const top = ctm
			? new DOMPoint(0, row.centerY).matrixTransform(ctm).y - top0
			: FALLBACK_TOP + i * FALLBACK_GAP;
		const visible = !ctm || height <= 0 || (top >= -CULL_MARGIN && top <= height + CULL_MARGIN);
		return { rank: row.rank, label: row.label, top, visible };
	});
});
</script>

<!-- Pinned to the canvas's left edge: chips stay put horizontally while the
     tree pans, and track their generation row vertically at any zoom. -->
<div class="pointer-events-none absolute inset-y-0 left-0 z-10" bind:clientHeight={height}>
	{#each chips as chip (chip.rank)}
		{#if chip.visible}
			<div
				role="text"
				aria-label={`Generation ${chip.label}`}
				class="absolute left-1 -translate-y-1/2 rounded-sm border border-line bg-surface px-1.5 py-0.5 font-serif text-xs text-ink-3 shadow-sm"
				style="top: {chip.top}px"
			>
				G{chip.label}
			</div>
		{/if}
	{/each}
</div>
