<!--
    Component: VirtualList

    High-performance virtualized list for large datasets with:
    - Virtual scrolling (only renders visible items + overscan)
    - Keyboard navigation (ArrowUp/Down, Home, End)
    - Fixed or dynamic item heights
    - ARIA listbox/option pattern with single tab stop
-->
<script lang="ts" generics="T">
import { createVirtualizer } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import {
    type Snippet,
    tick,
} from 'svelte';

interface Props {
    /**
     * Array of items to render in the virtual list.
     *
     * @template T - The type of items in the list
     */
    items: T[];
    /**
     * Height for each item, either as a fixed number
     * or a function that returns height per index.
     * @default 80
     */
    itemHeight?: number | ((index: number) => number);
    /**
     * Optional overscan value for the virtual list.
     * @default 5
     */
    overscan?: number;
    /**
     * Optional CSS class string for styling the container
     * (follows shadcn convention for className prop)
     */
    class?: string;
    /**
     * Snippet for rendering individual list items.
     *
     * The snippet receives an object containing:
     * - `item`: The item from the items array (type T)
     * - `index`: The current item's index in the array
     *
     * This pattern provides type safety and flexibility for
     * rendering different item types without prop drilling.
     *
     * @template T - The type of items in the list
     */
    children: Snippet<[{ item: T; index: number }]>;
}

let { items, itemHeight = 80, overscan = 5, class: className, children }: Props = $props();

let activeIndex = $state(0);
const itemRefs = new Map<number, HTMLElement>();

const virtual = createVirtualizer(() => ({
    count: items.length,
    estimateSize: typeof itemHeight === 'function' ? itemHeight : () => itemHeight,
    overscan,
}));

function registerItem(node: HTMLElement, index: number) {
    itemRefs.set(index, node);
    return {
        destroy() {
            itemRefs.delete(index);
        },
    };
}

async function focusItem(index: number) {
    activeIndex = index;
    virtual.scrollToIndex(index, { align: 'auto' });
    await tick();
    itemRefs.get(index)?.focus();
}

async function handleKeydown(event: KeyboardEvent) {
    let nextIndex = activeIndex;
    if (event.key === 'ArrowDown') nextIndex++;
    else if (event.key === 'ArrowUp') nextIndex--;
    else if (event.key === 'Home') nextIndex = 0;
    else if (event.key === 'End') nextIndex = items.length - 1;
    else return;

    if (nextIndex >= 0 && nextIndex < items.length) {
        event.preventDefault();
        await focusItem(nextIndex);
    }
}
</script>

<!--
    Scroll container with single tab stop pattern:
    - tabindex="0" on container, tabindex="-1" on items
    - Arrow keys navigate within, Tab moves out
-->
<div
    bind:this={virtual.scrollElement}
    class={cn(
        'relative overflow-auto border rounded-md bg-background',
        'outline-none focus-visible:ring-2 ring-ring ring-offset-2',
        'h-full w-full',
        className,
    )}
    role="listbox"
    tabindex="0"
    onkeydown={handleKeydown}
    onfocusin={(e => e.target === virtual.scrollElement && focusItem(activeIndex))}
>
    <!-- Total scrollable height placeholder -->
    <div
        class="relative w-full"
        style:height="{virtual.totalSize}px"
    >
        {#each virtual.items as row (row.key)}
            <!-- Individual item positioned absolutely via GPU-accelerated transform -->
            <div
                use:registerItem={row.index}
                data-index={row.index}
                role="option"
                aria-selected={activeIndex === row.index}
                tabindex="-1"
                onmousedown={() => (activeIndex = row.index)}
                class="absolute top-0 left-0 w-full outline-none focus:bg-accent focus:text-accent-foreground"
                style:height="{row.size}px"
                style:transform="translateY({row.start}px)"
            >
                {@render children({ item: items[row.index], index: row.index })}
            </div>
        {/each}
    </div>
</div>
