import { diffChars } from "diff";
import { getLanguagesAndVersions, getPageComponents, } from "../services/contentService";
export async function resolveVersionDiffTargets({ iframe, pageItem, getItem, baseline, previousVersionCache, }) {
    const renderedFields = collectRenderedFields(iframe, pageItem);
    const renderedComponents = collectRenderedComponents(iframe);
    const itemsToResolve = getUniqueItems([
        pageItem,
        ...renderedFields.map((field) => field.descriptor.item),
    ]);
    if (renderedFields.length === 0 && renderedComponents.length === 0) {
        return { fieldTargets: [], componentTargets: [] };
    }
    const baselineEntries = baseline.type === "compare"
        ? resolveCompareBaselineEntries(itemsToResolve, baseline.compareTo)
        : await resolvePreviousVersionBaselineEntries(itemsToResolve, getItem, previousVersionCache);
    const baselinePageEntry = baselineEntries.find((entry) => entry.currentItemKey === itemKey(pageItem));
    if (baselineEntries.length === 0) {
        return { fieldTargets: [], componentTargets: [] };
    }
    const currentItemsByCurrentItemKey = new Map();
    const baselineItemsByCurrentItemKey = new Map();
    await Promise.all(baselineEntries.map(async (entry) => {
        const [currentItem, baselineItem] = await Promise.all([
            getItem(entry.currentDescriptor),
            getItem(entry.baselineDescriptor),
        ]);
        if (currentItem) {
            currentItemsByCurrentItemKey.set(entry.currentItemKey, currentItem);
        }
        if (baselineItem) {
            baselineItemsByCurrentItemKey.set(entry.currentItemKey, baselineItem);
        }
    }));
    const fieldTargets = renderedFields
        .map((renderedField) => {
        const currentItemKey = itemKey(renderedField.descriptor.item);
        const currentItem = currentItemsByCurrentItemKey.get(currentItemKey);
        const baselineItem = baselineItemsByCurrentItemKey.get(currentItemKey);
        const currentField = currentItem?.fields.find((field) => normalizeId(field.id) ===
            normalizeId(renderedField.descriptor.fieldId));
        const baselineField = baselineItem?.fields.find((field) => normalizeId(field.id) ===
            normalizeId(renderedField.descriptor.fieldId));
        if (!baselineField)
            return undefined;
        const baselineText = getFieldText(baselineField);
        const currentText = currentField
            ? getFieldText(currentField)
            : renderedField.currentText;
        if (baselineText === currentText)
            return undefined;
        const ranges = getChangedCurrentRanges(baselineText, renderedField.currentText);
        return {
            ...renderedField,
            baselineText,
            ranges,
            hasDeletionOnlyChange: ranges.length === 0,
        };
    })
        .filter((target) => !!target);
    const [currentProviderComponents, baselineProviderComponents] = await Promise.all([
        getPageComponents(pageItem),
        baselinePageEntry
            ? getPageComponents(baselinePageEntry.baselineDescriptor)
            : Promise.resolve([]),
    ]);
    const baselineProviderIds = new Set(baselineProviderComponents.map((component) => normalizeId(component.id)));
    const componentTargets = currentProviderComponents
        .filter((component) => !baselineProviderIds.has(normalizeId(component.id)))
        .map(componentToDiffTarget);
    return { fieldTargets, componentTargets };
}
function collectRenderedFields(iframe, fallbackItem) {
    const body = iframe.contentWindow?.document.body;
    if (!body)
        return [];
    const renderedFieldsByKey = new Map();
    body
        .querySelectorAll("[data-fieldid][data-itemid]")
        .forEach((element) => {
        const itemId = element.getAttribute("data-itemid");
        const fieldId = element.getAttribute("data-fieldid");
        if (!itemId || !fieldId)
            return;
        const rawVersion = parseInt(element.getAttribute("data-version") || "", 10);
        const descriptor = {
            item: {
                id: itemId,
                language: element.getAttribute("data-language") || fallbackItem.language,
                version: Number.isFinite(rawVersion)
                    ? rawVersion
                    : fallbackItem.version,
                database: fallbackItem.database,
            },
            fieldId,
        };
        const key = fieldKey(descriptor);
        if (!renderedFieldsByKey.has(key)) {
            renderedFieldsByKey.set(key, {
                descriptor,
                currentText: element.textContent || "",
            });
        }
    });
    return Array.from(renderedFieldsByKey.values());
}
function resolveCompareBaselineEntries(currentItems, compareTo) {
    if (!compareTo?.language)
        return [];
    return currentItems
        .map((item) => {
        const baselineDescriptor = {
            ...item,
            language: compareTo.language,
            version: compareTo.version,
            database: item.database ?? compareTo.database,
        };
        return {
            currentItemKey: itemKey(item),
            currentDescriptor: item,
            baselineDescriptor,
        };
    })
        .filter((entry) => !!entry);
}
async function resolvePreviousVersionBaselineEntries(currentItems, getItem, previousVersionCache) {
    const entries = await Promise.all(currentItems.map(async (item) => {
        const currentItem = await getItem(item);
        const currentVersion = currentItem?.version || item.version;
        if (!currentVersion || currentVersion <= 1)
            return undefined;
        const cacheKey = `${normalizeId(item.id)}|${item.language}|${currentVersion}`;
        let previousVersion = previousVersionCache.get(cacheKey);
        if (previousVersion === undefined) {
            const result = await getLanguagesAndVersions({
                ...item,
                version: currentVersion,
            });
            const versions = result?.data?.versions?.map((x) => x.version) || [];
            previousVersion =
                versions
                    .filter((version) => version < currentVersion)
                    .sort((a, b) => b - a)[0] ?? null;
            previousVersionCache.set(cacheKey, previousVersion);
        }
        if (!previousVersion)
            return undefined;
        return {
            currentItemKey: itemKey(item),
            currentDescriptor: item,
            baselineDescriptor: {
                ...item,
                version: previousVersion,
            },
        };
    }));
    return entries.filter((entry) => !!entry);
}
function getUniqueItems(itemDescriptors) {
    const items = new Map();
    itemDescriptors.forEach((item) => {
        const key = itemKey(item);
        if (!items.has(key))
            items.set(key, item);
    });
    return Array.from(items.values());
}
function collectRenderedComponents(iframe) {
    const body = iframe.contentWindow?.document.body;
    if (!body)
        return [];
    const components = new Map();
    body
        .querySelectorAll("script[data-component-start]")
        .forEach((element) => {
        const id = element.getAttribute("data-component-start");
        if (!id || components.has(normalizeId(id)))
            return;
        components.set(normalizeId(id), {
            id,
            visualId: id,
            dataItemId: element.getAttribute("data-itemid"),
            dataTypeId: element.getAttribute("data-typeid"),
            dataLayoutId: element.getAttribute("data-layoutid"),
            dataPlaceholderKey: element.getAttribute("data-placeholderkey"),
        });
    });
    return Array.from(components.values());
}
function componentToDiffTarget(component) {
    const visualId = component.dataSourceItem?.id || component.id;
    return {
        id: component.id,
        visualId,
        dataItemId: component.dataSourceItem?.id,
        dataTypeId: component.renderingItemId,
        providerPlaceholder: component.placeholder,
        providerRenderingItemId: component.renderingItemId,
    };
}
function getChangedCurrentRanges(baselineText, currentText) {
    const ranges = [];
    let currentPosition = 0;
    diffChars(baselineText, currentText).forEach((part) => {
        if (part.added) {
            ranges.push([currentPosition, currentPosition + part.value.length]);
            currentPosition += part.value.length;
            return;
        }
        if (!part.removed) {
            currentPosition += part.value.length;
        }
    });
    return mergeRanges(ranges);
}
function mergeRanges(ranges) {
    if (ranges.length <= 1)
        return ranges;
    const sorted = [...ranges].sort((a, b) => a[0] - b[0]);
    const merged = [];
    sorted.forEach(([start, end]) => {
        const previous = merged[merged.length - 1];
        if (previous && start <= previous[1] + 1) {
            previous[1] = Math.max(previous[1], end);
            return;
        }
        merged.push([start, end]);
    });
    return merged;
}
export function findFieldElements(iframe, descriptor) {
    const body = iframe.contentWindow?.document.body;
    if (!body)
        return [];
    return Array.from(body.querySelectorAll("[data-fieldid][data-itemid]")).filter((element) => {
        const itemId = element.getAttribute("data-itemid");
        const fieldId = element.getAttribute("data-fieldid");
        return (normalizeId(itemId) === normalizeId(descriptor.item.id) &&
            normalizeId(fieldId) === normalizeId(descriptor.fieldId));
    });
}
export function getTextNodeAtOffset(root, offset) {
    const doc = root.ownerDocument;
    const nodeFilter = doc.defaultView?.NodeFilter ?? NodeFilter;
    const walker = doc.createTreeWalker(root, nodeFilter.SHOW_TEXT);
    let currentNode;
    while ((currentNode = walker.nextNode())) {
        const nodeLength = currentNode.textContent?.length || 0;
        if (offset <= nodeLength) {
            return { node: currentNode, offset };
        }
        offset -= nodeLength;
    }
    return null;
}
function getFieldText(field) {
    const value = typeof field.rawValue === "string"
        ? field.rawValue
        : typeof field.value === "string"
            ? field.value
            : field.value == null
                ? ""
                : String(field.value);
    return stripHtml(value);
}
export function findComponentRectById(iframe, componentId) {
    const body = iframe.contentWindow?.document.body;
    if (!body)
        return null;
    const startMarker = body.querySelector(`[data-component-start='${componentId}']`);
    const endMarker = body.querySelector(`[data-component-end='${componentId}']`);
    let startElement = startMarker?.nextElementSibling;
    while (startElement &&
        startElement !== endMarker &&
        startElement.tagName === "SCRIPT") {
        startElement = startElement.nextElementSibling;
    }
    if (!startElement || startElement === endMarker) {
        startElement = body.querySelector(`[data-component-id='${componentId}']`);
    }
    if (!startElement)
        return null;
    let endElement = endMarker?.previousElementSibling || startElement;
    while (endElement && endElement.tagName === "SCRIPT") {
        endElement = endElement.previousElementSibling;
    }
    const startRect = startElement.getBoundingClientRect();
    const endRect = (endElement || startElement).getBoundingClientRect();
    return new DOMRect(startRect.x, startRect.y, endRect.x + endRect.width - startRect.x, endRect.y + endRect.height - startRect.y);
}
function stripHtml(value) {
    if (!value.includes("<"))
        return value;
    const doc = globalThis.document;
    if (!doc)
        return value.replace(/<[^>]*>/g, "");
    const element = doc.createElement("div");
    element.innerHTML = value;
    return element.textContent || element.innerText || "";
}
export function fieldKey(descriptor) {
    return `${itemKey(descriptor.item)}|${normalizeId(descriptor.fieldId)}`;
}
function itemKey(item) {
    return `${normalizeId(item.id)}|${item.language}|${item.version}`;
}
function normalizeId(id) {
    return (id || "").replace(/[{}]/g, "").toLowerCase();
}
//# sourceMappingURL=versionDiffTargets.js.map