export type ShuffleState = {
	/**
	 * Upcoming indices into the *current queue* in the order they will be visited
	 * when calling `next()` while shuffle is enabled.
	 */
	order: number[];

	/**
	 * Visited indices into the *current queue* in visit order.
	 * `cursor` points at the currently active entry within this array.
	 */
	history: number[];

	/**
	 * Index into `history` that represents the current item.
	 */
	cursor: number;
};

export type ReindexResult = {
	order: number[];
	history: number[];
	cursor: number;
	currentIndex: number | null;
};

/**
 * Build a shuffled play order for the *future* without affecting past history.
 *
 * - `queueLength`: length of the queue.
 * - `visited`: indices that are considered "already visited" (typically history[0..cursor]).
 * - `rng`: optional deterministic RNG for tests.
 */
export function buildInitialShuffleOrder(
	queueLength: number,
	visited: Set<number>,
	rng: () => number = Math.random,
): number[] {
	const remaining: number[] = [];
	for (let i = 0; i < queueLength; i += 1) {
		if (!visited.has(i)) remaining.push(i);
	}
	return shuffleArray(remaining, rng);
}

/**
 * Insert a track so it becomes the "next" item, even while shuffle is enabled.
 *
 * This modifies ONLY the future order. Past history is preserved.
 *
 * - If `nextIndex` is already in `order`, it is moved to the front.
 * - If `nextIndex` is currently in "visited" history (<= cursor), we do not
 *   reschedule it as next (that would violate history semantics). In that case,
 *   this function is a no-op.
 * - If `track` is the current item (history[cursor]), no-op.
 */
export function injectNextIntoShuffleOrder(args: {
	order: number[];
	history: number[];
	cursor: number;
	nextIndex: number;
}): number[] {
	const { order, history, cursor, nextIndex } = args;

	if (!Number.isInteger(nextIndex) || nextIndex < 0) return order;

	const current = history[cursor];
	if (current === nextIndex) return order;

	// Preserve past: don't schedule already-visited entries as "next"
	const visited = new Set(history.slice(0, cursor + 1));
	if (visited.has(nextIndex)) return order;

	const nextOrder: number[] = [];
	nextOrder.push(nextIndex);

	for (const i of order) {
		if (i === nextIndex) continue;
		nextOrder.push(i);
	}

	return nextOrder;
}

/**
 * Reindex a shuffle traversal state after a queue mutation that changes indices.
 *
 * Provide:
 * - `fromIndex`: original index of the moved/removed item
 * - `toIndex`: new index of the moved item, or `null` if the item was removed
 * - `currentIndex`: the current queue index before reindexing (may be null)
 *
 * This updates `history`, `order`, and `currentIndex` so they point to correct
 * indices in the new queue.
 *
 * Notes:
 * - This assumes a *single-item* move or remove.
 * - For "insert new item" (not previously present), you typically don't need
 *   this; instead you just insert the index into `order` as desired.
 */
export function reindexAfterMoveOrRemove(args: {
	order: number[];
	history: number[];
	cursor: number;
	currentIndex: number | null;

	fromIndex: number;
	toIndex: number | null;
}): ReindexResult {
	const { fromIndex, toIndex } = args;

	const remap = (i: number): number | null => {
		if (!Number.isInteger(i) || i < 0) return null;

		// Removal
		if (toIndex == null) {
			if (i === fromIndex) return null;
			if (i > fromIndex) return i - 1;
			return i;
		}

		// Move
		if (fromIndex === toIndex) return i;

		// Moving earlier: items between [toIndex .. fromIndex-1] shift +1
		if (toIndex < fromIndex) {
			if (i === fromIndex) return toIndex;
			if (i >= toIndex && i < fromIndex) return i + 1;
			return i;
		}

		// Moving later: items between [fromIndex+1 .. toIndex] shift -1
		// (because we remove fromIndex then insert at toIndex)
		if (i === fromIndex) return toIndex;
		if (i > fromIndex && i <= toIndex) return i - 1;
		return i;
	};

	const history = compactAndDedupePreservingOrder(
		args.history.map((i) => remap(i)).filter((i): i is number => i != null),
	);

	// Cursor points into history; keep it at the same logical "current" where possible.
	// If the current item was removed, clamp.
	let cursor = args.cursor;
	if (history.length === 0) cursor = 0;
	else cursor = Math.max(0, Math.min(cursor, history.length - 1));

	const order = compactAndDedupePreservingOrder(
		args.order.map((i) => remap(i)).filter((i): i is number => i != null),
	);

	const currentIndex =
		args.currentIndex == null ? null : remap(args.currentIndex);

	return { order, history, cursor, currentIndex };
}

function shuffleArray<T>(arr: T[], rng: () => number) {
	// Fisher–Yates shuffle (in place), return copy for convenience
	const a = [...arr];
	for (let i = a.length - 1; i > 0; i -= 1) {
		const j = Math.floor(rng() * (i + 1));
		const tmp = a[i];
		a[i] = a[j];
		a[j] = tmp;
	}
	return a;
}

function compactAndDedupePreservingOrder(indices: number[]) {
	const seen = new Set<number>();
	const out: number[] = [];
	for (const i of indices) {
		if (!Number.isInteger(i) || i < 0) continue;
		if (seen.has(i)) continue;
		seen.add(i);
		out.push(i);
	}
	return out;
}
