"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.findAndScoreElementAndAncestorPairings = findAndScoreElementAndAncestorPairings;
const mablApi_1 = require("@mablhq-internal/runtime/mablApi");
const pageSourceUtil_1 = require("../../util/pageSourceUtil");
const matchers_1 = require("./android/matchers");
const matchers_2 = require("./ios/matchers");
const AncestorAndTargetFindStrategy_1 = require("./strategies/AncestorAndTargetFindStrategy");
const MAX_ANCESTOR_DEPTH = 10;
const IOS_ROOT_WINDOW = 'XCUIElementTypeWindow';
function getAndroidAttributeWeightMap() {
    return {
        'resource-id': matchers_1.ANDROID_ATTRIBUTE_RESOURCE_ID_WEIGHT,
        'content-desc': matchers_1.ANDROID_ATTRIBUTE_CONTENT_DESC_WEIGHT,
        text: matchers_1.ANDROID_ATTRIBUTE_TEXT_WEIGHT,
        bounds: matchers_1.ANDROID_ATTRIBUTE_RECT_WEIGHT,
        checkable: matchers_1.ANDROID_ATTRIBUTE_CHECKABLE_WEIGHT,
        checked: matchers_1.ANDROID_ATTRIBUTE_CHECKED_WEIGHT,
        class: matchers_1.ANDROID_ATTRIBUTE_CLASS_WEIGHT,
        clickable: matchers_1.ANDROID_ATTRIBUTE_CLICKABLE_WEIGHT,
        displayed: matchers_1.ANDROID_ATTRIBUTE_DISPLAYED_WEIGHT,
        focusable: matchers_1.ANDROID_ATTRIBUTE_FOCUSABLE_WEIGHT,
        focused: matchers_1.ANDROID_ATTRIBUTE_FOCUSED_WEIGHT,
        hint: matchers_1.ANDROID_ATTRIBUTE_HINT_WEIGHT,
        'long-clickable': matchers_1.ANDROID_ATTRIBUTE_LONG_CLICKABLE_WEIGHT,
        password: matchers_1.ANDROID_ATTRIBUTE_PASSWORD_WEIGHT,
        scrollable: matchers_1.ANDROID_ATTRIBUTE_SCROLLABLE_WEIGHT,
        selected: matchers_1.ANDROID_ATTRIBUTE_SELECTED_WEIGHT,
    };
}
function getiOSAttributeWeightMap() {
    return {
        label: matchers_2.IOS_ATTRIBUTE_LABEL_WEIGHT,
        name: matchers_2.IOS_ATTRIBUTE_NAME_WEIGHT,
        type: matchers_2.IOS_ATTRIBUTE_TYPE_WEIGHT,
        index: matchers_2.IOS_ATTRIBUTE_INDEX_WEIGHT,
        enabled: matchers_2.IOS_ATTRIBUTE_ENABLED_WEIGHT,
        selected: matchers_2.IOS_ATTRIBUTE_SELECTED_WEIGHT,
    };
}
function findAndScoreElementAndAncestorPairings(pageSourceElement, elementsMap, platform) {
    const targetElementId = pageSourceElement.mablMetadata?.id;
    if (!targetElementId) {
        return [];
    }
    const targetAndAncestorElements = [pageSourceElement];
    let currentAncestor = elementsMap[pageSourceElement.mablMetadata?.parentId ?? ''];
    let ancestorDepth = 1;
    while (!!currentAncestor &&
        currentAncestor.class !== pageSourceUtil_1.ANDROID_ROOT_NODE &&
        currentAncestor.type !== pageSourceUtil_1.IOS_ROOT_NODE &&
        currentAncestor.type !== IOS_ROOT_WINDOW &&
        ancestorDepth <= MAX_ANCESTOR_DEPTH) {
        targetAndAncestorElements.push(currentAncestor);
        currentAncestor = elementsMap[currentAncestor.mablMetadata?.parentId ?? ''];
        ancestorDepth++;
    }
    const targetElementCandidateIdMap = {};
    targetAndAncestorElements.forEach((targetElement) => {
        if (targetElement.mablMetadata?.id) {
            targetElementCandidateIdMap[targetElement.mablMetadata.id] =
                new Set();
        }
    });
    const allPageSourceElements = Object.values(elementsMap);
    allPageSourceElements.forEach((element) => {
        if (!element.mablMetadata?.id) {
            return;
        }
        targetAndAncestorElements.forEach((targetElement) => {
            if (targetElement.mablMetadata?.id &&
                isElementACandidate(targetElement, element, platform)) {
                targetElementCandidateIdMap[targetElement.mablMetadata.id].add(element.mablMetadata.id);
            }
        });
    });
    const targetAndAncestorCandidatesAndScores = {};
    Object.entries(targetElementCandidateIdMap).forEach(([targetElementId, candidateElementIds]) => {
        const scoredElements = scoreCandidatesWithTargetElement(targetElementId, [...candidateElementIds], elementsMap, platform);
        targetAndAncestorCandidatesAndScores[targetElementId] = scoredElements;
    });
    const ancestorAdjustedResults = adjustTargetCandidateScoresByAncestors(targetElementId, targetAndAncestorCandidatesAndScores);
    ancestorAdjustedResults.sort((a, b) => {
        if (b.adjustedScoreDifference === a.adjustedScoreDifference &&
            !!b.ancestorBaseScore &&
            !!a.ancestorBaseScore) {
            return b.ancestorBaseScore - a.ancestorBaseScore;
        }
        return b.adjustedScoreDifference - a.adjustedScoreDifference;
    });
    return ancestorAdjustedResults;
}
function adjustTargetCandidateScoresByAncestors(targetElementId, targetAndAncestorCandidatesAndScores) {
    const targetElementCandidates = targetAndAncestorCandidatesAndScores[targetElementId];
    const targetElementAndScore = targetElementCandidates.shift();
    if (!targetElementAndScore) {
        throw new Error('Did not find any valid targets');
    }
    const { element: targetElement, score: targetElementScore } = targetElementAndScore;
    if (!targetElementCandidates.length) {
        return [
            generateAncestorAdjustedResults(targetElement, targetElementScore, targetElementScore),
        ];
    }
    const ancestorCandidatesMap = { ...targetAndAncestorCandidatesAndScores };
    delete ancestorCandidatesMap[targetElementId];
    if (!Object.keys(ancestorCandidatesMap).length) {
        const { element: secondCandidateElement, score: baseSecondCandidateScore } = targetElementCandidates[0];
        return [
            generateAncestorAdjustedResults(targetElement, targetElementScore, targetElementScore, secondCandidateElement, baseSecondCandidateScore, baseSecondCandidateScore),
        ];
    }
    const ancestorAdjustedResults = [];
    Object.values(ancestorCandidatesMap).forEach((scoredAncestorCandidates) => {
        if (!scoredAncestorCandidates.length) {
            return;
        }
        let adjustedTargetScore = targetElementScore;
        let baseSecondCandidateScore = 0;
        let adjustedSecondHighestScore = 0;
        let secondCandidateElement;
        const { element: correctAncestorElement, score: correctAncestorScore } = scoredAncestorCandidates[0];
        let scoreAdjustmentWeight = AncestorAndTargetFindStrategy_1.ANCESTOR_BOUNDS_WEIGHT;
        if (scoredAncestorCandidates[1]) {
            const { score } = scoredAncestorCandidates[1];
            const weightAdjustmentPercentage = Math.min(AncestorAndTargetFindStrategy_1.MIN_ANCESTOR_WEIGHT_PERCENTAGE, (correctAncestorScore - score) / correctAncestorScore);
            scoreAdjustmentWeight =
                scoreAdjustmentWeight * weightAdjustmentPercentage;
        }
        adjustedTargetScore = isEncapsulatedByAncestor(correctAncestorElement, targetElement)
            ? targetElementScore + scoreAdjustmentWeight
            : targetElementScore;
        targetElementCandidates.forEach((targetElementCandidate) => {
            const { element: candidate, score } = targetElementCandidate;
            const adjustedScore = isEncapsulatedByAncestor(correctAncestorElement, candidate)
                ? score + scoreAdjustmentWeight
                : score;
            if (adjustedScore > adjustedSecondHighestScore) {
                adjustedSecondHighestScore = adjustedScore;
                baseSecondCandidateScore = score;
                secondCandidateElement = candidate;
            }
        });
        ancestorAdjustedResults.push(generateAncestorAdjustedResults(targetElement, targetElementScore, adjustedTargetScore, secondCandidateElement, baseSecondCandidateScore, adjustedSecondHighestScore, correctAncestorElement, correctAncestorScore));
    });
    return ancestorAdjustedResults;
}
function generateAncestorAdjustedResults(targetElement, targetElementScore, adjustedTargetScore, secondCandidateElement, secondCandidateScore = 0, adjustedSecondCandidateScore = 0, ancestorElement, ancestorBaseScore) {
    return {
        ancestorElement,
        ancestorBaseScore,
        targetElement,
        secondCandidateElement,
        baseTargetScore: targetElementScore,
        baseSecondCandidateScore: secondCandidateScore,
        baseScoreDifference: targetElementScore - secondCandidateScore,
        adjustedTargetScore,
        adjustedSecondHighestScore: adjustedSecondCandidateScore,
        adjustedScoreDifference: adjustedTargetScore - adjustedSecondCandidateScore,
    };
}
function isEncapsulatedByAncestor(ancestorElement, element) {
    return (ancestorElement.x <= element.x &&
        ancestorElement.y <= element.y &&
        ancestorElement.x + ancestorElement.width >= element.x + element.width &&
        ancestorElement.y + ancestorElement.height >= element.y + element.height);
}
function isElementACandidate(targetElement, currentElement, platform) {
    if (targetElement.class !== currentElement.class ||
        targetElement.type !== currentElement.type) {
        return false;
    }
    if (isAttributePresent(currentElement, 'xpath') &&
        targetElement.xpath === currentElement.xpath) {
        return true;
    }
    switch (platform) {
        case mablApi_1.MobilePlatformEnum.Android:
            const matchesContentDesc = isAttributePresent(currentElement, 'content-desc') &&
                targetElement['content-desc'] === currentElement['content-desc'];
            const matchesResourceId = isAttributePresent(currentElement, 'resource-id') &&
                targetElement['resource-id'] === currentElement['resource-id'];
            const matchesText = isAttributePresent(currentElement, 'text') &&
                targetElement.text === currentElement.text;
            return matchesContentDesc || matchesResourceId || matchesText;
        case mablApi_1.MobilePlatformEnum.Ios:
            const matchesAccessibilityId = isAttributePresent(currentElement, 'name') &&
                targetElement.name === currentElement.name;
            const matchesLabel = isAttributePresent(currentElement, 'label') &&
                targetElement.label === currentElement.label;
            return matchesAccessibilityId || matchesLabel;
    }
}
function scoreCandidatesWithTargetElement(targetElementId, candidateElementIds, elementsMap, platform) {
    const attributeWeightMap = platform === mablApi_1.MobilePlatformEnum.Ios
        ? getiOSAttributeWeightMap()
        : getAndroidAttributeWeightMap();
    const targetElement = elementsMap[targetElementId];
    const candidateElements = candidateElementIds.map((candidateElementId) => elementsMap[candidateElementId]);
    const adjustedWeightMap = Object.entries(attributeWeightMap).reduce((weightMap, [attribute, baseWeight]) => {
        const attributeName = attribute;
        const expectedAttributeValue = targetElement[attributeName];
        const matchingCandidatesCount = candidateElements.filter((candidateElement) => candidateElement[attributeName] === expectedAttributeValue).length;
        weightMap[attributeName] =
            baseWeight /
                (matchingCandidatesCount > 0 ? matchingCandidatesCount : 1);
        return weightMap;
    }, {});
    let scoredCandidates = candidateElements.map((candidateElement) => {
        const score = Object.entries(adjustedWeightMap).reduce((score, [attribute, weight]) => {
            const attributeValue = candidateElement[attribute];
            if (attributeValue === undefined) {
                return score;
            }
            const expectedAttributeValue = targetElement[attribute];
            if (attributeValue === expectedAttributeValue) {
                return score + weight;
            }
            return score;
        }, 0);
        return { element: candidateElement, score };
    });
    if (platform === mablApi_1.MobilePlatformEnum.Ios) {
        const matchingBoundsCandidatesCount = candidateElements.filter((candidateElement) => elementMatchesTargetBounds(targetElement, candidateElement)).length;
        const adjustedBoundsWeight = matchers_2.IOS_ATTRIBUTE_RECT_WEIGHT /
            (matchingBoundsCandidatesCount > 0 ? matchingBoundsCandidatesCount : 1);
        scoredCandidates = scoredCandidates.map((scoredCandidate) => {
            const { element, score } = scoredCandidate;
            if (elementMatchesTargetBounds(targetElement, element)) {
                const newScore = score + adjustedBoundsWeight;
                return { element, score: newScore };
            }
            return { element, score };
        });
    }
    scoredCandidates.sort((a, b) => b.score - a.score);
    return scoredCandidates;
}
function elementMatchesTargetBounds(targetElement, candidateElement) {
    return (candidateElement.x === targetElement.x &&
        candidateElement.y === targetElement.y &&
        candidateElement.height === targetElement.height &&
        candidateElement.width === targetElement.width);
}
function isAttributePresent(element, attribute) {
    return element[attribute] !== undefined && element[attribute] !== null;
}
