import type { SceneTracks, TimelineElement, TimelineTrack } from "@/timeline/types";
import type { RippleAdjustment } from "./apply";

interface Interval {
	startTime: number;
	endTime: number;
}

interface ElementSpan extends Interval {
	id: string;
}

export function computeRippleAdjustments({
	beforeTracks,
	afterTracks,
}: {
	beforeTracks: SceneTracks;
	afterTracks: SceneTracks;
}): RippleAdjustment[] {
	const beforeTrackList = [
		...beforeTracks.overlay,
		beforeTracks.main,
		...beforeTracks.audio,
	];
	const afterTrackList = [
		...afterTracks.overlay,
		afterTracks.main,
		...afterTracks.audio,
	];
	const afterTracksById = new Map(afterTrackList.map((track) => [track.id, track]));
	const allAfterElementIds = new Set(
		afterTrackList.flatMap((track) => track.elements.map((element) => element.id)),
	);

	return beforeTrackList.flatMap((beforeTrack): RippleAdjustment[] =>
		computeTrackRippleAdjustments({
			trackId: beforeTrack.id,
			beforeElements: beforeTrack.elements,
			afterElements: afterTracksById.get(beforeTrack.id)?.elements ?? [],
			allAfterElementIds,
		}),
	);
}

function computeTrackRippleAdjustments({
	trackId,
	beforeElements,
	afterElements,
	allAfterElementIds,
}: {
	trackId: string;
	beforeElements: TimelineElement[];
	afterElements: TimelineElement[];
	allAfterElementIds: Set<string>;
}): RippleAdjustment[] {
	const beforeElementsById = buildElementSpanMap({ elements: beforeElements });
	const afterElementsById = buildElementSpanMap({ elements: afterElements });
	const { vacatedIntervals, joinedIntervals } = collectTrackIntervals({
		beforeElementsById,
		afterElementsById,
		allAfterElementIds,
	});
	const freedIntervals = subtractIntervalSets({
		sourceIntervals: vacatedIntervals,
		overlappingIntervals: joinedIntervals,
	});

	return buildAdjustments({ trackId, intervals: freedIntervals });
}

function buildElementSpanMap({
	elements,
}: {
	elements: TimelineElement[];
}): Map<string, ElementSpan> {
	return new Map(
		elements.map((element) => [
			element.id,
			{
				id: element.id,
				startTime: element.startTime,
				endTime: element.startTime + element.duration,
			},
		]),
	);
}

function collectTrackIntervals({
	beforeElementsById,
	afterElementsById,
	allAfterElementIds,
}: {
	beforeElementsById: Map<string, ElementSpan>;
	afterElementsById: Map<string, ElementSpan>;
	allAfterElementIds: Set<string>;
}): {
	vacatedIntervals: Interval[];
	joinedIntervals: Interval[];
} {
	const vacatedIntervals: Interval[] = [];
	const joinedIntervals: Interval[] = [];

	for (const beforeElement of beforeElementsById.values()) {
		const afterElement = afterElementsById.get(beforeElement.id);
		if (!afterElement) {
			const wasMovedToAnotherTrack = allAfterElementIds.has(beforeElement.id);
			if (!wasMovedToAnotherTrack) {
				pushInterval({
					intervals: vacatedIntervals,
					startTime: beforeElement.startTime,
					endTime: beforeElement.endTime,
				});
			}
			continue;
		}

		if (beforeElement.endTime > afterElement.endTime) {
			pushInterval({
				intervals: vacatedIntervals,
				startTime: afterElement.endTime,
				endTime: beforeElement.endTime,
			});
		}
	}

	for (const afterElement of afterElementsById.values()) {
		if (beforeElementsById.has(afterElement.id)) {
			continue;
		}

		pushInterval({
			intervals: joinedIntervals,
			startTime: afterElement.startTime,
			endTime: afterElement.endTime,
		});
	}

	return {
		vacatedIntervals: normalizeIntervals({ intervals: vacatedIntervals }),
		joinedIntervals: normalizeIntervals({ intervals: joinedIntervals }),
	};
}

function buildAdjustments({
	trackId,
	intervals,
}: {
	trackId: string;
	intervals: Interval[];
}): RippleAdjustment[] {
	return intervals.flatMap((interval): RippleAdjustment[] => {
		const shiftAmount = interval.endTime - interval.startTime;
		if (shiftAmount <= 0) {
			return [];
		}

		return [
			{
				trackId,
				afterTime: interval.endTime,
				shiftAmount,
			},
		];
	});
}

function subtractIntervalSets({
	sourceIntervals,
	overlappingIntervals,
}: {
	sourceIntervals: Interval[];
	overlappingIntervals: Interval[];
}): Interval[] {
	const normalizedSourceIntervals = normalizeIntervals({
		intervals: sourceIntervals,
	});
	const normalizedOverlappingIntervals = normalizeIntervals({
		intervals: overlappingIntervals,
	});

	return normalizedSourceIntervals.flatMap((sourceInterval) =>
		subtractSingleInterval({
			sourceInterval,
			overlappingIntervals: normalizedOverlappingIntervals,
		}),
	);
}

function normalizeIntervals({
	intervals,
}: {
	intervals: Interval[];
}): Interval[] {
	const validIntervals: Interval[] = [];
	for (const interval of intervals) {
		pushInterval({
			intervals: validIntervals,
			startTime: interval.startTime,
			endTime: interval.endTime,
		});
	}

	const sortedIntervals = validIntervals.sort(
		(leftInterval, rightInterval) =>
			leftInterval.startTime - rightInterval.startTime,
	);

	if (sortedIntervals.length === 0) {
		return [];
	}

	const mergedIntervals: Interval[] = [{ ...sortedIntervals[0] }];
	for (const interval of sortedIntervals.slice(1)) {
		const previousInterval = mergedIntervals[mergedIntervals.length - 1];
		if (interval.startTime <= previousInterval.endTime) {
			previousInterval.endTime = Math.max(
				previousInterval.endTime,
				interval.endTime,
			);
			continue;
		}

		mergedIntervals.push({ ...interval });
	}

	return mergedIntervals;
}

function subtractSingleInterval({
	sourceInterval,
	overlappingIntervals,
}: {
	sourceInterval: Interval;
	overlappingIntervals: Interval[];
}): Interval[] {
	let remainingIntervals: Interval[] = [{ ...sourceInterval }];

	for (const overlappingInterval of overlappingIntervals) {
		remainingIntervals = remainingIntervals.flatMap((remainingInterval) => {
		if (
			overlappingInterval.endTime <= remainingInterval.startTime ||
			overlappingInterval.startTime >= remainingInterval.endTime
		) {
				return [remainingInterval];
			}

			const nextIntervals: Interval[] = [];
			pushInterval({
				intervals: nextIntervals,
				startTime: remainingInterval.startTime,
				endTime: overlappingInterval.startTime,
			});
			pushInterval({
				intervals: nextIntervals,
				startTime: overlappingInterval.endTime,
				endTime: remainingInterval.endTime,
			});
			return nextIntervals;
		});

		if (remainingIntervals.length === 0) {
			return [];
		}
	}

	return remainingIntervals;
}

function pushInterval({
	intervals,
	startTime,
	endTime,
}: { intervals: Interval[]; startTime: number; endTime: number }): void {
	if (endTime <= startTime) {
		return;
	}

	intervals.push({ startTime, endTime });
}
