import { defaultMinorOrdering, ordering } from "./create/seeding";
import type {
	DeepPartial,
	Duel,
	GroupType,
	MatchData,
	MatchResults,
	ParticipantResult,
	ParticipantSlot,
	Result,
	SeedOrdering,
	Side,
	StageData,
	StageType,
} from "./types";
import { MatchStatus } from "./types";

/**
 * Makes a list of rounds containing the matches of a round-robin group.
 *
 * @param participants The participants to distribute.
 * @param mode The round-robin mode.
 */
export function makeRoundRobinMatches<T>(participants: T[]): [T, T][][] {
	const n = participants.length;
	const n1 = n % 2 === 0 ? n : n + 1;
	const roundCount = n1 - 1;
	const matchPerRound = n1 / 2;

	const rounds: [T, T][][] = [];

	for (let roundId = 0; roundId < roundCount; roundId++) {
		const matches: [T, T][] = [];

		for (let matchId = 0; matchId < matchPerRound; matchId++) {
			if (matchId === 0 && n % 2 === 1) continue;

			const opponentsIds = [
				(roundId - matchId - 1 + n1) % (n1 - 1),
				matchId === 0 ? n1 - 1 : (roundId + matchId) % (n1 - 1),
			];

			matches.push([
				participants[opponentsIds[0]],
				participants[opponentsIds[1]],
			]);
		}

		rounds.push(matches);
	}

	return rounds;
}

/**
 * Makes a list of rounds containing the matches of a bipartite (A/B divisions) round-robin group.
 *
 * Every A team plays every B team exactly once; there are no A-vs-A or B-vs-B matches.
 * Round 1 is cross-seeded (strongest A vs weakest B), and B is rotated cyclically downward
 * in each subsequent round.
 *
 * When the divisions have different sizes, the shorter side is padded with bye slots so the
 * rotation still works. Those bye pairings are filtered out of the output, so each round has
 * exactly `min(|A|, |B|)` real matches and the total is `|A| * |B|`.
 *
 * @param divisionA Participants in division A, ordered by seed.
 * @param divisionB Participants in division B, ordered by seed.
 */
export function makeAbDivisionRoundRobinMatches<T>(
	divisionA: T[],
	divisionB: T[],
): [T, T][][] {
	const n = Math.max(divisionA.length, divisionB.length);
	const paddedA: (T | null)[] = [
		...divisionA,
		...Array(n - divisionA.length).fill(null),
	];
	const paddedB: (T | null)[] = [
		...divisionB,
		...Array(n - divisionB.length).fill(null),
	];
	const rounds: [T, T][][] = [];

	for (let roundIdx = 0; roundIdx < n; roundIdx++) {
		const matches: [T, T][] = [];

		for (let i = 0; i < n; i++) {
			const bIdx = (((n - 1 - i - roundIdx) % n) + n) % n;
			const a = paddedA[i];
			const b = paddedB[bIdx];
			if (a === null || b === null) continue;
			matches.push([a, b]);
		}

		rounds.push(matches);
	}

	return rounds;
}

/**
 * Distributes A/B division participants into groups such that each group has an
 * equal number of A and B participants.
 *
 * The snake ordering used by `groups.seed_optimized` is applied independently to
 * each pool, so that relative seed order within each pool is preserved within
 * every group.
 *
 * @param divisionA Participants in division A, ordered by seed.
 * @param divisionB Participants in division B, ordered by seed.
 * @param groupCount Number of groups to distribute into.
 */
export function makeAbDivisionGroups<T>(
	divisionA: T[],
	divisionB: T[],
	groupCount: number,
): { a: T[]; b: T[] }[] {
	if (groupCount <= 0) throw Error("Group count must be strictly positive.");

	if (divisionA.length !== divisionB.length) {
		if (groupCount !== 1)
			throw Error(
				"Uneven A/B divisions are only supported with a single group.",
			);

		return [{ a: divisionA, b: divisionB }];
	}

	if (divisionA.length % groupCount !== 0)
		throw Error("Pool size must be divisible by group count.");

	const aOrdered = ordering["groups.seed_optimized"](divisionA, groupCount);
	const bOrdered = ordering["groups.seed_optimized"](divisionB, groupCount);

	const perPoolGroupSize = divisionA.length / groupCount;
	const groups: { a: T[]; b: T[] }[] = [];

	for (let i = 0; i < groupCount; i++) {
		groups.push({
			a: aOrdered.slice(i * perPoolGroupSize, (i + 1) * perPoolGroupSize),
			b: bOrdered.slice(i * perPoolGroupSize, (i + 1) * perPoolGroupSize),
		});
	}

	return groups;
}

/**
 * A helper to assert our generated round-robin is correct.
 *
 * @param input The input seeding.
 * @param output The resulting distribution of seeds in groups.
 */
export function assertRoundRobin(
	input: number[],
	output: [number, number][][],
): void {
	const n = input.length;
	const matchPerRound = Math.floor(n / 2);
	const roundCount = n % 2 === 0 ? n - 1 : n;

	if (output.length !== roundCount) throw Error("Round count is wrong");
	if (!output.every((round) => round.length === matchPerRound))
		throw Error("Not every round has the good number of matches");

	const checkAllOpponents = Object.fromEntries(
		input.map((element) => [element, new Set<number>()]),
	) as Record<number, Set<number>>;

	for (const round of output) {
		const checkUnique = new Set<number>();

		for (const match of round) {
			if (match.length !== 2) throw Error("One match is not a pair");

			if (checkUnique.has(match[0]))
				throw Error("This team is already playing");
			checkUnique.add(match[0]);

			if (checkUnique.has(match[1]))
				throw Error("This team is already playing");
			checkUnique.add(match[1]);

			if (checkAllOpponents[match[0]].has(match[1]))
				throw Error("The team has already matched this team");
			checkAllOpponents[match[0]].add(match[1]);

			if (checkAllOpponents[match[1]].has(match[0]))
				throw Error("The team has already matched this team");
			checkAllOpponents[match[1]].add(match[0]);
		}
	}
}

/**
 * A helper to assert our generated bipartite round-robin is correct.
 *
 * @param divisionA Seeds in division A (ordered by seed).
 * @param divisionB Seeds in division B (ordered by seed).
 * @param output The resulting rounds of matches.
 */
export function assertAbDivisionRoundRobin(
	divisionA: number[],
	divisionB: number[],
	output: [number, number][][],
): void {
	const roundCount = Math.max(divisionA.length, divisionB.length);
	const matchesPerRound = Math.min(divisionA.length, divisionB.length);

	if (output.length !== roundCount) throw Error("Round count is wrong");
	if (!output.every((round) => round.length === matchesPerRound))
		throw Error("Not every round has the good number of matches");

	const aSet = new Set(divisionA);
	const bSet = new Set(divisionB);
	const seenPairings = new Set<string>();

	for (const round of output) {
		const playingInRound = new Set<number>();

		for (const match of round) {
			if (match.length !== 2) throw Error("One match is not a pair");

			const [a, b] = match;

			if (!aSet.has(a)) throw Error(`${a} is not a division A participant`);
			if (!bSet.has(b)) throw Error(`${b} is not a division B participant`);

			if (playingInRound.has(a)) throw Error("This team is already playing");
			playingInRound.add(a);

			if (playingInRound.has(b)) throw Error("This team is already playing");
			playingInRound.add(b);

			const pairingKey = `${a}-${b}`;
			if (seenPairings.has(pairingKey))
				throw Error("The teams have already been paired");
			seenPairings.add(pairingKey);
		}
	}

	if (seenPairings.size !== divisionA.length * divisionB.length)
		throw Error("Not every A vs B pairing was generated");
}

/**
 * Distributes elements in groups of equal size.
 *
 * @param elements A list of elements to distribute in groups.
 * @param groupCount The group count.
 */
export function makeGroups<T>(elements: T[], groupCount: number): T[][] {
	const groupSize = Math.ceil(elements.length / groupCount);
	const result: T[][] = [];

	for (let i = 0; i < elements.length; i++) {
		if (i % groupSize === 0) result.push([]);

		result[result.length - 1].push(elements[i]);
	}

	return result;
}

/**
 * Makes pairs with each element and its next one.
 *
 * @example [1, 2, 3, 4] --> [[1, 2], [3, 4]]
 * @param array A list of elements.
 */
export function makePairs<T>(array: T[]): [T, T][] {
	return array
		.map((_, i) => (i % 2 === 0 ? [array[i], array[i + 1]] : []))
		.filter((v): v is [T, T] => v.length === 2);
}

/**
 * Ensures there are no duplicates in a list of elements.
 *
 * @param array A list of elements.
 */
export function ensureNoDuplicates<T>(array: (T | null)[]): void {
	const nonNull = getNonNull(array);
	const unique = nonNull.filter((item, index) => {
		const stringifiedItem = JSON.stringify(item);
		return (
			nonNull.findIndex((obj) => JSON.stringify(obj) === stringifiedItem) ===
			index
		);
	});

	if (unique.length < nonNull.length)
		throw new Error("The seeding has a duplicate participant.");
}

/**
 * Ensures that the participant count is valid.
 *
 * @param stageType Type of the stage to test.
 * @param participantCount The number to test.
 */
export function ensureValidSize(
	stageType: StageType,
	participantCount: number,
): void {
	if (participantCount < 2)
		throw Error("Impossible to create a stage with less than 2 participants.");

	if (stageType === "round_robin") {
		// Round robin supports any number of participants.
		return;
	}

	if (!Number.isInteger(Math.log2(participantCount)))
		throw Error(
			"The library only supports a participant count which is a power of two.",
		);
}

/**
 * Converts a participant slot to a result stored in storage, with the position the participant is coming from.
 *
 * @param slot A participant slot.
 */
export function toResultWithPosition(slot: ParticipantSlot): ParticipantSlot {
	return (
		slot && {
			id: slot.id,
			position: slot.position,
		}
	);
}

/**
 * Returns the pre-computed winner for a match because of BYEs.
 *
 * @param opponents Two opponents.
 */
export function byeWinner(opponents: Duel): ParticipantSlot {
	if (opponents[0] === null && opponents[1] === null)
		// Double BYE.
		return null; // BYE.

	if (opponents[0] === null && opponents[1] !== null)
		// opponent1 BYE.
		return { id: opponents[1].id }; // opponent2.

	if (opponents[0] !== null && opponents[1] === null)
		// opponent2 BYE.
		return { id: opponents[0].id }; // opponent1.

	return { id: null }; // Normal.
}

/**
 * Returns the pre-computed winner for a match because of BYEs in a lower bracket.
 *
 * @param opponents Two opponents.
 */
export function byeWinnerToGrandFinal(opponents: Duel): ParticipantSlot {
	const winner = byeWinner(opponents);
	if (winner) winner.position = 1;
	return winner;
}

/**
 * Returns the pre-computed loser for a match because of BYEs.
 *
 * Only used for loser bracket.
 *
 * @param opponents Two opponents.
 * @param index The index of the duel in the round.
 */
export function byeLoser(opponents: Duel, index: number): ParticipantSlot {
	if (opponents[0] === null || opponents[1] === null)
		// At least one BYE.
		return null; // BYE.

	return { id: null, position: index + 1 }; // Normal.
}

/**
 * Returns the winner side or `null` if no winner.
 *
 * @param match A match's results.
 */
export function getMatchResult(match: MatchResults): Side | null {
	if (!isMatchCompleted(match)) return null;

	if (match.opponent1 === null && match.opponent2 === null) return null;

	let winner: Side | null = null;

	if (match.opponent1?.result === "win" || match.opponent2 === null)
		winner = "opponent1";

	if (match.opponent2?.result === "win" || match.opponent1 === null) {
		if (winner !== null) throw Error("There are two winners.");
		winner = "opponent2";
	}

	return winner;
}

/**
 * Gets the side where the winner of the given match will go in the next match.
 *
 * @param matchNumber Number of the match.
 */
function getSide(matchNumber: number): Side {
	return matchNumber % 2 === 1 ? "opponent1" : "opponent2";
}

/**
 * Gets the other side of a match.
 *
 * @param side The side that we don't want.
 */
export function getOtherSide(side: Side): Side {
	return side === "opponent1" ? "opponent2" : "opponent1";
}

/**
 * Checks if a match is started.
 *
 * @param match Partial match results.
 */
function isMatchStarted(match: DeepPartial<MatchResults>): boolean {
	return (
		match.opponent1?.score !== undefined || match.opponent2?.score !== undefined
	);
}

/**
 * Checks if a match is completed.
 *
 * @param match Partial match results.
 */
function isMatchCompleted(match: DeepPartial<MatchResults>): boolean {
	return isMatchByeCompleted(match) || isMatchWinCompleted(match);
}

/**
 * Checks if a match is completed because of a win.
 *
 * @param match Partial match results.
 */
function isMatchWinCompleted(match: DeepPartial<MatchResults>): boolean {
	return (
		match.opponent1?.result === "win" ||
		match.opponent2?.result === "win" ||
		match.opponent1?.result === "loss" ||
		match.opponent2?.result === "loss"
	);
}

/**
 * Checks if a match is completed because of at least one BYE.
 *
 * A match "BYE vs. TBD" isn't considered completed yet.
 *
 * @param match Partial match results.
 */
export function isMatchByeCompleted(match: DeepPartial<MatchResults>): boolean {
	return (
		(match.opponent1 === null && match.opponent2?.id !== null) || // BYE vs. someone
		(match.opponent2 === null && match.opponent1?.id !== null) || // someone vs. BYE
		(match.opponent1 === null && match.opponent2 === null)
	); // BYE vs. BYE
}

/**
 * Checks if a match's results can't be updated.
 *
 * @param match The match to check.
 */
export function isMatchUpdateLocked(match: MatchResults): boolean {
	return (
		match.status === MatchStatus.Locked || match.status === MatchStatus.Waiting
	);
}

/**
 * Indicates whether a match has at least one BYE or not.
 *
 * @param match Partial match results.
 */
export function hasBye(match: DeepPartial<MatchResults>): boolean {
	return match.opponent1 === null || match.opponent2 === null;
}

/**
 * Returns the status of a match based on the opponents of a match.
 *
 * @param opponents The opponents of a match.
 */
export function getMatchStatus(opponents: Duel): MatchStatus;

/**
 * Returns the status of a match based on the results of a match.
 *
 * @param match Partial match results.
 */
export function getMatchStatus(match: MatchResults): MatchStatus;

/**
 * Returns the status of a match based on information about it.
 *
 * @param arg The opponents or partial results of the match.
 */
export function getMatchStatus(arg: Duel | MatchResults): MatchStatus {
	const match = Array.isArray(arg)
		? {
				opponent1: arg[0],
				opponent2: arg[1],
			}
		: arg;

	if (hasBye(match))
		// At least one BYE.
		return MatchStatus.Locked;

	if (match.opponent1?.id === null && match.opponent2?.id === null)
		// Two TBD opponents.
		return MatchStatus.Locked;

	if (match.opponent1?.id === null || match.opponent2?.id === null)
		// One TBD opponent.
		return MatchStatus.Waiting;

	if (isMatchCompleted(match)) return MatchStatus.Completed;

	if (isMatchStarted(match)) return MatchStatus.Running;

	return MatchStatus.Ready;
}

/**
 * Updates a match results based on an input.
 *
 * @param stored A reference to what will be updated in the storage.
 * @param match Input of the update.
 */
export function setMatchResults(
	stored: MatchResults,
	match: DeepPartial<MatchResults>,
): {
	statusChanged: boolean;
	resultChanged: boolean;
} {
	const completed = isMatchCompleted(match);
	const currentlyCompleted = isMatchCompleted(stored);

	setExtraFields(stored, match);
	handleOpponentsInversion(stored, match);

	const statusChanged = setScores(stored, match);

	if (completed && currentlyCompleted) {
		// Ensure everything is good.
		setCompleted(stored, match);
		return { statusChanged: false, resultChanged: true };
	}

	if (completed && !currentlyCompleted) {
		setCompleted(stored, match);
		return { statusChanged: true, resultChanged: true };
	}

	if (!completed && currentlyCompleted) {
		resetMatchResults(stored);
		return { statusChanged: true, resultChanged: true };
	}

	return { statusChanged, resultChanged: false };
}

/**
 * Resets the results of a match. (status, forfeit, result)
 *
 * @param stored A reference to what will be updated in the storage.
 */
export function resetMatchResults(stored: MatchResults): void {
	if (stored.opponent1) {
		stored.opponent1.result = undefined;
	}

	if (stored.opponent2) {
		stored.opponent2.result = undefined;
	}

	stored.status = getMatchStatus(stored);
}

/**
 * Passes user-defined extra fields to the stored match.
 *
 * @param stored A reference to what will be updated in the storage.
 * @param match Input of the update.
 */
function setExtraFields(
	stored: MatchResults,
	match: DeepPartial<MatchResults>,
): void {
	const partialAssign = (
		target: unknown,
		update: unknown,
		ignoredKeys: string[],
	): void => {
		if (!target || !update) return;

		const retainedKeys = Object.keys(update).filter(
			(key) => !ignoredKeys.includes(key),
		);

		for (const key of retainedKeys) {
			(target as Record<string, unknown>)[key] = (
				update as Record<string, unknown>
			)[key];
		}
	};

	const ignoredKeys: Array<keyof MatchData> = [
		"id",
		"number",
		"stage_id",
		"group_id",
		"round_id",
		"status",
		"opponent1",
		"opponent2",
	];

	const ignoredOpponentKeys: Array<keyof ParticipantResult> = [
		"id",
		"score",
		"position",
		"forfeit",
		"result",
	];

	partialAssign(stored, match, ignoredKeys);
	partialAssign(stored.opponent1, match.opponent1, ignoredOpponentKeys);
	partialAssign(stored.opponent2, match.opponent2, ignoredOpponentKeys);
}

/**
 * Gets the id of the opponent at the given side of the given match.
 *
 * @param match The match to get the opponent from.
 * @param side The side where to get the opponent from.
 */
function getOpponentId(match: MatchResults, side: Side): number | null {
	const opponent = match[side];
	return opponent?.id ?? null;
}

/**
 * Gets the origin position of a side of a match.
 *
 * @param match The match.
 * @param side The side.
 */
export function getOriginPosition(match: MatchData, side: Side): number {
	const matchNumber = match[side]?.position;
	if (matchNumber === undefined) throw Error("Position is undefined.");

	return matchNumber;
}

/**
 * Gets the side the winner of the current match will go to in the next match.
 *
 * @param matchNumber Number of the current match.
 * @param roundNumber Number of the current round.
 * @param roundCount Count of rounds.
 * @param matchLocation Location of the current match.
 */
export function getNextSide(
	matchNumber: number,
	roundNumber: number,
	roundCount: number,
	matchLocation: GroupType,
): Side {
	// The nextSide comes from the same bracket.
	if (matchLocation === "loser_bracket" && roundNumber % 2 === 1)
		return "opponent2";

	// The nextSide comes from the loser bracket to the final group.
	if (matchLocation === "loser_bracket" && roundNumber === roundCount)
		return "opponent2";

	return getSide(matchNumber);
}

/**
 * Gets the side the winner of the current match in loser bracket will go in the next match.
 *
 * @param matchNumber Number of the match.
 * @param nextMatch The next match.
 * @param roundNumber Number of the current round.
 */
export function getNextSideLoserBracket(
	matchNumber: number,
	nextMatch: MatchData,
	roundNumber: number,
): Side {
	// The nextSide comes from the WB.
	if (roundNumber > 1) return "opponent1";

	// The nextSide comes from the WB round 1.
	if (nextMatch.opponent1?.position === matchNumber) return "opponent1";

	return "opponent2";
}

export type SetNextOpponent = (
	nextMatch: MatchData,
	nextSide: Side,
	match?: MatchData,
	currentSide?: Side,
) => void;

/**
 * Sets an opponent in the next match he has to go.
 *
 * @param nextMatch A match which follows the current one.
 * @param nextSide The side the opponent will be on in the next match.
 * @param match The current match.
 * @param currentSide The side the opponent is currently on.
 */
export function setNextOpponent(
	nextMatch: MatchData,
	nextSide: Side,
	match?: MatchData,
	currentSide?: Side,
): void {
	nextMatch[nextSide] = match![currentSide!] && {
		// Keep BYE.
		id: getOpponentId(match!, currentSide!), // This implementation of SetNextOpponent always has those arguments.
		position: nextMatch[nextSide]?.position, // Keep position.
	};

	nextMatch.status = getMatchStatus(nextMatch);
}

/**
 * Resets an opponent in the match following the current one.
 *
 * @param nextMatch A match which follows the current one.
 * @param nextSide The side the opponent will be on in the next match.
 */
export function resetNextOpponent(nextMatch: MatchData, nextSide: Side): void {
	nextMatch[nextSide] = nextMatch[nextSide] && {
		// Keep BYE.
		id: null,
		position: nextMatch[nextSide]?.position, // Keep position.
	};

	nextMatch.status = MatchStatus.Locked;
}

/**
 * Inverts opponents if requested by the input.
 *
 * @param stored A reference to what will be updated in the storage.
 * @param match Input of the update.
 */
function handleOpponentsInversion(
	stored: MatchResults,
	match: DeepPartial<MatchResults>,
): void {
	const id1 = match.opponent1?.id;
	const id2 = match.opponent2?.id;

	const storedId1 = stored.opponent1?.id;
	const storedId2 = stored.opponent2?.id;

	if (Number.isInteger(id1) && id1 !== storedId1 && id1 !== storedId2)
		throw Error("The given opponent1 ID does not exist in this match.");

	if (Number.isInteger(id2) && id2 !== storedId1 && id2 !== storedId2)
		throw Error("The given opponent2 ID does not exist in this match.");

	if (
		(Number.isInteger(id1) && id1 === storedId2) ||
		(Number.isInteger(id2) && id2 === storedId1)
	)
		invertOpponents(match);
}

/**
 * Inverts `opponent1` and `opponent2` in a match.
 *
 * @param match A match to update.
 */
function invertOpponents(match: DeepPartial<MatchResults>): void {
	[match.opponent1, match.opponent2] = [match.opponent2, match.opponent1];
}

/**
 * Updates the scores of a match.
 *
 * @param stored A reference to what will be updated in the storage.
 * @param match Input of the update.
 * @returns `true` if the status of the match changed, `false` otherwise.
 */
function setScores(
	stored: MatchResults,
	match: DeepPartial<MatchResults>,
): boolean {
	// Skip if no score update.
	if (
		match.opponent1?.score === stored.opponent1?.score &&
		match.opponent2?.score === stored.opponent2?.score
	)
		return false;

	const oldStatus = stored.status;
	stored.status = MatchStatus.Running;

	if (match.opponent1 && stored.opponent1)
		stored.opponent1.score = match.opponent1.score;

	if (match.opponent2 && stored.opponent2)
		stored.opponent2.score = match.opponent2.score;

	return stored.status !== oldStatus;
}

/**
 * Completes a match and handles results and forfeits.
 *
 * @param stored A reference to what will be updated in the storage.
 * @param match Input of the update.
 */
function setCompleted(
	stored: MatchResults,
	match: DeepPartial<MatchResults>,
): void {
	stored.status = MatchStatus.Completed;

	setResults(stored, match, "win", "loss");
	setResults(stored, match, "loss", "win");

	if (stored.opponent1 && !stored.opponent2) stored.opponent1.result = "win"; // Win against opponent 2 BYE.

	if (!stored.opponent1 && stored.opponent2) stored.opponent2.result = "win"; // Win against opponent 1 BYE.
}

/**
 * Enforces the symmetry between opponents.
 *
 * Sets an opponent's result to something, based on the result on the other opponent.
 *
 * @param stored A reference to what will be updated in the storage.
 * @param match Input of the update.
 * @param check A result to check in each opponent.
 * @param change A result to set in each other opponent if `check` is correct.
 */
function setResults(
	stored: MatchResults,
	match: DeepPartial<MatchResults>,
	check: Result,
	change: Result,
): void {
	if (match.opponent1 && match.opponent2) {
		if (match.opponent1.result === "win" && match.opponent2.result === "win")
			throw Error("There are two winners.");

		if (match.opponent1.result === "loss" && match.opponent2.result === "loss")
			throw Error("There are two losers.");
	}

	if (match.opponent1?.result === check) {
		if (stored.opponent1) stored.opponent1.result = check;
		else stored.opponent1 = { id: null, result: check };

		if (stored.opponent2) stored.opponent2.result = change;
		else stored.opponent2 = { id: null, result: change };
	}

	if (match.opponent2?.result === check) {
		if (stored.opponent2) stored.opponent2.result = check;
		else stored.opponent2 = { id: null, result: check };

		if (stored.opponent1) stored.opponent1.result = change;
		else stored.opponent1 = { id: null, result: change };
	}
}

/**
 * Returns only the non null elements.
 *
 * @param array The array to process.
 */
function getNonNull<T>(array: (T | null)[]): T[] {
	// Use a TS type guard to exclude null from the resulting type.
	const nonNull = array.filter((element): element is T => element !== null);
	return nonNull;
}

/**
 * Makes the transition to a major round for duels of the previous round. The duel count is divided by 2.
 *
 * @param previousDuels The previous duels to transition from.
 */
export function transitionToMajor(previousDuels: Duel[]): Duel[] {
	const currentDuelCount = previousDuels.length / 2;
	const currentDuels: Duel[] = [];

	for (let duelIndex = 0; duelIndex < currentDuelCount; duelIndex++) {
		const prevDuelId = duelIndex * 2;
		currentDuels.push([
			byeWinner(previousDuels[prevDuelId]),
			byeWinner(previousDuels[prevDuelId + 1]),
		]);
	}

	return currentDuels;
}

/**
 * Makes the transition to a minor round for duels of the previous round. The duel count stays the same.
 *
 * @param previousDuels The previous duels to transition from.
 * @param losers Losers from the previous major round.
 * @param method The ordering method for the losers.
 */
export function transitionToMinor(
	previousDuels: Duel[],
	losers: ParticipantSlot[],
	method?: SeedOrdering,
): Duel[] {
	const orderedLosers = method ? ordering[method](losers) : losers;
	const currentDuelCount = previousDuels.length;
	const currentDuels: Duel[] = [];

	for (let duelIndex = 0; duelIndex < currentDuelCount; duelIndex++) {
		const prevDuelId = duelIndex;
		currentDuels.push([
			orderedLosers[prevDuelId],
			byeWinner(previousDuels[prevDuelId]),
		]);
	}

	return currentDuels;
}

/**
 * Returns the number of rounds an upper bracket has given the number of participants in the stage.
 *
 * @param participantCount The number of participants in the stage.
 */
export function getUpperBracketRoundCount(participantCount: number): number {
	return Math.log2(participantCount);
}

/**
 * Returns the count of round pairs (major & minor) in a loser bracket.
 *
 * @param participantCount The number of participants in the stage.
 */
export function getRoundPairCount(participantCount: number): number {
	return getUpperBracketRoundCount(participantCount) - 1;
}

/**
 * Determines whether a double elimination stage is really necessary.
 *
 * If the size is only two (less is impossible), then a lower bracket and a grand final are not necessary.
 *
 * @param participantCount The number of participants in the stage.
 */
export function isDoubleEliminationNecessary(
	participantCount: number,
): boolean {
	return participantCount > 2;
}

/**
 * Returns the real (because of loser ordering) number of a match in a loser bracket.
 *
 * @param participantCount The number of participants in a stage.
 * @param roundNumber Number of the round.
 * @param matchNumber Number of the match.
 * @param method The method used for the round.
 */
export function findLoserMatchNumber(
	participantCount: number,
	roundNumber: number,
	matchNumber: number,
	method?: SeedOrdering,
): number {
	const loserCount = getLoserRoundLoserCount(participantCount, roundNumber);
	const losers = Array.from(Array(loserCount), (_, i) => i + 1);
	const ordered = method ? ordering[method](losers) : losers;
	const matchNumberLB = ordered.indexOf(matchNumber) + 1;

	// For LB round 1, the list of losers is spread over the matches 2 by 2.
	if (roundNumber === 1) return Math.ceil(matchNumberLB / 2);

	return matchNumberLB;
}

/**
 * Returns the count of matches in a round of a loser bracket.
 *
 * @param participantCount The number of participants in a stage.
 * @param roundNumber Number of the round.
 */
function getLoserRoundMatchCount(
	participantCount: number,
	roundNumber: number,
): number {
	const roundPairIndex = Math.ceil(roundNumber / 2) - 1;
	const roundPairCount = getRoundPairCount(participantCount);
	const matchCount = 2 ** (roundPairCount - roundPairIndex - 1);
	return matchCount;
}

/**
 * Returns the count of losers in a round of a loser bracket.
 *
 * @param participantCount The number of participants in a stage.
 * @param roundNumber Number of the round.
 */
function getLoserRoundLoserCount(
	participantCount: number,
	roundNumber: number,
): number {
	const matchCount = getLoserRoundMatchCount(participantCount, roundNumber);

	// Two per match for LB round 1 (losers coming from WB round 1).
	if (roundNumber === 1) return matchCount * 2;

	return matchCount; // One per match for LB minor rounds.
}

/**
 * Returns the ordering method of a round of a loser bracket.
 *
 * @param participantCount The number of participants in the stage.
 * @param roundNumber Number of the round.
 */
export function getLoserOrdering(
	participantCount: number,
	roundNumber: number,
): SeedOrdering | undefined {
	return defaultMinorOrdering[participantCount]?.[Math.floor(roundNumber / 2)];
}

/**
 * Returns the match number of the corresponding match in the next round by dividing by two.
 *
 * @param matchNumber The current match number.
 */
export function getDiagonalMatchNumber(matchNumber: number): number {
	return Math.ceil(matchNumber / 2);
}

/**
 * Checks if a stage is a round-robin stage.
 *
 * @param stage The stage to check.
 */
export function isRoundRobin(stage: StageData): boolean {
	return stage.type === "round_robin";
}

/**
 * Checks if a stage is a swiss stage.
 *
 * @param stage The stage to check.
 */
export function isSwiss(stage: StageData): boolean {
	return stage.type === "swiss";
}

/**
 * Checks if a group is a winner bracket.
 *
 * It's not always the opposite of `isLoserBracket()`: it could be the only bracket of a single elimination stage.
 *
 * @param stageType Type of the stage.
 * @param groupNumber Number of the group.
 */
function isWinnerBracket(stageType: StageType, groupNumber: number): boolean {
	return stageType === "double_elimination" && groupNumber === 1;
}

/**
 * Checks if a group is a loser bracket.
 *
 * @param stageType Type of the stage.
 * @param groupNumber Number of the group.
 */
function isLoserBracket(stageType: StageType, groupNumber: number): boolean {
	return stageType === "double_elimination" && groupNumber === 2;
}

/**
 * Checks if a group is a final group (consolation final or grand final).
 *
 * @param stageType Type of the stage.
 * @param groupNumber Number of the group.
 */
function isFinalGroup(stageType: StageType, groupNumber: number): boolean {
	return (
		(stageType === "single_elimination" && groupNumber === 2) ||
		(stageType === "double_elimination" && groupNumber === 3)
	);
}

/**
 * Returns the type of group the match is located into.
 *
 * @param stageType Type of the stage.
 * @param groupNumber Number of the group.
 */
export function getMatchLocation(
	stageType: StageType,
	groupNumber: number,
): GroupType {
	if (isWinnerBracket(stageType, groupNumber)) return "winner_bracket";

	if (isLoserBracket(stageType, groupNumber)) return "loser_bracket";

	if (isFinalGroup(stageType, groupNumber)) return "final_group";

	return "single_bracket";
}
