<!--
	Inner preview surface used by the full-screen PreviewModal and the
	shareable /photo/[uid] deep-link route. Renders the focused photo or
	video, with optional prev/next chevrons that walk `order` via
	setFocused.

	Video mounting is debounced 250 ms so arrow-skim across a stretch of
	video tiles doesn't open (and immediately cancel) range requests we'd
	throw away. Until the timer fires, the poster image stands in.
-->
<script lang="ts">
	import { createQuery, useQueryClient } from '@tanstack/svelte-query';
	import { getPhoto } from '$lib/services/photoprism';
	import { thumbSrc, thumbSrcSet, thumbUrl, videoUrl } from '$lib/stores/session.svelte';
	import { setAnchor, setFocused } from '$lib/stores/selection.svelte';
	import { view } from '$lib/stores/view.svelte';
	import VideoPlayer from '$lib/components/preview/VideoPlayer.svelte';
	import { isVideo, primaryFile, videoFile, type PpPhoto } from '$lib/types/photoprism';
	import { EmptyState, InlineLoader } from '$lib/components/feedback';
	import { AlertCircle, Image as ImageIcon } from 'lucide-svelte';

	interface Props {
		uid: string | null;
		order: string[];
		/** Hide the floating prev/next chevrons (callers that wrap their
		 *  own nav can disable to avoid duplication). */
		showChevrons?: boolean;
	}
	let { uid, order, showChevrons = true }: Props = $props();

	const qc = useQueryClient();

	const photoQuery = createQuery<PpPhoto>(() => ({
		queryKey: ['photo', uid ?? ''],
		queryFn: () => getPhoto(uid as string),
		enabled: Boolean(uid)
	}));

	const currentIndex = $derived(uid ? order.indexOf(uid) : -1);

	/** Mirrors PreviewCarousel.lookup — pulls a PpPhoto out of TanStack's
	 *  cache without firing a fetch, so we can resolve adjacent hashes for
	 *  prefetching without making the prefetch itself trigger more work. */
	function lookupCached(target: string): PpPhoto | null {
		const direct = qc.getQueryData<PpPhoto>(['photo', target]);
		if (direct) return direct;
		const lists = qc.getQueriesData({ queryKey: ['photos'] });
		for (const [, data] of lists) {
			if (!data) continue;
			if (Array.isArray(data)) {
				const hit = (data as PpPhoto[]).find((p) => p.UID === target);
				if (hit) return hit;
				continue;
			}
			const pages = (data as { pages?: PpPhoto[][] }).pages;
			if (!Array.isArray(pages)) continue;
			for (const page of pages) {
				const hit = page?.find?.((p) => p.UID === target);
				if (hit) return hit;
			}
		}
		return null;
	}

	// Prefetch fit_1280 for the ±2 neighbours of the focused photo so
	// arrow-skim feels instant. We `new Image()` rather than `<link
	// rel=preload>` because the URLs are runtime-derived and a throwaway
	// Image() reuses the browser's HTTP cache the same way.
	$effect(() => {
		if (!uid || currentIndex < 0) return;
		for (const offset of [-1, 1, -2, 2]) {
			const idx = currentIndex + offset;
			if (idx < 0 || idx >= order.length) continue;
			const photo = lookupCached(order[idx]);
			if (!photo) continue;
			const hash = primaryFile(photo).Hash;
			if (!hash) continue;
			const img = new Image();
			img.src = thumbUrl(hash, 'fit_1280');
		}
	});

	const VIDEO_LOAD_DELAY_MS = 250;
	let armedUid = $state<string | null>(null);

	$effect(() => {
		const target = uid;
		if (!target) {
			armedUid = null;
			return;
		}
		const t = setTimeout(() => {
			armedUid = target;
		}, VIDEO_LOAD_DELAY_MS);
		return () => clearTimeout(t);
	});

	function focusAt(i: number) {
		const next = order[i];
		if (!next) return;
		setFocused(next);
		setAnchor(next);
	}
</script>

<div class="relative flex h-full w-full items-center justify-center bg-black/40 p-4">
	{#if uid === null}
		<EmptyState icon={ImageIcon} title="Select a photo to preview" />
	{:else if photoQuery.isPending}
		<InlineLoader label="Loading photo…" align="center" />
	{:else if photoQuery.isError}
		<EmptyState tone="destructive" icon={AlertCircle} title="Failed to load photo" />
	{:else if photoQuery.data}
		{@const pf = primaryFile(photoQuery.data)}
		{#if showChevrons && currentIndex > 0}
			<button
				class="absolute left-2 top-1/2 z-10 -translate-y-1/2 rounded-full bg-background/80 px-3 py-2 text-lg hover:bg-background"
				onclick={() => focusAt(currentIndex - 1)}
				aria-label="Previous photo"
			>
				‹
			</button>
		{/if}
		{#if showChevrons && currentIndex >= 0 && currentIndex < order.length - 1}
			<button
				class="absolute right-2 top-1/2 z-10 -translate-y-1/2 rounded-full bg-background/80 px-3 py-2 text-lg hover:bg-background"
				onclick={() => focusAt(currentIndex + 1)}
				aria-label="Next photo"
			>
				›
			</button>
		{/if}

		{#if isVideo(photoQuery.data) && armedUid === uid}
			{@const vf = videoFile(photoQuery.data)}
			{#key vf.Hash}
				<VideoPlayer
					src={videoUrl(vf.Hash)}
					poster={thumbUrl(pf.Hash, 'fit_1280')}
					title={photoQuery.data.OriginalName ?? pf.Name ?? ''}
				/>
			{/key}
		{:else}
			{@const altText =
				photoQuery.data.OriginalName ??
				pf.Name ??
				(isVideo(photoQuery.data) ? 'Video' : 'Photo')}
			{#if pf.Width && pf.Height}
				<!-- LQIP layer: the same URL the grid loaded, blurred to mask
				     the tile_*'s square center-crop against the sharp image's
				     true aspect. Sized via aspect-ratio + max-* + m-auto so it
				     lands in the exact same bounding box as the sharp <img>
				     beside it (object-contain semantics, but expressible on a
				     positioned element). Paints from the HTTP cache the moment
				     the modal opens. -->
				<img
					src={thumbSrc(pf.Hash, view.thumbnailSize)}
					srcset={thumbSrcSet(pf.Hash, view.thumbnailSize)}
					alt=""
					aria-hidden="true"
					class="pointer-events-none absolute inset-0 m-auto max-h-full max-w-full rounded-md object-cover blur-2xl"
					style="aspect-ratio: {pf.Width} / {pf.Height};"
				/>
			{/if}
			<img
				src={thumbUrl(pf.Hash, 'fit_1280')}
				alt={altText}
				fetchpriority="high"
				decoding="async"
				class="relative max-h-full max-w-full rounded-md object-contain shadow-2xl"
			/>
		{/if}
	{/if}
</div>
