import { jsx as _jsx } from "react/jsx-runtime";
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, } from "react";
import { getOverlayMetrics, } from "./geometry";
import { SPLITTER_RESIZE_EVENT } from "../../ui/splitterResizeEvent";
export const IFRAME_OVERLAY_BRIDGE_SCROLL_EVENT = "parhelia:iframe-overlay-bridge-scroll";
export const IFRAME_OVERLAY_BRIDGE_GEOMETRY_EVENT = "parhelia:iframe-overlay-bridge-geometry";
// Component drag gesture consumed by DragInsertionOverlay. Two sources dispatch
// it:
// - the host (whole-component grab), re-dispatched from PageViewerFrame, with
//   coordinates in host-viewport space (`space: "host"`, the default); and
// - parent-DOM grips (the frame-menu header), which own the pointer in the
//   parent and report PARENT-client coordinates (`space: "client"`).
// The overlay maps each space with the matching coordinate mapper.
export const IFRAME_OVERLAY_BRIDGE_DRAG_GESTURE_EVENT = "parhelia:iframe-overlay-bridge-drag-gesture";
const ALL_REASONS = [
    "viewport",
    "layout",
    "text",
    "adornment-data",
];
const EMPTY_METRICS = {
    scrollLeft: 0,
    scrollTop: 0,
    scrollScale: 1,
    visualScale: 1,
    viewportWidth: 0,
    viewportHeight: 0,
    scrollWidth: 0,
    scrollHeight: 0,
};
const IframeOverlayContext = createContext(null);
export function useIframeOverlay() {
    const context = useContext(IframeOverlayContext);
    if (!context) {
        throw new Error("useIframeOverlay must be used inside IframeOverlayProvider");
    }
    return context;
}
export function IframeOverlayProvider({ iframe, scrollScale = 1, visualScale = 1, invalidationKey, deferredInvalidationMs = 0, mode = "scrolling", bleed = 0, children, }) {
    const layerRef = useRef(null);
    const subscribersRef = useRef(new Set());
    const scrollSubscribersRef = useRef(new Set());
    const pendingReasonsRef = useRef(new Set());
    const geometryRafRef = useRef(null);
    const scrollRafRef = useRef(null);
    const lastMetricsRef = useRef(EMPTY_METRICS);
    const iframeRef = useRef(iframe);
    const bridgeScrollRef = useRef(null);
    const bridgeGeometryMetricsRef = useRef(null);
    const scrollScaleRef = useRef(scrollScale);
    const visualScaleRef = useRef(visualScale);
    useEffect(() => {
        iframeRef.current = iframe;
        bridgeScrollRef.current = null;
        bridgeGeometryMetricsRef.current = null;
    }, [iframe]);
    useEffect(() => {
        scrollScaleRef.current = scrollScale;
        visualScaleRef.current = visualScale;
    }, [scrollScale, visualScale]);
    const getMetrics = useCallback(() => {
        const metrics = getOverlayMetrics(iframeRef.current, scrollScaleRef.current, visualScaleRef.current);
        return bridgeScrollRef.current
            ? {
                ...metrics,
                ...bridgeGeometryMetricsRef.current,
                ...bridgeScrollRef.current,
            }
            : bridgeGeometryMetricsRef.current
                ? { ...metrics, ...bridgeGeometryMetricsRef.current }
                : metrics;
    }, []);
    const writeScrollPosition = useCallback((metrics) => {
        const layer = layerRef.current;
        if (!layer)
            return;
        const translatedX = mode === "scrolling"
            ? -metrics.scrollLeft * metrics.scrollScale * metrics.visualScale
            : 0;
        const translatedY = mode === "scrolling"
            ? -metrics.scrollTop * metrics.scrollScale * metrics.visualScale
            : 0;
        layer.style.transform = `translate3d(${translatedX}px, ${translatedY}px, 0)`;
    }, [mode]);
    const applyScrollMetrics = useCallback((scrollMetrics) => {
        const metrics = {
            ...lastMetricsRef.current,
            ...scrollMetrics,
        };
        lastMetricsRef.current = metrics;
        writeScrollPosition(metrics);
        return metrics;
    }, [writeScrollPosition]);
    const writeLayerMetrics = useCallback((metrics) => {
        const layer = layerRef.current;
        if (!layer)
            return;
        lastMetricsRef.current = metrics;
        writeScrollPosition(metrics);
        const documentScale = metrics.scrollScale * metrics.visualScale;
        layer.style.width = `${Math.max(metrics.scrollWidth * documentScale, metrics.viewportWidth)}px`;
        layer.style.height = `${Math.max(metrics.scrollHeight * documentScale, metrics.viewportHeight)}px`;
    }, [writeScrollPosition]);
    const notifyScrollSubscribers = useCallback((metrics) => {
        scrollSubscribersRef.current.forEach((callback) => callback(metrics));
    }, []);
    useEffect(() => {
        if (!iframe)
            return;
        const handleBridgeScroll = (event) => {
            const detail = event
                .detail;
            if (!detail ||
                !Number.isFinite(detail.scrollLeft) ||
                !Number.isFinite(detail.scrollTop)) {
                return;
            }
            const bridgeScroll = {
                scrollLeft: detail.scrollLeft,
                scrollTop: detail.scrollTop,
            };
            bridgeScrollRef.current = bridgeScroll;
            applyScrollMetrics({
                ...bridgeScroll,
                scrollScale: scrollScaleRef.current,
                visualScale: visualScaleRef.current,
            });
            if (scrollRafRef.current != null)
                return;
            scrollRafRef.current = window.requestAnimationFrame(() => {
                scrollRafRef.current = null;
                notifyScrollSubscribers(lastMetricsRef.current);
            });
        };
        iframe.addEventListener(IFRAME_OVERLAY_BRIDGE_SCROLL_EVENT, handleBridgeScroll);
        return () => {
            iframe.removeEventListener(IFRAME_OVERLAY_BRIDGE_SCROLL_EVENT, handleBridgeScroll);
        };
    }, [iframe, applyScrollMetrics, notifyScrollSubscribers]);
    const flushMeasurements = useCallback(() => {
        geometryRafRef.current = null;
        const reasons = pendingReasonsRef.current.size > 0
            ? new Set(pendingReasonsRef.current)
            : new Set(ALL_REASONS);
        pendingReasonsRef.current.clear();
        const metrics = getMetrics();
        writeLayerMetrics(metrics);
        subscribersRef.current.forEach((subscriber) => {
            const shouldRun = subscriber.interests.size === 0 ||
                Array.from(reasons).some((reason) => subscriber.interests.has(reason));
            if (shouldRun)
                subscriber.callback(metrics, reasons);
        });
    }, [getMetrics, writeLayerMetrics]);
    const invalidateGeometry = useCallback((reason) => {
        const reasons = Array.isArray(reason) ? reason : [reason];
        reasons.forEach((item) => pendingReasonsRef.current.add(item));
        if (geometryRafRef.current != null)
            return;
        geometryRafRef.current = window.requestAnimationFrame(() => {
            flushMeasurements();
        });
    }, [flushMeasurements]);
    useEffect(() => {
        if (!iframe)
            return;
        const handleBridgeGeometry = (event) => {
            const detail = event
                .detail;
            if (detail && typeof detail === "object") {
                bridgeGeometryMetricsRef.current = {
                    ...(Number.isFinite(detail.scrollLeft)
                        ? { scrollLeft: detail.scrollLeft }
                        : {}),
                    ...(Number.isFinite(detail.scrollTop)
                        ? { scrollTop: detail.scrollTop }
                        : {}),
                    ...(Number.isFinite(detail.viewportWidth)
                        ? { viewportWidth: detail.viewportWidth }
                        : {}),
                    ...(Number.isFinite(detail.viewportHeight)
                        ? { viewportHeight: detail.viewportHeight }
                        : {}),
                    ...(Number.isFinite(detail.scrollWidth)
                        ? { scrollWidth: detail.scrollWidth }
                        : {}),
                    ...(Number.isFinite(detail.scrollHeight)
                        ? { scrollHeight: detail.scrollHeight }
                        : {}),
                };
                if (Number.isFinite(detail.scrollLeft) &&
                    Number.isFinite(detail.scrollTop)) {
                    bridgeScrollRef.current = {
                        scrollLeft: detail.scrollLeft,
                        scrollTop: detail.scrollTop,
                    };
                }
            }
            invalidateGeometry(["viewport", "layout", "adornment-data"]);
        };
        iframe.addEventListener(IFRAME_OVERLAY_BRIDGE_GEOMETRY_EVENT, handleBridgeGeometry);
        return () => {
            iframe.removeEventListener(IFRAME_OVERLAY_BRIDGE_GEOMETRY_EVENT, handleBridgeGeometry);
        };
    }, [iframe, invalidateGeometry]);
    const subscribeMeasurement = useCallback((callback, interests = ALL_REASONS) => {
        const subscriber = {
            callback,
            interests: new Set(interests),
        };
        subscribersRef.current.add(subscriber);
        callback(getMetrics(), new Set(ALL_REASONS));
        return () => {
            subscribersRef.current.delete(subscriber);
        };
    }, [getMetrics]);
    const subscribeScroll = useCallback((callback) => {
        scrollSubscribersRef.current.add(callback);
        return () => {
            scrollSubscribersRef.current.delete(callback);
        };
    }, []);
    useLayoutEffect(() => {
        const metrics = getMetrics();
        writeLayerMetrics(metrics);
    }, [getMetrics, mode, scrollScale, visualScale, writeLayerMetrics]);
    useEffect(() => {
        invalidateGeometry(["viewport", "layout", "adornment-data"]);
    }, [iframe, invalidateGeometry, mode, scrollScale, visualScale]);
    useEffect(() => {
        invalidateGeometry(["viewport", "layout", "adornment-data"]);
        if (deferredInvalidationMs <= 0)
            return;
        const timer = window.setTimeout(() => {
            invalidateGeometry(["viewport", "layout", "adornment-data"]);
        }, deferredInvalidationMs);
        return () => {
            window.clearTimeout(timer);
        };
    }, [deferredInvalidationMs, invalidationKey, invalidateGeometry]);
    useEffect(() => {
        const handleHostViewportChange = (event) => {
            if (event.type === SPLITTER_RESIZE_EVENT) {
                pendingReasonsRef.current.add("viewport");
                pendingReasonsRef.current.add("layout");
                if (geometryRafRef.current != null) {
                    window.cancelAnimationFrame(geometryRafRef.current);
                    geometryRafRef.current = null;
                }
                flushMeasurements();
                return;
            }
            invalidateGeometry(["viewport", "layout"]);
        };
        window.addEventListener("resize", handleHostViewportChange);
        window.addEventListener(SPLITTER_RESIZE_EVENT, handleHostViewportChange);
        return () => {
            window.removeEventListener("resize", handleHostViewportChange);
            window.removeEventListener(SPLITTER_RESIZE_EVENT, handleHostViewportChange);
        };
    }, [flushMeasurements, invalidateGeometry]);
    useEffect(() => {
        if (!iframe)
            return;
        let resizeObserver = null;
        try {
            resizeObserver = new ResizeObserver(() => {
                invalidateGeometry("viewport");
            });
            resizeObserver.observe(iframe);
        }
        catch {
            resizeObserver = null;
        }
        return () => {
            resizeObserver?.disconnect();
        };
    }, [iframe, invalidateGeometry]);
    useEffect(() => {
        return () => {
            if (geometryRafRef.current != null) {
                window.cancelAnimationFrame(geometryRafRef.current);
                geometryRafRef.current = null;
            }
            if (scrollRafRef.current != null) {
                window.cancelAnimationFrame(scrollRafRef.current);
                scrollRafRef.current = null;
            }
        };
    }, []);
    const contextValue = useMemo(() => ({
        iframe,
        getMetrics,
        invalidateGeometry,
        subscribeMeasurement,
        subscribeScroll,
    }), [
        iframe,
        getMetrics,
        invalidateGeometry,
        subscribeMeasurement,
        subscribeScroll,
    ]);
    const bleedInsets = resolveOverlayBleed(bleed);
    const hasBleed = bleedInsets.top > 0 ||
        bleedInsets.right > 0 ||
        bleedInsets.bottom > 0 ||
        bleedInsets.left > 0;
    return (_jsx(IframeOverlayContext.Provider, { value: contextValue, children: _jsx("div", { className: "pointer-events-none absolute inset-0 overflow-hidden", style: hasBleed
                ? {
                    top: -bleedInsets.top,
                    right: -bleedInsets.right,
                    bottom: -bleedInsets.bottom,
                    left: -bleedInsets.left,
                }
                : undefined, "data-testid": "iframe-overlay-root", children: _jsx("div", { ref: layerRef, className: "pointer-events-none absolute top-0 left-0 will-change-transform", style: hasBleed
                    ? {
                        top: bleedInsets.top,
                        left: bleedInsets.left,
                    }
                    : undefined, "data-testid": "iframe-overlay-layer", children: children }) }) }));
}
function resolveOverlayBleed(bleed) {
    const sanitize = (value) => typeof value === "number" && Number.isFinite(value)
        ? Math.max(0, value)
        : 0;
    if (typeof bleed === "number") {
        const value = sanitize(bleed);
        return { top: value, right: value, bottom: value, left: value };
    }
    return {
        top: sanitize(bleed.top),
        right: sanitize(bleed.right),
        bottom: sanitize(bleed.bottom),
        left: sanitize(bleed.left),
    };
}
//# sourceMappingURL=IframeOverlayProvider.js.map