<script lang="ts">
	import { FileUp, Folder, FileText } from '@lucide/svelte';

	import { Button } from '$lib/components/ui/button/index.js';
	import type { FileRejectedReason } from '$lib/components/ui/file-drop-zone';
	import { cn } from '$lib/utils';

	let {
		onUpload,
		onFileRejected,
		accept,
		maxFileSize,
		disabled = false,
		class: className
	}: {
		onUpload: (files: File[]) => Promise<void> | void;
		onFileRejected?: (opts: { reason: FileRejectedReason; file: File }) => void;
		/** Comma separated extensions and/or MIME types, as the `accept` attribute takes. */
		accept?: string;
		/** Bytes. */
		maxFileSize?: number;
		disabled?: boolean;
		class?: string;
	} = $props();

	/**
	 * Two inputs rather than one.
	 *
	 * `webkitdirectory` is not a filter — it switches the picker into folder mode,
	 * so a single input can offer files or folders but never both. The drop target
	 * has no such constraint and stays one area.
	 */
	let fileInput = $state<HTMLInputElement>();
	let folderInput = $state<HTMLInputElement>();

	let dragging = $state(false);
	let busy = $state(false);

	const active = $derived(!disabled && !busy);

	function accepts(file: File): FileRejectedReason | undefined {
		if (maxFileSize !== undefined && file.size > maxFileSize) return 'Maximum file size exceeded';
		if (!accept) return undefined;

		const name = file.name.toLowerCase();
		const type = file.type.toLowerCase();

		const ok = accept
			.split(',')
			.map((pattern) => pattern.trim().toLowerCase())
			.some((pattern) => {
				// Match on the pattern, not the file's type. Testing `type` here is
				// what makes MOBI fail: browsers report no MIME type for it, so a
				// ".mobi" rule never gets compared against the filename.
				if (pattern.startsWith('.')) return name.endsWith(pattern);
				if (pattern.endsWith('/*')) return type.startsWith(pattern.slice(0, -1));
				return type === pattern;
			});

		return ok ? undefined : 'File type not allowed';
	}

	/** readEntries hands back at most 100 at a time and signals the end with an empty batch. */
	function readAll(reader: FileSystemDirectoryReader): Promise<FileSystemEntry[]> {
		return new Promise((resolve, reject) => {
			const entries: FileSystemEntry[] = [];

			const next = () =>
				reader.readEntries((batch) => {
					if (batch.length === 0) return resolve(entries);
					entries.push(...batch);
					next();
				}, reject);

			next();
		});
	}

	/**
	 * Flattens a dropped entry into files, naming each one with its path inside the
	 * dropped folder so it matches what the folder picker puts in
	 * `webkitRelativePath` — which is what the upload form reads to keep structure.
	 */
	async function walk(entry: FileSystemEntry, prefix = ''): Promise<File[]> {
		if (entry.isFile) {
			const file = await new Promise<File>((resolve, reject) =>
				(entry as FileSystemFileEntry).file(resolve, reject)
			);
			return [new File([file], `${prefix}${file.name}`, { type: file.type })];
		}

		if (entry.isDirectory) {
			const entries = await readAll((entry as FileSystemDirectoryEntry).createReader());
			const nested = await Promise.all(entries.map((e) => walk(e, `${prefix}${entry.name}/`)));
			return nested.flat();
		}

		return [];
	}

	async function handleDrop(event: DragEvent) {
		event.preventDefault();
		dragging = false;
		if (!active) return;

		// Read the entries synchronously: DataTransfer is emptied as soon as this
		// handler yields, so awaiting first loses everything that was dropped.
		const entries = Array.from(event.dataTransfer?.items ?? [])
			.filter((item) => item.kind === 'file')
			.map((item) => item.webkitGetAsEntry())
			.filter((entry): entry is FileSystemEntry => entry !== null);

		// Older engines expose no entries; fall back to the flat list, which cannot
		// contain folders anyway.
		if (entries.length === 0) {
			await submit(Array.from(event.dataTransfer?.files ?? []));
			return;
		}

		busy = true;
		try {
			const nested = await Promise.all(entries.map((entry) => walk(entry)));
			await submit(nested.flat());
		} finally {
			busy = false;
		}
	}

	async function handleChange(event: Event) {
		const input = event.currentTarget as HTMLInputElement;
		const chosen = Array.from(input.files ?? []);
		// Reset first so picking the same file twice still fires a change event.
		input.value = '';
		await submit(chosen);
	}

	async function submit(candidates: File[]) {
		const accepted: File[] = [];

		for (const file of candidates) {
			const reason = accepts(file);
			if (reason) onFileRejected?.({ file, reason });
			else accepted.push(file);
		}

		if (accepted.length > 0) await onUpload(accepted);
	}
</script>

<div
	role="group"
	aria-label="Add books"
	aria-disabled={!active}
	ondragover={(e) => {
		e.preventDefault();
		if (active) dragging = true;
	}}
	ondragleave={() => (dragging = false)}
	ondrop={handleDrop}
	class={cn(
		'flex flex-col items-center gap-3 rounded-lg border-2 border-dashed border-border bg-accent/20 p-6 text-center transition-colors',
		dragging && 'border-primary bg-accent/50',
		!active && 'opacity-50',
		className
	)}
>
	<div
		class="flex size-12 place-items-center justify-center rounded-full border border-dashed border-border text-muted-foreground"
	>
		<FileUp class="size-5" />
	</div>

	<div class="flex flex-col gap-0.5">
		<span class="font-medium">
			{busy ? 'Reading folder…' : 'Drop books here'}
		</span>
		<span class="text-sm text-muted-foreground">A folder keeps its structure</span>
	</div>

	<span class="font-mono text-[10px] tracking-widest text-muted-foreground uppercase">or</span>

	<div class="flex flex-wrap justify-center gap-2">
		<Button
			type="button"
			variant="outline"
			size="sm"
			disabled={!active}
			onclick={() => fileInput?.click()}
		>
			<FileText class="size-4" />
			Choose files
		</Button>
		<Button
			type="button"
			variant="outline"
			size="sm"
			disabled={!active}
			onclick={() => folderInput?.click()}
		>
			<Folder class="size-4" />
			Choose folder
		</Button>
	</div>

	<input
		bind:this={fileInput}
		type="file"
		multiple
		{accept}
		class="hidden"
		onchange={handleChange}
	/>
	<!-- webkitdirectory is why this needs to be a second input: it turns the
	     picker into a folder chooser rather than filtering what it accepts. -->
	<input
		bind:this={folderInput}
		type="file"
		multiple
		webkitdirectory
		class="hidden"
		onchange={handleChange}
	/>
</div>
