import { encodeCanvasesInParallel } from "../encoding/canvasEncoder.js";
import { isTemporal, isVisibleAtTime } from "../previewTypes.js";
import { ScaleConfig } from "./ScaleConfig.js";

//#region src/preview/rendering/serializeTimelineDirect.ts
/**
* Collect document styles for shadow DOM injection.
*/
function collectDocumentStyles() {
	const rules = [];
	try {
		for (const sheet of document.styleSheets) try {
			if (sheet.cssRules) for (const rule of sheet.cssRules) rules.push(rule.cssText);
		} catch {}
	} catch (e) {
		console.warn("[collectDocumentStyles] Failed to access document.styleSheets:", e);
	}
	return rules.join("\n");
}
/**
* Elements to skip entirely when serializing.
* NOTE: SLOT is NOT skipped - it's handled specially to serialize light DOM children.
*/
const SKIP_TAGS = new Set([
	"EF-AUDIO",
	"EF-THUMBNAIL-STRIP",
	"EF-FILMSTRIP",
	"EF-TIMELINE",
	"EF-WORKBENCH",
	"SCRIPT",
	"STYLE",
	"TEMPLATE"
]);
/**
* HTML void elements - these cannot have children and must be self-closing in XHTML.
* Using `<br />` instead of `<br></br>`.
*/
const VOID_ELEMENTS = new Set([
	"area",
	"base",
	"br",
	"col",
	"embed",
	"hr",
	"img",
	"input",
	"link",
	"meta",
	"param",
	"source",
	"track",
	"wbr"
]);
/**
* CSS properties to serialize as inline styles.
*/
const SERIALIZED_STYLE_PROPERTIES = [
	"display",
	"visibility",
	"opacity",
	"position",
	"top",
	"right",
	"bottom",
	"left",
	"zIndex",
	"width",
	"height",
	"minWidth",
	"minHeight",
	"maxWidth",
	"maxHeight",
	"flexGrow",
	"flexShrink",
	"flexBasis",
	"flexDirection",
	"flexWrap",
	"justifyContent",
	"alignItems",
	"alignContent",
	"alignSelf",
	"gap",
	"gridTemplate",
	"gridColumn",
	"gridRow",
	"gridArea",
	"margin",
	"padding",
	"boxSizing",
	"border",
	"borderTop",
	"borderRight",
	"borderBottom",
	"borderLeft",
	"borderRadius",
	"background",
	"color",
	"boxShadow",
	"filter",
	"backdropFilter",
	"clipPath",
	"fontFamily",
	"fontSize",
	"fontWeight",
	"fontStyle",
	"fontVariant",
	"textAlign",
	"textDecoration",
	"textTransform",
	"letterSpacing",
	"wordSpacing",
	"whiteSpace",
	"textOverflow",
	"lineHeight",
	"verticalAlign",
	"transform",
	"transformOrigin",
	"transformStyle",
	"perspective",
	"perspectiveOrigin",
	"backfaceVisibility",
	"cursor",
	"pointerEvents",
	"userSelect",
	"overflow",
	"objectFit",
	"objectPosition"
];
/**
* Caption child elements that should preserve display:none.
* These use display:none for content visibility, not temporal visibility.
*/
const CAPTION_CHILD_TAGS = new Set([
	"EF-CAPTIONS-ACTIVE-WORD",
	"EF-CAPTIONS-BEFORE-ACTIVE-WORD",
	"EF-CAPTIONS-AFTER-ACTIVE-WORD",
	"EF-CAPTIONS-SEGMENT"
]);
/**
* Escape special XML characters.
*/
function escapeXML(str) {
	return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
}
/**
* Resolve the natural display value for an element that has display:none
* set as an inline style (e.g., from temporal visibility via updateAnimations).
*
* Temporarily removes the inline display override so getComputedStyle falls
* through to the element's stylesheet rules (including shadow DOM :host styles),
* reads the natural value, then restores the override.
*/
function resolveNaturalDisplay(element) {
	const htmlEl = element;
	if (htmlEl.style?.getPropertyValue("display") === "none" && htmlEl.style) {
		htmlEl.style.removeProperty("display");
		const natural = getComputedStyle(element).getPropertyValue("display");
		htmlEl.style.setProperty("display", "none");
		return natural || "block";
	}
	return "block";
}
/**
* Serialize computed styles as inline style string.
* Handles display:none recovery for non-caption elements by resolving
* the element's natural display value from its stylesheet rules.
* @param element - The element to serialize styles for
* @param styles - Optional pre-computed CSSStyleDeclaration (avoids redundant getComputedStyle calls)
*/
function serializeComputedStyles(element, styles) {
	const computed = styles ?? getComputedStyle(element);
	const styleParts = [];
	const tagName = element.tagName;
	const isCaptionChild = CAPTION_CHILD_TAGS.has(tagName);
	const htmlEl = element;
	const hasExplicitWidth = !!htmlEl.style?.getPropertyValue("width");
	const hasExplicitHeight = !!htmlEl.style?.getPropertyValue("height");
	for (const prop of SERIALIZED_STYLE_PROPERTIES) {
		const kebab = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
		const value = computed.getPropertyValue(kebab);
		if (!value || value === "") continue;
		let finalValue = value;
		if (prop === "display") {
			if (value === "none" && !isCaptionChild) finalValue = resolveNaturalDisplay(element);
		}
		if (prop === "visibility") finalValue = "visible";
		if (prop === "clipPath") continue;
		if (prop === "width" && !hasExplicitWidth) continue;
		if (prop === "height" && !hasExplicitHeight) continue;
		styleParts.push(`${kebab}:${finalValue}`);
	}
	styleParts.push("animation:none", "transition:none");
	return styleParts.join(";");
}
/**
* Serialize element attributes (excluding style, id, xmlns, event handlers).
*/
function serializeAttributes(element, parts) {
	for (const attr of element.attributes) {
		const name = attr.name.toLowerCase();
		if (name === "id" || name === "style" || name === "xmlns" || name.startsWith("on")) continue;
		parts.push(` ${attr.name}="${escapeXML(attr.value)}"`);
	}
}
/**
* Check if a canvas element should preserve alpha channel.
* EF-WAVEFORM always needs alpha, EF-IMAGE checks hasAlpha property.
* EF-SURFACE needs alpha because:
*   1. Without a target, its canvas is transparent and CSS background should show through
*   2. The target element may have transparent content
* Raw canvas elements must preserve alpha - we don't know what they contain.
*/
function shouldPreserveAlpha(sourceElement) {
	const tagName = sourceElement.tagName;
	if (tagName === "EF-WAVEFORM") return true;
	if (tagName === "EF-SURFACE") return true;
	if (tagName === "EF-IMAGE") return "hasAlpha" in sourceElement && sourceElement.hasAlpha === true;
	if (sourceElement instanceof HTMLCanvasElement) return true;
	return false;
}
/**
* Find the capture proxy canvas for an offscreen-rendered canvas.
* When a canvas is transferred to offscreen via transferControlToOffscreen(),
* the main thread can no longer read pixels from it. OffscreenCompositionCanvas
* creates a hidden capture canvas (marked with data-offscreen-capture) that
* receives ImageBitmap frames from the worker.
*/
function findCaptureProxy(canvas) {
	const container = canvas.parentElement;
	if (!container) return null;
	return container.querySelector("canvas[data-offscreen-capture=\"true\"]");
}
/**
* Read pixels directly from a WebGL canvas's drawing buffer via gl.readPixels().
*
* drawImage(webglCanvas) reads from the compositor's "presented" surface, which
* is only refreshed during requestAnimationFrame / compositing cycles. In hidden
* browser tabs, compositing is suspended, so drawImage returns stale pixels even
* though gl.render() produced new content in the drawing buffer.
*
* readPixels() reads from the drawing buffer directly, bypassing the compositor.
*
* Returns null for non-WebGL canvases (getContext returns null when a different
* context type is already active).
*/
function readWebGLPixels(canvas) {
	const gl = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
	if (!gl) return null;
	const width = canvas.width;
	const height = canvas.height;
	if (width === 0 || height === 0) return null;
	gl.bindFramebuffer(gl.FRAMEBUFFER, null);
	const pixels = new Uint8Array(width * height * 4);
	gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
	const rowSize = width * 4;
	const halfHeight = Math.floor(height / 2);
	const temp = new Uint8Array(rowSize);
	for (let y = 0; y < halfHeight; y++) {
		const topOffset = y * rowSize;
		const bottomOffset = (height - 1 - y) * rowSize;
		temp.set(pixels.subarray(topOffset, topOffset + rowSize));
		pixels.set(pixels.subarray(bottomOffset, bottomOffset + rowSize), topOffset);
		pixels.set(temp, bottomOffset);
	}
	return new Uint8ClampedArray(pixels.buffer);
}
/**
* Create a snapshot copy of a canvas's current pixels.
* This captures the pixels synchronously before any async encoding,
* preventing race conditions where the source canvas is modified.
*
* For WebGL canvases, uses gl.readPixels() to bypass the compositor's
* presentation layer (which is suspended in hidden browser tabs).
*
* For offscreen-rendered canvases, this automatically uses the capture proxy
* canvas instead of the transferred display canvas.
*/
function snapshotCanvas(canvas, scale, preserveAlpha) {
	const sourceCanvas = findCaptureProxy(canvas) ?? canvas;
	const targetWidth = Math.max(1, Math.floor(sourceCanvas.width * scale));
	const targetHeight = Math.max(1, Math.floor(sourceCanvas.height * scale));
	const copy = document.createElement("canvas");
	copy.width = targetWidth;
	copy.height = targetHeight;
	if (preserveAlpha) copy.dataset.preserveAlpha = "true";
	const ctx = copy.getContext("2d");
	if (ctx && sourceCanvas.width > 0 && sourceCanvas.height > 0) {
		const glPixels = document.hidden ? readWebGLPixels(sourceCanvas) : null;
		if (glPixels) {
			const srcW = sourceCanvas.width;
			const srcH = sourceCanvas.height;
			const imageData = new ImageData(glPixels, srcW, srcH);
			if (targetWidth === srcW && targetHeight === srcH) ctx.putImageData(imageData, 0, 0);
			else {
				const temp = document.createElement("canvas");
				temp.width = srcW;
				temp.height = srcH;
				temp.getContext("2d").putImageData(imageData, 0, 0);
				ctx.drawImage(temp, 0, 0, targetWidth, targetHeight);
			}
		} else ctx.drawImage(sourceCanvas, 0, 0, targetWidth, targetHeight);
	}
	return copy;
}
/**
* Serialize a canvas element as an <img> with base64 data URL.
* Creates a snapshot of current pixels before async encoding to prevent race conditions.
*
* OPTIMIZATION: Calculate optimal encoding resolution based on:
* 1. CSS display size (how big it actually appears)
* 2. Video export scale (output resolution multiplier)
* 3. Quality multiplier (for sharpness, default 1.5x)
*/
function serializeCanvas(sourceElement, canvas, parts, canvasJobs, options) {
	const sourceCanvas = findCaptureProxy(canvas) ?? canvas;
	const width = sourceCanvas.width;
	const height = sourceCanvas.height;
	if (width === 0 || height === 0) return;
	const computedStyle = getComputedStyle(sourceElement);
	const styleStr = serializeComputedStyles(sourceElement, computedStyle);
	const computedWidth = computedStyle.width;
	const computedHeight = computedStyle.height;
	const filteredParts = (styleStr ? styleStr.split(";").filter((s) => s.trim()) : []).filter((s) => {
		const trimmed = s.trim();
		return !trimmed.startsWith("width:") && !trimmed.startsWith("height:");
	});
	const displayWidth = computedWidth || `${width}px`;
	const displayHeight = computedHeight || `${height}px`;
	filteredParts.push(`width:${displayWidth}`);
	filteredParts.push(`height:${displayHeight}`);
	filteredParts.push(`display:block`);
	const finalStyle = filteredParts.join(";");
	const preserveAlpha = shouldPreserveAlpha(sourceElement);
	let optimalScale = options.scaleConfig.exportScale;
	try {
		const cssWidth = parseFloat(computedWidth) || sourceCanvas.width;
		const cssHeight = parseFloat(computedHeight) || sourceCanvas.height;
		optimalScale = options.scaleConfig.computeCanvasScale({
			naturalWidth: sourceCanvas.width,
			naturalHeight: sourceCanvas.height,
			displayWidth: cssWidth,
			displayHeight: cssHeight
		});
	} catch (e) {
		console.warn(`[serializeCanvas] Failed to get computed style for ${sourceElement.tagName}:`, e);
	}
	const snapshot = snapshotCanvas(canvas, optimalScale, preserveAlpha);
	parts.push(`<img style="${escapeXML(finalStyle)}" src="`);
	const promiseIndex = parts.length;
	options.sourceMap.set(snapshot, sourceElement);
	const encodePromise = encodeCanvasesInParallel([snapshot], {
		scale: 1,
		renderContext: options.renderContext,
		sourceMap: options.sourceMap
	}).then((results) => results[0]?.dataUrl || "");
	parts.push(encodePromise);
	canvasJobs.push({
		canvas: snapshot,
		sourceElement,
		promiseIndex
	});
	parts.push("\" />");
}
/**
* Serialize an image element as a canvas (for shadow DOM img elements).
*/
function serializeImageAsCanvas(sourceElement, img, parts, canvasJobs, options) {
	const canvas = document.createElement("canvas");
	canvas.width = img.naturalWidth;
	canvas.height = img.naturalHeight;
	const ctx = canvas.getContext("2d");
	if (ctx) try {
		ctx.drawImage(img, 0, 0);
	} catch (e) {
		return;
	}
	serializeCanvas(sourceElement, canvas, parts, canvasJobs, options);
}
/**
* Serialize slotted light DOM children of a host element.
*/
function serializeSlottedContent(slotHost, parts, canvasJobs, options, parentIsSVG) {
	for (const slottedChild of slotHost.childNodes) if (slottedChild.nodeType === Node.TEXT_NODE) {
		const text = slottedChild.textContent;
		if (text && text.length > 0) parts.push(escapeXML(text));
	} else if (slottedChild.nodeType === Node.ELEMENT_NODE) serializeElement(slottedChild, parts, canvasJobs, options, parentIsSVG, null);
}
/**
* Recursively serialize an element and its children to XML parts.
* @param slotHost - When serializing inside shadow DOM, the custom element whose light DOM children should be serialized for slots
*/
function serializeElement(element, parts, canvasJobs, options, parentIsSVG = false, slotHost = null) {
	if (SKIP_TAGS.has(element.tagName)) return;
	if (element.tagName === "SLOT" && slotHost) {
		serializeSlottedContent(slotHost, parts, canvasJobs, options, parentIsSVG);
		return;
	}
	if (!isVisibleAtTime(element, options.timeMs)) return;
	if (isTemporal(element) && element.style?.getPropertyValue("display") === "none") return;
	if (element.tagName.includes("-") && element.shadowRoot) {
		const shadowCanvas = element.shadowRoot.querySelector("canvas");
		if (shadowCanvas) {
			serializeCanvas(element, shadowCanvas, parts, canvasJobs, options);
			return;
		}
		const shadowImg = element.shadowRoot.querySelector("img");
		if (shadowImg?.complete && shadowImg.naturalWidth > 0) {
			serializeImageAsCanvas(element, shadowImg, parts, canvasJobs, options);
			return;
		}
		const computedStyle = getComputedStyle(element);
		let computedDisplay = computedStyle.display;
		if (computedDisplay === "none") computedDisplay = resolveNaturalDisplay(element);
		const containerTag = computedDisplay === "inline" || computedDisplay === "inline-block" || computedDisplay === "inline-flex" ? "span" : "div";
		let styleStr$1 = serializeComputedStyles(element, computedStyle);
		parts.push(`<${containerTag}`);
		for (const attr of element.attributes) {
			const name = attr.name.toLowerCase();
			if (name === "class" || name.startsWith("data-")) parts.push(` ${attr.name}="${escapeXML(attr.value)}"`);
		}
		if (styleStr$1) parts.push(` style="${escapeXML(styleStr$1)}"`);
		parts.push(">");
		for (const child of element.shadowRoot.childNodes) if (child.nodeType === Node.TEXT_NODE) {
			const text = child.textContent;
			if (text && text.length > 0) parts.push(escapeXML(text));
		} else if (child.nodeType === Node.ELEMENT_NODE) serializeElement(child, parts, canvasJobs, options, parentIsSVG, element);
		parts.push(`</${containerTag}>`);
		return;
	}
	if (element instanceof HTMLCanvasElement) {
		serializeCanvas(element, element, parts, canvasJobs, options);
		return;
	}
	const tagName = element.tagName.toLowerCase();
	const isSVG = element instanceof SVGElement;
	const isVoid = VOID_ELEMENTS.has(tagName);
	if (isSVG && !parentIsSVG) parts.push(`<${tagName} xmlns="http://www.w3.org/2000/svg"`);
	else parts.push(`<${tagName}`);
	serializeAttributes(element, parts);
	const styleStr = serializeComputedStyles(element);
	if (styleStr) parts.push(` style="${escapeXML(styleStr)}"`);
	if (isVoid) {
		parts.push(" />");
		return;
	}
	parts.push(">");
	const children = element.shadowRoot?.childNodes || element.childNodes;
	for (const child of children) if (child.nodeType === Node.TEXT_NODE) {
		const text = child.textContent;
		if (text && text.length > 0) parts.push(escapeXML(text));
	} else if (child.nodeType === Node.ELEMENT_NODE) serializeElement(child, parts, canvasJobs, options, isSVG, slotHost);
	parts.push(`</${tagName}>`);
}
/**
* TextEncoder instance for SVG-to-base64 encoding.
* encode() converts to UTF-8 bytes in a single native call, then we
* base64-encode the bytes. ~33% overhead vs ~200% for percent-encoding.
*/
const textEncoder = new TextEncoder();
/**
* Synchronous DOM capture phase. Walks the element tree, snapshots canvas
* pixels, and kicks off async encoding. Returns parts array containing
* string fragments and encoding promises.
*
* After this function returns, the source element's DOM is no longer
* referenced — the clone can safely be seeked to the next frame.
*
* SCALING ARCHITECTURE (unified via ScaleConfig):
*
* ScaleConfig centralizes all scaling logic and provides:
* 1. Output SVG dimensions (width * exportScale, height * exportScale)
* 2. DOM scaling wrapper (CSS transform:scale when exportScale < 1)
* 3. Per-canvas optimal encoding scale via computeCanvasScale()
*
* Canvas scaling is independent from DOM scaling because:
* - Canvas elements have intrinsic pixel dimensions and can be downsampled
*   efficiently before encoding (prevents encoding 1920px at full resolution
*   when displayed at 420px)
* - DOM content has no intrinsic resolution and must be scaled via CSS
*   transforms, which the browser handles during SVG foreignObject rendering
*
* Example: 1920x1080 @ 0.5 export scale
* - Output SVG: 960x540
* - DOM wrapper: transform:scale(0.5) on 1920x1080 content
* - Canvas (1920px displayed at 420px): encoded at ~0.16x (315px)
*   via computeCanvasScale(420/1920 * 0.5 * 1.5 quality = 0.164)
*/
function captureElementParts(element, width, height, options) {
	const parts = [];
	const canvasJobs = [];
	const sourceMap = /* @__PURE__ */ new WeakMap();
	const scaleConfig = ScaleConfig.fromOptions(width, height, options.canvasScale);
	const documentStyles = options.renderContext?.getCachedDocumentStyles() ?? collectDocumentStyles();
	if (options.renderContext && documentStyles) options.renderContext.setCachedDocumentStyles(documentStyles);
	parts.push(`<div xmlns="http://www.w3.org/1999/xhtml" style="width:${scaleConfig.outputWidth}px;height:${scaleConfig.outputHeight}px;overflow:hidden;position:relative;">`);
	if (documentStyles) parts.push(`<style type="text/css"><![CDATA[${documentStyles}]]></style>`);
	const domTransform = scaleConfig.getDOMTransform();
	if (domTransform) {
		const wrapperDims = scaleConfig.getDOMWrapperDimensions();
		parts.push(`<div style="transform:${domTransform};transform-origin:0 0;width:${wrapperDims.width}px;height:${wrapperDims.height}px;">`);
	}
	serializeElement(element, parts, canvasJobs, {
		renderContext: options.renderContext,
		timeMs: options.timeMs,
		scaleConfig,
		sourceMap
	});
	if (domTransform) parts.push("</div>");
	parts.push("</div>");
	return parts;
}
/**
* Synchronous capture with deferred data URI encoding.
*
* Walks the DOM and snapshots canvas pixels synchronously, then returns
* a promise that resolves to the SVG data URI once async canvas-to-base64
* encoding completes. The source element is NOT referenced after this
* function returns — the caller can immediately mutate/seek the clone.
*/
function captureTimelineToDataUri(element, width, height, options) {
	const scaleConfig = ScaleConfig.fromOptions(width, height, options.canvasScale);
	const parts = captureElementParts(element, width, height, options);
	return Promise.all(parts).then((resolvedParts) => {
		const xhtml = resolvedParts.join("");
		const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${scaleConfig.outputWidth}" height="${scaleConfig.outputHeight}"><foreignObject x="0" y="0" width="${scaleConfig.outputWidth}" height="${scaleConfig.outputHeight}">${xhtml}</foreignObject></svg>`;
		const bytes = textEncoder.encode(svg);
		let binary = "";
		for (let i = 0; i < bytes.length; i += 8192) binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192));
		return `data:image/svg+xml;base64,${btoa(binary)}`;
	});
}

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