<!--
  Generic fixed-height virtual list.
  Usage:
    <VirtualList items={myArray} itemHeight={64} overscan={5}>
      {#snippet row({ item, index })}
        <div>…</div>
      {/snippet}
    </VirtualList>
-->
<script lang="ts" generics="T">
  import type { Snippet } from "svelte";

  type Props = {
    items: T[];
    itemHeight: number;
    overscan?: number;
    class?: string;
    row: Snippet<[{ item: T; index: number }]>;
    empty?: Snippet;
    key?: (item: T, index: number) => unknown;
  };

  let {
    items,
    itemHeight,
    overscan = 5,
    class: className = "",
    row,
    empty,
    key,
  }: Props = $props();

  let containerEl = $state<HTMLDivElement | null>(null);
  let scrollTop = $state(0);
  let containerHeight = $state(0);

  const totalHeight = $derived(items.length * itemHeight);

  const startIndex = $derived(
    Math.max(0, Math.floor(scrollTop / itemHeight) - overscan),
  );
  const endIndex = $derived(
    Math.min(
      items.length,
      Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan,
    ),
  );
  const visibleItems = $derived(
    items.slice(startIndex, endIndex).map((item, i) => ({
      item,
      index: startIndex + i,
    })),
  );

  function onScroll(e: Event) {
    scrollTop = (e.target as HTMLDivElement).scrollTop;
  }

  export function scrollToIndex(index: number) {
    if (!containerEl) return;
    containerEl.scrollTop = Math.max(
      0,
      index * itemHeight - containerHeight / 2 + itemHeight / 2,
    );
  }

  $effect(() => {
    if (!containerEl) return;
    const ro = new ResizeObserver((entries) => {
      for (const entry of entries) {
        containerHeight = entry.contentRect.height;
      }
    });
    ro.observe(containerEl);
    return () => ro.disconnect();
  });
</script>

<div
  class="virtual-list-container {className}"
  bind:this={containerEl}
  onscroll={onScroll}
>
  {#if items.length === 0}
    {@render empty?.()}
  {:else}
    <div
      class="virtual-list-sentinel"
      style="height: {totalHeight}px; position: relative;"
    >
      {#each visibleItems as entry (key ? key(entry.item, entry.index) : entry.index)}
        <div
          class="virtual-list-item"
          style="position: absolute; top: {entry.index *
            itemHeight}px; left: 0; right: 0; height: {itemHeight}px;"
        >
          {@render row(entry)}
        </div>
      {/each}
    </div>
  {/if}
</div>

<style>
  .virtual-list-container {
    overflow-y: auto;
    flex: 1;
  }
</style>
