import { createContext, type ReactNode, type RefObject, useEffect } from 'react';

import { useConstant } from '#/lib/hooks/use-constant';
import { useNonReactiveCallback } from '#/lib/hooks/use-non-reactive-callback';

type SeenObserver = {
	connect(root: HTMLElement | null): () => void;
	register(node: Element, item: unknown): () => void;
};

export const ItemSeenContext = createContext<SeenObserver | null>(null);

// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- binds `onItemSeen`'s parameter to the list's element type; inlining it to `unknown` would flip the callback's variance
export function ItemSeenObserver<ItemT>({
	children,
	enabled,
	onItemSeen,
	root,
}: {
	children: ReactNode;
	enabled: boolean;
	onItemSeen: (item: ItemT) => void;
	root: RefObject<HTMLElement | null> | undefined;
}) {
	const reportSeen = useNonReactiveCallback(onItemSeen);

	const api = useConstant(() => {
		const rows = new Map<Element, { item: ItemT; timeout?: ReturnType<typeof setTimeout> }>();

		let observer: IntersectionObserver | null = null;

		return {
			connect(rootEl: HTMLElement | null) {
				observer = new IntersectionObserver(
					(entries) => {
						for (const entry of entries) {
							const row = rows.get(entry.target);
							if (!row) {
								continue;
							}

							if (entry.isIntersecting) {
								row.timeout ??= setTimeout(() => {
									row.timeout = undefined;
									reportSeen(row.item);
								}, 500);
							} else if (row.timeout != null) {
								clearTimeout(row.timeout);
								row.timeout = undefined;
							}
						}
					},
					{
						root: rootEl,
						rootMargin: '-200px 0px -200px 0px',
					},
				);

				for (const node of rows.keys()) {
					observer.observe(node);
				}

				return () => {
					observer?.disconnect();
					observer = null;

					for (const row of rows.values()) {
						if (row.timeout != null) {
							clearTimeout(row.timeout);
							row.timeout = undefined;
						}
					}
				};
			},

			register(node: Element, item: ItemT) {
				rows.set(node, { item });
				observer?.observe(node);

				return () => {
					const row = rows.get(node);
					if (row !== undefined) {
						clearTimeout(row.timeout);
						rows.delete(node);
					}

					observer?.unobserve(node);
				};
			},
		};
	});

	useEffect(() => {
		if (!enabled) {
			return;
		}

		return api.connect(root?.current ?? null);
	}, [api, enabled, root]);

	return <ItemSeenContext value={api}>{children}</ItemSeenContext>;
}
