<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 { untrack } from 'svelte';
	import { ChevronRight, FolderInput, 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 (MoveToFolderDialog) reuses the tree just for `onPick`. */
		onRename?: (path: string) => void;
		onDelete?: (path: string) => void;
		onCreateChild?: (parent: string) => void;
		/** Reparent this folder under a chosen destination (opens the shared
		 *  move-to-folder dialog). Sidebar only; the readonly picker omits it. */
		onMove?: (path: 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,
		onMove,
		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;
	}

	// Auto-expand the ancestor chain of the active folder so the
	// highlighted row is actually visible after a deep-link navigation
	// (RightSidebar's open-folder icon, URL hydration, etc.). Each
	// FolderTree instance only owns the openSet entries for the nodes
	// rendered at its depth, but since the root instance expands the
	// top-level ancestor first, the child instance for that subtree is
	// then mounted and runs the same effect — the cascade naturally
	// reaches the leaf. Skipped in `readonly` mode (the heap-convert
	// picker has its own selectedPath and shouldn't drive the sidebar
	// state). Skipped for top-level paths (nothing to expand).
	$effect(() => {
		if (readonly || !browser) return;
		const fp = selectedPath ?? filters.folderPath;
		if (!fp || fp === '/' || !fp.includes('/')) return;
		untrack(() => {
			const parts = fp.split('/');
			let changed = false;
			for (let i = 1; i < parts.length; i++) {
				const ancestor = parts.slice(0, i).join('/');
				if (ancestor && !openSet.has(ancestor)) {
					openSet.add(ancestor);
					changed = true;
				}
			}
			if (changed) {
				openSet = new Set(openSet);
				persist();
			}
		});
	});
</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-[22px] 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: {4 + depth * 12}px;"
			>
				{#if hasChildren}
					<button
						class="flex h-[18px] w-5 items-center justify-center rounded hover:text-foreground"
						class:text-muted-foreground={!active}
						onclick={() => toggle(node.path)}
						title={open ? 'Collapse' : 'Expand'}
						aria-label={open ? 'Collapse' : 'Expand'}
					>
						<ChevronRight
							class="h-4 w-4 transition-transform duration-150 {open ? 'rotate-90' : ''}"
						/>
					</button>
				{:else}
					<!-- Spacer keeps childless siblings aligned with their chevroned
					     peers at every depth, so labels share a common left edge
					     across the sidebar (folders, heaps, views, manage). -->
					<span class="inline-block h-[18px] w-5" 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 pl-1 text-left"
					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 flex h-4 min-w-[24px] flex-shrink-0 items-center justify-center 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>
							<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={() => onMove?.(node.path)}
							>
								<FolderInput class="h-3.5 w-3.5 text-muted-foreground" />
								Move to folder…
							</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}
					{onMove}
					{readonly}
					{selectedPath}
					{counts}
				/>
			{/if}
		</li>
	{/each}
</ul>
