/*!
 * Copyright (c) 2025-present, Vanilagy and contributors
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

import {
	Av1CodecInfo,
	AvcDecoderConfigurationRecord,
	HevcDecoderConfigurationRecord,
	Vp9CodecInfo,
} from './codec-data';
import { customAudioEncoders, customVideoEncoders } from './custom-coder';
import {
	Bitstream,
	COLOR_PRIMARIES_MAP,
	MATRIX_COEFFICIENTS_MAP,
	TRANSFER_CHARACTERISTICS_MAP,
	assert,
	bytesToHexString,
	isAllowSharedBufferSource,
	last,
	reverseBitsU32,
	toDataView,
} from './misc';
import { SubtitleMetadata } from './subtitles';

/**
 * List of known video codecs, ordered by encoding preference.
 * @public
 */
export const VIDEO_CODECS = [
	'avc',
	'hevc',
	'vp9',
	'av1',
	'vp8',
] as const;
/**
 * List of known PCM (uncompressed) audio codecs, ordered by encoding preference.
 * @public
 */
export const PCM_AUDIO_CODECS = [
	'pcm-s16', // We don't prefix 'le' so we're compatible with the WebCodecs-registered PCM codec strings
	'pcm-s16be',
	'pcm-s24',
	'pcm-s24be',
	'pcm-s32',
	'pcm-s32be',
	'pcm-f32',
	'pcm-f32be',
	'pcm-f64',
	'pcm-f64be',
	'pcm-u8',
	'pcm-s8',
	'ulaw',
	'alaw',
] as const;
/**
 * List of known compressed audio codecs, ordered by encoding preference.
 * @public
 */
export const NON_PCM_AUDIO_CODECS = [
	'aac',
	'opus',
	'mp3',
	'vorbis',
	'flac',
] as const;
/**
 * List of known audio codecs, ordered by encoding preference.
 * @public
 */
export const AUDIO_CODECS = [
	...NON_PCM_AUDIO_CODECS,
	...PCM_AUDIO_CODECS,
] as const;
/**
 * List of known subtitle codecs, ordered by encoding preference.
 * @public
 */
export const SUBTITLE_CODECS = [
	'webvtt',
] as const; // TODO add the rest

/**
 * Union type of known video codecs.
 * @public
 */
export type VideoCodec = typeof VIDEO_CODECS[number];
/**
 * Union type of known audio codecs.
 * @public
 */
export type AudioCodec = typeof AUDIO_CODECS[number];
export type PcmAudioCodec = typeof PCM_AUDIO_CODECS[number];
/**
 * Union type of known subtitle codecs.
 * @public
 */
export type SubtitleCodec = typeof SUBTITLE_CODECS[number];
/**
 * Union type of known media codecs.
 * @public
 */
export type MediaCodec = VideoCodec | AudioCodec | SubtitleCodec;

// https://en.wikipedia.org/wiki/Advanced_Video_Coding
const AVC_LEVEL_TABLE = [
	{ maxMacroblocks: 99, maxBitrate: 64000, level: 0x0A }, // Level 1
	{ maxMacroblocks: 396, maxBitrate: 192000, level: 0x0B }, // Level 1.1
	{ maxMacroblocks: 396, maxBitrate: 384000, level: 0x0C }, // Level 1.2
	{ maxMacroblocks: 396, maxBitrate: 768000, level: 0x0D }, // Level 1.3
	{ maxMacroblocks: 396, maxBitrate: 2000000, level: 0x14 }, // Level 2
	{ maxMacroblocks: 792, maxBitrate: 4000000, level: 0x15 }, // Level 2.1
	{ maxMacroblocks: 1620, maxBitrate: 4000000, level: 0x16 }, // Level 2.2
	{ maxMacroblocks: 1620, maxBitrate: 10000000, level: 0x1E }, // Level 3
	{ maxMacroblocks: 3600, maxBitrate: 14000000, level: 0x1F }, // Level 3.1
	{ maxMacroblocks: 5120, maxBitrate: 20000000, level: 0x20 }, // Level 3.2
	{ maxMacroblocks: 8192, maxBitrate: 20000000, level: 0x28 }, // Level 4
	{ maxMacroblocks: 8192, maxBitrate: 50000000, level: 0x29 }, // Level 4.1
	{ maxMacroblocks: 8704, maxBitrate: 50000000, level: 0x2A }, // Level 4.2
	{ maxMacroblocks: 22080, maxBitrate: 135000000, level: 0x32 }, // Level 5
	{ maxMacroblocks: 36864, maxBitrate: 240000000, level: 0x33 }, // Level 5.1
	{ maxMacroblocks: 36864, maxBitrate: 240000000, level: 0x34 }, // Level 5.2
	{ maxMacroblocks: 139264, maxBitrate: 240000000, level: 0x3C }, // Level 6
	{ maxMacroblocks: 139264, maxBitrate: 480000000, level: 0x3D }, // Level 6.1
	{ maxMacroblocks: 139264, maxBitrate: 800000000, level: 0x3E }, // Level 6.2
];

// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding
const HEVC_LEVEL_TABLE = [
	{ maxPictureSize: 36864, maxBitrate: 128000, tier: 'L', level: 30 }, // Level 1 (Low Tier)
	{ maxPictureSize: 122880, maxBitrate: 1500000, tier: 'L', level: 60 }, // Level 2 (Low Tier)
	{ maxPictureSize: 245760, maxBitrate: 3000000, tier: 'L', level: 63 }, // Level 2.1 (Low Tier)
	{ maxPictureSize: 552960, maxBitrate: 6000000, tier: 'L', level: 90 }, // Level 3 (Low Tier)
	{ maxPictureSize: 983040, maxBitrate: 10000000, tier: 'L', level: 93 }, // Level 3.1 (Low Tier)
	{ maxPictureSize: 2228224, maxBitrate: 12000000, tier: 'L', level: 120 }, // Level 4 (Low Tier)
	{ maxPictureSize: 2228224, maxBitrate: 30000000, tier: 'H', level: 120 }, // Level 4 (High Tier)
	{ maxPictureSize: 2228224, maxBitrate: 20000000, tier: 'L', level: 123 }, // Level 4.1 (Low Tier)
	{ maxPictureSize: 2228224, maxBitrate: 50000000, tier: 'H', level: 123 }, // Level 4.1 (High Tier)
	{ maxPictureSize: 8912896, maxBitrate: 25000000, tier: 'L', level: 150 }, // Level 5 (Low Tier)
	{ maxPictureSize: 8912896, maxBitrate: 100000000, tier: 'H', level: 150 }, // Level 5 (High Tier)
	{ maxPictureSize: 8912896, maxBitrate: 40000000, tier: 'L', level: 153 }, // Level 5.1 (Low Tier)
	{ maxPictureSize: 8912896, maxBitrate: 160000000, tier: 'H', level: 153 }, // Level 5.1 (High Tier)
	{ maxPictureSize: 8912896, maxBitrate: 60000000, tier: 'L', level: 156 }, // Level 5.2 (Low Tier)
	{ maxPictureSize: 8912896, maxBitrate: 240000000, tier: 'H', level: 156 }, // Level 5.2 (High Tier)
	{ maxPictureSize: 35651584, maxBitrate: 60000000, tier: 'L', level: 180 }, // Level 6 (Low Tier)
	{ maxPictureSize: 35651584, maxBitrate: 240000000, tier: 'H', level: 180 }, // Level 6 (High Tier)
	{ maxPictureSize: 35651584, maxBitrate: 120000000, tier: 'L', level: 183 }, // Level 6.1 (Low Tier)
	{ maxPictureSize: 35651584, maxBitrate: 480000000, tier: 'H', level: 183 }, // Level 6.1 (High Tier)
	{ maxPictureSize: 35651584, maxBitrate: 240000000, tier: 'L', level: 186 }, // Level 6.2 (Low Tier)
	{ maxPictureSize: 35651584, maxBitrate: 800000000, tier: 'H', level: 186 }, // Level 6.2 (High Tier)
];

// https://en.wikipedia.org/wiki/VP9
export const VP9_LEVEL_TABLE = [
	{ maxPictureSize: 36864, maxBitrate: 200000, level: 10 }, // Level 1
	{ maxPictureSize: 73728, maxBitrate: 800000, level: 11 }, // Level 1.1
	{ maxPictureSize: 122880, maxBitrate: 1800000, level: 20 }, // Level 2
	{ maxPictureSize: 245760, maxBitrate: 3600000, level: 21 }, // Level 2.1
	{ maxPictureSize: 552960, maxBitrate: 7200000, level: 30 }, // Level 3
	{ maxPictureSize: 983040, maxBitrate: 12000000, level: 31 }, // Level 3.1
	{ maxPictureSize: 2228224, maxBitrate: 18000000, level: 40 }, // Level 4
	{ maxPictureSize: 2228224, maxBitrate: 30000000, level: 41 }, // Level 4.1
	{ maxPictureSize: 8912896, maxBitrate: 60000000, level: 50 }, // Level 5
	{ maxPictureSize: 8912896, maxBitrate: 120000000, level: 51 }, // Level 5.1
	{ maxPictureSize: 8912896, maxBitrate: 180000000, level: 52 }, // Level 5.2
	{ maxPictureSize: 35651584, maxBitrate: 180000000, level: 60 }, // Level 6
	{ maxPictureSize: 35651584, maxBitrate: 240000000, level: 61 }, // Level 6.1
	{ maxPictureSize: 35651584, maxBitrate: 480000000, level: 62 }, // Level 6.2
];

// https://en.wikipedia.org/wiki/AV1
const AV1_LEVEL_TABLE = [
	{ maxPictureSize: 147456, maxBitrate: 1500000, tier: 'M', level: 0 }, // Level 2.0 (Main Tier)
	{ maxPictureSize: 278784, maxBitrate: 3000000, tier: 'M', level: 1 }, // Level 2.1 (Main Tier)
	{ maxPictureSize: 665856, maxBitrate: 6000000, tier: 'M', level: 4 }, // Level 3.0 (Main Tier)
	{ maxPictureSize: 1065024, maxBitrate: 10000000, tier: 'M', level: 5 }, // Level 3.1 (Main Tier)
	{ maxPictureSize: 2359296, maxBitrate: 12000000, tier: 'M', level: 8 }, // Level 4.0 (Main Tier)
	{ maxPictureSize: 2359296, maxBitrate: 30000000, tier: 'H', level: 8 }, // Level 4.0 (High Tier)
	{ maxPictureSize: 2359296, maxBitrate: 20000000, tier: 'M', level: 9 }, // Level 4.1 (Main Tier)
	{ maxPictureSize: 2359296, maxBitrate: 50000000, tier: 'H', level: 9 }, // Level 4.1 (High Tier)
	{ maxPictureSize: 8912896, maxBitrate: 30000000, tier: 'M', level: 12 }, // Level 5.0 (Main Tier)
	{ maxPictureSize: 8912896, maxBitrate: 100000000, tier: 'H', level: 12 }, // Level 5.0 (High Tier)
	{ maxPictureSize: 8912896, maxBitrate: 40000000, tier: 'M', level: 13 }, // Level 5.1 (Main Tier)
	{ maxPictureSize: 8912896, maxBitrate: 160000000, tier: 'H', level: 13 }, // Level 5.1 (High Tier)
	{ maxPictureSize: 8912896, maxBitrate: 60000000, tier: 'M', level: 14 }, // Level 5.2 (Main Tier)
	{ maxPictureSize: 8912896, maxBitrate: 240000000, tier: 'H', level: 14 }, // Level 5.2 (High Tier)
	{ maxPictureSize: 35651584, maxBitrate: 60000000, tier: 'M', level: 15 }, // Level 5.3 (Main Tier)
	{ maxPictureSize: 35651584, maxBitrate: 240000000, tier: 'H', level: 15 }, // Level 5.3 (High Tier)
	{ maxPictureSize: 35651584, maxBitrate: 60000000, tier: 'M', level: 16 }, // Level 6.0 (Main Tier)
	{ maxPictureSize: 35651584, maxBitrate: 240000000, tier: 'H', level: 16 }, // Level 6.0 (High Tier)
	{ maxPictureSize: 35651584, maxBitrate: 100000000, tier: 'M', level: 17 }, // Level 6.1 (Main Tier)
	{ maxPictureSize: 35651584, maxBitrate: 480000000, tier: 'H', level: 17 }, // Level 6.1 (High Tier)
	{ maxPictureSize: 35651584, maxBitrate: 160000000, tier: 'M', level: 18 }, // Level 6.2 (Main Tier)
	{ maxPictureSize: 35651584, maxBitrate: 800000000, tier: 'H', level: 18 }, // Level 6.2 (High Tier)
	{ maxPictureSize: 35651584, maxBitrate: 160000000, tier: 'M', level: 19 }, // Level 6.3 (Main Tier)
	{ maxPictureSize: 35651584, maxBitrate: 800000000, tier: 'H', level: 19 }, // Level 6.3 (High Tier)
];

const VP9_DEFAULT_SUFFIX = '.01.01.01.01.00';
const AV1_DEFAULT_SUFFIX = '.0.110.01.01.01.0';

export const buildVideoCodecString = (codec: VideoCodec, width: number, height: number, bitrate: number) => {
	if (codec === 'avc') {
		const profileIndication = 0x64; // High Profile
		const totalMacroblocks = Math.ceil(width / 16) * Math.ceil(height / 16);

		// Determine the level based on the table
		const levelInfo = AVC_LEVEL_TABLE.find(
			level => totalMacroblocks <= level.maxMacroblocks && bitrate <= level.maxBitrate,
		) ?? last(AVC_LEVEL_TABLE)!;
		const levelIndication = levelInfo ? levelInfo.level : 0;

		const hexProfileIndication = profileIndication.toString(16).padStart(2, '0');
		const hexProfileCompatibility = '00';
		const hexLevelIndication = levelIndication.toString(16).padStart(2, '0');

		return `avc1.${hexProfileIndication}${hexProfileCompatibility}${hexLevelIndication}`;
	} else if (codec === 'hevc') {
		const profilePrefix = ''; // Profile space 0
		const profileIdc = 1; // Main Profile

		const compatibilityFlags = '6'; // Taken from the example in ISO 14496-15

		const pictureSize = width * height;
		const levelInfo = HEVC_LEVEL_TABLE.find(
			level => pictureSize <= level.maxPictureSize && bitrate <= level.maxBitrate,
		) ?? last(HEVC_LEVEL_TABLE)!;

		const constraintFlags = 'B0'; // Progressive source flag

		return 'hev1.'
			+ `${profilePrefix}${profileIdc}.`
			+ `${compatibilityFlags}.`
			+ `${levelInfo.tier}${levelInfo.level}.`
			+ `${constraintFlags}`;
	} else if (codec === 'vp8') {
		return 'vp8'; // Easy, this one
	} else if (codec === 'vp9') {
		const profile = '00'; // Profile 0

		const pictureSize = width * height;
		const levelInfo = VP9_LEVEL_TABLE.find(
			level => pictureSize <= level.maxPictureSize && bitrate <= level.maxBitrate,
		) ?? last(VP9_LEVEL_TABLE)!;

		const bitDepth = '08'; // 8-bit

		return `vp09.${profile}.${levelInfo.level.toString().padStart(2, '0')}.${bitDepth}`;
	} else if (codec === 'av1') {
		const profile = 0; // Main Profile, single digit

		const pictureSize = width * height;
		const levelInfo = AV1_LEVEL_TABLE.find(
			level => pictureSize <= level.maxPictureSize && bitrate <= level.maxBitrate,
		) ?? last(AV1_LEVEL_TABLE)!;
		const level = levelInfo.level.toString().padStart(2, '0');

		const bitDepth = '08'; // 8-bit

		return `av01.${profile}.${level}${levelInfo.tier}.${bitDepth}`;
	}

	// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
	throw new TypeError(`Unhandled codec '${codec}'.`);
};

export const generateVp9CodecConfigurationFromCodecString = (codecString: string) => {
	// Reference: https://www.webmproject.org/docs/container/#vp9-codec-feature-metadata-codecprivate

	const parts = codecString.split('.'); // We can derive the required values from the codec string

	const profile = Number(parts[1]);
	const level = Number(parts[2]);
	const bitDepth = Number(parts[3]);
	const chromaSubsampling = parts[4] ? Number(parts[4]) : 1;

	return [
		1, 1, profile,
		2, 1, level,
		3, 1, bitDepth,
		4, 1, chromaSubsampling,
	];
};

export const generateAv1CodecConfigurationFromCodecString = (codecString: string) => {
	// Reference: https://aomediacodec.github.io/av1-isobmff/

	const parts = codecString.split('.'); // We can derive the required values from the codec string

	const marker = 1;
	const version = 1;
	const firstByte = (marker << 7) + version;

	const profile = Number(parts[1]);
	const levelAndTier = parts[2]!;
	const level = Number(levelAndTier.slice(0, -1));
	const secondByte = (profile << 5) + level;

	const tier = levelAndTier.slice(-1) === 'H' ? 1 : 0;
	const bitDepth = Number(parts[3]);
	const highBitDepth = bitDepth === 8 ? 0 : 1;
	const twelveBit = 0;
	const monochrome = parts[4] ? Number(parts[4]) : 0;
	const chromaSubsamplingX = parts[5] ? Number(parts[5][0]) : 1;
	const chromaSubsamplingY = parts[5] ? Number(parts[5][1]) : 1;
	const chromaSamplePosition = parts[5] ? Number(parts[5][2]) : 0; // CSP_UNKNOWN
	const thirdByte = (tier << 7)
		+ (highBitDepth << 6)
		+ (twelveBit << 5)
		+ (monochrome << 4)
		+ (chromaSubsamplingX << 3)
		+ (chromaSubsamplingY << 2)
		+ chromaSamplePosition;

	const initialPresentationDelayPresent = 0; // Should be fine
	const fourthByte = initialPresentationDelayPresent;

	return [firstByte, secondByte, thirdByte, fourthByte];
};

export const extractVideoCodecString = (trackInfo: {
	width: number;
	height: number;
	codec: VideoCodec | null;
	codecDescription: Uint8Array | null;
	colorSpace: VideoColorSpaceInit | null;
	avcCodecInfo: AvcDecoderConfigurationRecord | null;
	hevcCodecInfo: HevcDecoderConfigurationRecord | null;
	vp9CodecInfo: Vp9CodecInfo | null;
	av1CodecInfo: Av1CodecInfo | null;
}) => {
	const { codec, codecDescription, colorSpace, avcCodecInfo, hevcCodecInfo, vp9CodecInfo, av1CodecInfo } = trackInfo;

	if (codec === 'avc') {
		if (avcCodecInfo) {
			const bytes = new Uint8Array([
				avcCodecInfo.avcProfileIndication,
				avcCodecInfo.profileCompatibility,
				avcCodecInfo.avcLevelIndication,
			]);

			return `avc1.${bytesToHexString(bytes)}`;
		}

		if (!codecDescription || codecDescription.byteLength < 4) {
			throw new TypeError('AVC decoder description is not provided or is not at least 4 bytes long.');
		}

		return `avc1.${bytesToHexString(codecDescription.subarray(1, 4))}`;
	} else if (codec === 'hevc') {
		let generalProfileSpace: number;
		let generalProfileIdc: number;
		let compatibilityFlags: number;
		let generalTierFlag: number;
		let generalLevelIdc: number;
		let constraintFlags: number[];

		if (hevcCodecInfo) {
			generalProfileSpace = hevcCodecInfo.generalProfileSpace;
			generalProfileIdc = hevcCodecInfo.generalProfileIdc;
			compatibilityFlags = reverseBitsU32(hevcCodecInfo.generalProfileCompatibilityFlags);
			generalTierFlag = hevcCodecInfo.generalTierFlag;
			generalLevelIdc = hevcCodecInfo.generalLevelIdc;
			constraintFlags = [...hevcCodecInfo.generalConstraintIndicatorFlags];
		} else {
			if (!codecDescription || codecDescription.byteLength < 23) {
				throw new TypeError('HEVC decoder description is not provided or is not at least 23 bytes long.');
			}

			const view = toDataView(codecDescription);
			const profileByte = view.getUint8(1);

			generalProfileSpace = (profileByte >> 6) & 0x03;
			generalProfileIdc = profileByte & 0x1F;
			compatibilityFlags = reverseBitsU32(view.getUint32(2));
			generalTierFlag = (profileByte >> 5) & 0x01;
			generalLevelIdc = view.getUint8(12);

			constraintFlags = [];
			for (let i = 0; i < 6; i++) {
				constraintFlags.push(view.getUint8(6 + i));
			}
		}

		let codecString = 'hev1.';

		codecString += ['', 'A', 'B', 'C'][generalProfileSpace]! + generalProfileIdc;
		codecString += '.';
		codecString += compatibilityFlags.toString(16).toUpperCase();
		codecString += '.';
		codecString += generalTierFlag === 0 ? 'L' : 'H';
		codecString += generalLevelIdc;

		while (constraintFlags.length > 0 && constraintFlags[constraintFlags.length - 1] === 0) {
			constraintFlags.pop();
		}

		if (constraintFlags.length > 0) {
			codecString += '.';
			codecString += constraintFlags.map(x => x.toString(16).toUpperCase()).join('.');
		}

		return codecString;
	} else if (codec === 'vp8') {
		return 'vp8'; // Easy, this one
	} else if (codec === 'vp9') {
		if (!vp9CodecInfo) {
			// Calculate level based on dimensions
			const pictureSize = trackInfo.width * trackInfo.height;
			let level = last(VP9_LEVEL_TABLE)!.level; // Default to highest level
			for (const entry of VP9_LEVEL_TABLE) {
				if (pictureSize <= entry.maxPictureSize) {
					level = entry.level;
					break;
				}
			}

			// We don't really know better, so let's return a general-purpose, common codec string and hope for the best
			return `vp09.00.${level.toString().padStart(2, '0')}.08`;
		}

		const profile = vp9CodecInfo.profile.toString().padStart(2, '0');
		const level = vp9CodecInfo.level.toString().padStart(2, '0');
		const bitDepth = vp9CodecInfo.bitDepth.toString().padStart(2, '0');
		const chromaSubsampling = vp9CodecInfo.chromaSubsampling.toString().padStart(2, '0');
		const colourPrimaries = vp9CodecInfo.colourPrimaries.toString().padStart(2, '0');
		const transferCharacteristics = vp9CodecInfo.transferCharacteristics.toString().padStart(2, '0');
		const matrixCoefficients = vp9CodecInfo.matrixCoefficients.toString().padStart(2, '0');
		const videoFullRangeFlag = vp9CodecInfo.videoFullRangeFlag.toString().padStart(2, '0');

		let string = `vp09.${profile}.${level}.${bitDepth}.${chromaSubsampling}`;
		string += `.${colourPrimaries}.${transferCharacteristics}.${matrixCoefficients}.${videoFullRangeFlag}`;

		if (string.endsWith(VP9_DEFAULT_SUFFIX)) {
			string = string.slice(0, -VP9_DEFAULT_SUFFIX.length);
		}

		return string;
	} else if (codec === 'av1') {
		if (!av1CodecInfo) {
			// Calculate level based on dimensions
			const pictureSize = trackInfo.width * trackInfo.height;
			let level = last(VP9_LEVEL_TABLE)!.level; // Default to highest level
			for (const entry of VP9_LEVEL_TABLE) {
				if (pictureSize <= entry.maxPictureSize) {
					level = entry.level;
					break;
				}
			}

			// We don't really know better, so let's return a general-purpose, common codec string and hope for the best
			return `av01.0.${level.toString().padStart(2, '0')}M.08`;
		}

		// https://aomediacodec.github.io/av1-isobmff/#codecsparam
		const profile = av1CodecInfo.profile; // Single digit
		const level = av1CodecInfo.level.toString().padStart(2, '0');
		const tier = av1CodecInfo.tier ? 'H' : 'M';
		const bitDepth = av1CodecInfo.bitDepth.toString().padStart(2, '0');
		const monochrome = av1CodecInfo.monochrome ? '1' : '0';
		const chromaSubsampling = 100 * av1CodecInfo.chromaSubsamplingX
			+ 10 * av1CodecInfo.chromaSubsamplingY
			+ 1 * (
				av1CodecInfo.chromaSubsamplingX && av1CodecInfo.chromaSubsamplingY
					? av1CodecInfo.chromaSamplePosition
					: 0
			);

		// The defaults are 1 (ITU-R BT.709)
		const colorPrimaries = colorSpace?.primaries ? COLOR_PRIMARIES_MAP[colorSpace.primaries] : 1;
		const transferCharacteristics = colorSpace?.transfer ? TRANSFER_CHARACTERISTICS_MAP[colorSpace.transfer] : 1;
		const matrixCoefficients = colorSpace?.matrix ? MATRIX_COEFFICIENTS_MAP[colorSpace.matrix] : 1;

		const videoFullRangeFlag = colorSpace?.fullRange ? 1 : 0;

		let string = `av01.${profile}.${level}${tier}.${bitDepth}`;
		string += `.${monochrome}.${chromaSubsampling.toString().padStart(3, '0')}`;
		string += `.${colorPrimaries.toString().padStart(2, '0')}`;
		string += `.${transferCharacteristics.toString().padStart(2, '0')}`;
		string += `.${matrixCoefficients.toString().padStart(2, '0')}`;
		string += `.${videoFullRangeFlag}`;

		if (string.endsWith(AV1_DEFAULT_SUFFIX)) {
			string = string.slice(0, -AV1_DEFAULT_SUFFIX.length);
		}

		return string;
	}

	throw new TypeError(`Unhandled codec '${codec}'.`);
};

export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: number, sampleRate: number) => {
	if (codec === 'aac') {
		// If stereo or higher channels and lower sample rate, likely using HE-AAC v2 with PS
		if (numberOfChannels >= 2 && sampleRate <= 24000) {
			return 'mp4a.40.29'; // HE-AAC v2 (AAC LC + SBR + PS)
		}

		// If sample rate is low, likely using HE-AAC v1 with SBR
		if (sampleRate <= 24000) {
			return 'mp4a.40.5'; // HE-AAC v1 (AAC LC + SBR)
		}

		// Default to standard AAC-LC for higher sample rates
		return 'mp4a.40.2'; // AAC-LC
	} else if (codec === 'mp3') {
		return 'mp3';
	} else if (codec === 'opus') {
		return 'opus';
	} else if (codec === 'vorbis') {
		return 'vorbis';
	} else if (codec === 'flac') {
		return 'flac';
	} else if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
		return codec;
	}

	throw new TypeError(`Unhandled codec '${codec}'.`);
};

export type AacCodecInfo = {
	isMpeg2: boolean;
};

export const extractAudioCodecString = (trackInfo: {
	codec: AudioCodec | null;
	codecDescription: Uint8Array | null;
	aacCodecInfo: AacCodecInfo | null;
}) => {
	const { codec, codecDescription, aacCodecInfo } = trackInfo;

	if (codec === 'aac') {
		if (!aacCodecInfo) {
			throw new TypeError('AAC codec info must be provided.');
		}

		if (aacCodecInfo.isMpeg2) {
			return 'mp4a.67';
		} else {
			const audioSpecificConfig = parseAacAudioSpecificConfig(codecDescription);
			return `mp4a.40.${audioSpecificConfig.objectType}`;
		}
	} else if (codec === 'mp3') {
		return 'mp3';
	} else if (codec === 'opus') {
		return 'opus';
	} else if (codec === 'vorbis') {
		return 'vorbis';
	} else if (codec === 'flac') {
		return 'flac';
	} else if (codec && (PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
		return codec;
	}

	throw new TypeError(`Unhandled codec '${codec}'.`);
};

export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null) => {
	if (!bytes || bytes.byteLength < 2) {
		throw new TypeError('AAC description must be at least 2 bytes long.');
	}

	const bitstream = new Bitstream(bytes);

	let objectType = bitstream.readBits(5);
	if (objectType === 31) {
		objectType = 32 + bitstream.readBits(6);
	}

	const frequencyIndex = bitstream.readBits(4);
	let sampleRate: number | null = null;
	if (frequencyIndex === 15) {
		sampleRate = bitstream.readBits(24);
	} else {
		const freqTable = [
			96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050,
			16000, 12000, 11025, 8000, 7350,
		];
		if (frequencyIndex < freqTable.length) {
			sampleRate = freqTable[frequencyIndex]!;
		}
	}

	const channelConfiguration = bitstream.readBits(4);
	let numberOfChannels: number | null = null;
	if (channelConfiguration >= 1 && channelConfiguration <= 7) {
		const channelMap = {
			1: 1,
			2: 2,
			3: 3,
			4: 4,
			5: 5,
			6: 6,
			7: 8,
		};
		numberOfChannels = channelMap[channelConfiguration as keyof typeof channelMap];
	}

	return {
		objectType,
		frequencyIndex,
		sampleRate,
		channelConfiguration,
		numberOfChannels,
	};
};

export const OPUS_INTERNAL_SAMPLE_RATE = 48000;

const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/;

export const parsePcmCodec = (codec: PcmAudioCodec) => {
	assert(PCM_AUDIO_CODECS.includes(codec));

	if (codec === 'ulaw') {
		return { dataType: 'ulaw' as const, sampleSize: 1 as const, littleEndian: true, silentValue: 255 };
	} else if (codec === 'alaw') {
		return { dataType: 'alaw' as const, sampleSize: 1 as const, littleEndian: true, silentValue: 213 };
	}

	const match = PCM_CODEC_REGEX.exec(codec);
	assert(match);

	let dataType: 'unsigned' | 'signed' | 'float' | 'ulaw' | 'alaw';
	if (match[1] === 'u') {
		dataType = 'unsigned';
	} else if (match[1] === 's') {
		dataType = 'signed';
	} else {
		dataType = 'float';
	}

	const sampleSize = (Number(match[2]) / 8) as 1 | 2 | 3 | 4 | 8;
	const littleEndian = match[3] !== 'be';
	const silentValue = codec === 'pcm-u8' ? 2 ** 7 : 0;

	return { dataType, sampleSize, littleEndian, silentValue };
};

export const inferCodecFromCodecString = (codecString: string): MediaCodec | null => {
	// Video codecs
	if (codecString.startsWith('avc1') || codecString.startsWith('avc3')) {
		return 'avc';
	} else if (codecString.startsWith('hev1') || codecString.startsWith('hvc1')) {
		return 'hevc';
	} else if (codecString === 'vp8') {
		return 'vp8';
	} else if (codecString.startsWith('vp09')) {
		return 'vp9';
	} else if (codecString.startsWith('av01')) {
		return 'av1';
	}

	// Audio codecs
	if (codecString.startsWith('mp4a.40') || codecString === 'mp4a.67') {
		return 'aac';
	} else if (
		codecString === 'mp3'
		|| codecString === 'mp4a.69'
		|| codecString === 'mp4a.6B'
		|| codecString === 'mp4a.6b'
	) {
		return 'mp3';
	} else if (codecString === 'opus') {
		return 'opus';
	} else if (codecString === 'vorbis') {
		return 'vorbis';
	} else if (codecString === 'flac') {
		return 'flac';
	} else if (codecString === 'ulaw') {
		return 'ulaw';
	} else if (codecString === 'alaw') {
		return 'alaw';
	} else if (PCM_CODEC_REGEX.test(codecString)) {
		return codecString as PcmAudioCodec;
	}

	// Subtitle codecs
	if (codecString === 'webvtt') {
		return 'webvtt';
	}

	return null;
};

export const getVideoEncoderConfigExtension = (codec: VideoCodec) => {
	if (codec === 'avc') {
		return {
			avc: {
				format: 'avc' as const, // Ensure the format is not Annex B
			},
		};
	} else if (codec === 'hevc') {
		return {
			hevc: {
				format: 'hevc' as const, // Ensure the format is not Annex B
			},
		};
	}

	return {};
};

export const getAudioEncoderConfigExtension = (codec: AudioCodec) => {
	if (codec === 'aac') {
		return {
			aac: {
				format: 'aac' as const, // Ensure the format is not ADTS
			},
		};
	} else if (codec === 'opus') {
		return {
			opus: {
				format: 'opus' as const,
			},
		};
	}

	return {};
};

/**
 * Represents a subjective media quality level.
 * @public
 */
export class Quality {
	/** @internal */
	_factor: number;

	/** @internal */
	constructor(factor: number) {
		this._factor = factor;
	}

	/** @internal */
	_toVideoBitrate(codec: VideoCodec, width: number, height: number) {
		const pixels = width * height;

		const codecEfficiencyFactors = {
			avc: 1.0, // H.264/AVC (baseline)
			hevc: 0.6, // H.265/HEVC (~40% more efficient than AVC)
			vp9: 0.6, // Similar to HEVC
			av1: 0.4, // ~60% more efficient than AVC
			vp8: 1.2, // Slightly less efficient than AVC
		};

		const referencePixels = 1920 * 1080;
		const referenceBitrate = 3000000;
		const scaleFactor = Math.pow(pixels / referencePixels, 0.95); // Slight non-linear scaling
		const baseBitrate = referenceBitrate * scaleFactor;

		const codecAdjustedBitrate = baseBitrate * codecEfficiencyFactors[codec];
		const finalBitrate = codecAdjustedBitrate * this._factor;

		return Math.ceil(finalBitrate / 1000) * 1000;
	}

	/** @internal */
	_toAudioBitrate(codec: AudioCodec) {
		if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec) || codec === 'flac') {
			return undefined;
		}

		const baseRates = {
			aac: 128000, // 128kbps base for AAC
			opus: 64000, // 64kbps base for Opus
			mp3: 160000, // 160kbps base for MP3
			vorbis: 64000, // 64kbps base for Vorbis
		};

		const baseBitrate = baseRates[codec as keyof typeof baseRates];
		if (!baseBitrate) {
			throw new Error(`Unhandled codec: ${codec}`);
		}

		let finalBitrate = baseBitrate * this._factor;

		if (codec === 'aac') {
			// AAC only works with specific bitrates, let's find the closest
			const validRates = [96000, 128000, 160000, 192000];
			finalBitrate = validRates.reduce((prev, curr) =>
				Math.abs(curr - finalBitrate) < Math.abs(prev - finalBitrate) ? curr : prev,
			);
		} else if (codec === 'opus' || codec === 'vorbis') {
			finalBitrate = Math.max(6000, finalBitrate);
		} else if (codec === 'mp3') {
			const validRates = [
				8000, 16000, 24000, 32000, 40000, 48000, 64000, 80000,
				96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000,
			];
			finalBitrate = validRates.reduce((prev, curr) =>
				Math.abs(curr - finalBitrate) < Math.abs(prev - finalBitrate) ? curr : prev,
			);
		}

		return Math.round(finalBitrate / 1000) * 1000;
	}
}

/**
 * Represents a very low media quality.
 * @public
 */
export const QUALITY_VERY_LOW = new Quality(0.3);
/**
 * Represents a low media quality.
 * @public
 */
export const QUALITY_LOW = new Quality(0.6);
/**
 * Represents a medium media quality.
 * @public
 */
export const QUALITY_MEDIUM = new Quality(1);
/**
 * Represents a high media quality.
 * @public
 */
export const QUALITY_HIGH = new Quality(2);
/**
 * Represents a very high media quality.
 * @public
 */
export const QUALITY_VERY_HIGH = new Quality(4);

const VALID_VIDEO_CODEC_STRING_PREFIXES = ['avc1', 'avc3', 'hev1', 'hvc1', 'vp8', 'vp09', 'av01'];
const AVC_CODEC_STRING_REGEX = /^(avc1|avc3)\.[0-9a-fA-F]{6}$/;
const HEVC_CODEC_STRING_REGEX = /^(hev1|hvc1)\.(?:[ABC]?\d+)\.[0-9a-fA-F]{1,8}\.[LH]\d+(?:\.[0-9a-fA-F]{1,2}){0,6}$/;
const VP9_CODEC_STRING_REGEX = /^vp09(?:\.\d{2}){3}(?:(?:\.\d{2}){5})?$/;
const AV1_CODEC_STRING_REGEX = /^av01\.\d\.\d{2}[MH]\.\d{2}(?:\.\d\.\d{3}\.\d{2}\.\d{2}\.\d{2}\.\d)?$/;

export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata | undefined) => {
	if (!metadata) {
		throw new TypeError('Video chunk metadata must be provided.');
	}
	if (typeof metadata !== 'object') {
		throw new TypeError('Video chunk metadata must be an object.');
	}
	if (!metadata.decoderConfig) {
		throw new TypeError('Video chunk metadata must include a decoder configuration.');
	}
	if (typeof metadata.decoderConfig !== 'object') {
		throw new TypeError('Video chunk metadata decoder configuration must be an object.');
	}
	if (typeof metadata.decoderConfig.codec !== 'string') {
		throw new TypeError('Video chunk metadata decoder configuration must specify a codec string.');
	}
	if (!VALID_VIDEO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) {
		throw new TypeError(
			'Video chunk metadata decoder configuration codec string must be a valid video codec string as specified in'
			+ ' the WebCodecs Codec Registry.',
		);
	}
	if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth! <= 0) {
		throw new TypeError(
			'Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).',
		);
	}
	if (!Number.isInteger(metadata.decoderConfig.codedHeight) || metadata.decoderConfig.codedHeight! <= 0) {
		throw new TypeError(
			'Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).',
		);
	}
	if (metadata.decoderConfig.description !== undefined) {
		if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
			throw new TypeError(
				'Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an'
				+ ' ArrayBuffer view.',
			);
		}
	}
	if (metadata.decoderConfig.colorSpace !== undefined) {
		const { colorSpace } = metadata.decoderConfig;

		if (typeof colorSpace !== 'object') {
			throw new TypeError(
				'Video chunk metadata decoder configuration colorSpace, when provided, must be an object.',
			);
		}

		const primariesValues = Object.keys(COLOR_PRIMARIES_MAP);
		if (colorSpace.primaries != null && !primariesValues.includes(colorSpace.primaries)) {
			throw new TypeError(
				`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of`
				+ ` ${primariesValues.join(', ')}.`,
			);
		}

		const transferValues = Object.keys(TRANSFER_CHARACTERISTICS_MAP);
		if (colorSpace.transfer != null && !transferValues.includes(colorSpace.transfer)) {
			throw new TypeError(
				`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of`
				+ ` ${transferValues.join(', ')}.`,
			);
		}

		const matrixValues = Object.keys(MATRIX_COEFFICIENTS_MAP);
		if (colorSpace.matrix != null && !matrixValues.includes(colorSpace.matrix)) {
			throw new TypeError(
				`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of`
				+ ` ${matrixValues.join(', ')}.`,
			);
		}

		if (colorSpace.fullRange != null && typeof colorSpace.fullRange !== 'boolean') {
			throw new TypeError(
				'Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.',
			);
		}
	}

	if (metadata.decoderConfig.codec.startsWith('avc1') || metadata.decoderConfig.codec.startsWith('avc3')) {
		// AVC-specific validation

		if (!AVC_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
			throw new TypeError(
				'Video chunk metadata decoder configuration codec string for AVC must be a valid AVC codec string as'
				+ ' specified in Section 3.4 of RFC 6381.',
			);
		}

		// `description` may or may not be set, depending on if the format is AVCC or Annex B, so don't perform any
		// validation for it.
		// https://www.w3.org/TR/webcodecs-avc-codec-registration
	} else if (metadata.decoderConfig.codec.startsWith('hev1') || metadata.decoderConfig.codec.startsWith('hvc1')) {
		// HEVC-specific validation

		if (!HEVC_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
			throw new TypeError(
				'Video chunk metadata decoder configuration codec string for HEVC must be a valid HEVC codec string as'
				+ ' specified in Section E.3 of ISO 14496-15.',
			);
		}

		// `description` may or may not be set, depending on if the format is HEVC or Annex B, so don't perform any
		// validation for it.
		// https://www.w3.org/TR/webcodecs-hevc-codec-registration
	} else if (metadata.decoderConfig.codec.startsWith('vp8')) {
		// VP8-specific validation

		if (metadata.decoderConfig.codec !== 'vp8') {
			throw new TypeError('Video chunk metadata decoder configuration codec string for VP8 must be "vp8".');
		}
	} else if (metadata.decoderConfig.codec.startsWith('vp09')) {
		// VP9-specific validation

		if (!VP9_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
			throw new TypeError(
				'Video chunk metadata decoder configuration codec string for VP9 must be a valid VP9 codec string as'
				+ ' specified in Section "Codecs Parameter String" of https://www.webmproject.org/vp9/mp4/.',
			);
		}
	} else if (metadata.decoderConfig.codec.startsWith('av01')) {
		// AV1-specific validation

		if (!AV1_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
			throw new TypeError(
				'Video chunk metadata decoder configuration codec string for AV1 must be a valid AV1 codec string as'
				+ ' specified in Section "Codecs Parameter String" of https://aomediacodec.github.io/av1-isobmff/.',
			);
		}
	}
};

const VALID_AUDIO_CODEC_STRING_PREFIXES = ['mp4a', 'mp3', 'opus', 'vorbis', 'flac', 'ulaw', 'alaw', 'pcm'];

export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata | undefined) => {
	if (!metadata) {
		throw new TypeError('Audio chunk metadata must be provided.');
	}
	if (typeof metadata !== 'object') {
		throw new TypeError('Audio chunk metadata must be an object.');
	}
	if (!metadata.decoderConfig) {
		throw new TypeError('Audio chunk metadata must include a decoder configuration.');
	}
	if (typeof metadata.decoderConfig !== 'object') {
		throw new TypeError('Audio chunk metadata decoder configuration must be an object.');
	}
	if (typeof metadata.decoderConfig.codec !== 'string') {
		throw new TypeError('Audio chunk metadata decoder configuration must specify a codec string.');
	}
	if (!VALID_AUDIO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) {
		throw new TypeError(
			'Audio chunk metadata decoder configuration codec string must be a valid audio codec string as specified in'
			+ ' the WebCodecs Codec Registry.',
		);
	}
	if (!Number.isInteger(metadata.decoderConfig.sampleRate) || metadata.decoderConfig.sampleRate <= 0) {
		throw new TypeError(
			'Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).',
		);
	}
	if (!Number.isInteger(metadata.decoderConfig.numberOfChannels) || metadata.decoderConfig.numberOfChannels <= 0) {
		throw new TypeError(
			'Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).',
		);
	}
	if (metadata.decoderConfig.description !== undefined) {
		if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
			throw new TypeError(
				'Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an'
				+ ' ArrayBuffer view.',
			);
		}
	}

	if (
		metadata.decoderConfig.codec.startsWith('mp4a')
		// These three refer to MP3:
		&& metadata.decoderConfig.codec !== 'mp4a.69'
		&& metadata.decoderConfig.codec !== 'mp4a.6B'
		&& metadata.decoderConfig.codec !== 'mp4a.6b'
	) {
		// AAC-specific validation

		const validStrings = ['mp4a.40.2', 'mp4a.40.02', 'mp4a.40.5', 'mp4a.40.05', 'mp4a.40.29', 'mp4a.67'];
		if (!validStrings.includes(metadata.decoderConfig.codec)) {
			throw new TypeError(
				'Audio chunk metadata decoder configuration codec string for AAC must be a valid AAC codec string as'
				+ ' specified in https://www.w3.org/TR/webcodecs-aac-codec-registration/.',
			);
		}

		if (!metadata.decoderConfig.description) {
			throw new TypeError(
				'Audio chunk metadata decoder configuration for AAC must include a description, which is expected to be'
				+ ' an AudioSpecificConfig as specified in ISO 14496-3.',
			);
		}
	} else if (metadata.decoderConfig.codec.startsWith('mp3') || metadata.decoderConfig.codec.startsWith('mp4a')) {
		// MP3-specific validation

		if (
			metadata.decoderConfig.codec !== 'mp3'
			&& metadata.decoderConfig.codec !== 'mp4a.69'
			&& metadata.decoderConfig.codec !== 'mp4a.6B'
			&& metadata.decoderConfig.codec !== 'mp4a.6b'
		) {
			throw new TypeError(
				'Audio chunk metadata decoder configuration codec string for MP3 must be "mp3", "mp4a.69" or'
				+ ' "mp4a.6B".',
			);
		}
	} else if (metadata.decoderConfig.codec.startsWith('opus')) {
		// Opus-specific validation

		if (metadata.decoderConfig.codec !== 'opus') {
			throw new TypeError('Audio chunk metadata decoder configuration codec string for Opus must be "opus".');
		}

		if (metadata.decoderConfig.description && metadata.decoderConfig.description.byteLength < 18) {
			// Description is optional for Opus per-spec, so we shouldn't enforce it
			throw new TypeError(
				'Audio chunk metadata decoder configuration description, when specified, is expected to be an'
				+ ' Identification Header as specified in Section 5.1 of RFC 7845.',
			);
		}
	} else if (metadata.decoderConfig.codec.startsWith('vorbis')) {
		// Vorbis-specific validation

		if (metadata.decoderConfig.codec !== 'vorbis') {
			throw new TypeError('Audio chunk metadata decoder configuration codec string for Vorbis must be "vorbis".');
		}

		if (!metadata.decoderConfig.description) {
			throw new TypeError(
				'Audio chunk metadata decoder configuration for Vorbis must include a description, which is expected to'
				+ ' adhere to the format described in https://www.w3.org/TR/webcodecs-vorbis-codec-registration/.',
			);
		}
	} else if (metadata.decoderConfig.codec.startsWith('flac')) {
		// FLAC-specific validation

		if (metadata.decoderConfig.codec !== 'flac') {
			throw new TypeError('Audio chunk metadata decoder configuration codec string for FLAC must be "flac".');
		}

		const minDescriptionSize = 4 + 4 + 34; // 'fLaC' + metadata block header + STREAMINFO block
		if (!metadata.decoderConfig.description || metadata.decoderConfig.description.byteLength < minDescriptionSize) {
			throw new TypeError(
				'Audio chunk metadata decoder configuration for FLAC must include a description, which is expected to'
				+ ' adhere to the format described in https://www.w3.org/TR/webcodecs-flac-codec-registration/.',
			);
		}
	} else if (
		metadata.decoderConfig.codec.startsWith('pcm')
		|| metadata.decoderConfig.codec.startsWith('ulaw')
		|| metadata.decoderConfig.codec.startsWith('alaw')
	) {
		// PCM-specific validation

		if (!(PCM_AUDIO_CODECS as readonly string[]).includes(metadata.decoderConfig.codec)) {
			throw new TypeError(
				'Audio chunk metadata decoder configuration codec string for PCM must be one of the supported PCM'
				+ ` codecs (${PCM_AUDIO_CODECS.join(', ')}).`,
			);
		}
	}
};

export const validateSubtitleMetadata = (metadata: SubtitleMetadata | undefined) => {
	if (!metadata) {
		throw new TypeError('Subtitle metadata must be provided.');
	}
	if (typeof metadata !== 'object') {
		throw new TypeError('Subtitle metadata must be an object.');
	}
	if (!metadata.config) {
		throw new TypeError('Subtitle metadata must include a config object.');
	}
	if (typeof metadata.config !== 'object') {
		throw new TypeError('Subtitle metadata config must be an object.');
	}
	if (typeof metadata.config.description !== 'string') {
		throw new TypeError('Subtitle metadata config description must be a string.');
	}
};

/**
 * Checks if the browser is able to encode the given codec.
 * @public
 */
export const canEncode = (codec: MediaCodec) => {
	if ((VIDEO_CODECS as readonly string[]).includes(codec)) {
		return canEncodeVideo(codec as VideoCodec);
	} else if ((AUDIO_CODECS as readonly string[]).includes(codec)) {
		return canEncodeAudio(codec as AudioCodec);
	} else if ((SUBTITLE_CODECS as readonly string[]).includes(codec)) {
		return canEncodeSubtitles(codec as SubtitleCodec);
	}

	throw new TypeError(`Unknown codec '${codec}'.`);
};

/**
 * Checks if the browser is able to encode the given video codec with the given parameters.
 * @public
 */
export const canEncodeVideo = async (codec: VideoCodec, { width = 1280, height = 720, bitrate = 1e6 }: {
	width?: number;
	height?: number;
	bitrate?: number | Quality;
} = {}) => {
	if (!VIDEO_CODECS.includes(codec)) {
		return false;
	}
	if (!Number.isInteger(width) || width <= 0) {
		throw new TypeError('width must be a positive integer.');
	}
	if (!Number.isInteger(height) || height <= 0) {
		throw new TypeError('height must be a positive integer.');
	}
	if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
		throw new TypeError('bitrate must be a positive integer or a quality.');
	}

	const resolvedBitrate = bitrate instanceof Quality
		? bitrate._toVideoBitrate(codec, width, height)
		: bitrate;

	if (customVideoEncoders.length > 0) {
		const encoderConfig: VideoEncoderConfig = {
			codec: buildVideoCodecString(
				codec,
				width,
				height,
				resolvedBitrate,
			),
			width,
			height,
			bitrate: resolvedBitrate,
			...getVideoEncoderConfigExtension(codec),
		};

		if (customVideoEncoders.some(x => x.supports(codec, encoderConfig))) {
			// There's a custom encoder
			return true;
		}
	}

	if (typeof VideoEncoder === 'undefined') {
		return false;
	}

	const support = await VideoEncoder.isConfigSupported({
		codec: buildVideoCodecString(codec, width, height, resolvedBitrate),
		width,
		height,
		bitrate: resolvedBitrate,
		...getVideoEncoderConfigExtension(codec),
	});

	return support.supported === true;
};

/**
 * Checks if the browser is able to encode the given audio codec with the given parameters.
 * @public
 */
export const canEncodeAudio = async (codec: AudioCodec, { numberOfChannels = 2, sampleRate = 48000, bitrate = 128e3 }: {
	numberOfChannels?: number;
	sampleRate?: number;
	bitrate?: number | Quality;
} = {}) => {
	if (!AUDIO_CODECS.includes(codec)) {
		return false;
	}
	if (!Number.isInteger(numberOfChannels) || numberOfChannels <= 0) {
		throw new TypeError('numberOfChannels must be a positive integer.');
	}
	if (!Number.isInteger(sampleRate) || sampleRate <= 0) {
		throw new TypeError('sampleRate must be a positive integer.');
	}
	if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
		throw new TypeError('bitrate must be a positive integer.');
	}

	const resolvedBitrate = bitrate instanceof Quality
		? bitrate._toAudioBitrate(codec)
		: bitrate;

	if (customAudioEncoders.length > 0) {
		const encoderConfig: AudioEncoderConfig = {
			codec: buildAudioCodecString(
				codec,
				numberOfChannels,
				sampleRate,
			),
			numberOfChannels,
			sampleRate,
			bitrate: resolvedBitrate,
			...getAudioEncoderConfigExtension(codec),
		};

		if (customAudioEncoders.some(x => x.supports(codec, encoderConfig))) {
			// There's a custom encoder
			return true;
		}
	}

	if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
		return true; // Because we encode these ourselves
	}

	if (typeof AudioEncoder === 'undefined') {
		return false;
	}

	const support = await AudioEncoder.isConfigSupported({
		codec: buildAudioCodecString(codec, numberOfChannels, sampleRate),
		numberOfChannels,
		sampleRate,
		bitrate: resolvedBitrate,
		...getAudioEncoderConfigExtension(codec),
	});

	return support.supported === true;
};

/**
 * Checks if the browser is able to encode the given subtitle codec.
 * @public
 */
export const canEncodeSubtitles = async (codec: SubtitleCodec) => {
	if (!SUBTITLE_CODECS.includes(codec)) {
		return false;
	}

	return true;
};

/**
 * Returns the list of all media codecs that can be encoded by the browser.
 * @public
 */
export const getEncodableCodecs = async (): Promise<MediaCodec[]> => {
	const [videoCodecs, audioCodecs, subtitleCodecs] = await Promise.all([
		getEncodableVideoCodecs(),
		getEncodableAudioCodecs(),
		getEncodableSubtitleCodecs(),
	]);

	return [...videoCodecs, ...audioCodecs, ...subtitleCodecs];
};

/**
 * Returns the list of all video codecs that can be encoded by the browser.
 * @public
 */
export const getEncodableVideoCodecs = async (
	checkedCodecs = VIDEO_CODECS as unknown as VideoCodec[],
	options?: {
		width?: number;
		height?: number;
		bitrate?: number | Quality;
	},
): Promise<VideoCodec[]> => {
	const bools = await Promise.all(checkedCodecs.map(codec => canEncodeVideo(codec, options)));
	return checkedCodecs.filter((_, i) => bools[i]);
};

/**
 * Returns the list of all audio codecs that can be encoded by the browser.
 * @public
 */
export const getEncodableAudioCodecs = async (
	checkedCodecs = AUDIO_CODECS as unknown as AudioCodec[],
	options?: {
		numberOfChannels?: number;
		sampleRate?: number;
		bitrate?: number | Quality;
	},
): Promise<AudioCodec[]> => {
	const bools = await Promise.all(checkedCodecs.map(codec => canEncodeAudio(codec, options)));
	return checkedCodecs.filter((_, i) => bools[i]);
};

/**
 * Returns the list of all subtitle codecs that can be encoded by the browser.
 * @public
 */
export const getEncodableSubtitleCodecs = async (
	checkedCodecs = SUBTITLE_CODECS as unknown as SubtitleCodec[],
): Promise<SubtitleCodec[]> => {
	const bools = await Promise.all(checkedCodecs.map(canEncodeSubtitles));
	return checkedCodecs.filter((_, i) => bools[i]);
};

/**
 * Returns the first video codec from the given list that can be encoded by the browser.
 * @public
 */
export const getFirstEncodableVideoCodec = async (
	checkedCodecs: VideoCodec[],
	options?: {
		width?: number;
		height?: number;
		bitrate?: number | Quality;
	},
): Promise<VideoCodec | null> => {
	for (const codec of checkedCodecs) {
		if (await canEncodeVideo(codec, options)) {
			return codec;
		}
	}

	return null;
};

/**
 * Returns the first audio codec from the given list that can be encoded by the browser.
 * @public
 */
export const getFirstEncodableAudioCodec = async (
	checkedCodecs: AudioCodec[],
	options?: {
		numberOfChannels?: number;
		sampleRate?: number;
		bitrate?: number | Quality;
	},
): Promise<AudioCodec | null> => {
	for (const codec of checkedCodecs) {
		if (await canEncodeAudio(codec, options)) {
			return codec;
		}
	}

	return null;
};

/**
 * Returns the first subtitle codec from the given list that can be encoded by the browser.
 * @public
 */
export const getFirstEncodableSubtitleCodec = async (
	checkedCodecs: SubtitleCodec[],
): Promise<SubtitleCodec | null> => {
	for (const codec of checkedCodecs) {
		if (await canEncodeSubtitles(codec)) {
			return codec;
		}
	}

	return null;
};
