import type {
	AnimationChannel,
	AnimationPath,
	ChannelData,
	ElementAnimations,
	ElementKeyframe,
} from "@/animation/types";
import { formatLinearRgba } from "@/params";
import {
	getChannelEntriesFromData,
	isAnimationStorageKey,
} from "./channel-data";
import {
	getChannelValueAtTime,
	getScalarSegmentInterpolation,
	isScalarChannel,
} from "./interpolation";
import { isAnimationPath } from "./path";

function getChannelFallbackValue({
	channel,
}: {
	channel: AnimationChannel;
}) {
	if (channel.keys.length === 0) {
		return isScalarChannel(channel) ? 0 : false;
	}

	return channel.keys[0].value;
}

interface ChannelKeyframeMatch {
	componentKey: string;
	componentIndex: number;
	channel: AnimationChannel;
	keyframe: AnimationChannel["keys"][number];
}

function getChannelKeyframeMatches({
	data,
}: {
	data: ChannelData | undefined;
}): ChannelKeyframeMatch[] {
	return getChannelEntriesFromData({ data }).flatMap(
		([componentKey, channel], componentIndex) => {
			if (channel.keys.length === 0) {
				return [];
			}

			return channel.keys.map((keyframe) => ({
				componentKey,
				componentIndex,
				channel,
				keyframe,
			}));
		},
	);
}

function getUniqueChannelKeyframeMatches({
	data,
}: {
	data: ChannelData | undefined;
}): ChannelKeyframeMatch[] {
	const sortedMatches = getChannelKeyframeMatches({
		data,
	}).sort(
		(leftMatch, rightMatch) =>
			leftMatch.keyframe.time - rightMatch.keyframe.time ||
			leftMatch.componentIndex - rightMatch.componentIndex,
	);
	const uniqueMatches: ChannelKeyframeMatch[] = [];

	for (const match of sortedMatches) {
		const previousMatch = uniqueMatches[uniqueMatches.length - 1];
		if (
			!previousMatch ||
			previousMatch.keyframe.time !== match.keyframe.time
		) {
			uniqueMatches.push(match);
			continue;
		}

		if (
			previousMatch.componentIndex !== 0 &&
			match.componentIndex === 0
		) {
			uniqueMatches[uniqueMatches.length - 1] = match;
		}
	}

	return uniqueMatches;
}

function getPreferredChannelKeyframeMatch({
	matches,
}: {
	matches: ChannelKeyframeMatch[];
}): ChannelKeyframeMatch | null {
	return (
		matches.find((match) => match.componentIndex === 0) ??
		matches[0] ??
		null
	);
}

function getChannelValue({
	channel,
	time,
}: {
	channel: AnimationChannel;
	time: number;
}) {
	const fallbackValue = getChannelFallbackValue({ channel });
	if (typeof fallbackValue === "number") {
		return getChannelValueAtTime({
			channel: isScalarChannel(channel) ? channel : undefined,
			time,
			fallbackValue,
		});
	}
	return getChannelValueAtTime({
		channel: !isScalarChannel(channel) ? channel : undefined,
		time,
		fallbackValue,
	});
}

function getComposedChannelDataValueAtTime({
	data,
	time,
}: {
	data: ChannelData | undefined;
	time: number;
}) {
	const entries = getChannelEntriesFromData({ data });
	if (entries.length === 0) {
		return null;
	}
	if (entries.length === 1 && entries[0]?.[0] === "value") {
		return getChannelValue({ channel: entries[0][1], time });
	}

	const componentValues = Object.fromEntries(
		entries.map(([componentKey, channel]) => [
			componentKey,
			getChannelValue({ channel, time }),
		]),
	);
	if (
		typeof componentValues.r === "number" &&
		typeof componentValues.g === "number" &&
		typeof componentValues.b === "number" &&
		typeof componentValues.a === "number"
	) {
		return formatLinearRgba({
			color: {
				r: componentValues.r,
				g: componentValues.g,
				b: componentValues.b,
				a: componentValues.a,
			},
		});
	}
	return null;
}

function getKeyframeInterpolation({
	channel,
	keyframe,
}: {
	channel: AnimationChannel;
	keyframe: AnimationChannel["keys"][number];
}) {
	return isScalarChannel(channel) && "segmentToNext" in keyframe
		? getScalarSegmentInterpolation({ segment: keyframe.segmentToNext })
		: "hold";
}

function toElementKeyframe({
	data,
	propertyPath,
	keyframeMatch,
}: {
	data: ChannelData | undefined;
	propertyPath: AnimationPath;
	keyframeMatch: ChannelKeyframeMatch;
}): ElementKeyframe | null {
	const value = getComposedChannelDataValueAtTime({
		data,
		time: keyframeMatch.keyframe.time,
	});
	if (value === null) {
		return null;
	}

	return {
		propertyPath,
		id: keyframeMatch.keyframe.id,
		time: keyframeMatch.keyframe.time,
		value,
		interpolation: getKeyframeInterpolation({
			channel: keyframeMatch.channel,
			keyframe: keyframeMatch.keyframe,
		}),
	};
}

export function getElementKeyframes({
	animations,
}: {
	animations: ElementAnimations | undefined;
}): ElementKeyframe[] {
	if (!animations) {
		return [];
	}

	return Object.entries(animations).filter(([key]) =>
		isAnimationStorageKey({ key }),
	).flatMap(
		([propertyPath, data]) => {
			if (!data || !isAnimationPath(propertyPath)) {
				return [];
			}

			return getUniqueChannelKeyframeMatches({
				data,
			}).flatMap((keyframeMatch) => {
				const keyframe = toElementKeyframe({
					data,
					propertyPath,
					keyframeMatch,
				});
				if (!keyframe) {
					return [];
				}

				return [keyframe];
			});
		},
	);
}

export function hasKeyframesForPath({
	animations,
	propertyPath,
}: {
	animations: ElementAnimations | undefined;
	propertyPath: AnimationPath;
}): boolean {
	return getChannelEntriesFromData({ data: animations?.[propertyPath] }).some(
		([, channel]) => channel.keys.length > 0,
	);
}

export function getKeyframeAtTime({
	animations,
	propertyPath,
	time,
}: {
	animations: ElementAnimations | undefined;
	propertyPath: AnimationPath;
	time: number;
}): ElementKeyframe | null {
	const data = animations?.[propertyPath];
	if (!data) {
		return null;
	}

	const keyframeMatch = getPreferredChannelKeyframeMatch({
		matches: getChannelKeyframeMatches({ data }).filter(
			({ keyframe }) => keyframe.time === time,
		),
	});
	if (!keyframeMatch) {
		return null;
	}

	return toElementKeyframe({
		data,
		propertyPath,
		keyframeMatch,
	});
}

export function getKeyframeById({
	animations,
	propertyPath,
	keyframeId,
}: {
	animations: ElementAnimations | undefined;
	propertyPath: AnimationPath;
	keyframeId: string;
}): ElementKeyframe | null {
	const data = animations?.[propertyPath];
	if (!data) {
		return null;
	}

	const keyframeMatch = getPreferredChannelKeyframeMatch({
		matches: getChannelKeyframeMatches({ data }).filter(
			({ keyframe }) => keyframe.id === keyframeId,
		),
	});
	if (!keyframeMatch) {
		return null;
	}

	return toElementKeyframe({
		data,
		propertyPath,
		keyframeMatch,
	});
}
