<script lang="ts">
	import { deserialize, enhance } from '$app/forms';
	import { SvelteSet } from 'svelte/reactivity';
	import type { PageProps } from './$types';
	import type { Game } from '$lib/server/db/schema';

	type Entry = { id: number; availability: Availability; note: string | null };

	let { data }: PageProps = $props();

	const caught = new SvelteSet(data.caughtIds);

	// the full table is ~1000 rows x ~40 games; only render the rows inside
	// the scroll viewport (plus some overscan) to keep the DOM small
	const ROW_HEIGHT = 37; // keep in sync with h-[37px] on the rows
	const OVERSCAN = 10;
	let scrollEl = $state<HTMLDivElement>();
	let scrollTop = $state(0);
	let viewportHeight = $state(600);
	const startIndex = $derived(Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN));
	const endIndex = $derived(
		Math.min(data.pokemon.length, Math.ceil((scrollTop + viewportHeight) / ROW_HEIGHT) + OVERSCAN)
	);
	const visiblePokemon = $derived(data.pokemon.slice(startIndex, endIndex));

	// optimistic toggle without a <form> per cell — one shared handler instead
	// of ~10k use:enhance initializations
	async function toggleCaught(entryId: number, checked: boolean) {
		if (checked) caught.add(entryId);
		else caught.delete(entryId);

		const body = new FormData();
		body.set('entryId', String(entryId));
		if (checked) body.set('caught', 'on');
		let failed = false;
		try {
			const response = await fetch('?/togglePokemonCaught', { method: 'POST', body });
			const result = deserialize(await response.text());
			failed = result.type === 'failure' || result.type === 'error';
		} catch {
			failed = true;
		}
		if (failed) {
			// server rejected the toggle, roll back the optimistic update
			if (checked) caught.delete(entryId);
			else caught.add(entryId);
		}
	}

	// pokemonId -> gameId -> entry, for quick cell lookup
	const entryMap = $derived(
		new Map(
			data.pokemon.map((pkmn) => [
				pkmn.id,
				new Map(
					pkmn.entries.map(([id, gameId, code, note]) => [
						gameId,
						{ id, availability: availabilityValues[code], note: note ?? null } satisfies Entry
					])
				)
			])
		)
	);

	// empty selection = user hasn't set rules yet, show everything
	const selectedGameIds = $derived(new Set(data.selectedGameIds));
	const visibleGames = $derived(
		selectedGameIds.size > 0 ? data.games.filter((g) => selectedGameIds.has(g.id)) : data.games
	);
	const visibleGameIds = $derived(new Set(visibleGames.map((g) => g.id)));

	const catchableEntries = $derived(
		data.pokemon
			.flatMap((pkmn) => pkmn.entries)
			.filter(
				([, gameId, code]) => availabilityValues[code] === 'catchable' && visibleGameIds.has(gameId)
			)
	);
	const total = $derived(catchableEntries.length);
	const caughtCount = $derived(
		catchableEntries.reduce((n, [id]) => n + (caught.has(id) ? 1 : 0), 0)
	);
	const percentage = $derived(total > 0 ? ((caughtCount / total) * 100).toFixed(2) : '0.00');

	const gamesByGeneration = $derived(
		[...Map.groupBy(data.games, (g) => g.generation)].sort(([a], [b]) => a - b)
	);

	// rules modal
	let rulesDialog: HTMLDialogElement;
	let draft = $state(new SvelteSet<number>());

	function openRules() {
		draft = new SvelteSet(selectedGameIds.size > 0 ? selectedGameIds : data.games.map((g) => g.id));
		rulesDialog.showModal();
	}

	function tooltip(entry: Entry | undefined, game: Game): string | null {
		if (!data.user) return 'Log in to track your progress';
		if (!entry) return `Not available in ${game.name}`;
		const reason = {
			catchable: null,
			trade_only: 'Only obtainable by trading in this game',
			transfer_only: 'Only obtainable by transferring from another game',
			event_only: 'Only obtainable through an event distribution'
		}[entry.availability];
		if (reason === null) return null;
		return entry.note ? `${reason} (${entry.note})` : reason;
	}

	const typeColors: Record<string, string> = {
		Normal: '#A8A77A',
		Fire: '#EE8130',
		Water: '#6390F0',
		Electric: '#F7D02C',
		Grass: '#7AC74C',
		Ice: '#96D9D6',
		Fighting: '#C22E28',
		Poison: '#A33EA1',
		Ground: '#E2BF65',
		Flying: '#A98FF3',
		Psychic: '#F95587',
		Bug: '#A6B91A',
		Rock: '#B6A136',
		Ghost: '#735797',
		Dragon: '#6F35FC',
		Dark: '#705746',
		Steel: '#B7B7CE',
		Fairy: '#D685AD'
	};
</script>

<svelte:head>
	<title>catchemall</title>
</svelte:head>

<div class="flex flex-col">
	<h1 class="py-4 text-center text-2xl font-bold">Gotta catch 'em all!</h1>
</div>

<div class="flex justify-center">
	<p class="max-w-200 py-6 text-center">
		Below are all {data.pokemon.length} Pokemon across {data.games.length} games. Play through all generations
		and catch all catchable pokemon in each. Use the "Set rules" button to pick which games you want to
		complete.
	</p>
</div>

{#if !data.user}
	<p class="pb-4 text-center text-sm text-gray-500">
		<a href="/login" class="font-bold text-blue-500 underline">Log in</a> to save your progress.
	</p>
{/if}

<div class="flex items-center justify-center gap-4 py-4">
	<button
		class="cursor-pointer rounded-lg bg-blue-400 px-4 py-2 font-bold text-white hover:bg-blue-500"
		onclick={openRules}>Set rules</button
	>

	<p>{caughtCount}/{total} entries caught ({percentage} %)</p>
	<progress value={total > 0 ? caughtCount / total : 0}></progress>
</div>

<div
	bind:this={scrollEl}
	bind:clientHeight={viewportHeight}
	onscroll={() => (scrollTop = scrollEl?.scrollTop ?? 0)}
	class="max-h-[75vh] overflow-auto rounded-xl border border-gray-300"
>
	<table class="min-w-full table-fixed border-separate border-spacing-0 text-sm">
		<thead>
			<tr>
				<th
					class="sticky top-0 left-0 z-30 w-90 border-r border-b border-gray-300 bg-white px-2 text-left"
					>Pokemon</th
				>
				{#each visibleGames as game (game.id)}
					<th
						class="sticky top-0 z-20 w-9 border-b border-gray-300 bg-white px-1 pt-2 pb-1 align-bottom"
					>
						<span class="mx-auto rotate-180 [writing-mode:vertical-rl]">{game.name}</span>
					</th>
				{/each}
			</tr>
		</thead>
		<tbody>
			{#if startIndex > 0}
				<tr aria-hidden="true">
					<td colspan={visibleGames.length + 1} style="height: {startIndex * ROW_HEIGHT}px"></td>
				</tr>
			{/if}
			{#each visiblePokemon as pokemon (pokemon.id)}
				<tr class="h-[37px] hover:bg-blue-50">
					<th
						class="sticky left-0 z-10 border-r border-b border-gray-200 bg-white px-2 py-0.5 text-left font-normal whitespace-nowrap"
					>
						<div class="flex items-center gap-2">
							<img
								src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{pokemon.number}.png"
								alt=""
								loading="lazy"
								class="h-8 w-8"
							/>
							<span class="min-w-10 font-bold">{pokemon.number}</span>
							<span class="min-w-30">{pokemon.name}</span>
							<span
								class="rounded-lg px-2 py-0.5 text-xs"
								style:background-color={typeColors[pokemon.type1]}>{pokemon.type1}</span
							>
							{#if pokemon.type2}
								<span
									class="rounded-lg px-2 py-0.5 text-xs"
									style:background-color={typeColors[pokemon.type2]}>{pokemon.type2}</span
								>
							{/if}
						</div>
					</th>
					{#each visibleGames as game (game.id)}
						{@const entry = entryMap.get(pokemon.id)?.get(game.id)}
						{@const reason = tooltip(entry, game)}
						<td class="border-b border-gray-200 text-center">
							{#if entry && reason === null}
								<input
									type="checkbox"
									class="cursor-pointer"
									checked={caught.has(entry.id)}
									onchange={(e) => toggleCaught(entry.id, e.currentTarget.checked)}
								/>
							{:else}
								<span title={reason} class="cursor-help">
									<input type="checkbox" disabled checked={entry ? caught.has(entry.id) : false} />
								</span>
							{/if}
						</td>
					{/each}
				</tr>
			{/each}
			{#if endIndex < data.pokemon.length}
				<tr aria-hidden="true">
					<td
						colspan={visibleGames.length + 1}
						style="height: {(data.pokemon.length - endIndex) * ROW_HEIGHT}px"
					></td>
				</tr>
			{/if}
		</tbody>
	</table>
</div>

<dialog bind:this={rulesDialog} class="m-auto w-full max-w-2xl rounded-xl p-6 backdrop:bg-black/50">
	<h2 class="pb-2 text-xl font-bold">Set rules</h2>
	<p class="pb-4 text-sm text-gray-600">
		Pick the games you want to catch 'em all in. Only those show up in the table and count towards
		your progress.
	</p>

	<div class="flex gap-2 pb-4">
		<button
			type="button"
			class="cursor-pointer rounded-lg bg-gray-200 px-3 py-1 text-sm hover:bg-gray-300"
			onclick={() => (draft = new SvelteSet(data.games.map((g) => g.id)))}>All games</button
		>
		<button
			type="button"
			class="cursor-pointer rounded-lg bg-gray-200 px-3 py-1 text-sm hover:bg-gray-300"
			onclick={() => (draft = new SvelteSet())}>None</button
		>
	</div>

	<form
		method="POST"
		action="?/setGames"
		use:enhance={() => {
			return async ({ result, update }) => {
				await update();
				if (result.type === 'success') rulesDialog.close();
			};
		}}
	>
		<div class="grid max-h-96 grid-cols-2 gap-x-6 overflow-y-auto sm:grid-cols-3">
			{#each gamesByGeneration as [generation, games] (generation)}
				<fieldset class="pb-3">
					<legend class="flex items-center gap-2 pb-1 font-bold">
						<input
							type="checkbox"
							checked={games.every((g) => draft.has(g.id))}
							onchange={(e) => {
								for (const g of games) {
									if (e.currentTarget.checked) draft.add(g.id);
									else draft.delete(g.id);
								}
							}}
						/>
						Gen {generation}
					</legend>
					{#each games as game (game.id)}
						<label class="flex items-center gap-2 py-0.5 pl-4 text-sm">
							<input
								type="checkbox"
								name="gameIds"
								value={game.id}
								checked={draft.has(game.id)}
								onchange={(e) => {
									if (e.currentTarget.checked) draft.add(game.id);
									else draft.delete(game.id);
								}}
							/>
							{game.name}
						</label>
					{/each}
				</fieldset>
			{/each}
		</div>

		<div class="flex justify-end gap-2 pt-4">
			<button
				type="button"
				class="cursor-pointer rounded-lg bg-gray-200 px-4 py-2 font-bold hover:bg-gray-300"
				onclick={() => rulesDialog.close()}>Cancel</button
			>
			<button
				class="cursor-pointer rounded-lg bg-blue-400 px-4 py-2 font-bold text-white hover:bg-blue-500"
				disabled={!data.user}
				title={data.user ? undefined : 'Log in to save rules'}>Save</button
			>
		</div>
	</form>
</dialog>
