import { logger } from "./logger.js";
import { RenderContext } from "./RenderContext.js";
import { createPreviewContainer } from "./previewTypes.js";
import { captureTimelineToDataUri } from "./rendering/serializeTimelineDirect.js";
import { isNativeCanvasApiAvailable } from "./previewSettings.js";
import { renderToImageNative } from "./rendering/renderToImageNative.js";
import { resetRenderState, waitForVideoContent } from "./renderTimegroupToCanvas.js";
import { AudioBufferSource, BufferTarget, CanvasSource, Mp4OutputFormat, Output, QUALITY_HIGH, StreamTarget, canEncodeAudio, getEncodableAudioCodecs } from "mediabunny";

//#region src/preview/renderTimegroupToVideo.ts
var NoSupportedAudioCodecError = class extends Error {
	constructor(requestedCodecs, availableCodecs) {
		super(`No supported audio codec found. Requested: [${requestedCodecs.join(", ")}], Available: [${availableCodecs.length > 0 ? availableCodecs.join(", ") : "none"}]`);
		this.name = "NoSupportedAudioCodecError";
	}
};
var RenderCancelledError = class extends Error {
	constructor() {
		super("Render cancelled");
		this.name = "RenderCancelledError";
	}
};
function resolveConfig(timegroup, options = {}) {
	const fps = options.fps ?? timegroup.effectiveFps ?? 30;
	const codec = options.codec ?? "avc";
	const bitrate = options.bitrate ?? 8e6;
	const filename = options.filename ?? "timegroup-video.mp4";
	const scale = options.scale ?? 1;
	const keyFrameInterval = options.keyFrameInterval ?? 2;
	const streaming = options.streaming ?? true;
	const includeAudio = options.includeAudio ?? true;
	const audioBitrate = options.audioBitrate ?? 128e3;
	const contentReadyMode = options.contentReadyMode ?? "blocking";
	const blockingTimeoutMs = options.blockingTimeoutMs ?? 5e3;
	const returnBuffer = options.returnBuffer ?? false;
	const preferredAudioCodecs = options.preferredAudioCodecs ?? ["aac", "opus"];
	const benchmarkMode = options.benchmarkMode ?? false;
	const progressPreviewInterval = options.progressPreviewInterval ?? 60;
	const totalDurationMs = timegroup.durationMs;
	if (!totalDurationMs || totalDurationMs <= 0) throw new Error("Timegroup has no duration");
	const startMs = Math.max(0, options.fromMs ?? 0);
	const endMs = options.toMs !== void 0 ? Math.min(options.toMs, totalDurationMs) : totalDurationMs;
	const renderDurationMs = endMs - startMs;
	if (renderDurationMs <= 0) throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);
	timegroup.offsetHeight;
	let timegroupWidth = timegroup.offsetWidth;
	let timegroupHeight = timegroup.offsetHeight;
	if (!timegroupWidth || !timegroupHeight) {
		const rect = timegroup.getBoundingClientRect();
		if (rect.width > 0 && rect.height > 0) {
			timegroupWidth = rect.width;
			timegroupHeight = rect.height;
		}
	}
	if (!timegroupWidth || !timegroupHeight) {
		const computed = getComputedStyle(timegroup);
		const cw = parseFloat(computed.width);
		const ch = parseFloat(computed.height);
		if (cw > 0 && ch > 0) {
			timegroupWidth = cw;
			timegroupHeight = ch;
		}
	}
	if (!timegroupWidth || !timegroupHeight) throw new Error(`Timegroup has no dimensions (${timegroupWidth}x${timegroupHeight}). Ensure the timegroup element is in the document and has explicit width/height styles (e.g., class="w-[1920px] h-[1080px]")`);
	const width = Math.floor(timegroupWidth * scale);
	const height = Math.floor(timegroupHeight * scale);
	const videoWidth = width % 2 === 0 ? width : width - 1;
	const videoHeight = height % 2 === 0 ? height : height - 1;
	const frameDurationMs = 1e3 / fps;
	return {
		fps,
		codec,
		bitrate,
		filename,
		scale,
		keyFrameInterval,
		startMs,
		endMs,
		renderDurationMs,
		width,
		height,
		videoWidth,
		videoHeight,
		totalFrames: Math.ceil(renderDurationMs / frameDurationMs),
		frameDurationMs,
		frameDurationS: frameDurationMs / 1e3,
		streaming,
		includeAudio,
		audioBitrate,
		contentReadyMode,
		blockingTimeoutMs,
		returnBuffer,
		preferredAudioCodecs,
		benchmarkMode,
		progressPreviewInterval,
		canvasMode: (() => {
			const requested = options.canvasMode;
			if (!requested) return "foreignObject";
			if (requested === "native" && !isNativeCanvasApiAvailable()) {
				logger.debug("[renderTimegroupToVideo] Native canvas mode requested but not available, falling back to foreignObject");
				return "foreignObject";
			}
			return requested;
		})()
	};
}
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("[renderToVideo] File System Access failed:", e);
		return null;
	}
}
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));
}
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 getSupportedAudioCodecs(options) {
	const { numberOfChannels = 2, sampleRate = 48e3, bitrate = 128e3 } = options ?? {};
	return getEncodableAudioCodecs(void 0, {
		numberOfChannels,
		sampleRate,
		bitrate
	});
}
/**
* Renders a timegroup to an MP4 video file.
*
* Uses the EXACT same code path as thumbnail generation (captureFromClone).
* This ensures consistency - if thumbnails work, video export works.
*/
async function renderTimegroupToVideo(timegroup, options = {}) {
	const config = resolveConfig(timegroup, options);
	const { signal, onProgress } = options;
	const checkCancelled = () => {
		if (signal?.aborted) throw new RenderCancelledError();
	};
	resetRenderState();
	const { clone: renderClone, container: cloneContainer, cleanup: cleanupRenderClone } = await timegroup.createRenderClone();
	const timestamps = [];
	for (let i = 0; i < config.totalFrames; i++) timestamps.push(config.startMs + i * config.frameDurationMs);
	let output = null;
	let videoSource = null;
	let audioSource = null;
	let target = null;
	let fileStream = null;
	let useStreaming = false;
	let encodingCanvas = null;
	let encodingCtx = null;
	if (!config.benchmarkMode) {
		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
			});
		}
		encodingCanvas = new OffscreenCanvas(config.videoWidth, config.videoHeight);
		encodingCtx = encodingCanvas.getContext("2d");
		if (!encodingCtx) {
			cleanupRenderClone();
			throw new Error("Failed to get encoding canvas context");
		}
		if (!output) throw new Error("Output not initialized");
		const videoConfig = {
			codec: config.codec,
			bitrate: config.bitrate,
			keyFrameInterval: config.keyFrameInterval
		};
		videoSource = new CanvasSource(encodingCanvas, videoConfig);
		output.addVideoTrack(videoSource);
		if (config.includeAudio) {
			audioSource = new AudioBufferSource({
				codec: await selectAudioCodec(config.preferredAudioCodecs, {
					numberOfChannels: 2,
					sampleRate: 48e3,
					bitrate: config.audioBitrate
				}),
				bitrate: config.audioBitrate
			});
			output.addAudioTrack(audioSource);
		}
		await output.start();
	}
	const renderContext = new RenderContext();
	const previewContainer = createPreviewContainer({
		width: timegroup.offsetWidth || 1920,
		height: timegroup.offsetHeight || 1080,
		background: getComputedStyle(timegroup).background || "#000"
	});
	logger.debug(`[renderTimegroupToVideo] Using direct timeline serialization`);
	previewContainer.appendChild(cloneContainer);
	previewContainer.classList.add("ef-render-clone-container");
	previewContainer.style.cssText += ";position:fixed;left:-99999px;top:-99999px;pointer-events:none;";
	document.body.appendChild(previewContainer);
	renderClone.offsetHeight;
	logger.debug(`[renderTimegroupToVideo] Attached previewContainer to document.body (off-screen) for style computation`);
	const renderStartTime = performance.now();
	let lastRenderedAudioEndMs = config.startMs;
	const audioChunkDurationMs = 2e3;
	let thumbCanvas = null;
	let thumbCtx = null;
	if (onProgress && config.progressPreviewInterval > 0) {
		const previewWidth = 160;
		const previewHeight = Math.round(previewWidth * (config.videoHeight / config.videoWidth));
		thumbCanvas = document.createElement("canvas");
		thumbCanvas.width = previewWidth;
		thumbCanvas.height = previewHeight;
		thumbCtx = thumbCanvas.getContext("2d");
	}
	try {
		const MAX_AHEAD = 2;
		const pendingFrames = [];
		let nextSeekFrame = 0;
		let encodedFrames = 0;
		while (encodedFrames < config.totalFrames) {
			checkCancelled();
			while (nextSeekFrame < config.totalFrames && pendingFrames.length < MAX_AHEAD) {
				const fi = nextSeekFrame;
				const timeMs = timestamps[fi];
				const timestampS = fi * config.frameDurationMs / 1e3;
				await renderClone.seekForRender(timeMs);
				const entry = {
					frameIndex: fi,
					timeMs,
					timestampS,
					resolved: null,
					promise: null
				};
				if (config.contentReadyMode === "blocking") await waitForVideoContent(renderClone, timeMs, config.blockingTimeoutMs);
				if (config.canvasMode === "native") {
					entry.resolved = await renderToImageNative(renderClone, config.width, config.height, { skipDprScaling: true });
					entry.promise = Promise.resolve(entry.resolved);
				} else entry.promise = captureTimelineToDataUri(renderClone, config.width, config.height, {
					renderContext,
					canvasScale: config.scale,
					timeMs
				}).then((dataUri) => {
					return new Promise((resolve, reject) => {
						const image$1 = new Image();
						image$1.onload = () => {
							entry.resolved = image$1;
							resolve(image$1);
						};
						image$1.onerror = (e) => {
							console.error(`[Render] frame ${fi} image load error:`, e);
							reject(/* @__PURE__ */ new Error(`Failed to load image from data URI`));
						};
						image$1.src = dataUri;
					});
				});
				pendingFrames.push(entry);
				nextSeekFrame++;
			}
			const head = pendingFrames.shift();
			const preloaded = head.resolved !== null;
			let image;
			if (preloaded) image = head.resolved;
			else image = await head.promise;
			if (audioSource && head.timeMs >= lastRenderedAudioEndMs + audioChunkDurationMs) {
				const chunkEndMs = Math.min(head.timeMs + audioChunkDurationMs, config.endMs);
				try {
					const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, chunkEndMs, signal);
					if (audioBuffer && audioBuffer.length > 0) await audioSource.add(audioBuffer);
				} catch (e) {}
				lastRenderedAudioEndMs = chunkEndMs;
			}
			if (videoSource && output && encodingCtx) {
				encodingCtx.drawImage(image, 0, 0, image.width, image.height, 0, 0, config.videoWidth, config.videoHeight);
				await videoSource.add(head.timestampS, config.frameDurationS);
			}
			encodedFrames++;
			const currentFrame = encodedFrames;
			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;
			if (thumbCanvas && thumbCtx && head.frameIndex % config.progressPreviewInterval === 0) thumbCtx.drawImage(image, 0, 0, thumbCanvas.width, thumbCanvas.height);
			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 timegroup.renderAudio(lastRenderedAudioEndMs, config.endMs, signal);
			if (audioBuffer && audioBuffer.length > 0) await audioSource.add(audioBuffer);
		} catch (e) {}
		if (config.benchmarkMode) return;
		await output.finalize();
		{
			const elapsedMs = Math.round(performance.now() - renderStartTime);
			const endpoint = options.telemetryEndpoint ?? "https://editframe.com";
			const efMediaCount = timegroup.querySelectorAll("ef-video,ef-audio").length;
			const efImageCount = timegroup.querySelectorAll("ef-image").length;
			const efCaptionsCount = timegroup.querySelectorAll("ef-captions").length;
			const efTextCount = timegroup.querySelectorAll("ef-text").length;
			const headers = { "Content-Type": "application/json" };
			if (options.telemetryToken) headers["Authorization"] = `Bearer ${options.telemetryToken}`;
			fetch(`${endpoint}/api/v1/telemetry`, {
				method: "POST",
				headers,
				body: JSON.stringify({
					event_type: "render",
					render_path: "client",
					duration_ms: elapsedMs,
					width: config.videoWidth,
					height: config.videoHeight,
					fps: config.fps,
					feature_usage: {
						efMediaCount,
						efImageCount,
						efCaptionsCount,
						efTextCount
					}
				}),
				keepalive: true
			}).catch(() => {});
		}
		if (useStreaming) 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;
		}
	} finally {
		renderContext.dispose();
		if (previewContainer.parentNode) previewContainer.parentNode.removeChild(previewContainer);
		cleanupRenderClone();
	}
}

//#endregion
export { NoSupportedAudioCodecError, QUALITY_HIGH, RenderCancelledError, getSupportedAudioCodecs, renderTimegroupToVideo };
//# sourceMappingURL=renderTimegroupToVideo.js.map