import { type Accessor, createEffect, createSignal, onCleanup } from "solid-js";

/** A single virtual item with its position and index. */
export interface VirtualItem {
  index: number;
  start: number;
  end: number;
  size: number;
  key: string | number;
}

/** Options for createVirtualizer. */
export interface CreateVirtualizerOptions {
  /** Total number of items. Reactive. */
  count: Accessor<number>;
  /** The scrollable container element. */
  getScrollElement: Accessor<HTMLElement | null>;
  /** Estimated size in pixels for each item. Used for initial layout. */
  estimateSize: (index: number) => number;
  /** Number of items to render outside the visible area. @default 5 */
  overscan?: number;
  /** Whether scrolling is horizontal. @default false */
  horizontal?: boolean;
  /** Custom key extractor. @default index */
  getItemKey?: (index: number) => string | number;
}

/** State returned by createVirtualizer. */
export interface VirtualizerState {
  /** Virtual items currently in the visible range (plus overscan). */
  virtualItems: Accessor<VirtualItem[]>;
  /** Total scrollable size in pixels. */
  totalSize: Accessor<number>;
  /** Imperatively scroll to an item by index. */
  scrollToIndex: (index: number, options?: { align?: "start" | "center" | "end" }) => void;
  /** Current scroll offset. */
  scrollOffset: Accessor<number>;
  /** Measure a specific item's actual size after render. */
  measureElement: (element: HTMLElement | null) => void;
}

/**
 * Virtualizes a list of items, only rendering those visible in the scroll viewport.
 * Supports fixed and variable row heights, horizontal/vertical, and overscan.
 */
export function createVirtualizer(options: CreateVirtualizerOptions): VirtualizerState {
  const overscan = options.overscan ?? 5;
  const horizontal = options.horizontal ?? false;
  const getKey = options.getItemKey ?? ((i: number) => i);

  const [scrollOffset, setScrollOffset] = createSignal(0);
  const [containerSize, setContainerSize] = createSignal(0);
  const measurements = new Map<number, number>();

  function getSize(index: number): number {
    return measurements.get(index) ?? options.estimateSize(index);
  }

  function getItemOffset(index: number): number {
    let offset = 0;
    for (let i = 0; i < index; i++) {
      offset += getSize(i);
    }
    return offset;
  }

  const totalSize: Accessor<number> = () => {
    const count = options.count();
    let total = 0;
    for (let i = 0; i < count; i++) {
      total += getSize(i);
    }
    return total;
  };

  const virtualItems: Accessor<VirtualItem[]> = () => {
    const count = options.count();
    if (count === 0) return [];

    const offset = scrollOffset();
    const size = containerSize();
    if (size === 0) return [];

    let startIndex = 0;
    let accumulated = 0;
    while (startIndex < count && accumulated + getSize(startIndex) <= offset) {
      accumulated += getSize(startIndex);
      startIndex++;
    }

    let endIndex = startIndex;
    let visibleAccumulated = 0;
    while (endIndex < count && visibleAccumulated < size) {
      visibleAccumulated += getSize(endIndex);
      endIndex++;
    }

    const overscanStart = Math.max(0, startIndex - overscan);
    const overscanEnd = Math.min(count - 1, endIndex + overscan);

    const items: VirtualItem[] = [];
    for (let i = overscanStart; i <= overscanEnd; i++) {
      const itemSize = getSize(i);
      const start = getItemOffset(i);
      items.push({
        index: i,
        start,
        end: start + itemSize,
        size: itemSize,
        key: getKey(i),
      });
    }
    return items;
  };

  createEffect(() => {
    const el = options.getScrollElement();
    if (!el) return;

    const sizeKey = horizontal ? "clientWidth" : "clientHeight";
    const scrollKey = horizontal ? "scrollLeft" : "scrollTop";

    setContainerSize(el[sizeKey]);
    setScrollOffset(el[scrollKey]);

    const onScroll = () => {
      setScrollOffset(el[scrollKey]);
    };

    const resizeObserver = new ResizeObserver(() => {
      setContainerSize(el[sizeKey]);
    });

    el.addEventListener("scroll", onScroll, { passive: true });
    resizeObserver.observe(el);

    onCleanup(() => {
      el.removeEventListener("scroll", onScroll);
      resizeObserver.disconnect();
    });
  });

  function scrollToIndex(index: number, opts?: { align?: "start" | "center" | "end" }): void {
    const el = options.getScrollElement();
    if (!el) return;
    const itemOffset = getItemOffset(index);
    const itemSize = getSize(index);
    const viewSize = containerSize();
    const scrollKey = horizontal ? "scrollLeft" : "scrollTop";

    let target = itemOffset;
    if (opts?.align === "center") {
      target = itemOffset - viewSize / 2 + itemSize / 2;
    } else if (opts?.align === "end") {
      target = itemOffset - viewSize + itemSize;
    }
    el[scrollKey] = Math.max(0, target);
  }

  function measureElement(element: HTMLElement | null): void {
    if (!element) return;
    const index = Number(element.dataset.index);
    if (Number.isNaN(index)) return;
    const size = horizontal ? element.offsetWidth : element.offsetHeight;
    measurements.set(index, size);
  }

  return { virtualItems, totalSize, scrollToIndex, scrollOffset, measureElement };
}
