import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { 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";
export function MiniMap({ compareView, scroll, 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 [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 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(() => {
        calcAndUpdateScale();
    }, [
        isBridgeMinimap,
        pageViewContext?.zoom,
        pageViewContext.viewport,
        pageViewContext.deviceHeight,
        pageViewContext.deviceWidth,
        pageViewContext.bridgeDom,
        pageViewContext.bridgeGeometry,
        bridgeDomRevision,
    ]);
    const scrollDocumentTo = (y, behavior = "smooth") => {
        const metrics = getDocumentMetrics();
        if (!metrics)
            return;
        const maxScrollTop = Math.max(0, metrics.documentHeight - metrics.viewportHeight);
        const targetY = Math.min(maxScrollTop, Math.max(0, y));
        const currentY = pageViewContext.bridgeGeometry?.scroll.y ??
            scroll ??
            pageViewContext.bridgeDom?.scroll.y ??
            metrics.scrollTop;
        pageViewContext.requestBridgeScrollBy?.({
            y: targetY - currentY,
            behavior,
        });
    };
    // 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 metrics = getDocumentMetrics();
        if (!metrics)
            return;
        const iframeRect = iframeRef.current?.getBoundingClientRect();
        const reference = iframeRect && iframeRect.height > 0 ? iframeRect : containerRect;
        const ratio = Math.min(1, Math.max(0, (point.y - reference.top) / reference.height));
        const maxScrollTop = Math.max(0, metrics.documentHeight - metrics.viewportHeight);
        const target = Math.min(maxScrollTop, Math.max(0, ratio * maxScrollTop));
        const base = lastMinimapScrollTargetRef.current ??
            pageViewContext.bridgeGeometry?.scroll.y ??
            scroll ??
            pageViewContext.bridgeDom?.scroll.y ??
            metrics.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";
        if (iframeRef.current) {
            iframeRef.current.style.width = `${metrics?.viewportWidth ?? 0}px`;
            iframeRef.current.style.height = `${metrics?.documentHeight ?? 0}px`;
        }
    };
    const handleClick = (e) => {
        const metrics = getDocumentMetrics();
        if (!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.)
            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 maxScrollTop = Math.max(0, metrics.documentHeight - metrics.viewportHeight);
        e.stopPropagation();
        scrollDocumentTo(clickRatio * maxScrollTop);
    };
    const handleScroll = useCallback((e) => {
        pageViewContext.requestBridgeScrollBy?.({
            x: 0,
            y: e.deltaY,
            behavior: "instant",
        });
        e.stopPropagation();
    }, [pageViewContext]);
    const viewportHighlight = minimapMetrics
        ? getMinimapViewportHighlight(minimapMetrics, scale)
        : null;
    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, children: _jsx("iframe", { className: "pointer-events-none", ref: iframeRef, sandbox: "", srcDoc: bridgeMinimapHtml, style: {
                                border: 0,
                                display: "block",
                                height: `${minimapMetrics?.documentHeight ?? 0}px`,
                                visibility: minimapReady ? "visible" : "hidden",
                                width: `${minimapMetrics?.viewportWidth ?? 0}px`,
                            } }) }) }) }), _jsx("div", { "aria-hidden": "true", className: "absolute inset-0" }), viewportHighlight && minimapReady && (_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",
                } })), iframeRef.current && (_jsxs(IframeOverlayProvider, { iframe: iframeRef.current, 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.showSuggestedEdits && (_jsx(SuggestionHighlightings, { pageViewContext: pageViewContext, variant: "minimap" })), editContext.showVersionDiffHighlighting && !compareView && (_jsx(VersionDiffHighlightings, { pageViewContext: pageViewContext, variant: "minimap" }))] })), _jsx(MiniMapSelectionHighlights, { pageViewContext: pageViewContext, scale: scale, bounds: {
                    width: miniMapWidth,
                    height: minimapMetrics ? minimapMetrics.documentHeight * scale : 0,
                } })] }));
}
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 editContext = useEditContext();
    const selectionRects = useMemo(() => getMiniMapSelectionRects(editContext?.selection, pageViewContext.page, pageViewContext.bridgeGeometry, scale), [
        editContext?.selection,
        pageViewContext.page,
        pageViewContext.bridgeGeometry,
        scale,
    ]);
    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", children: selectionRects.map((rect, index) => {
            const highlightRect = getMiniMapSelectionHighlightRect(rect, bounds);
            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: highlightRect.height,
                    left: highlightRect.x,
                    top: highlightRect.y,
                    width: highlightRect.width,
                } }, index));
        }) }));
}
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")
            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,
    };
}
function createBridgeMinimapHtml(html, geometry, viewportWidth) {
    if (typeof DOMParser === "undefined")
        return html;
    try {
        const doc = new DOMParser().parseFromString(html, "text/html");
        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");
    });
}
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]"));
    const fieldElements = Array.from(doc.querySelectorAll("[data-fieldid]"));
    geometry.targets.forEach((target) => {
        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 && target.kind === "component") {
            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);
        }
        else if (target.kind === "field") {
            const fieldKey = normalizeBridgeKey(target.fieldId || target.key);
            element = fieldElements.find((candidate) => normalizeBridgeKey(candidate.getAttribute("data-fieldid")) ===
                fieldKey);
        }
        if (!element)
            return;
        if (target.kind === "field" || target.kind === "textRange")
            return;
        if (target.rect.width > 0) {
            element.style.setProperty("width", `${target.rect.width * rectScale}px`, "important");
            element.style.setProperty("min-width", `${target.rect.width * rectScale}px`, "important");
        }
        if (target.rect.height > 0) {
            element.style.setProperty("height", `${target.rect.height * rectScale}px`, "important");
            element.style.setProperty("min-height", `${target.rect.height * rectScale}px`, "important");
        }
    });
}
function normalizeBridgeKey(value) {
    return (value || "").replace(/[{}-]/g, "").toLowerCase();
}
//# sourceMappingURL=MiniMap.js.map