import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useMemo } from "react";
import { useEditContext, useFieldsEditContext } from "../client/editContext";
import { findFieldElement } from "../utils";
import { cn } from "../../lib/utils";
import { CommentIcon } from "../ui/Icons";
import { CommentDisplayPopover } from "../reviews/CommentDisplayPopover";
import { measureElementRect, rectListEquals, toOverlayRect, } from "./overlay/geometry";
import { getAccessibleIframeDocument } from "./overlay/iframeAccess";
import { useOverlayMeasurement } from "./overlay/useOverlayMeasurement";
function commentMeasurementsEqual(previous, next) {
    const previousKeys = Object.keys(previous);
    const nextKeys = Object.keys(next);
    if (previousKeys.length !== nextKeys.length)
        return false;
    for (const key of previousKeys) {
        const previousValue = previous[key];
        const nextValue = next[key];
        if (!previousValue || !nextValue)
            return false;
        if (previousValue.isTextRange !== nextValue.isTextRange)
            return false;
        if (!rectListEquals(previousValue.rects, nextValue.rects))
            return false;
    }
    return true;
}
export function CommentHighlightings({ iframe, }) {
    const editContext = useEditContext();
    const modifiedFields = useFieldsEditContext();
    const visibleComments = useMemo(() => (editContext?.comments || []).filter((comment) => !comment.suggestedEditId &&
        (editContext?.showResolvedComments ? true : !comment.isResolved)), [editContext?.comments, editContext?.showResolvedComments]);
    const measurements = useOverlayMeasurement((metrics) => {
        const iframeDocument = getAccessibleIframeDocument(iframe);
        if (!editContext || !iframeDocument?.body)
            return {};
        const nextMeasurements = {};
        for (const comment of visibleComments) {
            const fieldElement = comment.fieldId
                ? findFieldElement(iframe, {
                    item: {
                        id: comment.itemId,
                        language: comment.language,
                        version: comment.version,
                    },
                    fieldId: comment.fieldId,
                })
                : undefined;
            const range = fieldElement
                ? resolveCommentRange(comment, fieldElement, iframeDocument)
                : null;
            let rects = [];
            let isTextRange = false;
            if (fieldElement && range) {
                rects = measureVisibleTextRangeRects(iframeDocument, fieldElement, range[0], range[1], metrics);
                isTextRange = rects.length > 0;
            }
            if (rects.length === 0) {
                let fallbackElement = fieldElement;
                if (!fallbackElement && comment.itemId && editContext.page) {
                    const componentStart = iframeDocument.body.querySelector(`[data-component-start='${comment.itemId}']`);
                    fallbackElement = componentStart?.nextElementSibling;
                    while (fallbackElement && fallbackElement.tagName === "SCRIPT") {
                        fallbackElement = fallbackElement.nextElementSibling;
                    }
                }
                const fallbackRect = measureElementRect(fallbackElement, metrics);
                if (fallbackRect)
                    rects = [fallbackRect];
            }
            if (rects.length > 0) {
                nextMeasurements[comment.id] = { rects, isTextRange };
            }
        }
        return nextMeasurements;
    }, [iframe, editContext, visibleComments], {
        initialValue: {},
        equals: commentMeasurementsEqual,
        interests: ["viewport", "layout", "text", "adornment-data"],
    });
    if (!editContext)
        return null;
    return (_jsxs(_Fragment, { children: [_jsx("style", { children: commentHighlightingStyles }), visibleComments.flatMap((comment) => {
                const measurement = measurements[comment.id];
                if (!measurement?.rects.length)
                    return [];
                const isSelected = comment.id === editContext.selectedComment?.id;
                return measurement.rects.map((rect, index) => (_jsxs("div", { style: {
                        position: "absolute",
                        top: rect.y,
                        left: rect.x,
                        width: rect.width,
                        height: rect.height,
                        pointerEvents: "none",
                    }, children: [index === 0 && (_jsx(CommentDisplayPopover, { comment: comment, onCommentUpdated: () => {
                                editContext?.loadComments?.();
                            }, children: _jsx("div", { className: "pointer-events-auto absolute -top-5 right-0 h-5 w-5 cursor-pointer", "data-testid": "comment-highlight-icon", onClick: () => {
                                    editContext?.setSelectedComment(comment);
                                    editContext?.setScrollIntoView(comment.itemId);
                                    editContext?.select([comment.itemId]);
                                    if (comment.fieldId) {
                                        modifiedFields?.setFocusedField({
                                            fieldId: comment.fieldId,
                                            item: {
                                                id: comment.itemId,
                                                language: comment.language,
                                                version: comment.version,
                                            },
                                        }, false);
                                    }
                                }, children: _jsx(CommentIcon, { className: cn(comment.isResolved
                                        ? "text-feedback-green hover:text-feedback-green"
                                        : isSelected
                                            ? "text-feedback-red hover:text-feedback-red"
                                            : "text-feedback-blue hover:text-feedback-blue") }) }) })), measurement.isTextRange && (_jsx("div", { className: "absolute right-0 -bottom-0.5 left-0 h-2", style: {
                                ...getCommentUnderlineStyle(comment, isSelected, index),
                            } }))] }, `${comment.id}-${index}`)));
            })] }));
}
function measureVisibleTextRangeRects(iframeDocument, root, start, end, metrics) {
    const text = root.textContent || "";
    const boundedStart = Math.max(0, Math.min(start, text.length));
    const boundedEnd = Math.max(boundedStart, Math.min(end, text.length));
    if (boundedStart >= boundedEnd)
        return [];
    const rects = [];
    const walker = iframeDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
    let absoluteOffset = 0;
    let textNode;
    while ((textNode = walker.nextNode())) {
        const nodeText = textNode.textContent || "";
        const nodeStart = absoluteOffset;
        const nodeEnd = nodeStart + nodeText.length;
        if (nodeEnd > boundedStart && nodeStart < boundedEnd) {
            const localStart = Math.max(0, boundedStart - nodeStart);
            const localEnd = Math.min(nodeText.length, boundedEnd - nodeStart);
            rects.push(...measureVisibleTextNodeRects(iframeDocument, textNode, nodeText, localStart, localEnd, metrics));
        }
        absoluteOffset = nodeEnd;
        if (absoluteOffset >= boundedEnd)
            break;
    }
    return rects;
}
function measureVisibleTextNodeRects(iframeDocument, textNode, nodeText, start, end, metrics) {
    const rects = [];
    let cursor = start;
    while (cursor < end) {
        let lineEnd = cursor;
        while (lineEnd < end && !isLineBreak(nodeText[lineEnd])) {
            lineEnd++;
        }
        let visibleStart = cursor;
        while (visibleStart < lineEnd &&
            isHorizontalWhitespace(nodeText[visibleStart])) {
            visibleStart++;
        }
        let visibleEnd = lineEnd;
        while (visibleEnd > visibleStart &&
            isHorizontalWhitespace(nodeText[visibleEnd - 1])) {
            visibleEnd--;
        }
        if (visibleStart < visibleEnd) {
            try {
                const textRange = iframeDocument.createRange();
                textRange.setStart(textNode, visibleStart);
                textRange.setEnd(textNode, visibleEnd);
                rects.push(...Array.from(textRange.getClientRects())
                    .filter((rect) => rect.width > 1 && rect.height > 1)
                    .map((rect) => toOverlayRect(rect, metrics)));
            }
            catch {
                // Ignore detached or otherwise invalid text nodes while the iframe mutates.
            }
        }
        cursor = lineEnd;
        if (nodeText[cursor] === "\r" && nodeText[cursor + 1] === "\n") {
            cursor += 2;
        }
        else {
            cursor++;
        }
    }
    return rects;
}
function isLineBreak(value) {
    return value === "\n" || value === "\r";
}
function isHorizontalWhitespace(value) {
    return value === " " || value === "\t" || value === "\f" || value === "\v";
}
function getCommentUnderlineStyle(comment, isSelected, index) {
    const color = comment.isResolved
        ? "var(--color-feedback-green)"
        : isSelected
            ? "var(--color-feedback-red)"
            : "var(--color-feedback-blue)";
    const strength = isSelected ? 62 : 48;
    const textureStrength = isSelected ? 20 : 14;
    const shadowStrength = isSelected ? 24 : 14;
    const offset = (hashString(comment.id) + index * 17) % 64;
    const style = {
        "--comment-squiggle-offset": `${offset}px`,
        background: [
            `radial-gradient(circle at 18% 45%, color-mix(in srgb, ${color} ${textureStrength}%, transparent) 0 1px, transparent 1.5px)`,
            `radial-gradient(circle at 68% 55%, color-mix(in srgb, ${color} ${textureStrength}%, transparent) 0 1px, transparent 1.5px)`,
            `linear-gradient(90deg, color-mix(in srgb, ${color} ${strength}%, transparent), color-mix(in srgb, ${color} ${Math.max(strength - 10, 0)}%, transparent) 45%, color-mix(in srgb, ${color} ${strength}%, transparent))`,
        ].join(", "),
        backgroundSize: "15px 8px, 21px 8px, 100% 100%",
        backgroundPosition: `${offset % 15}px 0, -${offset % 21}px 0, 0 0`,
        filter: `drop-shadow(0 1px 1px color-mix(in srgb, ${color} ${shadowStrength}%, transparent))`,
        maskImage: `url("${squiggleUnderlineMask}")`,
        maskRepeat: "repeat-x",
        maskSize: "64px 8px",
        maskPosition: `var(--comment-squiggle-offset) 0`,
        WebkitMaskImage: `url("${squiggleUnderlineMask}")`,
        WebkitMaskRepeat: "repeat-x",
        WebkitMaskSize: "64px 8px",
        WebkitMaskPosition: `var(--comment-squiggle-offset) 0`,
        ...(isSelected
            ? {
                animation: "parhelia-comment-squiggle-pulse 3.2s ease-in-out infinite, parhelia-comment-squiggle-drift 8s linear infinite",
                willChange: "background-position, mask-position, -webkit-mask-position, filter, opacity",
            }
            : {}),
    };
    return style;
}
function hashString(value) {
    let hash = 0;
    for (let i = 0; i < value.length; i++) {
        hash = (hash * 31 + value.charCodeAt(i)) | 0;
    }
    return Math.abs(hash);
}
const squiggleUnderlineMask = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='8' viewBox='0 0 64 8'%3E%3Cpath d='M0 4.8 C 2.5 1.4 5.8 1.9 8.7 4.7 C 11.1 7 13.8 6.7 16.2 3.7 C 18.3 1 21.9 1.7 24.4 4.9 C 27.2 8.3 30.1 6 32.7 3.5 C 35 1.2 38 2.4 40.8 5.2 C 43.4 7.8 46.4 6.4 48.9 3.4 C 51.5 0.4 54.6 2.2 56.8 4.6 C 59.3 7.2 62 6.8 64 3.9' fill='none' stroke='black' stroke-width='4' stroke-linecap='round'/%3E%3Cpath d='M0 5.7 C 6 4.3 10 5.9 15 4.9 C 20.5 3.8 26 5.6 31 4.7 C 36.5 3.7 41.5 5.4 47 4.6 C 52.5 3.8 58 5.6 64 4.4' fill='none' stroke='black' stroke-opacity='.45' stroke-width='1.2' stroke-linecap='round'/%3E%3C/svg%3E";
const commentHighlightingStyles = `
@keyframes parhelia-comment-squiggle-pulse {
  0%, 100% {
    opacity: 0.86;
    filter: saturate(1);
  }
  50% {
    opacity: 0.96;
    filter: saturate(1.12) brightness(1.03);
  }
}

@keyframes parhelia-comment-squiggle-drift {
  from {
    background-position: 0 0, 0 0, 0 0;
    mask-position: var(--comment-squiggle-offset) 0;
    -webkit-mask-position: var(--comment-squiggle-offset) 0;
  }
  to {
    background-position: 15px 0, -21px 0, 0 0;
    mask-position: calc(var(--comment-squiggle-offset) + 64px) 0;
    -webkit-mask-position: calc(var(--comment-squiggle-offset) + 64px) 0;
  }
}
`;
function resolveCommentRange(comment, fieldElement, iframeDocument) {
    if (comment.rangeStart === undefined || comment.rangeEnd === undefined) {
        return null;
    }
    const originalRange = [
        Math.max(0, comment.rangeStart),
        Math.max(comment.rangeStart, comment.rangeEnd),
    ];
    const storedText = getStoredFieldText(comment, fieldElement, iframeDocument);
    const currentText = fieldElement.textContent || "";
    if (!storedText || storedText === currentText) {
        return originalRange;
    }
    const mapping = computeMapping(currentText, storedText);
    const mappedStart = mapping[originalRange[0]];
    const mappedEnd = mapping[originalRange[1]];
    if (mappedStart === undefined || mappedEnd === undefined) {
        return originalRange;
    }
    return [mappedStart, Math.max(mappedStart, mappedEnd)];
}
function getStoredFieldText(comment, fieldElement, iframeDocument) {
    if (!comment.fieldValue)
        return "";
    const isRichText = fieldElement.getAttribute("data-is-richtext") === "true";
    if (!isRichText)
        return comment.fieldValue;
    const template = iframeDocument.createElement("template");
    template.innerHTML = comment.fieldValue;
    return template.content.textContent || "";
}
function computeMapping(s1, s2) {
    const n = s1.length;
    const m = s2.length;
    const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
    const back = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(""));
    for (let i = 0; i <= n; i++) {
        dp[i][0] = i;
        if (i > 0)
            back[i][0] = "D";
    }
    for (let j = 0; j <= m; j++) {
        dp[0][j] = j;
        if (j > 0)
            back[0][j] = "I";
    }
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= m; j++) {
            const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
            const deletion = dp[i - 1][j] + 1;
            const insertion = dp[i][j - 1] + 1;
            const substitution = dp[i - 1][j - 1] + cost;
            let bestVal = deletion;
            let op = "D";
            if (insertion < bestVal) {
                bestVal = insertion;
                op = "I";
            }
            if (substitution < bestVal) {
                bestVal = substitution;
                op = cost === 0 ? "M" : "S";
            }
            dp[i][j] = bestVal;
            back[i][j] = op;
        }
    }
    const ops = [];
    let i = n;
    let j = m;
    while (i > 0 || j > 0) {
        const op = back[i][j];
        ops.push(op);
        if (op === "M" || op === "S") {
            i--;
            j--;
        }
        else if (op === "D") {
            i--;
        }
        else if (op === "I") {
            j--;
        }
    }
    ops.reverse();
    const mapping = new Array(m + 1);
    let iPos = 0;
    let jPos = 0;
    mapping[jPos] = iPos;
    for (const op of ops) {
        if (op === "M" || op === "S") {
            iPos++;
            jPos++;
            mapping[jPos] = iPos;
        }
        else if (op === "I") {
            jPos++;
            mapping[jPos] = iPos;
        }
        else if (op === "D") {
            iPos++;
        }
    }
    return mapping;
}
//# sourceMappingURL=CommentHighlightings.js.map