<!--
	Full-screen preview modal. Mounted once at the layout root so every
	route (timeline, /tags, future grids) can open it via Space or a
	double-click on a tile. Reads from the global selection store:

	  - selection.focused → which photo to display
	  - selection.order   → walked left/right + drives PreviewCarousel

	Layout: preview pane (left, fills) + RightSidebar (right) + action
	toolbar + thumbnail carousel along the bottom. Keyboard nav (←/→/Space/
	Esc) is owned here; gridKeyNav bails out of those keys while the modal
	is open so we don't double-handle. Action keys (X/S/U/⌘Z/etc.) continue
	to flow through gridKeyNav since they target selection state, which
	the modal shares.
-->
<script lang="ts">
	import { Dialog } from 'bits-ui';
	import { createQuery } from '@tanstack/svelte-query';
	import { X } from 'lucide-svelte';
	import { closePreview, view } from '$lib/stores/view.svelte';
	import {
		clearBulkToFirst,
		clearSelection,
		selectRange,
		selection,
		setAnchor,
		setFocused
	} from '$lib/stores/selection.svelte';
	import { getPhoto } from '$lib/services/photoprism';
	import { isAuthenticated } from '$lib/stores/session.svelte';
	import type { PpPhoto } from '$lib/types/photoprism';
	import PreviewPane from './PreviewPane.svelte';
	import PreviewCarousel from './PreviewCarousel.svelte';
	import RightSidebar from '$lib/components/sidebar/RightSidebar.svelte';
	import BulkActionBar from '$lib/components/timeline/BulkActionBar.svelte';
	import { InlineLoader } from '$lib/components/feedback';

	const focusedUid = $derived(selection.focused);

	// Fetch the focused photo so the RightSidebar always has full
	// metadata even when the user jumped here from a list that only
	// hydrated thumbnails.
	const focusedPhotoQuery = createQuery<PpPhoto>(() => ({
		queryKey: ['photo', focusedUid ?? ''],
		queryFn: () => getPhoto(focusedUid as string),
		enabled: Boolean(focusedUid) && isAuthenticated() && view.previewOpen
	}));

	function step(delta: number, extending: boolean) {
		const order = selection.order;
		const cur = focusedUid ? order.indexOf(focusedUid) : -1;
		if (cur < 0) return;
		const next = order[cur + delta];
		if (!next) return;
		// Plain arrow drops any prior multi-selection down to the cursor —
		// otherwise the previously selected tiles keep their blue ring and
		// the carousel reads as two simultaneously selected images (the old
		// ones still in ids, plus the freshly focused one). Mirrors
		// gridKeyNav.moveFocus's contract.
		if (!extending) clearSelection();
		setFocused(next);
		if (extending) selectRange(next);
		else setAnchor(next);
	}

	function onKeydown(e: KeyboardEvent) {
		// Don't steal keys from inputs inside the sidebar (editable
		// metadata fields, keyword chips, etc.).
		const t = e.target as HTMLElement | null;
		const tag = t?.tagName;
		if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || t?.isContentEditable) {
			return;
		}
		// stopImmediatePropagation prevents gridKeyNav's window-level
		// handler from firing afterwards — without it, closing on Space
		// would set previewOpen=false and then gridKeyNav's Space branch
		// would re-open the modal because selection.focused is still set.
		if (e.key === 'Escape') {
			e.preventDefault();
			e.stopImmediatePropagation();
			// First Esc collapses a bulk back to single-focus on its first
			// member, keeping the modal open so the user can keep working.
			// Second Esc (no bulk left) closes the modal.
			if (clearBulkToFirst()) return;
			closePreview();
			return;
		}
		if (e.key === ' ' || e.code === 'Space') {
			e.preventDefault();
			e.stopImmediatePropagation();
			closePreview();
			return;
		}
		if (e.key === 'ArrowLeft') {
			e.preventDefault();
			e.stopImmediatePropagation();
			step(-1, e.shiftKey);
			return;
		}
		if (e.key === 'ArrowRight') {
			e.preventDefault();
			e.stopImmediatePropagation();
			step(1, e.shiftKey);
		}
	}

	// Bind a window-level handler only while the modal is open. The
	// `capture` phase lets us pre-empt gridKeyNav's own listener for the
	// keys we own (arrows, Esc, Space) without having to coordinate
	// listener order.
	$effect(() => {
		if (!view.previewOpen) return;
		const handler = (e: KeyboardEvent) => onKeydown(e);
		window.addEventListener('keydown', handler, { capture: true });
		return () => window.removeEventListener('keydown', handler, { capture: true } as EventListenerOptions);
	});
</script>

<Dialog.Root
	open={view.previewOpen}
	onOpenChange={(o) => {
		if (!o) closePreview();
	}}
>
	<Dialog.Portal>
		<Dialog.Overlay
			class="fixed inset-0 z-40 bg-background/95 backdrop-blur-sm data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0"
		/>
		<Dialog.Content
			class="fixed inset-0 z-50 flex flex-col overflow-hidden bg-background outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0"
		>
			<Dialog.Title class="sr-only">Photo preview</Dialog.Title>
			<Dialog.Description class="sr-only">
				Full-screen preview of the focused photo with metadata and a thumbnail carousel.
			</Dialog.Description>

			<!-- Top row: preview pane (fills) + sidebar (fixed width).
			     BulkActionBar lives inside the main column — same shape as the
			     timeline (+page.svelte) so the bar stays bounded by the
			     column's width and doesn't stretch under the metadata
			     sidebar. -->
			<div class="flex min-h-0 flex-1">
				<div class="relative flex min-w-0 flex-1 flex-col">
					<button
						type="button"
						class="absolute right-3 top-3 z-20 inline-flex h-8 w-8 items-center justify-center rounded-full bg-background/80 text-foreground shadow hover:bg-background"
						onclick={closePreview}
						aria-label="Close preview"
						title="Close (Esc)"
					>
						<X class="h-4 w-4" />
					</button>
					<div class="flex min-h-0 flex-1">
						<PreviewPane uid={focusedUid} order={selection.order} />
					</div>
					<!-- Action toolbar (acts on selection.ids; falls back to focused). -->
					<BulkActionBar />
				</div>
				<!-- Sidebar stays mounted across photo changes so the preview
				     pane doesn't reflow on arrow-skim; contents swap between
				     the metadata panel and a small loader the same way the
				     timeline's right-aside does. -->
				<aside
					class="w-[300px] shrink-0 overflow-y-auto border-l border-border bg-card"
				>
					{#if focusedPhotoQuery.data}
						<RightSidebar photo={focusedPhotoQuery.data} />
					{:else if focusedPhotoQuery.isFetching}
						<InlineLoader size="sm" label="Loading metadata…" />
					{/if}
				</aside>
			</div>

			<!-- Bottom filmstrip across selection.order. -->
			<PreviewCarousel />
		</Dialog.Content>
	</Dialog.Portal>
</Dialog.Root>
