import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useEffect, useRef, useState } from "react";
import { useEditContext } from "../client/editContext";
import { PlaceholderDropZones } from "../page-editor-chrome/PlaceholderDropZones";
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";
function getAccessibleIframeDocument(iframe) {
    if (!iframe)
        return null;
    try {
        return iframe.contentDocument || iframe.contentWindow?.document || null;
    }
    catch {
        return null;
    }
}
function isElementNode(node) {
    return node?.nodeType === 1;
}
export function MiniMap({ compareView, deviceHeight, scroll, mainViewIframeRef, pageViewContext, }) {
    const editContext = useEditContext();
    const [scale, setScale] = useState(1);
    const minimapRef = useRef(null);
    const scaleContainerRef = useRef(null);
    const iframeRef = useRef(null);
    const [minimapReady, setMinimapReady] = useState(false);
    const [refreshScale, setRefreshScale] = useState(0);
    const observerRef = useRef(null);
    const resizeObserverRef = useRef(null);
    const assetLoadHandlerRef = useRef(null);
    const minimapContainerRef = useRef(null);
    const getScrollContainer = useCallback(() => {
        const doc = getAccessibleIframeDocument(mainViewIframeRef.current);
        return doc?.scrollingElement || doc?.body || null;
    }, [mainViewIframeRef]);
    const calcAndUpdateScale = () => {
        const viewport = pageViewContext.viewport;
        const minimapHeight = minimapContainerRef.current?.clientHeight;
        const scrollContainer = getScrollContainer();
        if (!viewport || !scrollContainer)
            return;
        const width = viewport.width;
        const scaleX = (editContext?.configuration.outline.width || 100) / width;
        const scaleY = Math.max(deviceHeight || 0, minimapHeight || viewport.height) /
            scrollContainer.scrollHeight;
        const newScale = Math.min(scaleX, scaleY, 1);
        updateScale(newScale);
        setScale(newScale);
        syncDimensions();
        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 * (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;
    }
    function getNodePath(node) {
        const path = [];
        let current = node;
        if (node?.tagName === "HTML")
            return null;
        while (current &&
            current !== current.ownerDocument?.documentElement &&
            current.parentNode) {
            const index = Array.prototype.indexOf.call(current.parentNode.childNodes, current);
            path.unshift(index);
            current = current.parentNode;
        }
        return path;
    }
    function findCorrespondingNode(node) {
        const path = getNodePath(node);
        if (!path)
            return null;
        const minimapDoc = getAccessibleIframeDocument(iframeRef.current);
        if (!minimapDoc)
            return null;
        let correspondingNode = minimapDoc.documentElement;
        let originalNode = getAccessibleIframeDocument(mainViewIframeRef.current)?.documentElement;
        for (const index of path) {
            if (!originalNode || !correspondingNode)
                return null;
            if (originalNode.childNodes.length !== correspondingNode.childNodes.length)
                return null;
            originalNode = originalNode.childNodes[index] ?? null;
            correspondingNode = correspondingNode.childNodes[index] ?? null;
        }
        return isElementNode(correspondingNode) ? correspondingNode : null;
    }
    function updateCorrespondingNode(node) {
        const path = getNodePath(node);
        if (!path)
            return;
        const minimapDoc = getAccessibleIframeDocument(iframeRef.current);
        if (!minimapDoc)
            return;
        let correspondingNode = minimapDoc.documentElement;
        let originalNode = getAccessibleIframeDocument(mainViewIframeRef.current)?.documentElement;
        for (const index of path) {
            if (originalNode && correspondingNode) {
                if (originalNode.childNodes.length !== correspondingNode.childNodes.length) {
                    // When the mismatch is at the document root (HTML), only sync the
                    // body element instead of replacing the entire innerHTML. Replacing
                    // HTML.innerHTML causes a visible flash and can create a duplicate
                    // HEAD element in the minimap iframe.
                    if (isElementNode(originalNode) && originalNode.tagName === "HTML") {
                        const srcBody = originalNode.ownerDocument?.body;
                        const dstBody = correspondingNode.ownerDocument?.body;
                        if (srcBody && dstBody) {
                            dstBody.innerHTML = srcBody.innerHTML;
                        }
                        return;
                    }
                    if (!isElementNode(originalNode) ||
                        !isElementNode(correspondingNode)) {
                        return;
                    }
                    correspondingNode.innerHTML = originalNode.innerHTML;
                    return;
                }
            }
            if (originalNode) {
                originalNode = originalNode.childNodes[index] ?? null;
            }
            if (correspondingNode) {
                correspondingNode = correspondingNode.childNodes[index] ?? null;
            }
        }
        if (isElementNode(correspondingNode) && isElementNode(originalNode)) {
            if (originalNode.tagName === "HEAD") {
                correspondingNode.innerHTML = originalNode.innerHTML;
            }
            else if (correspondingNode.parentNode?.nodeType === Node.DOCUMENT_NODE) {
                correspondingNode.innerHTML = originalNode.innerHTML;
            }
            else {
                correspondingNode.outerHTML = originalNode.outerHTML;
            }
        }
    }
    const handleLoad = () => {
        const sourceDoc = getAccessibleIframeDocument(mainViewIframeRef.current);
        if (!sourceDoc)
            return;
        if (!iframeRef.current || !mainViewIframeRef.current)
            return;
        observerRef.current?.disconnect();
        resizeObserverRef.current?.disconnect();
        if (assetLoadHandlerRef.current) {
            sourceDoc.removeEventListener("load", assetLoadHandlerRef.current, true);
            assetLoadHandlerRef.current = null;
        }
        mirrorIframeContent(mainViewIframeRef.current, iframeRef.current);
        const observer = new MutationObserver((mutationsList) => {
            const minimapDoc = getAccessibleIframeDocument(iframeRef.current);
            if (!minimapDoc)
                return;
            // Sync attribute mutations lightweight — copy only the changed attribute
            const attributeMutations = mutationsList.filter((x) => x.type === "attributes" && x.attributeName && x.target.parentElement);
            for (const mutation of attributeMutations) {
                const target = mutation.target;
                if (!isElementNode(target) || target.tagName === "HTML")
                    continue;
                const correspondingNode = findCorrespondingNode(target);
                if (!correspondingNode) {
                    updateCorrespondingNode(target.parentElement ?? target);
                    continue;
                }
                const attrName = mutation.attributeName;
                const value = target.getAttribute(attrName);
                if (value !== null) {
                    correspondingNode.setAttribute(attrName, value);
                }
                else {
                    correspondingNode.removeAttribute(attrName);
                }
            }
            // Structural mutations (childList / characterData) need full DOM sync
            const structuralMutations = mutationsList.filter((x) => x.type === "childList" || x.type === "characterData");
            if (structuralMutations.length === 0)
                return;
            const targetNodes = structuralMutations
                .filter((x) => x.type === "childList" && x.target.parentElement)
                .map((m) => m.target)
                .concat(structuralMutations
                .filter((x) => x.type === "characterData")
                .map((m) => m.target.parentElement));
            const distinctTargetNodes = Array.from(new Set(targetNodes));
            const nodesToRemove = new Set();
            for (let i = 0; i < distinctTargetNodes.length; i++) {
                for (let j = 0; j < distinctTargetNodes.length; j++) {
                    if (i !== j) {
                        if (distinctTargetNodes[i]?.contains(distinctTargetNodes[j])) {
                            nodesToRemove.add(distinctTargetNodes[j]);
                        }
                    }
                }
            }
            const filteredNodes = distinctTargetNodes.filter((node) => !nodesToRemove.has(node));
            filteredNodes.forEach((targetNode) => {
                if (isElementNode(targetNode)) {
                    updateCorrespondingNode(targetNode);
                }
            });
            // Re-bake dimensions in the same tick as the structural sync so the
            // source/destination tree pairing stays consistent — MutationObserver
            // callbacks are microtasks, no other JS can run between the two.
            syncDimensions();
            setRefreshScale((x) => x + 1);
        });
        observer.observe(sourceDoc, {
            childList: true, // observe direct children changes
            subtree: true, // observe all descendants changes
            characterData: true, // observe text changes
            attributes: true, // observe attribute changes (like style or class)
        });
        // Catch async layout changes that don't trigger the MutationObserver:
        // image/video metadata loading, web font swap-in, CSS animations
        // settling, and iframe-size changes from device-mode toggles.
        const ro = new ResizeObserver(() => {
            syncDimensions();
            setRefreshScale((x) => x + 1);
        });
        ro.observe(sourceDoc.documentElement);
        if (mainViewIframeRef.current) {
            ro.observe(mainViewIframeRef.current);
        }
        if (minimapContainerRef.current) {
            ro.observe(minimapContainerRef.current);
        }
        resizeObserverRef.current = ro;
        const onAssetLoad = () => {
            syncDimensions();
        };
        // <img>/<video>/<iframe> 'load' events don't bubble — capture phase
        // catches them at the document root.
        sourceDoc.addEventListener("load", onAssetLoad, true);
        assetLoadHandlerRef.current = onAssetLoad;
        setMinimapReady(true);
        observerRef.current = observer;
    };
    useEffect(() => {
        if (mainViewIframeRef?.current) {
            mainViewIframeRef?.current.addEventListener("load", handleLoad);
        }
        handleLoad();
        // Cleanup function
        return () => {
            observerRef.current?.disconnect();
            observerRef.current = null;
            resizeObserverRef.current?.disconnect();
            resizeObserverRef.current = null;
            const sourceDoc = getAccessibleIframeDocument(mainViewIframeRef.current);
            if (sourceDoc && assetLoadHandlerRef.current) {
                sourceDoc.removeEventListener("load", assetLoadHandlerRef.current, true);
            }
            assetLoadHandlerRef.current = null;
            mainViewIframeRef?.current?.removeEventListener("load", handleLoad);
        };
    }, [mainViewIframeRef.current, iframeRef.current]);
    useEffect(() => {
        calcAndUpdateScale();
    }, [
        refreshScale,
        mainViewIframeRef,
        //pageViewContext?.windowSize,
        pageViewContext?.zoom,
        pageViewContext.viewport,
        pageViewContext.deviceHeight,
        pageViewContext.deviceWidth,
    ]);
    const scrollDocumentTo = (y) => {
        if (!mainViewIframeRef?.current)
            return;
        const scrollContainer = getScrollContainer();
        if (!scrollContainer)
            return;
        const scrollOptions = { top: y, behavior: "smooth" };
        const scrollWindow = mainViewIframeRef.current.contentWindow;
        if (scrollWindow) {
            scrollWindow.scrollTo(scrollOptions);
            return;
        }
        scrollContainer.scrollTo(scrollOptions);
    };
    const scrollHeight = getScrollContainer()?.scrollHeight;
    const scrollTop = scroll;
    const viewport = pageViewContext.viewport;
    function mirrorIframeContent(sourceIframe, targetIframe) {
        const sourceDoc = getAccessibleIframeDocument(sourceIframe);
        const targetDoc = getAccessibleIframeDocument(targetIframe);
        // Check that both documents AND their documentElements exist
        if (sourceDoc?.documentElement && targetDoc?.documentElement) {
            const sourceHtml = sourceDoc.documentElement.innerHTML;
            targetDoc.documentElement.innerHTML = sourceHtml;
            targetDoc.documentElement.style.overflow = "hidden";
            const targetHead = targetDoc.documentElement.querySelector("head");
            // Inject CSS to constrain media elements in minimap. Width/height for
            // <video> are now baked by syncDimensions() so we no longer need the
            // viewport-unit (`max-height: 100vh`) hack that misresolved against the
            // tall minimap iframe.
            let style = targetHead?.querySelector("style[data-minimap-constraints]");
            if (!style && targetHead) {
                style = targetDoc.createElement("style");
                style.setAttribute("data-minimap-constraints", "true");
                style.textContent = `
          video, img, picture, iframe {
            max-width: 100% !important;
            height: auto !important;
          }
        `;
                targetHead.appendChild(style);
            }
        }
        syncDimensions();
        setRefreshScale((x) => x + 1);
    }
    // Bake the main iframe's resolved layout (width/height per element, plus
    // top/left for absolute/fixed elements) into the minimap clone as inline
    // !important px styles. This neutralizes vh/vw drift between the two
    // iframes (their layout viewports differ — main = deviceHeight, minimap =
    // scrollHeight) and any other unit/calc/percentage that would resolve
    // differently inside the minimap document.
    function syncDimensions() {
        const sourceDoc = getAccessibleIframeDocument(mainViewIframeRef.current);
        const targetDoc = getAccessibleIframeDocument(iframeRef.current);
        if (!sourceDoc?.body || !targetDoc?.body)
            return;
        const srcView = sourceDoc.defaultView;
        if (!srcView)
            return;
        walkDimensions(sourceDoc.body, targetDoc.body, srcView);
    }
    function walkDimensions(src, dst, srcView) {
        // We can't use `instanceof HTMLElement` here because src/dst live in
        // different window realms (main iframe / minimap iframe) than this
        // component, so each realm has its own HTMLElement constructor and the
        // check would always be false. Use duck typing instead.
        const srcEl = src;
        const dstEl = dst;
        const dstStyle = dstEl.style;
        if (dstStyle && typeof srcEl.getBoundingClientRect === "function") {
            const rect = srcEl.getBoundingClientRect();
            if (rect.width > 0 || rect.height > 0) {
                dstStyle.setProperty("width", `${rect.width}px`, "important");
                dstStyle.setProperty("height", `${rect.height}px`, "important");
            }
            const cs = srcView.getComputedStyle(srcEl);
            if (cs.position === "absolute" || cs.position === "fixed") {
                const offsetParent = srcEl.offsetParent;
                const parentRect = offsetParent?.getBoundingClientRect();
                if (parentRect) {
                    dstStyle.setProperty("left", `${rect.left - parentRect.left}px`, "important");
                    dstStyle.setProperty("top", `${rect.top - parentRect.top}px`, "important");
                    dstStyle.removeProperty("right");
                    dstStyle.removeProperty("bottom");
                }
            }
        }
        // Walk children in lock-step. Bail at any divergent subtree — the
        // MutationObserver path keeps structures aligned, so this is purely
        // defensive.
        const sChildren = src.children;
        const dChildren = dst.children;
        const len = Math.min(sChildren.length, dChildren.length);
        for (let i = 0; i < len; i++) {
            walkDimensions(sChildren[i], dChildren[i], srcView);
        }
    }
    const updateScale = (scale) => {
        const scrollContainer = getScrollContainer();
        if (!iframeRef.current || !scaleContainerRef.current)
            return;
        if (!scrollContainer)
            return;
        scaleContainerRef.current.style.transform = `scale(${scale})`;
        scaleContainerRef.current.style.transformOrigin = "0 0";
        iframeRef.current.style.width = `${scrollContainer?.clientWidth}px`;
        iframeRef.current.style.height = `${scrollContainer?.scrollHeight}px`;
    };
    const handleClick = (e) => {
        const scrollContainer = getScrollContainer();
        if (!scrollContainer)
            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.)
            return;
        }
        const containerRect = minimapContainerRef.current?.getBoundingClientRect();
        if (!containerRect)
            return;
        const iframeRect = iframeRef.current?.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));
        const visibleIframeHeight = mainViewIframeRef.current?.getBoundingClientRect().height ||
            mainViewIframeRef.current?.clientHeight;
        const currentViewportHeight = visibleIframeHeight ||
            scrollContainer.ownerDocument?.defaultView?.innerHeight ||
            scrollContainer.clientHeight ||
            viewport.height;
        const maxScrollTop = Math.max(0, scrollContainer.scrollHeight - currentViewportHeight);
        e.stopPropagation();
        scrollDocumentTo(clickRatio * maxScrollTop);
    };
    const handleScroll = useCallback((e) => {
        const scrollContainer = getScrollContainer();
        if (!scrollContainer)
            return;
        scrollContainer.scrollBy({
            behavior: "instant",
            left: 0,
            top: e.deltaY,
        });
        getAccessibleIframeDocument(mainViewIframeRef.current)?.documentElement?.scrollBy({
            behavior: "instant",
            left: 0,
            top: e.deltaY,
        });
        e.stopPropagation();
    }, [getScrollContainer, mainViewIframeRef]);
    if (!editContext || !mainViewIframeRef.current) {
        return null;
    }
    return (_jsxs("div", { "data-testid": "minimap-container", className: "relative 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, children: _jsx("iframe", { className: "pointer-events-none", ref: iframeRef, style: { visibility: minimapReady ? "visible" : "hidden" } }) }) }) }), _jsx("div", { "aria-hidden": "true", className: "absolute inset-0" }), scrollTop !== undefined &&
                scrollHeight !== undefined &&
                minimapReady && (_jsx("div", { className: "bg-neutral-grey-10 pointer-events-none absolute w-full opacity-60 hover:opacity-65", style: {
                    top: scrollTop * scale + "px",
                    height: viewport.height * scale + "px",
                } })), iframeRef.current && (_jsxs(IframeOverlayProvider, { iframe: iframeRef.current, mode: "static", scrollScale: 1, visualScale: scale, children: [editContext.mode === "edit" && (_jsx(PlaceholderDropZones, { iframe: iframeRef.current, size: "small", pageViewContext: pageViewContext })), _jsx(FieldEditedIndicators, { pageViewContext: pageViewContext }), editContext.showComments && (_jsx(CommentHighlightings, { iframe: iframeRef.current })), editContext.showSuggestedEdits && (_jsx(SuggestionHighlightings, { iframe: iframeRef.current })), editContext.showVersionDiffHighlighting && !compareView && (_jsx(VersionDiffHighlightings, { iframe: iframeRef.current, pageViewContext: pageViewContext, variant: "minimap" }))] }))] }));
}
//# sourceMappingURL=MiniMap.js.map