<script lang="ts">
	// SelectOption and SelectAlign live in ./selectField, which owns the shape both renderings
	// read; re-exporting them from here trips eslint's no-import-assign on module scripts
	import ChevronDown from "$icon/chevron-down";
	import type { Snippet } from "svelte";
	import Combobox from "./Combobox.svelte";
	import { selectField, type SelectAlign, type SelectOption } from "./selectField";

	interface Props {
		options: SelectOption[];
		/** two-way; the selected option's value. Pass without `bind:` when the truth lives
		 *  elsewhere — a ref in the url, say — and let `onSelect` do the navigating. */
		value?: string;
		/** Swap the native control for a searchable listbox. Same options, same box: reach for it
		 *  once a list is long enough that scanning it beats reading it. */
		rich?: boolean;
		onSelect?: (value: string) => void;
		/** empty-value row shown first, e.g. "Choose stat". Selectable, so it also clears. */
		placeholder?: string;
		/** names the control for screen readers */
		label?: string;
		error?: boolean;
		disabled?: boolean;
		/** goes on the control itself, so a Field's label can point at it */
		id?: string;
		class?: string;
		// rich only
		searchPlaceholder?: string;
		emptyLabel?: string;
		/** take filtering over, e.g. to query a server: `options` is then rendered as given */
		onSearch?: (query: string) => void;
		loading?: boolean;
		align?: SelectAlign;
		panelClass?: string;
		/** pinned below the list, e.g. a "view all branches" link */
		footer?: Snippet;
	}

	let {
		options,
		value = $bindable(""),
		rich = false,
		onSelect,
		placeholder,
		label,
		error = false,
		disabled = false,
		id,
		class: className,
		searchPlaceholder,
		emptyLabel,
		onSearch,
		loading,
		align,
		panelClass,
		footer
	}: Props = $props();

	// a native optgroup has to nest, so the flat array is folded into runs of the same group.
	// the rich rendering keeps the array flat and emits headings between rows instead.
	const groups = $derived(
		options.reduce<{ label?: string; options: SelectOption[] }[]>((groups, option) => {
			const last = groups.at(-1);
			if (last && last.label === option.group) last.options.push(option);
			else groups.push({ label: option.group, options: [option] });
			return groups;
		}, [])
	);

	const field = $derived(selectField({ error, disabled }));

	const onchange = (event: Event) => {
		onSelect?.((event.currentTarget as HTMLSelectElement).value);
	};
</script>

{#snippet nativeOptions(items: SelectOption[])}
	{#each items as option (option.value)}
		<option value={option.value} disabled={option.disabled}>{option.label ?? option.value}</option>
	{/each}
{/snippet}

{#if rich}
	<Combobox
		{options}
		bind:value
		{onSelect}
		{placeholder}
		{label}
		{error}
		{disabled}
		{id}
		{searchPlaceholder}
		{emptyLabel}
		{onSearch}
		{loading}
		{align}
		{panelClass}
		{footer}
		class={className}
	/>
{:else}
	<div class="relative {className ?? ''}">
		<select
			bind:value
			{id}
			{disabled}
			{onchange}
			aria-label={label}
			aria-invalid={error}
			class={field.box({ class: "min-h-9 appearance-none px-2 py-1 pr-8" })}
		>
			{#if placeholder}
				<option value="">{placeholder}</option>
			{/if}
			{#each groups as group (group.label ?? "")}
				{#if group.label}
					<optgroup label={group.label}>{@render nativeOptions(group.options)}</optgroup>
				{:else}
					{@render nativeOptions(group.options)}
				{/if}
			{/each}
		</select>
		<ChevronDown
			class={field.chevron({
				class: "pointer-events-none absolute top-1/2 right-2 size-3.5 -translate-y-1/2"
			})}
			aria-hidden="true"
		/>
	</div>
{/if}
