import { normalizeMarkerId } from "../../utils";
/**
 * Overlay frames (selection frame, drag parent outline) extend this many
 * pixels beyond the measured component rect on every side.
 */
export const FRAME_OUTSET = 2;
export function getOverlayScrollMetrics(_iframe, scrollScale, visualScale) {
    return {
        scrollLeft: 0,
        scrollTop: 0,
        scrollScale,
        visualScale,
    };
}
export function getOverlayMetrics(iframe, scrollScale, visualScale) {
    const scrollMetrics = getOverlayScrollMetrics(iframe, scrollScale, visualScale);
    const viewportWidth = iframe?.clientWidth || 0;
    const viewportHeight = iframe?.clientHeight || 0;
    return {
        ...scrollMetrics,
        viewportWidth,
        viewportHeight,
        scrollWidth: viewportWidth,
        scrollHeight: viewportHeight,
    };
}
export function toOverlayRect(viewportRect, metrics) {
    return {
        x: (viewportRect.left + metrics.scrollLeft * metrics.scrollScale) *
            metrics.visualScale,
        y: (viewportRect.top + metrics.scrollTop * metrics.scrollScale) *
            metrics.visualScale,
        width: viewportRect.width * metrics.visualScale,
        height: viewportRect.height * metrics.visualScale,
    };
}
export function rectEquals(a, b, tolerance = 0.5) {
    if (a === b)
        return true;
    if (!a || !b)
        return false;
    return (Math.abs(a.x - b.x) <= tolerance &&
        Math.abs(a.y - b.y) <= tolerance &&
        Math.abs(a.width - b.width) <= tolerance &&
        Math.abs(a.height - b.height) <= tolerance);
}
export function rectListEquals(a, b, tolerance = 0.5) {
    if (a === b)
        return true;
    if (!a || !b)
        return false;
    if (a.length !== b.length)
        return false;
    for (let i = 0; i < a.length; i++) {
        if (!rectEquals(a[i], b[i], tolerance))
            return false;
    }
    return true;
}
export function getTextNodeAtOffset(root, offset, affinity = "backward") {
    const ownerDocument = root.ownerDocument || document;
    const walker = ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
    let currentNode;
    let boundaryNode = null;
    while ((currentNode = walker.nextNode())) {
        const nodeLength = currentNode.textContent?.length || 0;
        if (offset < nodeLength) {
            return { node: currentNode, offset };
        }
        if (offset === nodeLength) {
            if (affinity === "backward") {
                return { node: currentNode, offset };
            }
            boundaryNode = currentNode;
            offset = 0;
            continue;
        }
        offset -= nodeLength;
    }
    return boundaryNode
        ? { node: boundaryNode, offset: boundaryNode.textContent?.length || 0 }
        : null;
}
export function createRangeFromTextOffsets(iframeDocument, root, start, end) {
    const startData = getTextNodeAtOffset(root, start, "forward");
    const endData = getTextNodeAtOffset(root, end, "backward");
    if (!startData || !endData) {
        if ((root.textContent ?? "") === "" && start === 0 && end === 0) {
            try {
                const emptyRange = iframeDocument.createRange();
                emptyRange.selectNodeContents(root);
                emptyRange.collapse(true);
                return emptyRange;
            }
            catch {
                return null;
            }
        }
        return null;
    }
    try {
        const textRange = iframeDocument.createRange();
        textRange.setStart(startData.node, startData.offset);
        textRange.setEnd(endData.node, endData.offset);
        return textRange;
    }
    catch {
        return null;
    }
}
export function measureTextRangeRects(iframeDocument, root, start, end, metrics) {
    if (start >= end)
        return [];
    const textRange = createRangeFromTextOffsets(iframeDocument, root, start, end);
    return textRange
        ? Array.from(textRange.getClientRects()).map((rect) => toOverlayRect(rect, metrics))
        : [];
}
export function measureCollapsedTextOffsetRect(iframeDocument, root, offset, metrics) {
    try {
        const caretRange = createRangeFromTextOffsets(iframeDocument, root, offset, offset);
        if (!caretRange)
            return null;
        const collapsedRect = Array.from(caretRange.getClientRects()).find((rect) => rect.height > 0);
        if (collapsedRect) {
            return toOverlayRect({
                left: collapsedRect.left,
                top: collapsedRect.top,
                width: Math.max(collapsedRect.width, 1),
                height: collapsedRect.height,
            }, metrics);
        }
        const textLength = root.textContent?.length ?? 0;
        const fallbackRects = offset > 0
            ? measureTextRangeRects(iframeDocument, root, offset - 1, offset, metrics)
            : offset < textLength
                ? measureTextRangeRects(iframeDocument, root, offset, offset + 1, metrics)
                : [];
        const fallbackRect = fallbackRects[0];
        if (!fallbackRect)
            return null;
        return {
            x: offset > 0 ? fallbackRect.x + fallbackRect.width : fallbackRect.x,
            y: fallbackRect.y,
            width: 1,
            height: fallbackRect.height,
        };
    }
    catch {
        return null;
    }
}
export function measureElementRect(element, metrics) {
    if (!element || !element.isConnected)
        return null;
    try {
        return toOverlayRect(element.getBoundingClientRect(), metrics);
    }
    catch {
        return null;
    }
}
/**
 * Find the bridge geometry target for a component (carries its `elementKey` and
 * raw host-viewport rect). Used by the sibling-shift preview to address elements
 * inside the iframe and size the gap.
 */
export function findBridgeComponentTarget(geometry, component) {
    if (!geometry || !component.id)
        return null;
    const componentId = normalizeMarkerId(component.id);
    return (geometry.targets.find((candidate) => {
        if (candidate.kind !== "component")
            return false;
        return (normalizeMarkerId(candidate.componentId) === componentId ||
            normalizeMarkerId(candidate.key) === componentId);
    }) ?? null);
}
export function measureBridgeComponentRect(geometry, component, metrics) {
    if (!geometry || !component.id)
        return null;
    const componentId = normalizeMarkerId(component.id);
    const target = geometry.targets.find((candidate) => {
        if (candidate.kind !== "component")
            return false;
        return (normalizeMarkerId(candidate.componentId) === componentId ||
            normalizeMarkerId(candidate.key) === componentId);
    });
    if (!target?.rect || !hasBridgeRectSize(target.rect))
        return null;
    return {
        rect: bridgeRectToOverlayRect(geometry, target.rect, metrics),
        elements: [],
    };
}
export function measureBridgePlaceholderRect(geometry, placeholderKey, metrics) {
    if (!geometry || !placeholderKey)
        return null;
    const target = geometry.targets.find((candidate) => {
        if (candidate.kind !== "placeholder")
            return false;
        return (bridgeKeysMatch(candidate.placeholderKey, placeholderKey) ||
            bridgeKeysMatch(candidate.key, placeholderKey));
    });
    if (!target?.rect || !hasBridgeRectSize(target.rect))
        return null;
    return {
        rect: bridgeRectToOverlayRect(geometry, target.rect, metrics),
        target,
    };
}
export function measureBridgeFieldRect(geometry, structure, fieldDescriptor, metrics) {
    const target = findBridgeFieldTarget(geometry, structure, fieldDescriptor);
    if (!geometry || !target?.rect || !hasBridgeRectSize(target.rect)) {
        return null;
    }
    return bridgeRectToOverlayRect(geometry, target.rect, metrics);
}
export function measureBridgeTextRangeRects(geometry, key, metrics) {
    if (!geometry || !key)
        return [];
    return geometry.targets
        .filter((target) => target.kind === "textRange" &&
        target.key === key &&
        hasBridgeRectSize(target.rect))
        .map((target) => bridgeRectToOverlayRect(geometry, target.rect, metrics));
}
// Bridge-reported scales are optional and must be positive/finite to be
// usable; anything else collapses to 1 (identity).
function resolveBridgeScale(value) {
    return typeof value === "number" && Number.isFinite(value) && value > 0
        ? value
        : 1;
}
export function bridgeRectToOverlayRect(geometry, rect, metrics) {
    const rectScale = resolveBridgeScale(geometry.rectScale);
    const scrollScale = resolveBridgeScale(geometry.scrollScale);
    const documentX = rect.left * rectScale +
        geometry.scroll.x * metrics.scrollScale * scrollScale;
    const documentY = rect.top * rectScale +
        geometry.scroll.y * metrics.scrollScale * scrollScale;
    return {
        x: documentX * metrics.visualScale,
        y: documentY * metrics.visualScale,
        width: rect.width * rectScale * metrics.visualScale,
        height: rect.height * rectScale * metrics.visualScale,
    };
}
/**
 * Map a point expressed in the host iframe's viewport coordinate space - the
 * same space `getBoundingClientRect` uses inside the host, i.e. the space of
 * the measured target rects - into overlay/document space.
 *
 * Used for pointer coordinates forwarded by the bridge host during a canvas
 * drag gesture. It reuses {@link bridgeRectToOverlayRect} (which already folds
 * in scroll plus the rect/scroll/visual scales) by treating the point as a
 * zero-size rect, so scroll is never added twice.
 */
export function hostViewportToOverlay(point, geometry, metrics) {
    const rect = {
        x: point.x,
        y: point.y,
        left: point.x,
        top: point.y,
        right: point.x,
        bottom: point.y,
        width: 0,
        height: 0,
    };
    const overlay = bridgeRectToOverlayRect(geometry, rect, metrics);
    return { x: overlay.x, y: overlay.y };
}
/**
 * Map a parent-document client point (e.g. a pointer/drag event over the
 * iframe) into overlay/document space so it can be compared with computed zone
 * positions.
 *
 * The overlay layer is only translated (by the scaled scroll); its children are
 * positioned at coordinates that already include the visual scale. So mapping a
 * client point is just its offset from the layer's on-screen origin - there is
 * no scale to divide out and no scroll term to add (both are already baked into
 * the layer's rect and the zone positions). Pass the overlay *layer* element's
 * bounding rect.
 */
export function clientToOverlay(clientX, clientY, overlayLayerRect) {
    return {
        x: clientX - overlayLayerRect.left,
        y: clientY - overlayLayerRect.top,
    };
}
function bridgeKeysMatch(left, right) {
    if (!left || !right)
        return false;
    return left === right || normalizeMarkerId(left) === normalizeMarkerId(right);
}
function findBridgeFieldTarget(geometry, structure, fieldDescriptor) {
    if (!geometry || !fieldDescriptor.fieldId)
        return undefined;
    const itemMatches = (item) => {
        if (!item)
            return false;
        if (!bridgeKeysMatch(item.id, fieldDescriptor.item.id))
            return false;
        if (fieldDescriptor.item.language &&
            item.language &&
            item.language !== fieldDescriptor.item.language) {
            return false;
        }
        if (fieldDescriptor.item.version !== undefined &&
            item.version !== undefined &&
            item.version !== fieldDescriptor.item.version) {
            return false;
        }
        return true;
    };
    const directTarget = geometry.targets.find((target) => {
        if (target.kind !== "field")
            return false;
        if (!bridgeKeysMatch(target.fieldId, fieldDescriptor.fieldId)) {
            return false;
        }
        return itemMatches(target.item);
    });
    if (directTarget)
        return directTarget;
    const structureField = structure?.fields.find((field) => {
        if (!bridgeKeysMatch(field.fieldId, fieldDescriptor.fieldId))
            return false;
        return itemMatches(field.item);
    });
    const structureTarget = structureField
        ? geometry.targets.find((target) => {
            if (target.kind !== "field")
                return false;
            if (structureField.elementKey &&
                bridgeKeysMatch(target.elementKey, structureField.elementKey)) {
                return true;
            }
            return false;
        })
        : undefined;
    if (structureTarget)
        return structureTarget;
    if (structureField || fieldDescriptor.item.id)
        return undefined;
    return geometry.targets.find((target) => {
        return (target.kind === "field" &&
            bridgeKeysMatch(target.fieldId, fieldDescriptor.fieldId));
    });
}
function hasBridgeRectSize(rect) {
    return rect.width > 0 || rect.height > 0;
}
//# sourceMappingURL=geometry.js.map