"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.IOS_ROOT_NODE = exports.ANDROID_ROOT_NODE = void 0;
exports.parsePageSource = parsePageSource;
exports.findPageSourceNodesThatCoordinatesIntersect = findPageSourceNodesThatCoordinatesIntersect;
exports.getLimitedPageSource = getLimitedPageSource;
const xmldom_1 = require("@xmldom/xmldom");
const uuid = __importStar(require("uuid"));
const mablApi_1 = require("@mablhq-internal/runtime/mablApi");
exports.ANDROID_ROOT_NODE = 'hierarchy';
exports.IOS_ROOT_NODE = 'XCUIElementTypeApplication';
function parsePageSource(pageSourceXml) {
    const parser = new xmldom_1.DOMParser();
    const parsedDocument = parser.parseFromString(pageSourceXml, 'application/xml');
    return buildPageSourceElementsTree(findRootMobileElementNode(parsedDocument));
}
function findPageSourceNodesThatCoordinatesIntersect(pageSourceXml, x, y) {
    const elementsMap = parsePageSource(pageSourceXml);
    const targetInfo = {
        x,
        y,
    };
    let matchingElements = findIntersectingElementBoundingBoxes(elementsMap, targetInfo);
    const isElementVisible = (element) => (element.visible === true || element.visible === undefined) &&
        !!element.height &&
        !!element.width;
    matchingElements = matchingElements.filter(isElementVisible);
    const notKeyboardAncestor = (e) => e.mablMetadata?.isIOSKeyboardAncestor === undefined ||
        !e.mablMetadata.isIOSKeyboardAncestor;
    matchingElements = matchingElements.filter(notKeyboardAncestor);
    matchingElements.reverse();
    return { candidates: matchingElements, elementsMap };
}
function getAttributesForNode(node) {
    const attrMap = {};
    if (node.attributes) {
        Object.values(node.attributes).forEach((attr) => {
            if (attr.name) {
                attrMap[attr.name] = attr.value;
            }
        });
    }
    return attrMap;
}
function findRootMobileElementNode(document) {
    let rootNode;
    const queue = [document];
    while (queue.length > 0 && !rootNode) {
        const currentNode = queue.pop();
        if (currentNode.nodeName === exports.ANDROID_ROOT_NODE ||
            currentNode.nodeName === exports.IOS_ROOT_NODE) {
            rootNode = currentNode;
        }
        else {
            if (currentNode.hasChildNodes()) {
                queue.push(...Array.from(currentNode.childNodes));
            }
        }
    }
    if (!rootNode) {
        throw new Error('Unable to find expected root node in page source');
    }
    return rootNode;
}
function parseBoolean(value) {
    return value === undefined ? undefined : value === 'true';
}
function buildPageSourceElementsTree(rootNode, androidScreenInfo) {
    const queue = [{ node: rootNode }];
    const elementsMap = {};
    let rootElement;
    while (queue.length) {
        const { node, parentElement } = queue.pop();
        const attrMap = getAttributesForNode(node);
        if (node.nodeName === exports.ANDROID_ROOT_NODE) {
            androidScreenInfo = {
                screenHeight: parseInt(attrMap.height),
                screenWidth: parseInt(attrMap.width),
            };
            if (!attrMap.x) {
                attrMap.x = '0';
            }
            if (!attrMap.y) {
                attrMap.y = '0';
            }
        }
        const mablMetadataId = parentElement ? uuid.v4() : uuid.NIL;
        const element = {
            mablMetadata: {
                id: mablMetadataId,
                parentId: parentElement?.mablMetadata?.id,
                childrenIds: [],
                isIOSKeyboardAncestor: isKeyboardOrAncestor(node),
            },
            ...attrMap,
            type: node.nodeName,
            x: parseInt(attrMap.x),
            y: parseInt(attrMap.y),
            width: parseInt(attrMap.width),
            height: parseInt(attrMap.height),
            index: attrMap.index ? parseInt(attrMap.index) : undefined,
            enabled: parseBoolean(attrMap.enabled),
            visible: parseBoolean(attrMap.visible),
            accessible: parseBoolean(attrMap.accessible),
            checkable: parseBoolean(attrMap.checkable),
            checked: parseBoolean(attrMap.checked),
            clickable: parseBoolean(attrMap.clickable),
            focusable: parseBoolean(attrMap.focusable),
            focused: parseBoolean(attrMap.focused),
            scrollable: parseBoolean(attrMap.scrollable),
            selected: parseBoolean(attrMap.selected),
            displayed: parseBoolean(attrMap.displayed),
            password: parseBoolean(attrMap.password),
        };
        if (attrMap.bounds &&
            androidScreenInfo?.screenHeight &&
            androidScreenInfo?.screenWidth) {
            try {
                const [[, left, top, right, bottom]] = attrMap.bounds.matchAll(/\[(-?\d*),(-?\d*)\]\[(\d*),(\d*)\]/gm);
                element.x = parseInt(left);
                element.y = parseInt(top);
                element.width = parseInt(right) - element.x;
                element.height = parseInt(bottom) - element.y;
            }
            catch (error) {
                console.error('Unable to produce proper bounds from android node element', attrMap.bounds, error);
            }
        }
        if (!rootElement) {
            rootElement = element;
        }
        const xpathInfo = getXPathForNode(node);
        element.xpath = xpathInfo.xpath;
        element.alternateXpaths = xpathInfo.alternateXpaths;
        elementsMap[mablMetadataId] = element;
        if (parentElement) {
            parentElement.mablMetadata?.childrenIds.push(mablMetadataId);
        }
        if (node.hasChildNodes()) {
            const childNodes = Array.from(node.childNodes)
                .filter((childNode) => !!childNode.attributes)
                .map((childNode) => ({
                node: childNode,
                parentElement: element,
            }));
            queue.push(...childNodes);
        }
    }
    return elementsMap;
}
function findIntersectingElementBoundingBoxes(elementsMap, targetInfo) {
    const matchingNodes = [];
    const queue = [elementsMap[uuid.NIL]];
    while (queue.length) {
        const currentElement = queue.pop();
        if (currentElement &&
            currentElement.class !== exports.ANDROID_ROOT_NODE &&
            currentElement.type !== exports.IOS_ROOT_NODE) {
            if (targetInfo.x >= currentElement.x &&
                targetInfo.x <= currentElement.x + currentElement.width &&
                targetInfo.y >= currentElement.y &&
                targetInfo.y <= currentElement.y + currentElement.height) {
                matchingNodes.push(currentElement);
            }
        }
        if (currentElement.mablMetadata?.childrenIds) {
            queue.push(...currentElement.mablMetadata.childrenIds.map((id) => elementsMap[id]));
        }
    }
    return matchingNodes;
}
function isKeyboardOrAncestor(node) {
    if (node.nodeName === 'XCUIElementTypeKeyboard') {
        return true;
    }
    if (!node.hasChildNodes()) {
        return false;
    }
    return Array.from(node.childNodes).some((n) => isKeyboardOrAncestor(n));
}
const XPATH_ATTRIBUTES = ['accessibility-id', 'content-desc', 'id', 'name'];
function getXPathForNode(node) {
    try {
        const alternateXpaths = [];
        let result = '';
        let currentNode = node;
        while (currentNode !== null &&
            currentNode.nodeName !== 'AppiumAUT' &&
            currentNode.nodeName !== '#document') {
            const nodeName = currentNode.nodeName;
            const uniqueAttributes = Object.entries(currentNode.attributes ?? {}).filter(([_attrIndex, attribute]) => XPATH_ATTRIBUTES.includes(attribute.name));
            if (uniqueAttributes.length &&
                currentNode.parentNode?.nodeName !== 'AppiumAUT') {
                uniqueAttributes.forEach(([_attrIndex, attribute]) => {
                    const altPath = `//${nodeName || '*'}[@${attribute.name}=${escapeQuotesInXPathExpression(attribute.value)}]` + result;
                    alternateXpaths.push(altPath);
                });
            }
            let nodeIndex = 1;
            let previousSibling = currentNode.previousSibling;
            while (previousSibling !== null) {
                if (previousSibling.nodeName === nodeName) {
                    nodeIndex++;
                }
                previousSibling = previousSibling.previousSibling;
            }
            let step = nodeName;
            if (nodeIndex > 1) {
                step += '[' + nodeIndex + ']';
            }
            result = '/' + step + result;
            currentNode = currentNode.parentNode;
        }
        return {
            xpath: `/${result}`,
            alternateXpaths,
        };
    }
    catch (error) {
        console.log('Unable to generate an xPath for the node', error);
        return {
            xpath: ``,
            alternateXpaths: [],
        };
    }
}
function escapeQuotesInXPathExpression(value) {
    if (!value.includes('"')) {
        return '"' + value + '"';
    }
    else if (!value.includes("'")) {
        return "'" + value + "'";
    }
    return "concat('" + value.replace(/'/g, "',\"'\",'") + "')";
}
async function getLimitedPageSource(driver, platform) {
    if (platform === mablApi_1.MobilePlatformEnum.Android) {
        return driver.getPageSource();
    }
    return driver.execute('mobile: source', {
        format: 'xml',
        excludedAttributes: 'visible,accessible',
    });
}
