import { FrameController } from "./FrameController.js";
import { updateAnimations } from "../elements/updateAnimations.js";
import { logger } from "./logger.js";
import { RenderContext } from "./RenderContext.js";
import { DEFAULT_BLOCKING_TIMEOUT_MS, DEFAULT_CAPTURE_SCALE, DEFAULT_HEIGHT, DEFAULT_WIDTH, isVisibleAtTime } from "./previewTypes.js";
import { captureTimelineToDataUri } from "./rendering/serializeTimelineDirect.js";
import { getRenderMode, isNativeCanvasApiAvailable } from "./previewSettings.js";
import { getEffectiveRenderMode, isCanvas, isImage } from "./renderers.js";
import { defaultProfiler } from "./RenderProfiler.js";
import { loadImageFromDataUri } from "./rendering/loadImage.js";
import { createDprCanvas, renderToImageNative } from "./rendering/renderToImageNative.js";
import { clearInlineImageCache, getInlineImageCacheSize } from "./rendering/inlineImages.js";

//#region src/preview/renderTimegroupToCanvas.ts
/** Number of rows to sample when checking canvas content */
const CANVAS_SAMPLE_STRIP_HEIGHT = 4;
/**
* Error thrown when video content is not ready within the blocking timeout.
*/
var ContentNotReadyError = class extends Error {
	constructor(timeMs, timeoutMs, blankVideos) {
		super(`Video content not ready at ${timeMs}ms after ${timeoutMs}ms timeout. Blank videos: ${blankVideos.join(", ")}`);
		this.timeMs = timeMs;
		this.timeoutMs = timeoutMs;
		this.blankVideos = blankVideos;
		this.name = "ContentNotReadyError";
	}
};
/**
* Module-level state for render operations.
* Note: xmlSerializer is lazy-initialized for Node.js compatibility
*/
const renderState = {
	inlineImageCache: /* @__PURE__ */ new Map(),
	layoutInitializedCanvases: /* @__PURE__ */ new WeakSet(),
	xmlSerializer: null,
	textEncoder: new TextEncoder(),
	metrics: {
		inlineImageCacheHits: 0,
		inlineImageCacheMisses: 0,
		inlineImageCacheEvictions: 0
	}
};
/**
* Get the current render state for testing and debugging.
* @returns The module-level render state object
*/
function getRenderState() {
	return renderState;
}
/**
* Get cache metrics for monitoring performance.
* @returns Object with cache hit/miss/eviction counts
*/
function getCacheMetrics() {
	return { ...renderState.metrics };
}
/**
* Reset cache metrics to zero.
*/
function resetCacheMetrics() {
	renderState.metrics.inlineImageCacheHits = 0;
	renderState.metrics.inlineImageCacheMisses = 0;
	renderState.metrics.inlineImageCacheEvictions = 0;
}
/**
* Reset all module state including profiling counters, caches, and logging flags.
* Call at the start of export sessions to ensure clean state.
*/
function resetRenderState() {
	defaultProfiler.reset();
	clearInlineImageCache();
	resetCacheMetrics();
}
/**
* DEBUG: Capture a single thumbnail at the current time.
* Call from console: window.debugCaptureThumbnail()
*/
if (typeof window !== "undefined") window.debugCaptureThumbnail = async function() {
	const timegroup = document.querySelector("ef-timegroup");
	if (!timegroup) {
		console.error("No timegroup found");
		return;
	}
	const currentTime = timegroup.currentTimeMs ?? 0;
	try {
		const result = await captureTimegroupAtTime(timegroup, {
			timeMs: currentTime,
			scale: .25,
			contentReadyMode: "blocking",
			blockingTimeoutMs: 1e3
		});
		const img = document.createElement("img");
		if (result instanceof HTMLCanvasElement) img.src = result.toDataURL();
		else if (result instanceof HTMLImageElement) img.src = result.src;
		img.style.cssText = "position:fixed;top:10px;right:10px;border:2px solid red;z-index:99999;";
		document.body.appendChild(img);
		return result;
	} catch (err) {
		console.error("[DEBUG] Capture failed:", err);
		throw err;
	}
};
/**
* Wait for next animation frame (allows browser to complete layout)
*/
function waitForFrame() {
	return new Promise((resolve) => requestAnimationFrame(() => resolve()));
}
/**
* Check if a canvas has any rendered content (not all transparent/uninitialized).
* Returns true if there's ANY non-transparent pixel.
*/
function canvasHasContent(canvas) {
	const ctx = canvas.getContext("2d", { willReadFrequently: true });
	if (!ctx) return false;
	try {
		const width = canvas.width;
		const height = canvas.height;
		if (width === 0 || height === 0) return false;
		const stripY = Math.floor(height / 2);
		const data = ctx.getImageData(0, stripY, width, CANVAS_SAMPLE_STRIP_HEIGHT).data;
		for (let i = 3; i < data.length; i += 4) if (data[i] !== 0) return true;
		return false;
	} catch {
		return true;
	}
}
/**
* Wait for video canvases within a timegroup to have content.
* Only checks videos that should be visible at the current time.
* Returns result with ready status and list of blank video names.
*/
async function waitForVideoContent(timegroup, timeMs, maxWaitMs) {
	const startTime = performance.now();
	const allVideos = timegroup.querySelectorAll("ef-video");
	if (allVideos.length === 0) return {
		ready: true,
		blankVideos: []
	};
	const visibleVideos = Array.from(allVideos).filter((video) => {
		if (!isVisibleAtTime(video, timeMs)) return false;
		let parent = video.parentElement;
		while (parent && parent !== timegroup) {
			if (parent.tagName === "EF-TIMEGROUP" && !isVisibleAtTime(parent, timeMs)) return false;
			parent = parent.parentElement;
		}
		return true;
	});
	if (visibleVideos.length === 0) return {
		ready: true,
		blankVideos: []
	};
	const getBlankVideoNames = () => visibleVideos.filter((video) => {
		const shadowCanvas = video.shadowRoot?.querySelector("canvas");
		return shadowCanvas && !canvasHasContent(shadowCanvas);
	}).map((v) => v.src || v.id || "unnamed");
	while (performance.now() - startTime < maxWaitMs) {
		let allHaveContent = true;
		for (const video of visibleVideos) {
			const shadowCanvas = video.shadowRoot?.querySelector("canvas");
			if (shadowCanvas && shadowCanvas.width > 0 && shadowCanvas.height > 0) {
				if (!canvasHasContent(shadowCanvas)) {
					allHaveContent = false;
					break;
				}
			}
		}
		if (allHaveContent) return {
			ready: true,
			blankVideos: []
		};
		await waitForFrame();
	}
	return {
		ready: false,
		blankVideos: getBlankVideoNames()
	};
}
/**
* Captures a frame from an already-seeked render clone.
* Used internally by captureBatch for efficiency (reuses one clone across all captures).
*
* @param renderClone - A render clone that has already been seeked to the target time
* @param renderContainer - The container holding the render clone (from createRenderClone)
* @param options - Capture options
* @returns Canvas or Image with the rendered frame (both are CanvasImageSource)
*/
async function captureFromClone(renderClone, _renderContainer, options = {}) {
	const { scale = DEFAULT_CAPTURE_SCALE, contentReadyMode = "immediate", blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS, originalTimegroup, timeMs: explicitTimeMs, canvasMode } = options;
	const timeMs = explicitTimeMs ?? renderClone.currentTimeMs;
	const sourceForDimensions = originalTimegroup ?? renderClone;
	const width = sourceForDimensions.offsetWidth || DEFAULT_WIDTH;
	const height = sourceForDimensions.offsetHeight || DEFAULT_HEIGHT;
	if (contentReadyMode === "blocking") {
		const result = await waitForVideoContent(renderClone, timeMs, blockingTimeoutMs);
		if (!result.ready) throw new ContentNotReadyError(timeMs, blockingTimeoutMs, result.blankVideos);
	}
	const effectiveCanvasMode = (() => {
		if (!canvasMode) return "foreignObject";
		if (canvasMode === "native" && !isNativeCanvasApiAvailable()) {
			logger.debug("[captureFromClone] Native canvas mode requested but not available, falling back to foreignObject");
			return "foreignObject";
		}
		return canvasMode;
	})();
	const renderContext = new RenderContext();
	try {
		if (effectiveCanvasMode === "native") {
			const t0 = performance.now();
			const canvas = await renderToImageNative(renderClone, width, height, { skipDprScaling: true });
			const renderTime = performance.now() - t0;
			logger.debug(`[captureFromClone] native render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`);
			return canvas;
		} else {
			const t0 = performance.now();
			const dataUri = await captureTimelineToDataUri(renderClone, width, height, {
				renderContext,
				canvasScale: scale,
				timeMs
			});
			const serializeTime = performance.now() - t0;
			const t1 = performance.now();
			const image = await loadImageFromDataUri(dataUri);
			const loadTime = performance.now() - t1;
			logger.debug(`[captureFromClone] foreignObject serialize=${serializeTime.toFixed(0)}ms, load=${loadTime.toFixed(0)}ms (canvasScale=${scale})`);
			return image;
		}
	} finally {
		renderContext.dispose();
	}
}
/**
* Captures a single frame from a timegroup at a specific time.
*
* CLONE-TIMELINE ARCHITECTURE:
* Creates an independent render clone, seeks it to the target time, and captures.
* Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.
*
* @param timegroup - The source timegroup
* @param options - Capture options including timeMs, scale, contentReadyMode
* @returns Canvas with the rendered frame
* @throws ContentNotReadyError if blocking mode times out waiting for video content
*/
async function captureTimegroupAtTime(timegroup, options) {
	const { timeMs, scale = DEFAULT_CAPTURE_SCALE, contentReadyMode = "immediate", blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS, canvasMode, skipClone = false } = options;
	if (skipClone) {
		const seekStart = performance.now();
		await timegroup.seekForRender(timeMs);
		const seekMs = performance.now() - seekStart;
		const renderStart = performance.now();
		const result = await captureFromClone(timegroup, timegroup.parentElement || document.body, {
			scale,
			contentReadyMode,
			blockingTimeoutMs,
			originalTimegroup: void 0,
			canvasMode,
			timeMs
		});
		const renderMs = performance.now() - renderStart;
		if (typeof result === "object" && result !== null) result.__perfTiming = {
			cloneMs: 0,
			seekMs,
			renderMs
		};
		return result;
	}
	const cloneStart = performance.now();
	const { clone: renderClone, container: renderContainer, cleanup: cleanupRenderClone } = await timegroup.createRenderClone();
	const cloneMs = performance.now() - cloneStart;
	try {
		const seekStart = performance.now();
		await renderClone.seekForRender(timeMs);
		const seekMs = performance.now() - seekStart;
		const renderStart = performance.now();
		const result = await captureFromClone(renderClone, renderContainer, {
			scale,
			contentReadyMode,
			blockingTimeoutMs,
			originalTimegroup: timegroup,
			canvasMode
		});
		const renderMs = performance.now() - renderStart;
		if (typeof result === "object" && result !== null) result.__perfTiming = {
			cloneMs,
			seekMs,
			renderMs
		};
		return result;
	} finally {
		cleanupRenderClone();
	}
}
/**
* Generate thumbnails using an existing render clone and mutable queue.
* The queue can be modified while generation is in progress.
*
* @param renderClone - Pre-created render clone to use
* @param renderContainer - Container for the render clone
* @param queue - Mutable queue that provides timestamps
* @param options - Capture options (scale, contentReadyMode, etc.)
* @yields Objects with { timeMs, canvas } for each captured thumbnail
*
* @example
* ```ts
* const queue = new MutableTimestampQueue();
* queue.reset([0, 100, 200]);
*
* for await (const { timeMs, canvas } of generateThumbnailsFromClone(clone, container, queue)) {
*   cache.set(timeMs, canvas);
*   // Queue can be modified here while generator continues
* }
* ```
*/
async function* generateThumbnailsFromClone(renderClone, renderContainer, queue, options = {}) {
	const { scale = DEFAULT_CAPTURE_SCALE, contentReadyMode = "immediate", blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS, signal } = options;
	while (true) {
		if (signal?.aborted) break;
		const timeMs = queue.shift();
		if (timeMs === void 0) break;
		await renderClone.seekForRender(timeMs);
		if (signal?.aborted) break;
		yield {
			timeMs,
			canvas: await captureFromClone(renderClone, renderContainer, {
				scale,
				contentReadyMode,
				blockingTimeoutMs,
				timeMs
			})
		};
	}
}
/**
* Generate thumbnails for multiple timestamps efficiently using a single render clone.
* This avoids the overhead of creating/destroying a clone for each thumbnail.
*
* @param timegroup - The timegroup to capture
* @param timestamps - Array of timestamps to capture (in milliseconds)
* @param options - Capture options (scale, contentReadyMode, etc.)
* @param signal - Optional AbortSignal to cancel generation
* @yields Objects with { timeMs, canvas } for each captured thumbnail
*
* @example
* ```ts
* for await (const { timeMs, canvas } of generateThumbnails(tg, [0, 100, 200])) {
*   console.log(`Got thumbnail for ${timeMs}ms`);
*   thumbnailCache.set(timeMs, canvas);
* }
* ```
*/
async function* generateThumbnails(timegroup, timestamps, options = {}, signal) {
	const { scale = DEFAULT_CAPTURE_SCALE, contentReadyMode = "immediate", blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS } = options;
	const { clone: renderClone, container: renderContainer, cleanup: cleanupRenderClone } = await timegroup.createRenderClone();
	try {
		for (const timeMs of timestamps) {
			signal?.throwIfAborted();
			await renderClone.seekForRender(timeMs);
			yield {
				timeMs,
				canvas: await captureFromClone(renderClone, renderContainer, {
					scale,
					contentReadyMode,
					blockingTimeoutMs,
					originalTimegroup: timegroup
				})
			};
		}
	} finally {
		cleanupRenderClone();
	}
}
/** Epsilon for comparing time values (ms) - times within this are considered equal */
const TIME_EPSILON_MS = 1;
/** Default scale for preview rendering */
const DEFAULT_PREVIEW_SCALE = 1;
/** Default resolution scale (full resolution) */
const DEFAULT_RESOLUTION_SCALE = 1;
/**
* Convert relative time to absolute time for a timegroup.
* Nested timegroup children have ABSOLUTE startTimeMs values,
* so relative capture times must be converted for temporal culling.
*/
function toAbsoluteTime(timegroup, relativeTimeMs) {
	return relativeTimeMs + (timegroup.startTimeMs ?? 0);
}
/**
* Renders a timegroup preview to a canvas using SVG foreignObject.
*
* Captures the prime timeline's current visual state including DOM changes
* from frame tasks (SVG paths, canvas content, text updates, etc.).
*
* Optimized with:
* - Passive clone structure rebuilt each frame from prime's current state
* - Temporal bucketing for time-based culling
* - RenderContext for canvas pixel caching across frames
* - Resolution scaling for performance (renders at lower resolution, CSS upscales)
*
* @param timegroup - The source timegroup to preview (prime timeline)
* @param scaleOrOptions - Scale factor (default 1) or options object
* @returns Object with canvas and refresh function
*/
function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCALE) {
	const options = typeof scaleOrOptions === "number" ? { scale: scaleOrOptions } : scaleOrOptions;
	const scale = options.scale ?? DEFAULT_PREVIEW_SCALE;
	let currentResolutionScale = options.resolutionScale ?? DEFAULT_RESOLUTION_SCALE;
	const width = timegroup.offsetWidth || DEFAULT_WIDTH;
	const height = timegroup.offsetHeight || DEFAULT_HEIGHT;
	const dpr = (typeof window !== "undefined" ? window.devicePixelRatio : 1) || 1;
	let renderWidth = Math.floor(width * currentResolutionScale);
	let renderHeight = Math.floor(height * currentResolutionScale);
	const canvas = createDprCanvas({
		renderWidth,
		renderHeight,
		scale,
		fullWidth: width,
		fullHeight: height,
		dpr
	});
	const wrapperContainer = canvas;
	const ctx = canvas.getContext("2d");
	if (!ctx) throw new Error("Failed to get canvas 2d context");
	let rendering = false;
	let lastTimeMs = -1;
	let disposed = false;
	const renderContext = new RenderContext();
	const frameController = new FrameController(timegroup);
	let hasLoggedScale = false;
	let pendingResolutionScale = null;
	const useNative = getRenderMode() === "native" && isNativeCanvasApiAvailable();
	let captureCanvas = null;
	let captureCtx = null;
	let originalParent = null;
	let originalNextSibling = null;
	let savedClipPath = "";
	let savedPointerEvents = "";
	if (useNative) {
		captureCanvas = document.createElement("canvas");
		captureCanvas.setAttribute("layoutsubtree", "");
		captureCanvas.layoutSubtree = true;
		captureCanvas.width = renderWidth;
		captureCanvas.height = renderHeight;
		captureCanvas.style.cssText = `position:fixed;left:0;top:0;width:${width}px;height:${height}px;opacity:0;pointer-events:none;z-index:-9999;`;
		originalParent = timegroup.parentNode;
		originalNextSibling = timegroup.nextSibling;
		savedClipPath = timegroup.style.clipPath;
		savedPointerEvents = timegroup.style.pointerEvents;
		timegroup.style.clipPath = "";
		timegroup.style.pointerEvents = "";
		captureCanvas.appendChild(timegroup);
		document.body.appendChild(captureCanvas);
		captureCtx = captureCanvas.getContext("2d");
		captureCanvas.offsetHeight;
		timegroup.offsetHeight;
	}
	/**
	* Apply pending resolution scale changes.
	* Called at the start of refresh() before rendering, so the old content
	* stays visible until new content is ready to be drawn.
	*/
	const applyPendingResolutionChange = () => {
		if (pendingResolutionScale === null) return;
		const newScale = pendingResolutionScale;
		pendingResolutionScale = null;
		currentResolutionScale = newScale;
		renderWidth = Math.floor(width * currentResolutionScale);
		renderHeight = Math.floor(height * currentResolutionScale);
		if (captureCanvas) {
			captureCanvas.width = renderWidth;
			captureCanvas.height = renderHeight;
		}
	};
	/**
	* Dynamically change resolution scale without rebuilding clone structure.
	* The actual change is deferred until next refresh() to avoid blanking -
	* old content stays visible until new content is ready.
	*/
	const setResolutionScale = (newScale) => {
		newScale = Math.max(.1, Math.min(1, newScale));
		if (newScale === currentResolutionScale && pendingResolutionScale === null) return;
		pendingResolutionScale = newScale;
		lastTimeMs = -1;
	};
	const getResolutionScale = () => pendingResolutionScale ?? currentResolutionScale;
	let frameCount = 0;
	let totalFrameControllerMs = 0;
	let totalCaptureMs = 0;
	let totalCopyMs = 0;
	let totalFrameMs = 0;
	const refresh = async () => {
		if (disposed) return;
		const sourceTimeMs = timegroup.currentTimeMs ?? 0;
		const userTimeMs = timegroup.userTimeMs ?? 0;
		if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;
		if (userTimeMs === lastTimeMs) return;
		if (rendering) return;
		lastTimeMs = userTimeMs;
		rendering = true;
		applyPendingResolutionChange();
		if (!hasLoggedScale) {
			hasLoggedScale = true;
			const mode = useNative ? "native" : "foreignObject";
			logger.debug(`[renderTimegroupToCanvas] Resolution scale: ${currentResolutionScale} (${width}x${height} → ${renderWidth}x${renderHeight}), canvas buffer: ${canvas.width}x${canvas.height}, CSS size: ${canvas.style.width}x${canvas.style.height}, renderMode: ${mode}`);
		}
		try {
			const tFrame = performance.now();
			const tFC0 = performance.now();
			await frameController.renderFrame(userTimeMs, {
				waitForLitUpdate: false,
				onAnimationsUpdate: (root) => {
					updateAnimations(root);
				}
			});
			const fcMs = performance.now() - tFC0;
			const tCapture0 = performance.now();
			if (useNative && captureCanvas && captureCtx) {
				if (captureCanvas.width !== width || captureCanvas.height !== height) {
					captureCtx.save();
					captureCtx.scale(captureCanvas.width / width, captureCanvas.height / height);
					captureCtx.drawElementImage(timegroup, 0, 0);
					captureCtx.restore();
				} else captureCtx.drawElementImage(timegroup, 0, 0);
				const captureMs = performance.now() - tCapture0;
				const tCopy0 = performance.now();
				const targetWidth = Math.floor(renderWidth * scale * dpr);
				const targetHeight = Math.floor(renderHeight * scale * dpr);
				if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
					canvas.width = targetWidth;
					canvas.height = targetHeight;
				} else ctx.clearRect(0, 0, canvas.width, canvas.height);
				ctx.drawImage(captureCanvas, 0, 0, canvas.width, canvas.height);
				const copyMs = performance.now() - tCopy0;
				const frameMs = performance.now() - tFrame;
				frameCount++;
				totalFrameControllerMs += fcMs;
				totalCaptureMs += captureMs;
				totalCopyMs += copyMs;
				totalFrameMs += frameMs;
				defaultProfiler.incrementRenderCount();
				if (defaultProfiler.shouldLogByFrameCount(60)) {
					frameCount = 0;
					totalFrameControllerMs = 0;
					totalCaptureMs = 0;
					totalCopyMs = 0;
					totalFrameMs = 0;
				}
			} else {
				const absoluteTimeMs = toAbsoluteTime(timegroup, userTimeMs);
				const dataUri = await captureTimelineToDataUri(timegroup, width, height, {
					renderContext,
					canvasScale: currentResolutionScale,
					timeMs: absoluteTimeMs
				});
				const captureMs = performance.now() - tCapture0;
				const tCopy0 = performance.now();
				const image = await loadImageFromDataUri(dataUri);
				const copyMs = performance.now() - tCopy0;
				const targetWidth = Math.floor(renderWidth * scale * dpr);
				const targetHeight = Math.floor(renderHeight * scale * dpr);
				if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
					canvas.width = targetWidth;
					canvas.height = targetHeight;
				} else ctx.clearRect(0, 0, canvas.width, canvas.height);
				ctx.save();
				ctx.scale(dpr * scale, dpr * scale);
				ctx.drawImage(image, 0, 0, renderWidth, renderHeight);
				ctx.restore();
				const frameMs = performance.now() - tFrame;
				frameCount++;
				totalFrameControllerMs += fcMs;
				totalCaptureMs += captureMs;
				totalCopyMs += copyMs;
				totalFrameMs += frameMs;
				defaultProfiler.incrementRenderCount();
				if (defaultProfiler.shouldLogByFrameCount(60)) {
					frameCount = 0;
					totalFrameControllerMs = 0;
					totalCaptureMs = 0;
					totalCopyMs = 0;
					totalFrameMs = 0;
				}
			}
		} catch (e) {
			logger.error("Canvas preview render failed:", e);
		} finally {
			rendering = false;
		}
	};
	/**
	* Dispose the preview and release resources.
	*/
	const dispose = () => {
		if (disposed) return;
		disposed = true;
		frameController.abort();
		renderContext.dispose();
		if (useNative && originalParent) {
			timegroup.style.clipPath = savedClipPath;
			timegroup.style.pointerEvents = savedPointerEvents;
			if (originalNextSibling) originalParent.insertBefore(timegroup, originalNextSibling);
			else originalParent.appendChild(timegroup);
			captureCanvas?.remove();
		}
	};
	refresh();
	return {
		container: wrapperContainer,
		canvas,
		refresh,
		setResolutionScale,
		getResolutionScale,
		dispose
	};
}

//#endregion
export { ContentNotReadyError, captureFromClone, captureTimegroupAtTime, clearInlineImageCache, generateThumbnails, generateThumbnailsFromClone, getCacheMetrics, getEffectiveRenderMode, getInlineImageCacheSize, getRenderState, isCanvas, isImage, loadImageFromDataUri, renderTimegroupToCanvas, resetCacheMetrics, resetRenderState, waitForVideoContent };
//# sourceMappingURL=renderTimegroupToCanvas.js.map