import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import morphdom from "morphdom";
import { useCanvasSelection, useEditContext } from "../client/editContext";
import { PlaceholderDropZones } from "../page-editor-chrome/PlaceholderDropZones";
import { MinimapDropTargets } from "./MinimapDropTargets";
import { FieldEditedIndicators } from "../page-editor-chrome/FieldEditedIndicators";
import { CommentHighlightings } from "../page-editor-chrome/CommentHighlightings";
import { SuggestionHighlightings } from "../page-editor-chrome/SuggestionHighlightings";
import { VersionDiffHighlightings } from "../page-editor-chrome/VersionDiffHighlightings";
import { IframeOverlayProvider } from "../page-editor-chrome/overlay/IframeOverlayProvider";
import { getAllComponentInstances } from "../componentTreeHelper";
export const BRIDGE_DOM_UPDATED_EVENT = "parhelia:bridge-dom-updated";
const MINI_MAP_FRAME_SLOTS = ["primary", "secondary"];
export function getMiniMapFrameRenderOrder() {
    // Keep both iframe nodes in a fixed DOM position. Moving an iframe while
    // promoting the loaded buffer reloads its browsing context in the browser,
    // which makes the minimap flash blank on every DOM snapshot (most visibly
    // while typing in an inline field). Visibility and z-index select the active
    // frame without reordering either node.
    return [...MINI_MAP_FRAME_SLOTS];
}
export function patchMiniMapFrameDocument(iframe, html) {
    try {
        const currentRoot = iframe.contentDocument?.documentElement;
        if (!currentRoot)
            return false;
        const nextDocument = new DOMParser().parseFromString(html, "text/html");
        const nextRoot = nextDocument.documentElement;
        if (!nextRoot)
            return false;
        morphdom(currentRoot, nextRoot, {
            getNodeKey: (node) => {
                if (node.nodeType !== 1)
                    return undefined;
                const element = node;
                return (element.getAttribute("data-parhelia-element-key") ||
                    element.id ||
                    undefined);
            },
            // Most bridge snapshots differ by only one text node. Avoid walking
            // identical subtrees (especially the large inlined stylesheet).
            onBeforeElUpdated: (fromElement, toElement) => !fromElement.isEqualNode(toElement),
        });
        return true;
    }
    catch {
        // Cross-origin access or an iframe that has not finished loading falls
        // back to the existing double-buffered srcdoc navigation path.
        return false;
    }
}
export function MiniMap({ compareView, scroll, mainViewIframeRef, pageViewContext, }) {
    const editContext = useEditContext();
    const [scale, setScale] = useState(1);
    const minimapRef = useRef(null);
    const scaleContainerRef = useRef(null);
    const iframeRefs = useRef({
        primary: null,
        secondary: null,
    });
    const [minimapFrameState, setMinimapFrameState] = useState({
        activeSlot: "primary",
        pendingSlot: null,
        htmlBySlot: {
            primary: undefined,
            secondary: undefined,
        },
        srcDocBySlot: {
            primary: undefined,
            secondary: undefined,
        },
        loadedHtmlBySlot: {
            primary: undefined,
            secondary: undefined,
        },
    });
    const [minimapReady, setMinimapReady] = useState(false);
    const [bridgeDomRevision, setBridgeDomRevision] = useState(0);
    const minimapContainerRef = useRef(null);
    const viewport = pageViewContext.viewport;
    const bridgeDom = pageViewContext.bridgeDom;
    const bridgeMetrics = bridgeDom
        ? getBridgeMinimapMetrics(bridgeDom, viewport, undefined, pageViewContext.bridgeGeometry)
        : null;
    const isBridgeMinimap = !!bridgeDom && !!bridgeMetrics;
    const bridgeMinimapHtml = useMemo(() => bridgeDom
        ? createBridgeMinimapHtml(bridgeDom.html, pageViewContext.bridgeGeometry, bridgeDom.viewport.width || viewport.width)
        : undefined, [
        bridgeDom,
        bridgeDomRevision,
        pageViewContext.bridgeGeometry,
        viewport.width,
    ]);
    const activeMinimapIframe = iframeRefs.current[minimapFrameState.activeSlot];
    const activeMinimapHtml = minimapFrameState.htmlBySlot[minimapFrameState.activeSlot];
    const activeMinimapFrameLoaded = !!activeMinimapHtml &&
        minimapFrameState.loadedHtmlBySlot[minimapFrameState.activeSlot] ===
            activeMinimapHtml;
    const minimapContentReady = minimapReady && activeMinimapFrameLoaded;
    const frameRenderOrder = getMiniMapFrameRenderOrder();
    const getDocumentMetrics = useCallback(() => {
        return getBridgeMinimapMetrics(pageViewContext.bridgeDom, viewport, scroll, pageViewContext.bridgeGeometry);
    }, [
        bridgeDomRevision,
        pageViewContext.bridgeDom,
        pageViewContext.bridgeGeometry,
        scroll,
        viewport,
    ]);
    const minimapMetrics = getDocumentMetrics();
    const calcAndUpdateScale = () => {
        const minimapHeight = minimapContainerRef.current?.clientHeight;
        const metrics = getDocumentMetrics();
        if (!metrics)
            return;
        const scaleX = (editContext?.configuration.outline.width || 100) / metrics.viewportWidth;
        const scaleY = (minimapHeight || metrics.viewportHeight) / metrics.documentHeight;
        const newScale = Math.min(scaleX, scaleY, 1);
        updateScale(newScale);
        setScale(newScale);
        return newScale;
    };
    // Prevent scale/width feedback loop by tracking previous values
    const prevScaleRef = useRef(scale);
    const prevWidthRef = useRef(0);
    const currentWidth = Math.max(30, scale *
        (minimapMetrics?.viewportWidth ||
            bridgeMetrics?.viewportWidth ||
            pageViewContext.viewport.width ||
            0));
    // Only update if change is significant (>1px) to avoid minor oscillations
    let miniMapWidth;
    if (prevWidthRef.current < currentWidth ||
        (Math.abs(currentWidth - prevWidthRef.current) > 1 &&
            Math.abs(scale - prevScaleRef.current) > 0.01)) {
        prevWidthRef.current = currentWidth;
        prevScaleRef.current = scale;
        miniMapWidth = currentWidth;
    }
    else {
        miniMapWidth = prevWidthRef.current;
    }
    useEffect(() => {
        setMinimapReady(isBridgeMinimap);
    }, [isBridgeMinimap]);
    useEffect(() => {
        const handleBridgeDomUpdated = () => {
            setBridgeDomRevision((revision) => revision + 1);
        };
        window.addEventListener(BRIDGE_DOM_UPDATED_EVENT, handleBridgeDomUpdated);
        return () => {
            window.removeEventListener(BRIDGE_DOM_UPDATED_EVENT, handleBridgeDomUpdated);
        };
    }, []);
    useEffect(() => {
        if (!bridgeMinimapHtml)
            return;
        const activeSlot = minimapFrameState.activeSlot;
        const activeHtml = minimapFrameState.htmlBySlot[activeSlot];
        const activeFrameLoaded = !!activeHtml &&
            minimapFrameState.loadedHtmlBySlot[activeSlot] === activeHtml;
        const activeFrame = iframeRefs.current[activeSlot];
        // Once the first srcdoc has loaded, keep its browsing context alive and
        // morph the inert snapshot in place. Assigning a new srcdoc navigates the
        // iframe and fires load even when only one text node changed, which made
        // the minimap reload on every keystroke.
        if (activeFrame &&
            activeFrameLoaded &&
            activeHtml !== bridgeMinimapHtml &&
            patchMiniMapFrameDocument(activeFrame, bridgeMinimapHtml)) {
            setMinimapFrameState((previous) => {
                if (previous.activeSlot !== activeSlot ||
                    previous.htmlBySlot[activeSlot] !== activeHtml ||
                    previous.loadedHtmlBySlot[activeSlot] !== activeHtml) {
                    return previous;
                }
                return {
                    ...previous,
                    pendingSlot: null,
                    htmlBySlot: {
                        ...previous.htmlBySlot,
                        [activeSlot]: bridgeMinimapHtml,
                    },
                    loadedHtmlBySlot: {
                        ...previous.loadedHtmlBySlot,
                        [activeSlot]: bridgeMinimapHtml,
                    },
                };
            });
            return;
        }
        setMinimapFrameState((previous) => {
            console.debug("[Parhelia] minimap ingest", JSON.stringify({
                len: bridgeMinimapHtml.length,
                active: previous.activeSlot,
                pending: previous.pendingSlot,
                activeLen: previous.htmlBySlot[previous.activeSlot]?.length,
                pendingLen: previous.pendingSlot
                    ? previous.htmlBySlot[previous.pendingSlot]?.length
                    : undefined,
                loadedActiveLen: previous.loadedHtmlBySlot[previous.activeSlot]?.length,
            }));
            const activeHtml = previous.htmlBySlot[previous.activeSlot];
            if (!activeHtml) {
                return {
                    ...previous,
                    htmlBySlot: {
                        ...previous.htmlBySlot,
                        [previous.activeSlot]: bridgeMinimapHtml,
                    },
                    srcDocBySlot: {
                        ...previous.srcDocBySlot,
                        [previous.activeSlot]: bridgeMinimapHtml,
                    },
                    pendingSlot: null,
                };
            }
            if (activeHtml === bridgeMinimapHtml)
                return previous;
            if (previous.pendingSlot &&
                previous.htmlBySlot[previous.pendingSlot] === bridgeMinimapHtml) {
                // The pending slot already carries this html. If its load event was
                // consumed while state still described an older html (load vs. React
                // commit race), nothing else will ever promote it — do it here.
                if (previous.loadedHtmlBySlot[previous.pendingSlot] === bridgeMinimapHtml) {
                    return {
                        ...previous,
                        activeSlot: previous.pendingSlot,
                        pendingSlot: null,
                    };
                }
                return previous;
            }
            const nextSlot = getInactiveMiniMapFrameSlot(previous.activeSlot);
            return {
                ...previous,
                htmlBySlot: {
                    ...previous.htmlBySlot,
                    [nextSlot]: bridgeMinimapHtml,
                },
                srcDocBySlot: {
                    ...previous.srcDocBySlot,
                    [nextSlot]: bridgeMinimapHtml,
                },
                pendingSlot: nextSlot,
            };
        });
    }, [
        bridgeMinimapHtml,
        minimapFrameState.activeSlot,
        minimapFrameState.htmlBySlot,
        minimapFrameState.loadedHtmlBySlot,
    ]);
    useEffect(() => {
        calcAndUpdateScale();
    }, [
        isBridgeMinimap,
        pageViewContext?.zoom,
        pageViewContext.viewport,
        pageViewContext.deviceHeight,
        pageViewContext.deviceWidth,
        pageViewContext.bridgeDom,
        pageViewContext.bridgeGeometry,
        bridgeDomRevision,
    ]);
    // Scroll-target math prefers the LIVE host document (same-origin) over the
    // bridge snapshot: the snapshot lags behind while edit operations and
    // refreshes are in flight, which used to send clicks to badly short targets.
    const getNavigationScrollMetrics = () => {
        const live = getLiveMainFrameMetrics(mainViewIframeRef.current);
        if (live) {
            return {
                maxScrollTop: Math.max(0, live.scrollHeight - live.viewportHeight),
                scrollTop: live.scrollTop,
            };
        }
        const metrics = getDocumentMetrics();
        if (!metrics)
            return null;
        return {
            maxScrollTop: Math.max(0, metrics.documentHeight - metrics.viewportHeight),
            scrollTop: pageViewContext.bridgeGeometry?.scroll.y ??
                scroll ??
                pageViewContext.bridgeDom?.scroll.y ??
                metrics.scrollTop,
        };
    };
    const scrollDocumentTo = (y, behavior = "smooth") => {
        const nav = getNavigationScrollMetrics();
        if (!nav)
            return;
        const targetY = Math.min(nav.maxScrollTop, Math.max(0, y));
        const accepted = pageViewContext.requestBridgeScrollBy?.({
            y: targetY - nav.scrollTop,
            behavior,
        });
        if (!accepted) {
            console.debug("[Parhelia] minimap scroll command not accepted by bridge", JSON.stringify({ targetY: Math.round(targetY) }));
        }
    };
    // Registered on pageViewContext so the drag orchestration can drive minimap
    // scrolling while the cursor hovers the minimap mid-drag (the pointer is
    // captured by the drag source, so the minimap's own pointer handlers never
    // fire during a drag). Given a PARENT-viewport point: if it is over the
    // minimap, scroll the canvas to that region (instant, so it tracks the cursor)
    // and return true; otherwise return false.
    // The last scroll position we COMMANDED via minimap hover. Deltas are computed
    // against this, not the live (laggy) scroll readout, so repeated commands for a
    // held cursor converge instead of oscillating. Reset to null on leave so the
    // next entry re-syncs to the real scroll.
    const lastMinimapScrollTargetRef = useRef(null);
    const minimapHoverRafRef = useRef(0);
    const pendingMinimapHoverRef = useRef(null);
    const flushMinimapHoverScroll = () => {
        minimapHoverRafRef.current = 0;
        const point = pendingMinimapHoverRef.current;
        if (!point)
            return;
        const containerRect = minimapContainerRef.current?.getBoundingClientRect();
        if (!containerRect)
            return;
        const nav = getNavigationScrollMetrics();
        if (!nav)
            return;
        const iframeRect = iframeRefs.current[minimapFrameState.activeSlot]?.getBoundingClientRect();
        const reference = iframeRect && iframeRect.height > 0 ? iframeRect : containerRect;
        const ratio = Math.min(1, Math.max(0, (point.y - reference.top) / reference.height));
        const target = Math.min(nav.maxScrollTop, Math.max(0, ratio * nav.maxScrollTop));
        const base = lastMinimapScrollTargetRef.current ?? nav.scrollTop;
        const delta = target - base;
        lastMinimapScrollTargetRef.current = target;
        if (Math.abs(delta) > 0.5) {
            pageViewContext.requestBridgeScrollBy?.({ y: delta, behavior: "auto" });
        }
    };
    const scrollViaMinimapPointRef = useRef(() => false);
    scrollViaMinimapPointRef.current = (clientX, clientY) => {
        const containerRect = minimapContainerRef.current?.getBoundingClientRect();
        if (!containerRect ||
            clientX < containerRect.left ||
            clientX > containerRect.right ||
            clientY < containerRect.top ||
            clientY > containerRect.bottom) {
            lastMinimapScrollTargetRef.current = null;
            pendingMinimapHoverRef.current = null;
            return false;
        }
        pendingMinimapHoverRef.current = { x: clientX, y: clientY };
        if (!minimapHoverRafRef.current) {
            minimapHoverRafRef.current = requestAnimationFrame(flushMinimapHoverScroll);
        }
        return true;
    };
    useEffect(() => {
        const handler = (x, y) => scrollViaMinimapPointRef.current(x, y);
        pageViewContext.scrollViaMinimapPoint = handler;
        return () => {
            if (pageViewContext.scrollViaMinimapPoint === handler) {
                pageViewContext.scrollViaMinimapPoint = undefined;
            }
            if (minimapHoverRafRef.current) {
                cancelAnimationFrame(minimapHoverRafRef.current);
                minimapHoverRafRef.current = 0;
            }
        };
    }, [pageViewContext]);
    const updateScale = (scale) => {
        if (!scaleContainerRef.current)
            return;
        const metrics = getDocumentMetrics();
        if (!metrics)
            return;
        scaleContainerRef.current.style.transform = `scale(${scale})`;
        scaleContainerRef.current.style.transformOrigin = "0 0";
        MINI_MAP_FRAME_SLOTS.forEach((slot) => {
            const iframe = iframeRefs.current[slot];
            if (!iframe)
                return;
            iframe.style.width = `${metrics?.viewportWidth ?? 0}px`;
            iframe.style.height = `${metrics?.documentHeight ?? 0}px`;
        });
    };
    const handleIframeLoad = useCallback((slot) => {
        const iframe = iframeRefs.current[slot];
        const loadedSrcDoc = iframe?.getAttribute("srcdoc") ?? undefined;
        setMinimapFrameState((previous) => {
            const loadedHtml = previous.htmlBySlot[slot];
            const expectedSrcDoc = previous.srcDocBySlot[slot];
            console.debug("[Parhelia] minimap load-event", JSON.stringify({
                slot,
                attrLen: loadedSrcDoc?.length,
                stateLen: expectedSrcDoc?.length,
                match: loadedSrcDoc === expectedSrcDoc,
                pending: previous.pendingSlot,
            }));
            // Load events race React commits (null ref / stale srcdoc attribute at
            // event time), so a mismatch is simply ignored here; the pending-slot
            // watchdog below re-runs this handler until ref, attribute and state
            // line up.
            if (!loadedHtml || loadedSrcDoc !== expectedSrcDoc)
                return previous;
            const loadedHtmlBySlot = {
                ...previous.loadedHtmlBySlot,
                [slot]: loadedHtml,
            };
            if (previous.pendingSlot === slot) {
                return {
                    ...previous,
                    activeSlot: slot,
                    loadedHtmlBySlot,
                    pendingSlot: null,
                };
            }
            return {
                ...previous,
                loadedHtmlBySlot,
            };
        });
    }, []);
    // Watchdog for slot loading: a load event consumed mid-commit (ignored
    // above) would otherwise never be recorded — a pending slot never promotes,
    // and an active slot whose first load was missed stays hidden
    // (minimapContentReady requires the active slot to be marked loaded). While
    // either slot is in that state, periodically re-run the load handler — it
    // is idempotent and settles as soon as ref, attribute and state line up.
    useEffect(() => {
        const { pendingSlot, activeSlot, htmlBySlot, loadedHtmlBySlot } = minimapFrameState;
        const activeUnloaded = !!htmlBySlot[activeSlot] &&
            loadedHtmlBySlot[activeSlot] !== htmlBySlot[activeSlot];
        const watchedSlot = pendingSlot ?? (activeUnloaded ? activeSlot : null);
        if (!watchedSlot)
            return;
        const timer = setInterval(() => handleIframeLoad(watchedSlot), 250);
        return () => clearInterval(timer);
    }, [minimapFrameState, handleIframeLoad]);
    const handleClick = (e) => {
        const nav = getNavigationScrollMetrics();
        if (!nav) {
            console.debug("[Parhelia] minimap click ignored: no scroll metrics");
            return;
        }
        // Check if click was on an actual control. Passive minimap overlays also
        // use pointer-events-auto so they can display hover affordances, but they
        // should not prevent normal minimap navigation.
        const target = e.target;
        const isInteractiveElement = target.closest('button, a, [role="button"], [data-testid="placeholder-dropzone-button"]');
        if (isInteractiveElement) {
            // Don't scroll if user clicked on an interactive element (like dropzone buttons, comment icons, etc.)
            console.debug("[Parhelia] minimap click ignored: interactive element", isInteractiveElement.outerHTML?.slice(0, 120));
            return;
        }
        const containerRect = minimapContainerRef.current?.getBoundingClientRect();
        if (!containerRect) {
            console.debug("[Parhelia] minimap click ignored: no container rect");
            return;
        }
        const iframeRect = iframeRefs.current[minimapFrameState.activeSlot]?.getBoundingClientRect();
        const clickReferenceRect = iframeRect && iframeRect.height > 0 ? iframeRect : containerRect;
        const absY = e.clientY - clickReferenceRect.top;
        const clickRatio = Math.min(1, Math.max(0, absY / clickReferenceRect.height));
        e.stopPropagation();
        console.debug("[Parhelia] minimap click -> scroll", JSON.stringify({
            clickRatio: Number(clickRatio.toFixed(3)),
            maxScrollTop: Math.round(nav.maxScrollTop),
            scrollTop: Math.round(nav.scrollTop),
        }));
        scrollDocumentTo(clickRatio * nav.maxScrollTop);
    };
    const handleScroll = useCallback((e) => {
        pageViewContext.requestBridgeScrollBy?.({
            x: 0,
            y: e.deltaY,
            behavior: "instant",
        });
        e.stopPropagation();
    }, [pageViewContext]);
    const viewportHighlight = minimapMetrics
        ? getMinimapViewportHighlight(minimapMetrics, scale)
        : null;
    const selectionBounds = useMemo(() => ({
        width: miniMapWidth,
        height: minimapMetrics ? minimapMetrics.documentHeight * scale : 0,
    }), [miniMapWidth, minimapMetrics?.documentHeight, scale]);
    if (!editContext || !isBridgeMinimap) {
        return null;
    }
    return (_jsxs("div", { "data-testid": "minimap-container", className: "relative shrink-0 cursor-pointer overflow-hidden bg-white", ref: minimapContainerRef, onClickCapture: handleClick, onWheelCapture: handleScroll, style: {
            height: "100%",
            width: miniMapWidth + "px",
            boxShadow: "5px 0 5px -5px var(--color-neutral-grey-100)",
        }, children: [_jsx("div", { className: "bg-opacity-50 absolute inset-0 overflow-hidden select-none", style: {
                    boxShadow: "var(--color-neutral-grey-15) -10px 1px 13px -10px",
                }, children: _jsx("div", { ref: minimapRef, onDragOverCapture: handleClick, className: "h-full w-full", children: _jsx("div", { ref: scaleContainerRef, style: { position: "relative" }, children: frameRenderOrder.map((slot) => {
                            const slotHtml = minimapFrameState.htmlBySlot[slot];
                            const slotSrcDoc = minimapFrameState.srcDocBySlot[slot];
                            if (!slotHtml || !slotSrcDoc)
                                return null;
                            const isActive = slot === minimapFrameState.activeSlot;
                            return (_jsx("iframe", { className: "pointer-events-none", "data-testid": isActive ? "minimap-active-frame" : "minimap-buffer-frame", "aria-hidden": !isActive || !minimapContentReady, ref: (element) => {
                                    iframeRefs.current[slot] = element;
                                }, onLoad: () => handleIframeLoad(slot), 
                                // allow-same-origin (without allow-scripts) so subresource
                                // loads (fonts via /parhelia/proxy/*) run same-origin with
                                // auth cookies; an opaque origin makes every font request
                                // CORS-checked and cookie-less, so they 302 to login.
                                sandbox: "allow-same-origin", srcDoc: slotSrcDoc, style: {
                                    backfaceVisibility: "hidden",
                                    border: 0,
                                    contain: "paint",
                                    display: "block",
                                    height: `${minimapMetrics?.documentHeight ?? 0}px`,
                                    inset: isActive ? undefined : 0,
                                    position: isActive ? "relative" : "absolute",
                                    transform: "translateZ(0)",
                                    visibility: isActive && minimapContentReady ? "visible" : "hidden",
                                    willChange: "transform",
                                    width: `${minimapMetrics?.viewportWidth ?? 0}px`,
                                    zIndex: isActive ? 1 : 0,
                                } }, slot));
                        }) }) }) }), _jsx("div", { "aria-hidden": "true", className: "absolute inset-0" }), viewportHighlight && minimapContentReady && (_jsx("div", { "data-testid": "minimap-viewport-highlight", className: "bg-neutral-grey-10 pointer-events-none absolute w-full opacity-60 hover:opacity-65", style: {
                    top: viewportHighlight.top + "px",
                    height: viewportHighlight.height + "px",
                } })), activeMinimapIframe && minimapContentReady && (_jsxs(IframeOverlayProvider, { iframe: activeMinimapIframe, mode: "static", scrollScale: 1, visualScale: scale, children: [editContext.mode === "edit" && (_jsxs(_Fragment, { children: [_jsx(PlaceholderDropZones, { size: "small", pageViewContext: pageViewContext, scopeInsertModeToHoveredComponent: false, avoidOverlaps: false }), _jsx(MinimapDropTargets, { pageViewContext: pageViewContext })] })), _jsx(FieldEditedIndicators, { pageViewContext: pageViewContext }), editContext.showComments && (_jsx(CommentHighlightings, { pageViewContext: pageViewContext, variant: "minimap" })), (editContext.showComments || editContext.showSuggestedEdits) && (_jsx(SuggestionHighlightings, { pageViewContext: pageViewContext, variant: "minimap" })), editContext.showVersionDiffHighlighting && !compareView && (_jsx(VersionDiffHighlightings, { pageViewContext: pageViewContext, variant: "minimap" }))] })), _jsx(MiniMapSelectionHighlights, { pageViewContext: pageViewContext, scale: scale, bounds: selectionBounds })] }));
}
/**
 * Reads the current scroll metrics straight out of the host document when the
 * iframe is same-origin. Returns null for cross-origin hosts (or before the
 * frame is ready); callers then fall back to the bridge snapshot, which can
 * lag while refreshes are in flight.
 */
export function getLiveMainFrameMetrics(iframe) {
    try {
        const win = iframe?.contentWindow;
        const doc = win?.document;
        const scrollingElement = doc?.scrollingElement;
        if (!win || !doc || !scrollingElement)
            return null;
        const scrollHeight = scrollingElement.scrollHeight || 0;
        const viewportHeight = win.innerHeight || doc.documentElement?.clientHeight || 0;
        if (!scrollHeight || !viewportHeight)
            return null;
        return {
            scrollHeight,
            viewportHeight,
            scrollTop: Math.max(win.scrollY || 0, scrollingElement.scrollTop || 0),
        };
    }
    catch {
        // Cross-origin hosts throw on contentWindow member access.
        return null;
    }
}
export function getBridgeMinimapMetrics(snapshot, fallbackViewport, scrollOverride, geometry) {
    if (!snapshot)
        return null;
    const scrollScale = getBridgeMinimapScrollScale(geometry);
    const viewportWidth = snapshot.viewport.width || fallbackViewport.width || 0;
    const viewportHeight = snapshot.viewport.height || fallbackViewport.height || 0;
    const scrollTop = Math.max(0, scrollOverride ?? snapshot.scroll?.y ?? 0);
    const documentHeight = Math.max(viewportHeight, scrollTop + viewportHeight, snapshot.scrollHeight || 0, getBridgeGeometryDocumentHeight(geometry) / scrollScale);
    if (!viewportWidth || !documentHeight)
        return null;
    return {
        viewportWidth,
        viewportHeight,
        documentHeight,
        scrollTop,
        scrollScale,
    };
}
export function getMinimapViewportHighlight(metrics, scale) {
    const scaledDocumentHeight = metrics.documentHeight * scale;
    const viewportHeight = Math.min(metrics.viewportHeight, metrics.documentHeight);
    const maxScrollTop = Math.max(0, metrics.documentHeight - viewportHeight);
    const top = Math.min(maxScrollTop, Math.max(0, metrics.scrollTop)) * scale;
    const height = Math.min(viewportHeight * scale, Math.max(0, scaledDocumentHeight - top));
    return { top, height };
}
const MINIMAP_SELECTION_BORDER_WIDTH = 2;
export function getMiniMapSelectionHighlightRect(rect, bounds) {
    const outset = MINIMAP_SELECTION_BORDER_WIDTH;
    const x = Math.max(0, rect.x - outset);
    const y = Math.max(0, rect.y - outset);
    const right = clampSelectionHighlightEdge(rect.x + rect.width + outset, bounds?.width);
    const bottom = clampSelectionHighlightEdge(rect.y + rect.height + outset, bounds?.height);
    return {
        x,
        y,
        width: Math.max(0, right - x),
        height: Math.max(0, bottom - y),
    };
}
function clampSelectionHighlightEdge(value, limit) {
    return typeof limit === "number" && Number.isFinite(limit) && limit > 0
        ? Math.min(value, limit)
        : value;
}
export function getMiniMapSelectionRects(selection, page, geometry, scale) {
    if (!selection?.length || !page || !geometry)
        return [];
    return getSelectedMiniMapComponents(selection, page)
        .map((component) => getMiniMapComponentRect(component, geometry, scale))
        .filter((rect) => !!rect);
}
function MiniMapSelectionHighlights({ pageViewContext, scale, bounds, }) {
    const canvasSelection = useCanvasSelection();
    const rawSelectionRects = useMemo(() => getMiniMapSelectionRects(canvasSelection, pageViewContext.page, pageViewContext.bridgeGeometry, scale), [
        canvasSelection,
        pageViewContext.page,
        pageViewContext.bridgeGeometry,
        scale,
    ]);
    const selectionRects = useStableMiniMapSelectionRects(rawSelectionRects);
    const highlightRects = useMemo(() => selectionRects.map((rect) => getMiniMapSelectionHighlightRect(rect, bounds)), [bounds, selectionRects]);
    if (selectionRects.length === 0)
        return null;
    return (_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 z-20", "data-testid": "minimap-selection-highlights", style: {
            contain: "layout paint",
            transform: "translateZ(0)",
            willChange: "transform",
        }, children: highlightRects.map((rect, index) => (_jsx(MiniMapSelectionHighlight, { rect: rect }, index))) }));
}
const MiniMapSelectionHighlight = memo(function MiniMapSelectionHighlight({ rect, }) {
    return (_jsx("div", { className: "absolute", "data-testid": "minimap-selection-highlight", style: {
            border: `${MINIMAP_SELECTION_BORDER_WIDTH}px solid var(--color-component-default)`,
            boxSizing: "border-box",
            height: rect.height,
            left: rect.x,
            top: rect.y,
            width: rect.width,
        } }));
});
function useStableMiniMapSelectionRects(rects) {
    const stableRef = useRef(null);
    const signature = getMiniMapSelectionRectsSignature(rects);
    if (stableRef.current?.signature === signature) {
        return stableRef.current.rects;
    }
    stableRef.current = { rects, signature };
    return rects;
}
function getMiniMapSelectionRectsSignature(rects) {
    return rects
        .map((rect) => `${formatMiniMapSelectionRectNumber(rect.x)},${formatMiniMapSelectionRectNumber(rect.y)},${formatMiniMapSelectionRectNumber(rect.width)},${formatMiniMapSelectionRectNumber(rect.height)}`)
        .join("|");
}
function formatMiniMapSelectionRectNumber(value) {
    return String(Math.round(value * 1000) / 1000);
}
function getInactiveMiniMapFrameSlot(activeSlot) {
    return activeSlot === "primary" ? "secondary" : "primary";
}
function getSelectedMiniMapComponents(selection, page) {
    const uniqueById = new Map();
    for (const id of selection) {
        for (const component of getAllComponentInstances(id, page)) {
            if (!uniqueById.has(component.id)) {
                uniqueById.set(component.id, component);
            }
        }
    }
    return Array.from(uniqueById.values());
}
function getMiniMapComponentRect(component, geometry, scale) {
    const componentId = normalizeBridgeKey(component.id);
    const target = geometry.targets.find((candidate) => {
        if (candidate.kind !== "component" || candidate.decorationsSuppressed) {
            return false;
        }
        return (normalizeBridgeKey(candidate.componentId) === componentId ||
            normalizeBridgeKey(candidate.key) === componentId);
    });
    if (!target?.rect || (target.rect.width <= 0 && target.rect.height <= 0)) {
        return null;
    }
    const rectScale = getBridgeMinimapRectScale(geometry);
    const scrollScale = getBridgeMinimapScrollScale(geometry);
    const minimapScale = scale / scrollScale;
    return {
        x: (target.rect.left * rectScale + geometry.scroll.x * scrollScale) *
            minimapScale,
        y: (target.rect.top * rectScale + geometry.scroll.y * scrollScale) *
            minimapScale,
        width: target.rect.width * rectScale * minimapScale,
        height: target.rect.height * rectScale * minimapScale,
    };
}
export function createBridgeMinimapHtml(html, geometry, viewportWidth) {
    if (typeof DOMParser === "undefined")
        return html;
    try {
        const doc = new DOMParser().parseFromString(html, "text/html");
        stripBridgeMinimapVolatileState(doc);
        neutralizeBridgeMinimapZoom(doc);
        constrainBridgeMinimapWidth(doc, viewportWidth);
        if (geometry) {
            bakeBridgeGeometryDimensions(doc, geometry);
        }
        return `<!DOCTYPE html>${doc.documentElement.outerHTML}`;
    }
    catch {
        return html;
    }
}
function getBridgeMinimapScrollScale(geometry) {
    return typeof geometry?.scrollScale === "number" &&
        Number.isFinite(geometry.scrollScale) &&
        geometry.scrollScale > 0
        ? geometry.scrollScale
        : 1;
}
function getBridgeMinimapRectScale(geometry) {
    return typeof geometry?.rectScale === "number" &&
        Number.isFinite(geometry.rectScale) &&
        geometry.rectScale > 0
        ? geometry.rectScale
        : 1;
}
function getBridgeGeometryDocumentHeight(geometry) {
    if (!geometry)
        return 0;
    const rectScale = getBridgeMinimapRectScale(geometry);
    const scrollScale = getBridgeMinimapScrollScale(geometry);
    return Math.max(geometry.viewport.height || 0, ...geometry.targets.map((target) => target.rect.bottom * rectScale + geometry.scroll.y * scrollScale));
}
function neutralizeBridgeMinimapZoom(doc) {
    const root = doc.documentElement;
    if (!root)
        return;
    root.style.removeProperty("zoom");
    root.style.removeProperty("transform");
    root.style.removeProperty("transform-origin");
    root.style.removeProperty("will-change");
}
function constrainBridgeMinimapWidth(doc, viewportWidth) {
    if (!viewportWidth || viewportWidth <= 0)
        return;
    [doc.documentElement, doc.body].forEach((element) => {
        if (!element)
            return;
        element.style.setProperty("box-sizing", "border-box", "important");
        element.style.setProperty("width", `${viewportWidth}px`, "important");
        element.style.setProperty("max-width", `${viewportWidth}px`, "important");
        element.style.setProperty("overflow-x", "hidden", "important");
    });
}
const BRIDGE_MINIMAP_VOLATILE_ATTRIBUTES = [
    "data-parhelia-selected",
    "data-parhelia-previous-contenteditable",
    "data-parhelia-inline-editing",
    "data-parhelia-inline-edit-active",
    "data-parhelia-suggestions-cursor",
    "data-parhelia-suggestions-cursor-style",
];
function stripBridgeMinimapVolatileState(doc) {
    const selector = BRIDGE_MINIMAP_VOLATILE_ATTRIBUTES.map((attribute) => `[${attribute}]`).join(",");
    const elements = Array.from(doc.querySelectorAll(selector));
    if (doc.documentElement.matches(selector)) {
        elements.unshift(doc.documentElement);
    }
    elements.forEach((element) => {
        BRIDGE_MINIMAP_VOLATILE_ATTRIBUTES.forEach((attribute) => {
            element.removeAttribute(attribute);
        });
    });
}
function formatBridgeMinimapCssPixels(value) {
    return `${Math.round(value * 1000) / 1000}px`;
}
function bakeBridgeGeometryDimensions(doc, geometry) {
    const rectScale = getBridgeMinimapRectScale(geometry);
    const keyedElements = Array.from(doc.querySelectorAll("[data-parhelia-element-key]"));
    const componentElements = Array.from(doc.querySelectorAll("[data-component-id], [data-component-start], [data-renderingid], [data-rendering-id]"));
    geometry.targets.forEach((target) => {
        // Only component targets describe a single element's own box. Placeholder
        // targets carry the union rect of their whole content region — and since
        // the dropzone anchoring work their elementKey points at the first
        // component inside the region, so baking them here would blow that
        // component up to the size of the entire placeholder.
        if (target.kind !== "component") {
            return;
        }
        if (!target.rect || (target.rect.width <= 0 && target.rect.height <= 0)) {
            return;
        }
        let element;
        if (target.elementKey) {
            const elementKey = normalizeBridgeKey(target.elementKey);
            element = keyedElements.find((candidate) => normalizeBridgeKey(candidate.getAttribute("data-parhelia-element-key")) === elementKey);
        }
        if (!element) {
            const componentKey = normalizeBridgeKey(target.componentId || target.key);
            element = componentElements.find((candidate) => normalizeBridgeKey(candidate.getAttribute("data-component-id") ||
                candidate.getAttribute("data-component-start") ||
                candidate.getAttribute("data-renderingid") ||
                candidate.getAttribute("data-rendering-id")) === componentKey);
        }
        if (!element)
            return;
        if (target.rect.width > 0) {
            element.style.setProperty("width", formatBridgeMinimapCssPixels(target.rect.width * rectScale), "important");
            element.style.setProperty("min-width", formatBridgeMinimapCssPixels(target.rect.width * rectScale), "important");
        }
        if (target.rect.height > 0) {
            element.style.setProperty("height", formatBridgeMinimapCssPixels(target.rect.height * rectScale), "important");
            element.style.setProperty("min-height", formatBridgeMinimapCssPixels(target.rect.height * rectScale), "important");
        }
    });
}
function normalizeBridgeKey(value) {
    return (value || "").replace(/[{}-]/g, "").toLowerCase();
}
//# sourceMappingURL=MiniMap.js.map