<script lang="ts" module>
	/**
	 * Build a nested folder tree from PhotoPrism's flat `Path`-keyed
	 * folder list. The API returns one row per subfolder
	 * (`2024`, `2024/lyon`, `2024/paris`, …); we group by the parent
	 * segment so the UI can render a real <ul> tree.
	 */
	export interface TreeNode {
		path: string;
		name: string;
		children: TreeNode[];
	}

	export function buildTree(paths: string[]): TreeNode[] {
		const root: TreeNode = { path: '', name: '', children: [] };
		const index = new Map<string, TreeNode>([['', root]]);
		const sorted = [...paths].sort();
		for (const p of sorted) {
			const parts = p.split('/');
			let parentPath = '';
			for (let i = 0; i < parts.length; i++) {
				const here = parts.slice(0, i + 1).join('/');
				if (!index.has(here)) {
					const node: TreeNode = {
						path: here,
						name: parts[i],
						children: []
					};
					const parent = index.get(parentPath);
					if (parent) parent.children.push(node);
					index.set(here, node);
				}
				parentPath = here;
			}
		}
		return root.children;
	}
</script>

<script lang="ts">
	import { filters } from '$lib/stores/filters.svelte';
	import { browser } from '$app/environment';
	import { FolderPlus, Pencil, Trash2 } from 'lucide-svelte';
	import Self from './FolderTree.svelte';
	import KebabMenu, { Item, Separator } from './KebabMenu.svelte';

	interface Props {
		nodes: TreeNode[];
		depth?: number;
		onPick: (path: string) => void;
		/** Mutating callbacks are only required when readonly !== true. The
		 *  picker (HeapConvertDialog) reuses the tree just for `onPick`. */
		onRename?: (path: string) => void;
		onDelete?: (path: string) => void;
		onCreateChild?: (parent: string) => void;
		/** Read-only mode: hides the kebab menu and disables double-click
		 *  rename, so the tree can be reused as a folder picker. */
		readonly?: boolean;
		/** Override the active-row predicate. By default rows light up when
		 *  `filters.folderPath` matches (the sidebar nav case); the picker
		 *  passes its own selection so the dialog has independent state. */
		selectedPath?: string | null;
		/** Optional per-path photo count. When provided, each row renders a
		 *  compact badge with the count — matching the heaps section's
		 *  "{n} photos" affordance. Undefined keeps the badge off entirely
		 *  (the picker dialog doesn't need it). */
		counts?: Record<string, number>;
	}
	let {
		nodes,
		depth = 0,
		onPick,
		onRename,
		onDelete,
		onCreateChild,
		readonly = false,
		selectedPath,
		counts
	}: Props = $props();

	// Auto-expanded folders, persisted to localStorage so the tree state
	// survives reloads. Empty set = everything collapsed at start.
	const KEY = 'mule_folder_open';
	let openSet = $state<Set<string>>(loadOpen());
	function loadOpen(): Set<string> {
		if (!browser) return new Set();
		try {
			const raw = localStorage.getItem(KEY);
			return raw ? new Set(JSON.parse(raw)) : new Set();
		} catch {
			return new Set();
		}
	}
	function persist() {
		if (browser) localStorage.setItem(KEY, JSON.stringify([...openSet]));
	}
	function toggle(p: string) {
		if (openSet.has(p)) openSet.delete(p);
		else openSet.add(p);
		openSet = new Set(openSet); // re-trigger reactivity
		persist();
	}

	function isActive(path: string): boolean {
		if (selectedPath !== undefined) return selectedPath === path;
		return filters.folderPath === path;
	}
</script>

<ul>
	{#each nodes as node (node.path)}
		{@const open = openSet.has(node.path)}
		{@const active = isActive(node.path)}
		{@const hasChildren = node.children.length > 0}
		<li>
			<!--
				Indent via padding-left rather than nested margin+border, so the
				active row's background bleeds edge-to-edge of the sidebar (matches
				mule-image's compact tree). 8px baseline aligns the depth-0 chevron
				with the px-2 of Views/Heaps rows; +12px per nested level.
			-->
			<div
				class="group flex h-[24px] items-center rounded pr-2 text-[12px] leading-tight hover:bg-accent"
				class:bg-primary={active}
				class:text-primary-foreground={active}
				class:hover:bg-primary={active}
				style="padding-left: {8 + depth * 12}px;"
			>
				{#if hasChildren}
					<button
						class="flex h-[18px] w-4 items-center justify-center text-[10px]"
						class:text-muted-foreground={!active}
						onclick={() => toggle(node.path)}
						title={open ? 'Collapse' : 'Expand'}
						aria-label={open ? 'Collapse' : 'Expand'}
					>
						{open ? '▾' : '▸'}
					</button>
				{:else if depth > 0}
					<!-- Spacer keeps childless siblings aligned with their chevroned
					     peers at nested depths. Skipped at depth 0 so root folders
					     left-align with the Views/Heaps rows. -->
					<span class="inline-block h-[18px] w-4" aria-hidden="true"></span>
				{/if}
				<!--
					Count badge lives INSIDE the button so the entire row (label
					+ badge) is one hit target — the badge was previously a dead
					zone right where the user's eye lands.
				-->
				<button
					class="flex min-w-0 flex-1 items-center text-left"
					class:px-1={hasChildren || depth > 0}
					onclick={() => onPick(node.path)}
					ondblclick={readonly ? undefined : () => onRename?.(node.path)}
					title={node.path}
				>
					<span class="truncate">{node.name}</span>
					{#if counts && counts[node.path] !== undefined}
						{@const n = counts[node.path]}
						<span
							class="ml-auto shrink-0 rounded px-1 text-[10px] tabular-nums {active
								? 'bg-primary-foreground/15 text-primary-foreground'
								: 'bg-secondary text-muted-foreground'}"
						>
							{n >= 1000 ? '1000+' : n}
						</span>
					{/if}
				</button>
				{#if !readonly}
					<!-- Hover-revealed kebab. `display: none` until row hover
					     (or while the menu is open via has-[[data-state=open]])
					     so the count holds the row's right edge by default
					     and the kebab pushes it left when it appears.
					     Suppressed in readonly mode (picker). -->
					<div class="ml-1 hidden group-hover:block has-[[data-state=open]]:block">
						<KebabMenu label="Folder actions">
							<Item
								class="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-[12px] outline-none hover:bg-accent focus:bg-accent"
								onSelect={() => onCreateChild?.(node.path)}
							>
								<FolderPlus class="h-3.5 w-3.5 text-muted-foreground" />
								New subfolder
							</Item>
							<Item
								class="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-[12px] outline-none hover:bg-accent focus:bg-accent"
								onSelect={() => onRename?.(node.path)}
							>
								<Pencil class="h-3.5 w-3.5 text-muted-foreground" />
								Rename
							</Item>
							<Separator class="my-1 h-px bg-border" />
							<Item
								class="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-[12px] text-destructive outline-none hover:bg-destructive/10 focus:bg-destructive/10"
								onSelect={() => onDelete?.(node.path)}
							>
								<Trash2 class="h-3.5 w-3.5" />
								Delete folder…
							</Item>
						</KebabMenu>
					</div>
				{/if}
			</div>
			{#if hasChildren && open}
				<Self
					nodes={node.children}
					depth={depth + 1}
					{onPick}
					{onRename}
					{onDelete}
					{onCreateChild}
					{readonly}
					{selectedPath}
					{counts}
				/>
			{/if}
		</li>
	{/each}
</ul>
