export interface CursorPage<T, C> {
	items: T[];
	cursor?: C;
}

type CursorPagerState =
	| { kind: "at-page"; page: number }
	| { kind: "loading"; page: number }
	| { kind: "failed"; page: number };

export const pageCount = (total: number, limit: number): number =>
	Math.max(1, Math.ceil(Math.max(total, 0) / limit));

// search uses this since there isn't a known limit
export const discoveredPageCount = (page: number, hasNext: boolean): number =>
	Math.max(1, page + (hasNext ? 1 : 0));

export const createCursorPager = <T, C>(
	initial: CursorPage<T, C>,
	load: (cursor: C) => Promise<CursorPage<T, C>>,
	hasNext: (cursor: C | undefined) => boolean = (cursor) => cursor !== undefined
) => {
	let state = $state<CursorPagerState>({ kind: "at-page", page: 1 });
	let pages = $state<Record<number, CursorPage<T, C>>>({ 1: initial });

	const select = async (target: number) => {
		if (state.kind === "loading" || target < 1 || target === state.page) return;

		const currentPage = state.page;
		state = { kind: "loading", page: currentPage };
		try {
			const loaded = { ...pages };
			for (let next = 2; next <= target; next++) {
				if (loaded[next]) continue;
				const cursor = loaded[next - 1]?.cursor;
				if (!hasNext(cursor)) break;
				loaded[next] = await load(cursor as C);
			}

			pages = loaded;
			state = { kind: "at-page", page: loaded[target] ? target : currentPage };
		} catch {
			state = { kind: "failed", page: currentPage };
		}
	};

	return {
		get state() {
			return state;
		},
		get items() {
			return pages[state.page]?.items ?? [];
		},
		get hasNext() {
			return hasNext(pages[state.page]?.cursor);
		},
		select
	};
};
