/*

Ingest Metadata Layout:

data = {
	version: 1,
	meta: {
		name: 'Livestream 1',
		description: 'Live from earth. Powered by datarhei/restreamer.',
		author: {
			name: '',
			description: '',
		},
	},
	license: '',
	player: {},
	sources: [{
		type: "network",
		settings: {
			mode: 'pull',
			address: 'https://ch-fra-n4.livespotting.com:443/vpu/rm1naghi/85pwd6iv.m3u8',
			udp: false,
		},
		inputs: [{
			address: 'https://ch-fra-n4.livespotting.com:443/vpu/rm1naghi/85pwd6iv.m3u8',
			options: ['-re'],
		}],
		streams: [{
			"url": "https://ch-fra-n4.livespotting.com:443/vpu/rm1naghi/85pwd6iv.m3u8",
			"format": "hls",
			"index": 0,
			"stream": 0,
			"type": "video",
			"codec": "h264",
			"coder": "h264",
			"bitrate_kbps": 0,
			"fps": 0,
			"pix_fmt": "yuvj420p",
			"width": 320,
			"height": 180,
			"sampling_hz": 0,
			"layout": "",
			"channels": 0
		},{
			"url": "https://ch-fra-n4.livespotting.com:443/vpu/rm1naghi/85pwd6iv.m3u8",
			"format": "hls",
			"index": 0,
			"stream": 1,
			"type": "video",
			"codec": "h264",
			"coder": "h264",
			"bitrate_kbps": 0,
			"fps": 0,
			"pix_fmt": "yuvj420p",
			"width": 1280,
			"height": 720,
			"sampling_hz": 0,
			"layout": "",
			"channels": 0
		}],
	},{
		type: "virtualaudio",
		settings: {...},
		inputs: [{
			address: 'anullsrc=r=44100:cl=stereo',
			options: [
				'-f', 'lavfi',
			],
		}],
		streams: [{
			"url": "anullsrc=r=44100:cl=stereo",
			"format": "lavfi",
			"index": 0, <-- this is the index of the "inputs" array
			"stream": 0, <-- this will be used for the -map parameter
			"type": "audio",
			"codec": "pcm_u8",
			"coder": "pcm_u8",
			"bitrate_kbps": 705,
			"sampling_hz": 44100,
			"layout": "stereo",
			"channels": 2
		}]
	}],
	profiles: [{
		audio: {
			source: 1, <-- this is the index of the "sources" array
			stream: 0, <-- this is the index of the "streams" array in the referenced source
			encoder: {
				coder: 'aac',
				codec: 'aac',
				settings: {
					bitrate: '64',
					channels: '2',
					sampling: '44100'
				},
				mapping: [
					'-codec:a', 'aac',
					'-b:a', '64k',
					'-bsf:a', 'aac_adtstoasc',
					'-shortest',
					'-af', 'aresample=osr=44100:ocl=2'
				]
			},
			decoder: null,
		},
		video: {
			source: 0,
			stream: 1,
			encoder: {
				coder: 'copy',
				codec: 'h264',
				settings: {},
				mapping: [
					'-codec:v', 'copy',
					'-vsync 0',
					'-copyts',
					'-start_at_zero',
				]
			},
			decoder: null,
		},
		"or": {},
		"video": {
			source: 0,
			stream: 1,
			encoder: {
				coder: 'libx264',
				codec: 'h264',
				settings: {
					preset: 'ultrafast',
					bitrate: '4096',
					fps: '25',
					profile: 'auto',
					tune: 'zerolatency',
				},
				mapping: [
					'-codec:v', 'libx264',
					'-preset:v', 'ultrafast',
					'-b:v', '4096k',
					'-maxrate', '4096k',
					'-bufsize', '4096k',
					'-r', '25',
					'-g', '50',
					'-pix_fmt', 'yuv420p',
					'-vsync', '1',
					'-profile:v', 'high',
					'-tune:v', 'zerolatency',
				]
			},
			decoder: {
				coder: 'h264_cuvid',
				settings: {},
				mapping: [
					'-c:v h264_cuvid'
				]
			}
		}
	}],
	streams: [
		{
			index: 0,
			stream: 0,
			type: 'video',
			codec: 'h264',
			width: 1920,
			height: 1080,
			sampling_hz: 0,
			layout: '',
			channels: 0,
		},
		{
			index: 0,
			stream: 1,
			type: 'audio',
			codec: 'aac',
			width: 0,
			height: 0,
			sampling_hz: 44100,
			layout: 'stereo',
			channels: 2,
		}
	],
	control: {
		hls: {
			segmentDuration: 2,
			listSize: 6,
		},
		process: {
			autostart: true,
			reconnect: true,
			delay: 15,
			staleTimeout: 30
		},
		snapshot: {
			enable: true,
			interval: 60,
		},
	},
};

Egress Metadata Layout:

data = {
	version: 1,
	name: "foobar",
	control: {
		process: {
			autostart: true,
			reconnect: true,
			delay: 15,
			staleTimeout: 30
		},
	},
	output: {
		address: "rtmp://...",
		options: [],
	},
	settings: {
		...
	},
};

*/

import * as Coders from '../misc/coders/Encoders';

const defaultMetadata = {
	version: 1,
	playersite: {},
};

const defaultIngestMetadata = {
	version: 1,
	sources: [],
	profiles: [{}],
	streams: [],
	control: {
		hls: {
			lhls: false,
			segmentDuration: 2,
			listSize: 6,
		},
		process: {
			autostart: true,
			reconnect: true,
			delay: 15,
			staleTimeout: 30,
		},
		snapshot: {
			enable: true,
			interval: 60,
		},
	},
	player: {},
	meta: {
		name: '',
		description: '',
		author: {
			name: '',
			description: '',
		},
	},
	license: 'CC BY 4.0',
};

const defaultEgressMetadata = {
	version: 1,
	name: '',
	control: {
		process: {
			autostart: false,
			reconnect: true,
			delay: 15,
			staleTimeout: 30,
		},
	},
	outputs: [],
	settings: {},
	profiles: [{}],
	streams: [],
};

const getDefaultMetadata = () => {
	// poor mans deep copy
	return JSON.parse(JSON.stringify(defaultMetadata));
};

const getDefaultIngestMetadata = () => {
	// poor mans deep copy
	return JSON.parse(JSON.stringify(defaultIngestMetadata));
};

const getDefaultEgressMetadata = () => {
	// poor mans deep copy
	return JSON.parse(JSON.stringify(defaultEgressMetadata));
};

const mergeMetadata = (metadata, base) => {
	if (!metadata) {
		metadata = {};
	}

	const defaultMetadata = getDefaultMetadata();

	if (!base) {
		base = getDefaultMetadata();
	}

	metadata = {
		...base,
		...metadata,
	};

	if (metadata.version !== defaultMetadata.version) {
		metadata = {
			...defaultMetadata,
		};
	}

	metadata.playersite = {
		...base.playersite,
		...metadata.playersite,
	};

	return metadata;
};

const initMetadata = (initialMetadata) => {
	return mergeMetadata(initialMetadata);
};

const initIngestMetadata = (initialMetadata) => {
	return mergeIngestMetadata(initialMetadata);
};

const mergeIngestMetadata = (metadata, base) => {
	if (!metadata) {
		metadata = {};
	}

	const defaultMetadata = getDefaultIngestMetadata();

	if (!base) {
		base = getDefaultIngestMetadata();
	}

	metadata = {
		...base,
		...metadata,
	};

	if (metadata.version !== defaultMetadata.version) {
		metadata = {
			...defaultMetadata,
		};
	}

	metadata.meta = {
		...base.meta,
		...metadata.meta,
	};

	metadata.meta.author = {
		...base.meta.author,
		...metadata.meta.author,
	};

	metadata.player = {
		...base.player,
		...metadata.player,
	};

	metadata.control = {
		...base.control,
		...metadata.control,
	};

	metadata.control.hls = {
		...base.control.hls,
		...metadata.control.hls,
	};

	metadata.control.process = {
		...base.control.process,
		...metadata.control.process,
	};

	metadata.control.snapshot = {
		...base.control.snapshot,
		...metadata.control.snapshot,
	};

	if (!Array.isArray(metadata.sources)) {
		metadata.sources = [];
	} else {
		for (let i = 0; i < metadata.sources.length; i++) {
			metadata.sources[i] = initSource(metadata.sources[i].type, metadata.sources[i]);
		}
	}

	if (!Array.isArray(metadata.profiles)) {
		metadata.profiles = [initProfile({})];
	} else {
		for (let i = 0; i < metadata.profiles.length; i++) {
			metadata.profiles[i] = initProfile(metadata.profiles[i]);
		}
	}

	if (!Array.isArray(metadata.streams)) {
		metadata.streams = [];
	} else {
		for (let i = 0; i < metadata.streams.length; i++) {
			metadata.streams[i] = initStream(metadata.streams[i]);
		}
	}

	return metadata;
};

const initEgressMetadata = (initialMetadata) => {
	return mergeEgressMetadata(initialMetadata);
};

const mergeEgressMetadata = (metadata, base) => {
	if (!metadata) {
		metadata = {};
	}

	const defaultMetadata = getDefaultEgressMetadata();

	if (!base) {
		base = getDefaultEgressMetadata();
	}

	metadata = {
		...base,
		...metadata,
	};

	if (metadata.version !== defaultMetadata.version) {
		metadata = {
			...defaultMetadata,
		};
	}

	metadata.control = {
		...base.control,
		...metadata.control,
	};

	metadata.control.process = {
		...base.control.process,
		...metadata.control.process,
	};

	if (!Array.isArray(metadata.outputs)) {
		metadata.outputs = [];
	} else {
		for (let i = 0; i < metadata.outputs.length; i++) {
			metadata.outputs[i] = initOutput(metadata.outputs[i]);
		}
	}

	if (!Array.isArray(metadata.profiles)) {
		metadata.profiles = [initProfile({})];
	} else {
		for (let i = 0; i < metadata.profiles.length; i++) {
			metadata.profiles[i] = initProfile(metadata.profiles[i]);
		}
	}

	if (!Array.isArray(metadata.streams)) {
		metadata.streams = [];
	} else {
		for (let i = 0; i < metadata.streams.length; i++) {
			metadata.streams[i] = initStream(metadata.streams[i]);
		}
	}

	return metadata;
};

const validateProfile = (sources, profile) => {
	let validVideo = false;

	if (!('video' in profile)) {
		profile.video = initProfile({});
	} else {
		profile.video = initProfile(profile.video);
	}

	if (profile.video.source !== -1 && profile.video.source < sources.length) {
		const source = sources[profile.video.source];

		if (profile.video.stream !== -1 && profile.video.stream < source.streams.length) {
			const stream = source.streams[profile.video.stream];

			if (stream.index < source.inputs.length) {
				if (stream.type === 'video') {
					validVideo = true;
				}
			}
		}
	}

	let validAudio = false;

	if (!('audio' in profile)) {
		profile.audio = initProfile({});
	} else {
		profile.audio = initProfile(profile.audio);
	}

	if (profile.audio.source !== -1 && profile.audio.source < sources.length) {
		const source = sources[profile.audio.source];

		if (profile.audio.stream !== -1 && profile.audio.stream < source.streams.length) {
			const stream = source.streams[profile.audio.stream];

			if (stream.index < source.inputs.length) {
				if (stream.type === 'audio') {
					validAudio = true;
				}
			}
		}
	}

	if (validVideo === false) {
		profile.video.source = -1;
		profile.video.stream = -1;
	}

	if (validAudio === false) {
		profile.audio.source = -1;
		profile.audio.stream = -1;
	}

	let complete = true;

	if (profile.video.encoder.coder === 'none' || profile.video.source === -1 || profile.video.stream === -1) {
		complete = false;
	}

	return complete;
};

const createInputsOutputs = (sources, profiles) => {
	const source2inputMap = new Map();

	const inputs = [];
	const outputs = [];

	// For each profile get the source and do the proper mapping
	for (let profile of profiles) {
		const complete = validateProfile(sources, profile);
		if (complete === false) {
			continue;
		}

		let index = -1;

		const source = sources[profile.video.source];
		const stream = source.streams[profile.video.stream];
		const input = source.inputs[stream.index];

		input.options = [...profile.video.decoder.mapping, ...input.options];

		const id = profile.video.source + ':' + stream.index;

		// Check if we already use this input. If not, add it to the final inputs and
		// keep track of the mapping index.
		if (source2inputMap.has(id) === false) {
			const i = inputs.push(input);
			source2inputMap.set(id, i - 1);
		}

		index = source2inputMap.get(id);

		const options = ['-map', index + ':' + stream.stream, ...profile.video.encoder.mapping];

		if (profile.audio.encoder.coder !== 'none' && profile.audio.source !== -1 && profile.audio.stream !== -1) {
			const source = sources[profile.audio.source];
			const stream = source.streams[profile.audio.stream];
			const input = source.inputs[stream.index];

			input.options = [...profile.audio.decoder.mapping, ...input.options];

			const id = profile.audio.source + ':' + stream.index;

			if (source2inputMap.has(id) === false) {
				const i = inputs.push(input);
				source2inputMap.set(id, i - 1);
			}

			index = source2inputMap.get(id);

			options.push('-map', index + ':' + stream.stream, ...profile.audio.encoder.mapping);
		} else {
			options.push('-an');
		}

		outputs.push({
			address: '',
			options: options,
		});
	}

	return [inputs, outputs];
};

const createOutputStreams = (sources, profiles) => {
	const streams = [];

	// Generate a list of output streams from the profiles
	for (let profile of profiles) {
		const complete = validateProfile(sources, profile);
		if (complete === false) {
			continue;
		}

		const source = sources[profile.video.source];
		const stream = source.streams[profile.video.stream];

		const s = initStream({
			index: 0,
			stream: streams.length,
			type: stream.type,
			width: stream.width,
			height: stream.height,
		});

		if (profile.video.encoder.coder !== 'copy') {
			const encoder = Coders.Video.Get(profile.video.encoder.coder);
			if (encoder) {
				s.codec = encoder.codec;
			}
		} else {
			s.codec = stream.codec;
		}

		streams.push(s);

		if (profile.audio.encoder.coder !== 'none' && profile.audio.source !== -1 && profile.audio.stream !== -1) {
			const source = sources[profile.audio.source];
			const stream = source.streams[profile.audio.stream];

			const s = initStream({
				index: 0,
				stream: streams.length,
				type: stream.type,
				sampling_hz: stream.sampling_hz,
				layout: stream.layout,
				channels: stream.channels,
			});

			if (profile.audio.encoder.coder !== 'copy') {
				const encoder = Coders.Audio.Get(profile.audio.encoder.coder);
				if (encoder) {
					s.codec = encoder.codec;
				}
			} else {
				s.codec = stream.codec;
			}

			streams.push(s);
		}
	}

	return streams;
};

const initSource = (type, initialSource) => {
	if (!initialSource) {
		initialSource = {};
	}

	let source = {
		type: '',
		settings: {},
		inputs: [],
		streams: [],
	};

	// Default pre-selection for custom audio
	if (type === 'audio') {
		source.type = 'virtualaudio';
		source.settings.source = 'silence';
	}

	source = {
		...source,
		...initialSource,
	};

	return source;
};

const initProfile = (initialProfile) => {
	if (!initialProfile) {
		initialProfile = {};
	}

	const profile = {
		video: {},
		audio: {},
		...initialProfile,
	};

	profile.video = {
		source: -1,
		stream: -1,
		encoder: {},
		decoder: {},
		...profile.video,
	};

	profile.video.encoder = {
		coder: 'none',
		settings: {},
		mapping: [],
		...profile.video.encoder,
	};

	profile.video.decoder = {
		coder: 'default',
		settings: {},
		mapping: [],
		...profile.video.decoder,
	};

	profile.audio = {
		source: -1,
		stream: -1,
		encoder: {},
		decoder: {},
		...profile.audio,
	};

	profile.audio.encoder = {
		coder: 'none',
		settings: {},
		mapping: [],
		...profile.audio.encoder,
	};

	profile.audio.decoder = {
		coder: 'default',
		settings: {},
		mapping: [],
		...profile.audio.decoder,
	};

	profile.custom = {
		selected: profile.audio.source === 1,
		stream: profile.audio.source === 1 ? -2 : profile.audio.stream,
		...profile.custom,
	};

	return profile;
};

const initStream = (initialStream) => {
	if (!initialStream) {
		initialStream = {};
	}

	const stream = {
		index: 0,
		stream: 0,
		type: '',
		codec: '',
		width: 0,
		height: 0,
		sampling_hz: 0,
		layout: '',
		channels: 0,
		...initialStream,
	};

	return stream;
};

const initOutput = (initialOutput) => {
	if (!initialOutput) {
		initialOutput = {};
	}

	const output = {
		address: '',
		options: [],
		...initialOutput,
	};

	return output;
};

const analyzeStreams = (type, streams) => {
	let video = null;
	let audio = null;

	for (let stream of streams) {
		if (stream.type === 'video') {
			if (video === null) {
				video = stream;
				continue;
			}
		} else if (stream.type === 'audio') {
			if (audio === null) {
				audio = stream;
				continue;
			}
		}

		if (video !== null && audio !== null) {
			break;
		}
	}

	let status = 'success';

	if (video === null && audio === null) {
		status = 'error';
	} else if (type === 'video' && video === null) {
		status = 'nostream';
	} else if (type === 'audio' && audio === null) {
		status = 'nostream';
	}

	return status;
};

/**
 * Preselect a profile based on the available streams and encoders.
 *
 * @param {*} type Either 'video' or 'audio'
 * @param {*} streams Array of streams
 * @param {*} profile A profile
 * @param {*} encoders Array of supported (by ffmpeg) encoders
 * @returns A profile
 */
const preselectProfile = (type, streams, profile, encoders) => {
	const preselectAudioProfile = (streams, audio) => {
		audio.stream = -1;
		audio.encoder.coder = 'none';

		for (let i = 0; i < streams.length; i++) {
			if (streams[i].type !== 'audio') {
				continue;
			}

			audio.stream = i;

			if (streams[i].codec === 'aac' || streams[i].codec === 'mp3') {
				audio.encoder.coder = 'copy';
			} else {
				let coder = Coders.Audio.GetCoderForCodec('aac', encoders.audio);
				if (coder === null) {
					coder = Coders.Audio.GetCoderForCodec('mp3', encoders.audio);
					if (coder === null) {
						audio.encoder.coder = 'none';
					} else {
						audio.encoder.coder = coder.coder;
					}
				} else {
					audio.encoder.coder = coder.coder;
				}
			}

			break;
		}

		return audio;
	};

	const isVideoPlausible = (streams, video) => {
		if (video.stream < 0) {
			return false;
		}

		if (video.stream >= streams.length) {
			return false;
		}

		if (streams[video.stream].type !== 'video') {
			return false;
		}

		if (streams[video.stream].codec !== 'h264') {
			if (video.encoder.coder === 'copy') {
				return false;
			}
		} else {
			if (video.encoder.coder === 'copy') {
				return true;
			}
		}

		const coder = Coders.Video.Get(video.encoder.coder);
		if (coder === null) {
			return false;
		}

		if (coder.codec !== 'h264') {
			return false;
		}

		return true;
	};

	const isAudioPlausible = (streams, audio) => {
		if (audio.stream < 0) {
			return false;
		}

		if (audio.stream >= streams.length) {
			return false;
		}

		if (streams[audio.stream].type !== 'audio') {
			return false;
		}

		if (streams[audio.stream].codec !== 'aac' || streams[audio.stream].codec !== 'mp3') {
			if (audio.encoder.coder === 'copy') {
				return false;
			}
		} else {
			if (audio.encoder.coder === 'copy') {
				return true;
			}
		}

		const coder = Coders.Audio.Get(audio.encoder.coder);
		if (coder === null) {
			return false;
		}

		if (coder.codec !== 'aac' && coder.codec !== 'mp3') {
			return false;
		}

		return true;
	};

	if (type === 'video') {
		if (isVideoPlausible(streams, profile.video) === false) {
			const video = profile.video;

			video.stream = -1;
			video.encoder.coder = 'none';

			for (let i = 0; i < streams.length; i++) {
				if (streams[i].type !== 'video') {
					continue;
				}

				video.source = 0;
				video.stream = i;

				if (streams[i].codec === 'h264') {
					video.encoder.coder = 'copy';
				} else {
					let coder = Coders.Video.GetCoderForCodec('h264', encoders.video);
					if (coder === null) {
						video.encoder.coder = 'none';
					} else {
						video.encoder.coder = coder.coder;
					}
				}

				break;
			}

			profile.video = video;
		}

		if (isAudioPlausible(streams, profile.audio) === false) {
			profile.audio = preselectAudioProfile(streams, profile.audio);

			if (profile.audio.stream >= 0) {
				profile.audio.source = 0;

				profile.custom.selected = false;
				profile.custom.stream = profile.audio.stream;
			} else {
				profile.custom.selected = true;
				profile.custom.stream = -2;
			}
		}
	} else if (type === 'audio') {
		if (isAudioPlausible(streams, profile.audio) === false) {
			profile.audio = preselectAudioProfile(streams, profile.audio);
		}

		profile.audio.source = 1;
	}

	return profile;
};

const cleanupSources = (sources) => {
	return [sources.video, sources.audio];
};

const cleanupProfile = (profile) => {
	profile.video.source = 0;
	profile.audio.source = 0;

	if (profile.custom.selected === true) {
		profile.audio.source = 1;
	}

	if (profile.video.stream === -1) {
		profile.video.source = -1;
	}

	if (profile.audio.stream === -1) {
		profile.audio.source = -1;
	}

	return {
		audio: profile.audio,
		video: profile.video,
		custom: profile.custom,
	};
};

export {
	getDefaultMetadata,
	getDefaultIngestMetadata,
	getDefaultEgressMetadata,
	initMetadata,
	initIngestMetadata,
	initEgressMetadata,
	mergeMetadata,
	mergeIngestMetadata,
	mergeEgressMetadata,
	validateProfile,
	createInputsOutputs,
	createOutputStreams,
	initSource,
	initProfile,
	analyzeStreams,
	preselectProfile,
	cleanupProfile,
	cleanupSources,
};
