import type { GridAsset, TimeBucket } from './shared';
import type { Selection } from './useSelection';
import { useEffect, useMemo, useRef } from 'react';
import { AssetTile } from './AssetTile';
import { estimateBucketHeight, formatBucket, justifyRows } from './shared';
import { useContainerWidth } from './useContainerWidth';

// The month-bucketed timeline.
//
// Every month in the library is rendered from the moment the screen opens, but only as a correctly-sized empty
// box: the bucket index gives a count, and `estimateBucketHeight` turns a count into a height. That is what
// makes the scrollbar honest before anything is downloaded — the page is already its true length, so dragging
// to the bottom lands on the oldest month instead of on a spinner that then pushes the content away.
//
// An IntersectionObserver with a tall rootMargin turns a box into a real request just before it reaches the
// viewport. Loaded months are never unloaded; React Query holds them and scrolling back up is instant.

const ROW_HEIGHT = 210;
const GAP = 4;

type TimelineGridProps = {
  buckets: TimeBucket[];
  assetsByBucket: Record<string, GridAsset[]>;
  onBucketVisible: (bucket: string) => void;
  token: string | null;
  selection: Selection;
  /** The scroll container the observer should watch. Panels scroll their own body, not the window. */
  scrollRef: React.RefObject<HTMLElement | null>;
};

export const TimelineGrid = ({
  buckets,
  assetsByBucket,
  onBucketVisible,
  token,
  selection,
  scrollRef,
}: TimelineGridProps) => {
  const [ref, width] = useContainerWidth<HTMLDivElement>();
  const nodes = useRef(new Map<string, HTMLElement>());
  const notify = useRef(onBucketVisible);
  notify.current = onBucketVisible;

  useEffect(() => {
    if (!width) return;

    const observer = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          const bucket = entry.target.getAttribute('data-bucket');
          if (entry.isIntersecting && bucket) notify.current(bucket);
        }
      },
      // Two viewports of lead time in each direction: enough that a fast scroll rarely outruns the fetch,
      // small enough that opening the screen doesn't request the entire library.
      { root: scrollRef.current ?? null, rootMargin: '200% 0px' },
    );

    for (const node of nodes.current.values()) observer.observe(node);
    return () => observer.disconnect();
  }, [width, buckets, scrollRef]);

  return (
    <div ref={ref} className="w-full">
      {buckets.map((bucket) => (
        <BucketSection
          key={bucket.timeBucket}
          bucket={bucket}
          assets={assetsByBucket[bucket.timeBucket]}
          width={width}
          token={token}
          selection={selection}
          register={(node) => {
            if (node) nodes.current.set(bucket.timeBucket, node);
            else nodes.current.delete(bucket.timeBucket);
          }}
        />
      ))}
    </div>
  );
};

type BucketSectionProps = {
  bucket: TimeBucket;
  assets: GridAsset[] | undefined;
  width: number;
  token: string | null;
  selection: Selection;
  register: (node: HTMLElement | null) => void;
};

const BucketSection = ({ bucket, assets, width, token, selection, register }: BucketSectionProps) => {
  const rows = useMemo(
    () =>
      assets && width > 0
        ? justifyRows({
            items: assets,
            ratioOf: (a) => a.ratio,
            containerWidth: width,
            targetHeight: ROW_HEIGHT,
            gap: GAP,
          })
        : [],
    [assets, width],
  );

  // Until the month arrives, hold its place with the height it will occupy. The estimate uses the same row
  // height and gap the real layout will, so the jump when it loads is a few pixels rather than a page.
  const placeholder =
    !assets && width > 0
      ? estimateBucketHeight({ count: bucket.count, containerWidth: width, rowHeight: ROW_HEIGHT, gap: GAP })
      : 0;

  return (
    <section ref={register} data-bucket={bucket.timeBucket} className="mb-6">
      <h2 className="sticky top-0 z-10 -mx-1 mb-2 bg-background/85 px-1 py-1.5 text-sm font-semibold backdrop-blur">
        {formatBucket(bucket.timeBucket)}
        <span className="ml-2 text-xs font-normal text-muted-foreground">{bucket.count}</span>
      </h2>

      {assets ? (
        <div className="flex flex-col" style={{ gap: GAP }}>
          {rows.map((row, index) => (
            <div key={index} className="flex" style={{ gap: GAP }}>
              {row.tiles.map(({ item, width: w, height: h }) => (
                <AssetTile
                  key={item.id}
                  asset={item}
                  token={token}
                  width={w}
                  height={h}
                  selected={selection.selected.has(item.id)}
                  selecting={selection.isSelecting}
                  onToggle={(id, ev) => selection.toggle(id, ev)}
                />
              ))}
            </div>
          ))}
        </div>
      ) : (
        <div style={{ height: placeholder }} className="w-full rounded-sm bg-white/[0.03]" />
      )}
    </section>
  );
};
