import { logger } from "./logger.js";
import { NoSupportedAudioCodecError, RenderCancelledError } from "./renderTimegroupToVideo.js";
import { AudioBufferSource, BufferTarget, CanvasSource, Mp4OutputFormat, Output, StreamTarget, canEncodeAudio, getEncodableAudioCodecs } from "mediabunny";

//#region src/preview/renderVideoToVideo.ts
async function resolveVideoConfig(video, options = {}) {
	const fps = options.fps ?? 30;
	const codec = options.codec ?? "avc";
	const bitrate = options.bitrate ?? 8e6;
	const filename = options.filename ?? "video-export.mp4";
	const scale = options.scale ?? 1;
	const keyFrameInterval = options.keyFrameInterval ?? 2;
	const streaming = options.streaming ?? false;
	const includeAudio = options.includeAudio ?? true;
	const audioBitrate = options.audioBitrate ?? 128e3;
	const returnBuffer = options.returnBuffer ?? false;
	const preferredAudioCodecs = options.preferredAudioCodecs ?? ["aac", "opus"];
	const progressPreviewInterval = options.progressPreviewInterval ?? 60;
	const trimStartMs = video.trimStartMs ?? 0;
	const trimEndMs = video.trimEndMs ?? 0;
	const intrinsicDurationMs = video.intrinsicDurationMs;
	if (!intrinsicDurationMs || intrinsicDurationMs <= 0) throw new Error("Video has no intrinsic duration. Ensure the media engine is loaded.");
	const effectiveDurationMs = intrinsicDurationMs - trimStartMs - trimEndMs;
	if (effectiveDurationMs <= 0) throw new Error(`Invalid trim range: trimStart=${trimStartMs}ms, trimEnd=${trimEndMs}ms, intrinsicDuration=${intrinsicDurationMs}ms leaves no content.`);
	const startMs = options.fromMs !== void 0 ? Math.max(0, options.fromMs) : 0;
	const endMs = options.toMs !== void 0 ? Math.min(options.toMs, effectiveDurationMs) : effectiveDurationMs;
	const renderDurationMs = endMs - startMs;
	if (renderDurationMs <= 0) throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);
	let width;
	let height;
	{
		const firstFrame = await video.getVideoFrameAtSourceTime(trimStartMs, { quality: "main" });
		try {
			width = firstFrame.displayWidth;
			height = firstFrame.displayHeight;
		} finally {
			firstFrame.close();
		}
	}
	const videoWidth = Math.floor(width * scale);
	const videoHeight = Math.floor(height * scale);
	const evenWidth = videoWidth % 2 === 0 ? videoWidth : videoWidth - 1;
	const evenHeight = videoHeight % 2 === 0 ? videoHeight : videoHeight - 1;
	const frameDurationMs = 1e3 / fps;
	return {
		fps,
		codec,
		bitrate,
		filename,
		scale,
		keyFrameInterval,
		startMs,
		endMs,
		renderDurationMs,
		videoWidth: evenWidth,
		videoHeight: evenHeight,
		totalFrames: Math.ceil(renderDurationMs / frameDurationMs),
		frameDurationMs,
		frameDurationS: frameDurationMs / 1e3,
		streaming,
		includeAudio,
		audioBitrate,
		returnBuffer,
		preferredAudioCodecs,
		progressPreviewInterval,
		trimStartMs
	};
}
function isFileSystemAccessSupported() {
	return typeof window !== "undefined" && "showSaveFilePicker" in window;
}
async function getFileWritableStream(filename) {
	if (!isFileSystemAccessSupported()) return null;
	try {
		const writable = await (await window.showSaveFilePicker({
			suggestedName: filename,
			types: [{
				description: "MP4 Video",
				accept: { "video/mp4": [".mp4"] }
			}]
		})).createWritable();
		return {
			writable,
			close: async () => {
				await writable.close();
			}
		};
	} catch (e) {
		if (e.name !== "AbortError") logger.warn("[renderVideoToVideo] File System Access failed:", e);
		return null;
	}
}
function downloadBlob(blob, filename) {
	const url = URL.createObjectURL(blob);
	const a = document.createElement("a");
	a.href = url;
	a.download = filename;
	document.body.appendChild(a);
	a.click();
	document.body.removeChild(a);
	URL.revokeObjectURL(url);
}
async function selectAudioCodec(preferredCodecs, encodingOptions) {
	for (const codec of preferredCodecs) try {
		if (await canEncodeAudio(codec, encodingOptions)) return codec;
	} catch (e) {
		logger.warn(`[selectAudioCodec] Check failed for ${codec}:`, e);
	}
	throw new NoSupportedAudioCodecError(preferredCodecs, await getEncodableAudioCodecs(void 0, encodingOptions));
}
/**
* Render a single EFVideo element directly to MP4.
*
* This is the fast path: frames are decoded from the media engine,
* drawn to an encoding canvas (with CSS filter/opacity applied),
* and encoded to video. No DOM serialization involved.
*/
async function renderVideoToVideo(video, options = {}) {
	const { signal, onProgress } = options;
	const checkCancelled = () => {
		if (signal?.aborted) throw new RenderCancelledError();
	};
	await video.waitForMediaDurations(signal);
	checkCancelled();
	const config = await resolveVideoConfig(video, options);
	const pc = video.playbackController;
	const computedStyle = getComputedStyle(video);
	const cssFilter = computedStyle.filter;
	const cssOpacity = parseFloat(computedStyle.opacity);
	const hasFilter = cssFilter && cssFilter !== "none";
	const hasOpacity = cssOpacity < 1;
	logger.debug(`[renderVideoToVideo] starting: ${config.totalFrames} frames, ${config.videoWidth}x${config.videoHeight} @ ${config.fps}fps, trim=[${config.trimStartMs}, -${video.trimEndMs ?? 0}], css: filter=${hasFilter ? cssFilter : "none"}, opacity=${cssOpacity}`);
	let output = null;
	let videoSource = null;
	let audioSource = null;
	let target = null;
	let fileStream = null;
	let useStreaming = false;
	const encodingCanvas = new OffscreenCanvas(config.videoWidth, config.videoHeight);
	const encodingCtx = encodingCanvas.getContext("2d", hasFilter || hasOpacity ? { willReadFrequently: true } : void 0);
	if (!encodingCtx) throw new Error("Failed to get encoding canvas context");
	if (hasFilter) encodingCtx.filter = cssFilter;
	if (hasOpacity) encodingCtx.globalAlpha = cssOpacity;
	if (options.customWritableStream) {
		target = new StreamTarget(options.customWritableStream);
		output = new Output({
			format: new Mp4OutputFormat({ fastStart: "fragmented" }),
			target
		});
		useStreaming = true;
	} else if (config.streaming) {
		fileStream = await getFileWritableStream(config.filename);
		useStreaming = fileStream !== null;
		if (useStreaming && fileStream) {
			target = new StreamTarget(fileStream.writable);
			output = new Output({
				format: new Mp4OutputFormat({ fastStart: "fragmented" }),
				target
			});
		}
	}
	if (!target) {
		target = new BufferTarget();
		output = new Output({
			format: new Mp4OutputFormat(),
			target
		});
	}
	if (!output) throw new Error("Output not initialized");
	videoSource = new CanvasSource(encodingCanvas, {
		codec: config.codec,
		bitrate: config.bitrate,
		keyFrameInterval: config.keyFrameInterval
	});
	output.addVideoTrack(videoSource);
	if (config.includeAudio) try {
		audioSource = new AudioBufferSource({
			codec: await selectAudioCodec(config.preferredAudioCodecs, {
				numberOfChannels: 2,
				sampleRate: 48e3,
				bitrate: config.audioBitrate
			}),
			bitrate: config.audioBitrate
		});
		output.addAudioTrack(audioSource);
	} catch (e) {
		logger.warn("[renderVideoToVideo] Audio codec selection failed, rendering without audio:", e);
	}
	await output.start();
	const renderStartTime = performance.now();
	let lastRenderedAudioEndMs = config.startMs;
	const audioChunkDurationMs = 2e3;
	let thumbCanvas = null;
	let thumbCtx = null;
	if (config.progressPreviewInterval > 0) {
		const thumbScale = 160 / config.videoWidth;
		thumbCanvas = document.createElement("canvas");
		thumbCanvas.width = Math.round(config.videoWidth * thumbScale);
		thumbCanvas.height = Math.round(config.videoHeight * thumbScale);
		thumbCtx = thumbCanvas.getContext("2d");
	}
	let totalSeekMs = 0;
	let totalDrawMs = 0;
	let totalEncodeMs = 0;
	pc?.suspendSelfRender();
	try {
		for (let frameIndex = 0; frameIndex < config.totalFrames; frameIndex++) {
			checkCancelled();
			const timelineTimeMs = config.startMs + frameIndex * config.frameDurationMs;
			const sourceTimeMs = timelineTimeMs + config.trimStartMs;
			const timestampS = frameIndex * config.frameDurationMs / 1e3;
			const seekStart = performance.now();
			const videoFrame = await video.getVideoFrameAtSourceTime(sourceTimeMs, {
				quality: "main",
				signal
			});
			totalSeekMs += performance.now() - seekStart;
			try {
				const drawStart = performance.now();
				encodingCtx.drawImage(videoFrame, 0, 0, videoFrame.displayWidth, videoFrame.displayHeight, 0, 0, config.videoWidth, config.videoHeight);
				totalDrawMs += performance.now() - drawStart;
			} finally {
				videoFrame.close();
			}
			const encodeStart = performance.now();
			await videoSource.add(timestampS, config.frameDurationS);
			totalEncodeMs += performance.now() - encodeStart;
			if (audioSource && timelineTimeMs >= lastRenderedAudioEndMs + audioChunkDurationMs) {
				const chunkEndMs = Math.min(timelineTimeMs + audioChunkDurationMs, config.endMs);
				try {
					const audioBuffer = await video.renderAudio(lastRenderedAudioEndMs, chunkEndMs);
					if (audioBuffer && audioBuffer.length > 0) await audioSource.add(audioBuffer);
				} catch (e) {}
				lastRenderedAudioEndMs = chunkEndMs;
			}
			if (thumbCanvas && thumbCtx && frameIndex % config.progressPreviewInterval === 0) thumbCtx.drawImage(encodingCanvas, 0, 0, thumbCanvas.width, thumbCanvas.height);
			const currentFrame = frameIndex + 1;
			const progress = currentFrame / config.totalFrames;
			const renderedMs = currentFrame * config.frameDurationMs;
			const elapsedMs = performance.now() - renderStartTime;
			const msPerFrame = elapsedMs / currentFrame;
			const estimatedRemainingMs = (config.totalFrames - currentFrame) * msPerFrame;
			const speedMultiplier = renderedMs / elapsedMs;
			onProgress?.({
				progress,
				currentFrame,
				totalFrames: config.totalFrames,
				renderedMs,
				totalDurationMs: config.renderDurationMs,
				elapsedMs,
				estimatedRemainingMs,
				speedMultiplier,
				framePreviewCanvas: thumbCanvas || void 0
			});
		}
		if (audioSource && lastRenderedAudioEndMs < config.endMs) try {
			const audioBuffer = await video.renderAudio(lastRenderedAudioEndMs, config.endMs);
			if (audioBuffer && audioBuffer.length > 0) await audioSource.add(audioBuffer);
		} catch (e) {}
		const totalElapsed = performance.now() - renderStartTime;
		logger.debug(`[renderVideoToVideo] complete: ${config.totalFrames} frames in ${totalElapsed.toFixed(0)}ms (seek=${totalSeekMs.toFixed(0)}ms, draw=${totalDrawMs.toFixed(0)}ms, encode=${totalEncodeMs.toFixed(0)}ms) speed=${(config.renderDurationMs / totalElapsed).toFixed(1)}x`);
		await output.finalize();
		if (useStreaming) {
			if (fileStream) await fileStream.close();
			return;
		} else {
			const videoBuffer = target.buffer;
			if (!videoBuffer) throw new Error("Video encoding failed: no buffer produced");
			if (config.returnBuffer) return new Uint8Array(videoBuffer);
			downloadBlob(new Blob([videoBuffer], { type: "video/mp4" }), config.filename);
			return;
		}
	} catch (error) {
		try {
			await output?.finalize();
		} catch {}
		throw error;
	} finally {
		pc?.resumeSelfRender();
	}
}

//#endregion
export { renderVideoToVideo };
//# sourceMappingURL=renderVideoToVideo.js.map