<script lang="ts" generics="T">
  import { onMount, tick } from "svelte";

  interface Props {
    items: T[];
    itemHeight: number;
    overscan?: number;
    containerClass?: string;
    getKey: (item: T, index: number) => string;
    children: import("svelte").Snippet<[T, number]>;
    onScrollTop?: () => void;
    onScrollBottom?: () => void;
  }

  let {
    items,
    itemHeight,
    overscan = 5,
    containerClass = "",
    getKey,
    children,
    onScrollTop,
    onScrollBottom,
  }: Props = $props();

  let containerRef: HTMLDivElement | null = $state(null);
  let scrollTop = $state(0);
  let containerHeight = $state(0);

  // Calculate visible range
  const visibleRange = $derived(() => {
    const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
    const visibleCount = Math.ceil(containerHeight / itemHeight) + overscan * 2;
    const endIndex = Math.min(items.length, startIndex + visibleCount);
    return { startIndex, endIndex };
  });

  // Get visible items with their indices
  const visibleItems = $derived(() => {
    const { startIndex, endIndex } = visibleRange();
    return items.slice(startIndex, endIndex).map((item, i) => ({
      item,
      index: startIndex + i,
    }));
  });

  // Total height of the list
  const totalHeight = $derived(items.length * itemHeight);

  // Offset for visible items
  const offsetY = $derived(visibleRange().startIndex * itemHeight);

  function handleScroll(e: Event) {
    const target = e.target as HTMLDivElement;
    scrollTop = target.scrollTop;

    // Check for scroll to top (load more)
    if (target.scrollTop < 100 && onScrollTop) {
      onScrollTop();
    }

    // Check for scroll to bottom
    const distanceToBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
    if (distanceToBottom < 100 && onScrollBottom) {
      onScrollBottom();
    }
  }

  function updateContainerHeight() {
    if (containerRef) {
      containerHeight = containerRef.clientHeight;
    }
  }

  onMount(() => {
    updateContainerHeight();
    const resizeObserver = new ResizeObserver(updateContainerHeight);
    if (containerRef) {
      resizeObserver.observe(containerRef);
    }
    return () => resizeObserver.disconnect();
  });

  // Scroll to bottom
  export async function scrollToBottom() {
    await tick();
    if (containerRef) {
      containerRef.scrollTop = containerRef.scrollHeight;
    }
  }

  // Scroll to specific index
  export function scrollToIndex(index: number) {
    if (containerRef) {
      containerRef.scrollTop = index * itemHeight;
    }
  }
</script>

<div
  bind:this={containerRef}
  class="overflow-y-auto {containerClass}"
  onscroll={handleScroll}
>
  <div style="height: {totalHeight}px; position: relative;">
    <div style="transform: translateY({offsetY}px);">
      {#each visibleItems() as { item, index } (getKey(item, index))}
        <div style="height: {itemHeight}px;">
          {@render children(item, index)}
        </div>
      {/each}
    </div>
  </div>
</div>
