import html2canvas from "html2canvas-pro";
import { getComponentById } from "../../componentTreeHelper";
import { findComponentRect, getAbsolutePosition } from "../../utils";
import { buildPageViewerNotOpenFailure, validateBrowserBoundCaptureRequest, } from "./browserBoundCapture";
const MAX_TOOL_RESULT_IMAGE_DIMENSION = 1280;
const PNG_SIZE_FALLBACK_THRESHOLD = 2_500_000;
const JPEG_QUALITY = 0.9;
function normalizeScope(scope) {
    switch (scope) {
        case "editor-ui":
        case "selection":
        case "component":
        case "component-range":
        case "page":
            return scope;
        case "viewport":
        default:
            return "viewport";
    }
}
function getPageDimensions(doc) {
    const body = doc.body;
    const root = doc.documentElement;
    return {
        width: Math.max(root?.scrollWidth ?? 0, root?.clientWidth ?? 0, body?.scrollWidth ?? 0, body?.clientWidth ?? 0),
        height: Math.max(root?.scrollHeight ?? 0, root?.clientHeight ?? 0, body?.scrollHeight ?? 0, body?.clientHeight ?? 0),
    };
}
function getWindowViewportBounds(targetWindow, zoom = 1, viewportWidth = targetWindow.innerWidth, viewportHeight = targetWindow.innerHeight) {
    const safeZoom = zoom > 0 ? zoom : 1;
    return {
        x: Math.floor(targetWindow.scrollX ?? 0),
        y: Math.floor(targetWindow.scrollY ?? 0),
        width: Math.max(1, Math.ceil(viewportWidth / safeZoom)),
        height: Math.max(1, Math.ceil(viewportHeight / safeZoom)),
    };
}
function clampBounds(bounds, pageSize, padding = 0) {
    const x = Math.max(0, Math.floor(bounds.x - padding));
    const y = Math.max(0, Math.floor(bounds.y - padding));
    const right = Math.min(pageSize.width, Math.ceil(bounds.x + bounds.width + padding));
    const bottom = Math.min(pageSize.height, Math.ceil(bounds.y + bounds.height + padding));
    return {
        x,
        y,
        width: Math.max(1, right - x),
        height: Math.max(1, bottom - y),
    };
}
function unionBounds(boundsList) {
    const minX = Math.min(...boundsList.map((bounds) => bounds.x));
    const minY = Math.min(...boundsList.map((bounds) => bounds.y));
    const maxRight = Math.max(...boundsList.map((bounds) => bounds.x + bounds.width));
    const maxBottom = Math.max(...boundsList.map((bounds) => bounds.y + bounds.height));
    return {
        x: minX,
        y: minY,
        width: maxRight - minX,
        height: maxBottom - minY,
    };
}
function normalizeComponentIdentifier(value) {
    return value?.trim().replace(/[{}]/g, "").toLowerCase() ?? "";
}
function matchesComponentIdentifier(candidate, target) {
    const normalizedCandidate = normalizeComponentIdentifier(candidate);
    const normalizedTarget = normalizeComponentIdentifier(target);
    return !!normalizedCandidate && normalizedCandidate === normalizedTarget;
}
function findExactComponentById(page, componentId) {
    const targetId = normalizeComponentIdentifier(componentId);
    if (!targetId) {
        return null;
    }
    const queue = [...(page.rootComponent.placeholders ?? [])];
    while (queue.length > 0) {
        const placeholder = queue.shift();
        if (!placeholder) {
            continue;
        }
        for (const component of placeholder.components) {
            if (matchesComponentIdentifier(component.id, targetId)) {
                return component;
            }
            if (component.placeholders?.length) {
                queue.push(...component.placeholders);
            }
        }
    }
    return null;
}
function getElementBounds(iframe, element) {
    const rect = getAbsolutePosition(element, iframe);
    if (rect.width <= 0 || rect.height <= 0) {
        return null;
    }
    return {
        x: Math.floor(rect.x),
        y: Math.floor(rect.y),
        width: Math.ceil(rect.width),
        height: Math.ceil(rect.height),
    };
}
function getComponentBoundsFromLiveDom(iframe, componentId) {
    const doc = iframe.contentDocument;
    if (!doc?.body) {
        return null;
    }
    const matchingElements = Array.from(doc.querySelectorAll("[data-component-id]")).filter((element) => {
        return (element instanceof HTMLElement &&
            matchesComponentIdentifier(element.getAttribute("data-component-id"), componentId));
    });
    const bounds = matchingElements
        .map((element) => getElementBounds(iframe, element))
        .filter((item) => !!item && item.width > 0 && item.height > 0);
    if (bounds.length === 0) {
        return null;
    }
    return unionBounds(bounds);
}
function getComponentBounds(iframe, page, componentId) {
    const exactComponent = findExactComponentById(page, componentId);
    const component = exactComponent ?? getComponentById(componentId, page);
    const candidateIds = [
        componentId,
        component?.id,
        component?.datasourceItem?.id,
    ].filter((value, index, values) => {
        return (!!value &&
            values.findIndex((other) => matchesComponentIdentifier(other, value)) ===
                index);
    });
    if (component) {
        const componentRect = findComponentRect(iframe, component, true)?.rect;
        if (componentRect) {
            return {
                x: Math.floor(componentRect.x),
                y: Math.floor(componentRect.y),
                width: Math.ceil(componentRect.width),
                height: Math.ceil(componentRect.height),
            };
        }
    }
    for (const candidateId of candidateIds) {
        const liveDomBounds = getComponentBoundsFromLiveDom(iframe, candidateId);
        if (liveDomBounds) {
            return liveDomBounds;
        }
    }
    return null;
}
function getViewportBounds(iframe, zoom) {
    const iframeWindow = iframe.contentWindow;
    return getWindowViewportBounds(iframeWindow ?? window, zoom, iframe.clientWidth, iframe.clientHeight);
}
function resolveCaptureArea(iframe, page, selection, zoom, params, pageSize) {
    const scope = normalizeScope(params.scope);
    const padding = Math.max(0, params.padding ?? 0);
    if (scope === "page") {
        return {
            scope,
            bounds: clampBounds({
                x: 0,
                y: 0,
                width: pageSize.width,
                height: pageSize.height,
            }, pageSize, padding),
        };
    }
    if (!page) {
        if (scope === "viewport") {
            return {
                scope,
                bounds: clampBounds(getViewportBounds(iframe, zoom), pageSize, padding),
            };
        }
        throw new Error("No page model is loaded, so component-based capture is unavailable.");
    }
    const fromComponentId = params.fromComponentId?.trim();
    const toComponentId = params.toComponentId?.trim();
    if (scope === "selection") {
        const bounds = selection
            .map((componentId) => getComponentBounds(iframe, page, componentId))
            .filter((item) => !!item && item.width > 0 && item.height > 0);
        if (!bounds.length) {
            throw new Error("There are no selected components to capture.");
        }
        return {
            scope,
            bounds: clampBounds(unionBounds(bounds), pageSize, padding),
        };
    }
    if (fromComponentId && toComponentId) {
        const bounds = [fromComponentId, toComponentId]
            .map((componentId) => getComponentBounds(iframe, page, componentId))
            .filter((item) => !!item && item.width > 0 && item.height > 0);
        if (bounds.length !== 2) {
            throw new Error("Could not resolve both component boundaries for the requested capture range.");
        }
        return {
            scope: "component-range",
            bounds: clampBounds(unionBounds(bounds), pageSize, padding),
        };
    }
    if (fromComponentId) {
        const bounds = getComponentBounds(iframe, page, fromComponentId);
        if (!bounds) {
            throw new Error(`Could not resolve a visible component for '${fromComponentId}'.`);
        }
        return {
            scope: "component",
            bounds: clampBounds(bounds, pageSize, padding),
        };
    }
    if (scope === "component" || scope === "component-range") {
        return {
            scope: "viewport",
            bounds: clampBounds(getViewportBounds(iframe, zoom), pageSize, padding),
        };
    }
    return {
        scope: "viewport",
        bounds: clampBounds(getViewportBounds(iframe, zoom), pageSize, padding),
    };
}
async function canvasToBlob(canvas, contentType, quality) {
    const blob = await new Promise((resolve, reject) => {
        canvas.toBlob((value) => {
            if (value) {
                resolve(value);
            }
            else {
                reject(new Error("Failed to convert screenshot canvas into an image."));
            }
        }, contentType, quality);
    });
    return blob;
}
async function blobToDataUrl(blob) {
    return await new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => {
            const value = reader.result;
            if (typeof value === "string" && value.length > 0) {
                resolve(value);
                return;
            }
            reject(new Error("Failed to encode screenshot image data."));
        };
        reader.onerror = () => {
            reject(reader.error ?? new Error("Failed to encode screenshot image data."));
        };
        reader.readAsDataURL(blob);
    });
}
function buildFileName(scope, extension = "png") {
    const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
    return `page-screenshot-${scope}-${timestamp}.${extension}`;
}
function scaleCanvasForToolResult(canvas) {
    const maxDimension = Math.max(canvas.width, canvas.height);
    if (maxDimension <= MAX_TOOL_RESULT_IMAGE_DIMENSION) {
        return canvas;
    }
    const scale = MAX_TOOL_RESULT_IMAGE_DIMENSION / maxDimension;
    const scaledCanvas = document.createElement("canvas");
    scaledCanvas.width = Math.max(1, Math.round(canvas.width * scale));
    scaledCanvas.height = Math.max(1, Math.round(canvas.height * scale));
    const context = scaledCanvas.getContext("2d");
    if (!context) {
        throw new Error("Failed to prepare the screenshot for the tool result.");
    }
    context.drawImage(canvas, 0, 0, scaledCanvas.width, scaledCanvas.height);
    return scaledCanvas;
}
function replaceFileExtension(fileName, extension) {
    return fileName.replace(/\.[^.]+$/, "") + `.${extension}`;
}
async function canvasToResultImage(canvas, scope) {
    const processedCanvas = scaleCanvasForToolResult(canvas);
    let contentType = "image/png";
    let fileName = buildFileName(scope, "png");
    let blob = await canvasToBlob(processedCanvas, contentType);
    if (blob.size > PNG_SIZE_FALLBACK_THRESHOLD) {
        contentType = "image/jpeg";
        fileName = replaceFileExtension(fileName, "jpg");
        blob = await canvasToBlob(processedCanvas, contentType, JPEG_QUALITY);
    }
    return {
        image: {
            dataUrl: await blobToDataUrl(blob),
            fileName,
            fileSize: blob.size,
            contentType,
        },
        width: processedCanvas.width,
        height: processedCanvas.height,
    };
}
function getActiveEditorIframe(editContext) {
    return editContext.getActiveSlotContext?.()?.primaryPageViewContext.editorIframe;
}
export async function capturePageScreenshot(request, params, editContext) {
    const normalizedScope = normalizeScope(params.scope);
    const captureEditorUi = normalizedScope === "editor-ui";
    if (!editContext) {
        return buildPageViewerNotOpenFailure({
            request,
            editContext,
            scope: normalizedScope,
        });
    }
    const validationFailure = validateBrowserBoundCaptureRequest({
        request,
        editContext,
        scope: normalizedScope,
    });
    if (validationFailure) {
        return validationFailure;
    }
    let targetElement;
    let pageSize;
    let captureArea;
    const activeSlotContext = editContext.getActiveSlotContext?.();
    if (captureEditorUi) {
        if (!document.documentElement || !document.body) {
            return {
                success: false,
                scope: normalizedScope,
                error: "The Parhelia UI has not finished loading yet.",
            };
        }
        targetElement = document.documentElement;
        pageSize = getPageDimensions(document);
        const padding = Math.max(0, params.padding ?? 0);
        captureArea = {
            scope: "editor-ui",
            bounds: clampBounds(getWindowViewportBounds(window), pageSize, padding),
        };
    }
    else {
        const iframe = getActiveEditorIframe(editContext);
        if (!iframe) {
            return buildPageViewerNotOpenFailure({
                request,
                editContext,
                scope: normalizedScope,
            });
        }
        let iframeDocument = null;
        let iframeWindow = null;
        try {
            iframeWindow = iframe.contentWindow;
            iframeDocument = iframe.contentDocument;
            void iframeWindow?.document?.body;
        }
        catch {
            return {
                success: false,
                scope: normalizedScope,
                error: "The current page iframe is not accessible from the editor, so screenshot capture is unavailable.",
            };
        }
        if (!iframeDocument?.documentElement || !iframeDocument.body || !iframeWindow) {
            return {
                success: false,
                scope: normalizedScope,
                error: "The current page has not finished loading inside the editor iframe.",
            };
        }
        targetElement = iframeDocument.documentElement;
        pageSize = getPageDimensions(iframeDocument);
        const zoom = activeSlotContext?.primaryPageViewContext.zoom ?? 1;
        try {
            captureArea = resolveCaptureArea(iframe, editContext?.page, editContext?.selection ?? [], zoom, params, pageSize);
        }
        catch (error) {
            return {
                success: false,
                scope: normalizedScope,
                error: error instanceof Error ? error.message : "Failed to resolve capture bounds.",
            };
        }
    }
    const rootStyle = targetElement.style;
    const previousTransform = rootStyle.transform;
    const previousTransformOrigin = rootStyle.transformOrigin;
    const previousOverflow = rootStyle.overflow;
    let canvas;
    try {
        rootStyle.transform = "none";
        rootStyle.transformOrigin = "";
        rootStyle.overflow = previousOverflow || "auto";
        await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));
        canvas = await html2canvas(targetElement, {
            backgroundColor: "#ffffff",
            logging: false,
            removeContainer: true,
            scale: Math.min(globalThis.devicePixelRatio || 1, 2),
            useCORS: true,
            windowWidth: pageSize.width,
            windowHeight: pageSize.height,
            scrollX: 0,
            scrollY: 0,
            x: captureArea.bounds.x,
            y: captureArea.bounds.y,
            width: captureArea.bounds.width,
            height: captureArea.bounds.height,
        });
    }
    catch (error) {
        return {
            success: false,
            scope: captureArea.scope,
            error: error instanceof Error
                ? error.message
                : "Failed to render the requested screenshot.",
        };
    }
    finally {
        rootStyle.transform = previousTransform;
        rootStyle.transformOrigin = previousTransformOrigin;
        rootStyle.overflow = previousOverflow;
    }
    try {
        const resultImage = await canvasToResultImage(canvas, captureArea.scope);
        return {
            success: true,
            scope: captureArea.scope,
            image: resultImage.image,
            bounds: captureArea.bounds,
            imageWidth: resultImage.width,
            imageHeight: resultImage.height,
        };
    }
    catch (error) {
        return {
            success: false,
            scope: captureArea.scope,
            error: error instanceof Error
                ? error.message
                : "Failed to encode the screenshot for the tool result.",
        };
    }
}
//# sourceMappingURL=capturePageScreenshot.js.map