/**
 * Fires `onHit` whenever the attached element scrolls near the bottom of
 * its scroll container. Mirrors PhotoPrism's infinite-scroll trigger from
 * `frontend/src/page/photos.vue`: an IntersectionObserver on a sentinel
 * div with a rootMargin equal to ~4 viewport heights, so the next page is
 * fetched well before the user actually reaches the end.
 *
 * Usage: attach to a sentinel <div /> placed at the bottom of the scroll
 * area. The host gates calls via `enabled` (= `hasNextPage && !isFetching`).
 *
 *   <div use:nearBottom={{ onHit: fetchNextPage, enabled: canFetch }} />
 */
export interface NearBottomParams {
	onHit: () => void;
	/** When false the observer ignores intersections (use for the
	 *  hasNextPage + !isFetchingNextPage gate). */
	enabled?: boolean;
	/** Pre-load distance in pixels. PhotoPrism uses `innerHeight * 4`;
	 *  we default to the same. Caller can pass a number for tests. */
	preloadPx?: number;
	/** Optional scroll root (defaults to the viewport). Pass the
	 *  scrolling ancestor when the page itself doesn't scroll, which is
	 *  our case — the timeline scrolls inside `<main>`. */
	root?: Element | null;
}

export function nearBottom(node: HTMLElement, params: NearBottomParams) {
	let current: NearBottomParams = params;
	let io: IntersectionObserver | null = null;
	// IntersectionObserver only emits on state changes. With a 4-viewport
	// preload zone, the sentinel typically stays continuously intersecting
	// across a whole fetchNextPage cycle: enabled flips false (fetching),
	// the IO callback runs but no-ops, enabled flips back true — and no new
	// event is emitted because the intersection state never changed. We'd
	// stall mid-pagination. Remember the last reported intersection so the
	// next `enabled` rising edge can re-fire manually.
	let lastIntersecting = false;

	function buildObserver(p: NearBottomParams) {
		io?.disconnect();
		const preload = p.preloadPx ?? Math.max(800, window.innerHeight * 4);
		io = new IntersectionObserver(
			(entries) => {
				for (const e of entries) {
					lastIntersecting = e.isIntersecting;
				}
				if (lastIntersecting && current.enabled) current.onHit();
			},
			{
				root: p.root ?? null,
				// Inflate the root's bottom edge so we trip well before
				// the sentinel actually enters the viewport.
				rootMargin: `0px 0px ${preload}px 0px`
			}
		);
		io.observe(node);
	}

	buildObserver(current);

	return {
		update(next: NearBottomParams) {
			const rootChanged = next.root !== current.root;
			const preloadChanged = next.preloadPx !== current.preloadPx;
			const enabledRose = !current.enabled && !!next.enabled;
			current = next;
			if (rootChanged || preloadChanged) {
				buildObserver(current);
				return;
			}
			// `enabled` rising while the sentinel is still in the preload
			// zone — no IO event coming, so fire manually.
			if (enabledRose && lastIntersecting) current.onHit();
		},
		destroy() {
			io?.disconnect();
		}
	};
}
