import { SvelteSet } from 'svelte/reactivity';

/**
 * Multi-select state for the timeline + duplicate views. Tracks which photo
 * UIDs are picked, plus an anchor for shift-range extension and a focus
 * cursor for arrow-key navigation. Components import `selection` and read
 * its reactive fields; mutations go through the helpers below.
 *
 * SvelteSet is required (not plain Set) so component-level `selection.ids`
 * reads re-run when membership changes.
 */
export const selection = $state<{
	ids: SvelteSet<string>;
	anchor: string | null;
	focused: string | null;
	/**
	 * Mirror of the active ordered photo list, kept in sync by the timeline.
	 * Needed for shift-range extension and arrow-key navigation.
	 */
	order: string[];
}>({
	ids: new SvelteSet<string>(),
	anchor: null,
	focused: null,
	order: []
});

/**
 * Side index of `order`. Rebuilt by `setOrder`. Keeping the array around
 * (rather than dropping it entirely) lets preview navigation and
 * `cullTargets` iterate cheaply; the map exists solely to take
 * `selectRange` and arrow-key navigation from O(n) to O(1) on large
 * libraries. Not reactive — only `setOrder` reads/writes it.
 */
const orderIndex = new Map<string, number>();

/** O(1) index lookup. Returns -1 when the uid isn't in the current order
 *  (consistent with `Array.indexOf`). */
export function indexOf(uid: string | null): number {
	if (uid === null) return -1;
	const i = orderIndex.get(uid);
	return i === undefined ? -1 : i;
}

export function isSelected(uid: string): boolean {
	return selection.ids.has(uid);
}

export function clearSelection(): void {
	selection.ids.clear();
	selection.anchor = null;
}

export function toggle(uid: string): void {
	if (selection.ids.has(uid)) {
		selection.ids.delete(uid);
	} else {
		selection.ids.add(uid);
		selection.anchor = uid;
	}
}

export function selectOnly(uid: string): void {
	selection.ids.clear();
	selection.ids.add(uid);
	selection.anchor = uid;
}

export function selectRange(uid: string): void {
	// When the user hasn't explicitly anchored (no toggle/selectOnly before
	// this shift-click), treat the focused tile as the anchor — that's the
	// "starting photo" the user just clicked or arrow-keyed to. Without this
	// fallback, shift-clicking after a plain click would select only the
	// shift-clicked tile and the starting photo would be dropped.
	const anchor = selection.anchor ?? selection.focused;
	if (!anchor) {
		selectOnly(uid);
		return;
	}
	const a = indexOf(anchor);
	const b = indexOf(uid);
	if (a < 0 || b < 0) {
		selectOnly(uid);
		return;
	}
	const [lo, hi] = a < b ? [a, b] : [b, a];
	selection.ids.clear();
	for (let i = lo; i <= hi; i++) selection.ids.add(selection.order[i]);
	// Promote the anchor we used so subsequent shift-clicks keep the same
	// start point (otherwise focus-as-anchor would drift each move).
	selection.anchor = anchor;
}

export function setOrder(order: string[]): void {
	selection.order = order;
	// Rebuild the side index. `clear` + per-element `set` is O(n) and
	// allocation-free vs `new Map(order.map(...))` which would churn GC
	// on every page append in the infinite-scroll path.
	orderIndex.clear();
	for (let i = 0; i < order.length; i++) orderIndex.set(order[i], i);
}

export function setFocused(uid: string | null): void {
	selection.focused = uid;
}

/**
 * Promote a uid to the shift-range anchor without adding it to the selection.
 * Used on plain click + arrow-key navigation so the next shift-click extends
 * from the user's most recent interaction (the "starting photo"), even when
 * the selection set is empty.
 */
export function setAnchor(uid: string | null): void {
	selection.anchor = uid;
}

/**
 * Advance focus to the photo immediately after `excluded` in the current
 * order, skipping any uid that's in `excluded`. Falls back to the closest
 * non-excluded uid *before* the excluded set when the user is already at
 * the tail. Returns `null` when nothing else is left.
 *
 * Used right after a mutation that removes the focused photo from the
 * current view (archive / restore / approve / delete) — calling this
 * *before* the photo cache refetches keeps focus stable instead of the
 * effect-driven anchor falling back to photo[0].
 */
export function focusAfter(excluded: Iterable<string>): string | null {
	const excludedSet = excluded instanceof Set ? excluded : new Set(excluded);
	const order = selection.order;
	if (order.length === 0) {
		setFocused(null);
		return null;
	}
	// Anchor index: prefer current focus, else the first excluded uid we
	// can find (covers the case where focus was already null).
	let anchorIdx = indexOf(selection.focused);
	if (anchorIdx === -1) {
		for (let i = 0; i < order.length; i++) {
			if (excludedSet.has(order[i])) {
				anchorIdx = i;
				break;
			}
		}
	}
	if (anchorIdx === -1) return null;
	for (let i = anchorIdx + 1; i < order.length; i++) {
		if (!excludedSet.has(order[i])) {
			setFocused(order[i]);
			setAnchor(order[i]);
			return order[i];
		}
	}
	for (let i = anchorIdx - 1; i >= 0; i--) {
		if (!excludedSet.has(order[i])) {
			setFocused(order[i]);
			setAnchor(order[i]);
			return order[i];
		}
	}
	setFocused(null);
	return null;
}
