import * as PropertySymbol from '../../PropertySymbol.js';
import NodeTypeEnum from './NodeTypeEnum.js';
/**
 * Node utility.
 */
export default class NodeUtility {
    /**
     * Returns whether the passed node is a text node, and narrows its type.
     *
     * @param node The node to be tested.
     * @returns "true" if the node is a text node.
     */
    static isTextNode(node) {
        return node?.[PropertySymbol.nodeType] === NodeTypeEnum.textNode;
    }
    /**
     * Returns boolean indicating if "ancestorNode" is an inclusive ancestor of "referenceNode".
     *
     * Based on:
     * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/helpers/node.js
     *
     * @see https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor
     * @param ancestorNode Ancestor node.
     * @param referenceNode Reference node.
     * @param [includeShadowRoots = false] Include shadow roots.
     * @returns "true" if inclusive ancestor.
     */
    static isInclusiveAncestor(ancestorNode, referenceNode, includeShadowRoots = false) {
        if (ancestorNode === null || referenceNode === null) {
            return false;
        }
        if (ancestorNode === referenceNode) {
            return true;
        }
        if (!ancestorNode[PropertySymbol.nodeArray].length) {
            return false;
        }
        if (includeShadowRoots &&
            referenceNode[PropertySymbol.isConnected] !== ancestorNode[PropertySymbol.isConnected]) {
            return false;
        }
        if (includeShadowRoots &&
            ancestorNode === referenceNode[PropertySymbol.ownerDocument] &&
            referenceNode[PropertySymbol.isConnected]) {
            return true;
        }
        let parent = referenceNode[PropertySymbol.parentNode];
        while (parent) {
            if (ancestorNode === parent) {
                return true;
            }
            parent = parent[PropertySymbol.parentNode]
                ? parent[PropertySymbol.parentNode]
                : includeShadowRoots && parent.host
                    ? parent.host
                    : null;
        }
        return false;
    }
    /**
     * Returns boolean indicating if nodeB is following nodeA in the document tree.
     *
     * Based on:
     * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/helpers/node.js
     *
     * @see https://dom.spec.whatwg.org/#concept-tree-following
     * @param nodeA Node A.
     * @param nodeB Node B.
     * @returns "true" if following.
     */
    static isFollowing(nodeA, nodeB) {
        if (nodeA === nodeB) {
            return false;
        }
        let current = nodeB;
        while (current) {
            current = this.following(current);
            if (current === nodeA) {
                return true;
            }
        }
        return false;
    }
    /**
     * Node length.
     *
     * Based on:
     * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/helpers/node.js
     *
     * @see https://dom.spec.whatwg.org/#concept-node-length
     * @param node Node.
     * @returns Node length.
     */
    static getNodeLength(node) {
        switch (node[PropertySymbol.nodeType]) {
            case NodeTypeEnum.documentTypeNode:
                return 0;
            case NodeTypeEnum.textNode:
            case NodeTypeEnum.processingInstructionNode:
            case NodeTypeEnum.commentNode:
                return node.data.length;
            default:
                return node[PropertySymbol.nodeArray].length;
        }
    }
    /**
     * Returns boolean indicating if nodeB is following nodeA in the document tree.
     *
     * Based on:
     * https://github.com/jsdom/js-symbol-tree/blob/master/lib/SymbolTree.js#L220
     *
     * @param node Node.
     * @param [root] Root.
     * @returns Following node.
     */
    static following(node, root) {
        const firstChild = node.firstChild;
        if (firstChild) {
            return firstChild;
        }
        let current = node;
        while (current) {
            if (current === root) {
                return null;
            }
            const nextSibling = current.nextSibling;
            if (nextSibling) {
                return nextSibling;
            }
            current = current[PropertySymbol.parentNode];
        }
        return null;
    }
    /**
     * Returns the next sibling or parents sibling.
     *
     * @param node Node.
     * @returns Next descendant node.
     */
    static nextDescendantNode(node) {
        while (node && !node.nextSibling) {
            node = node[PropertySymbol.parentNode];
        }
        if (!node) {
            return null;
        }
        return node.nextSibling;
    }
    /**
     * Needed by https://dom.spec.whatwg.org/#concept-node-equals
     *
     * @param elementA
     * @param elementB
     */
    static attributeListsEqual(elementA, elementB) {
        const attributesA = Array.from(elementA[PropertySymbol.attributes][PropertySymbol.namedItems].values());
        const attributesB = Array.from(elementB[PropertySymbol.attributes][PropertySymbol.namedItems].values());
        for (const attributeA of attributesA) {
            let found = false;
            for (const attributeB of attributesB) {
                if (attributeA[PropertySymbol.namespaceURI] === attributeB[PropertySymbol.namespaceURI] &&
                    attributeA.localName === attributeB.localName &&
                    attributeA[PropertySymbol.value] === attributeB[PropertySymbol.value]) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                return false;
            }
        }
        return true;
    }
    /**
     * Check if node nodeA equals node nodeB.
     * Reference: https://dom.spec.whatwg.org/#concept-node-equals
     *
     * @param nodeA Node A.
     * @param nodeB Node B.
     */
    static isEqualNode(nodeA, nodeB) {
        if (nodeA[PropertySymbol.nodeType] !== nodeB[PropertySymbol.nodeType]) {
            return false;
        }
        switch (nodeA[PropertySymbol.nodeType]) {
            case NodeTypeEnum.documentTypeNode:
                const documentTypeA = nodeA;
                const documentTypeB = nodeB;
                if (documentTypeA.name !== documentTypeB.name ||
                    documentTypeA.publicId !== documentTypeB.publicId ||
                    documentTypeA.systemId !== documentTypeB.systemId) {
                    return false;
                }
                break;
            case NodeTypeEnum.elementNode:
                const elementA = nodeA;
                const elementB = nodeB;
                if (elementA[PropertySymbol.namespaceURI] !== elementB[PropertySymbol.namespaceURI] ||
                    elementA[PropertySymbol.prefix] !== elementB[PropertySymbol.prefix] ||
                    elementA[PropertySymbol.localName] !== elementB[PropertySymbol.localName] ||
                    elementA[PropertySymbol.attributes][PropertySymbol.namespaceItems].size !==
                        elementB[PropertySymbol.attributes][PropertySymbol.namespaceItems].size) {
                    return false;
                }
                break;
            case NodeTypeEnum.attributeNode:
                const attributeA = nodeA;
                const attributeB = nodeB;
                if (attributeA[PropertySymbol.namespaceURI] !== attributeB[PropertySymbol.namespaceURI] ||
                    attributeA.localName !== attributeB.localName ||
                    attributeA[PropertySymbol.value] !== attributeB[PropertySymbol.value]) {
                    return false;
                }
                break;
            case NodeTypeEnum.processingInstructionNode:
                const processingInstructionA = nodeA;
                const processingInstructionB = nodeB;
                if (processingInstructionA.target !== processingInstructionB.target ||
                    processingInstructionA.data !== processingInstructionB.data) {
                    return false;
                }
                break;
            case NodeTypeEnum.textNode:
            case NodeTypeEnum.commentNode:
                const textOrCommentA = nodeA;
                const textOrCommentB = nodeB;
                if (textOrCommentA.data !== textOrCommentB.data) {
                    return false;
                }
                break;
        }
        if (nodeA[PropertySymbol.nodeType] === NodeTypeEnum.elementNode &&
            !NodeUtility.attributeListsEqual(nodeA, nodeB)) {
            return false;
        }
        if (nodeA[PropertySymbol.nodeArray].length !==
            nodeB[PropertySymbol.nodeArray].length) {
            return false;
        }
        for (let i = 0; i < nodeA[PropertySymbol.nodeArray].length; i++) {
            const childNodeA = nodeA[PropertySymbol.nodeArray][i];
            const childNodeB = nodeB[PropertySymbol.nodeArray][i];
            if (!NodeUtility.isEqualNode(childNodeA, childNodeB)) {
                return false;
            }
        }
        return true;
    }
}
//# sourceMappingURL=NodeUtility.js.map