<script lang="ts">
	import { onDestroy } from 'svelte';
	import { getMap, plotPrediction, plotAnimatedMarker, plotEndMarker, plotBoundingBox } from '$map';
	import { timelineStore } from '$features/timeline/store';
	import { workspacesStore } from './store';
	import type { Workspace } from './types';
	import { computeBoundingBox, DEFAULT_BBOX_MARGIN_KM, type LatLngTuple } from '$domain';

	/**
	 * Renders every workspace onto the shared map. Each workspace gets its own
	 * named scene (`ws/<id>`) so its layers can be cleared independently, plus
	 * a `cursor/<id>` scene for the animated playback marker.
	 *
	 * The component is placed as a child of <Map /> so `getMap()` returns a
	 * non-null instance via context.
	 */

	const map = getMap();
	if (!map) throw new Error('WorkspaceRenderer must be a descendant of <Map />');

	// Track the scenes we currently own so we can dispose the ones that
	// belong to workspaces which were removed from the store since last tick.
	const ownedPlotScenes = new Set<string>();
	const ownedCursorScenes = new Set<string>();
	const ownedBboxScenes = new Set<string>();
	// Last-rendered state per workspace scene — skip re-plot when nothing changed.
	const plotCache = new Map<string, { result: unknown; color: string; opacity: number }>();
	// Cursor scenes that have reached their flight end and show a static end marker.
	const doneCursorScenes = new Set<string>();

	const sceneName = (w: Workspace) => `ws/${w.id}`;
	const cursorName = (w: Workspace) => `cursor/${w.id}`;
	const bboxName = (w: Workspace) => `bbox/${w.id}`;

	function updateGlobalRange(items: Workspace[]) {
		let maxDuration = 0;
		const entries: Array<{ duration: number; color: string }> = [];
		for (const w of items) {
			if (!w.visible || !w.result) continue;
			const duration =
				w.result.landing.datetime.getTime() - w.result.launch.datetime.getTime();
			if (duration > maxDuration) maxDuration = duration;
			entries.push({ duration, color: w.color });
		}
		timelineStore.setRange(0, maxDuration);
		const seen = new Set<number>();
		const markers = entries
			.filter(({ duration }) => {
				if (duration >= maxDuration || seen.has(duration)) return false;
				seen.add(duration);
				return true;
			})
			.map(({ duration, color }) => ({ time: duration, color }));
		timelineStore.setMarkers(markers);
	}

	function renderAll(items: Workspace[]) {
		if (!map) return;

		const live = new Set<string>();
		for (const w of items) {
			const name = sceneName(w);
			live.add(name);
			if (!w.visible || !w.result) {
				if (ownedPlotScenes.has(name)) {
					map.disposeScene(name);
					ownedPlotScenes.delete(name);
					plotCache.delete(name);
				}
				continue;
			}
			const cached = plotCache.get(name);
			if (!cached || cached.result !== w.result || cached.color !== w.color || cached.opacity !== w.opacity) {
				const scene = map.scene(name);
				plotPrediction(scene, w.result, { color: w.color, opacity: w.opacity });
				// Frame a freshly-arrived result. A flight spans only tens of km,
				// which is a few dozen pixels at the default camera height — the
				// track ends up completely hidden under its own launch/burst/
				// landing markers and reads as a single dot. Deliberately not done
				// for colour/opacity edits, so tweaking one workspace while
				// comparing several does not yank the camera around.
				if (!cached || cached.result !== w.result) {
					map.fitBounds(w.result.flight_path, 50);
				}
				ownedPlotScenes.add(name);
				plotCache.set(name, { result: w.result, color: w.color, opacity: w.opacity });
			}
		}

		for (const name of Array.from(ownedPlotScenes)) {
			if (!live.has(name)) {
				map.disposeScene(name);
				ownedPlotScenes.delete(name);
				plotCache.delete(name);
			}
		}
	}

	function renderBboxes(items: Workspace[]) {
		if (!map) return;

		const live = new Set<string>();
		for (const w of items) {
			const name = bboxName(w);
			live.add(name);
			const box =
				w.visible && w.result && w.bboxVisible
					? computeBoundingBox(w.result.flight_path, w.bboxMargin ?? DEFAULT_BBOX_MARGIN_KM)
					: null;
			if (!box) {
				if (ownedBboxScenes.has(name)) {
					map.disposeScene(name);
					ownedBboxScenes.delete(name);
				}
				continue;
			}
			plotBoundingBox(map.scene(name), box);
			ownedBboxScenes.add(name);
		}

		for (const name of Array.from(ownedBboxScenes)) {
			if (!live.has(name)) {
				map.disposeScene(name);
				ownedBboxScenes.delete(name);
			}
		}
	}

	function positionAt(path: LatLngTuple[], elapsed: number, durationMs: number): LatLngTuple | null {
		if (path.length === 0) return null;
		if (durationMs === 0) return path[0];
		const t = Math.max(0, Math.min(1, elapsed / durationMs));
		const raw = t * (path.length - 1);
		const idx = Math.floor(raw);
		if (idx >= path.length - 1) return path[path.length - 1];
		const frac = raw - idx;
		const a = path[idx];
		const b = path[idx + 1];
		return [a[0] + (b[0] - a[0]) * frac, a[1] + (b[1] - a[1]) * frac] as LatLngTuple;
	}

	function renderCursors(items: Workspace[], time: number) {
		if (!map) return;

		const live = new Set<string>();
		for (const w of items) {
			const name = cursorName(w);
			live.add(name);
			if (!w.visible || !w.result) {
				if (ownedCursorScenes.has(name)) {
					map.disposeScene(name);
					ownedCursorScenes.delete(name);
					doneCursorScenes.delete(name);
				}
				continue;
			}
			const durationMs =
				w.result.landing.datetime.getTime() - w.result.launch.datetime.getTime();
			const p = positionAt(w.result.flight_path, time, durationMs);
			if (!p) continue;
			const scene = map.scene(name);
			const done = time >= durationMs;
			if (done) {
				if (!doneCursorScenes.has(name)) {
					// Transition into done state: swap to static end marker.
					scene.clear();
					plotEndMarker(scene, p[1], p[0]);
					doneCursorScenes.add(name);
				}
				// Position is clamped to landing — nothing more to update.
			} else {
				if (doneCursorScenes.has(name)) {
					// Transition back to active (user seeked backwards).
					scene.clear();
					doneCursorScenes.delete(name);
				}
				plotAnimatedMarker(scene, p[1], p[0]);
			}
			ownedCursorScenes.add(name);
		}

		for (const name of Array.from(ownedCursorScenes)) {
			if (!live.has(name)) {
				map.disposeScene(name);
				ownedCursorScenes.delete(name);
				doneCursorScenes.delete(name);
			}
		}
	}

	$effect(() => {
		const items = $workspacesStore.items;
		renderAll(items);
		renderBboxes(items);
		updateGlobalRange(items);
	});

	$effect(() => {
		renderCursors($workspacesStore.items, $timelineStore.time);
	});

	onDestroy(() => {
		if (!map) return;
		for (const name of ownedPlotScenes) map.disposeScene(name);
		for (const name of ownedCursorScenes) map.disposeScene(name);
		for (const name of ownedBboxScenes) map.disposeScene(name);
		ownedPlotScenes.clear();
		ownedCursorScenes.clear();
		ownedBboxScenes.clear();
		doneCursorScenes.clear();
	});
</script>
