import * as PropertySymbol from '../PropertySymbol.js';
import NodeList from '../nodes/node/NodeList.js';
import NodeTypeEnum from '../nodes/node/NodeTypeEnum.js';
import SelectorCombinatorEnum from './SelectorCombinatorEnum.js';
import SelectorParser from './SelectorParser.js';
/**
 * Invalid Selector RegExp.
 */
const INVALID_SELECTOR_REGEXP = /^[.#\[]?\d|[.#]$/;
/**
 * Utility for query selection in an HTML element.
 *
 * @class QuerySelector
 */
export default class QuerySelector {
    /**
     * Finds elements based on a query selector.
     *
     * @param node Node to search in.
     * @param selector Selector.
     * @returns HTML elements.
     */
    static querySelectorAll(node, selector) {
        const window = node[PropertySymbol.window];
        if (selector === '') {
            throw new window.DOMException(`Failed to execute 'querySelectorAll' on '${node.constructor.name}': The provided selector is empty.`);
        }
        if (typeof selector === 'function') {
            throw new window.DOMException(`Failed to execute 'querySelectorAll' on '${node.constructor.name}': '${selector}' is not a valid selector.`);
        }
        if (typeof selector === 'symbol') {
            throw new window.TypeError(`Failed to execute 'querySelectorAll' on '${node.constructor.name}': Cannot convert a Symbol value to a string`);
        }
        selector = String(selector);
        if (INVALID_SELECTOR_REGEXP.test(selector)) {
            throw new window.DOMException(`Failed to execute 'querySelectorAll' on '${node.constructor.name}': '${selector}' is not a valid selector.`);
        }
        const cache = node[PropertySymbol.cache].querySelectorAll;
        const cachedResult = cache.get(selector);
        if (cachedResult) {
            if (cachedResult.result !== null) {
                const result = cachedResult.result.deref();
                if (result) {
                    return result;
                }
            }
            cache.delete(selector);
        }
        const scope = node[PropertySymbol.nodeType] === NodeTypeEnum.documentNode
            ? node.documentElement
            : node;
        const groups = new SelectorParser({ window, scope }).getSelectorGroups(selector);
        const items = [];
        const nodeList = new NodeList(PropertySymbol.illegalConstructor, items);
        const matchesMap = new Map();
        const matchedPositions = [];
        const cachedItem = {
            result: new WeakRef(nodeList)
        };
        node[PropertySymbol.cache].querySelectorAll.set(selector, cachedItem);
        if (node[PropertySymbol.isConnected]) {
            // Document is affected for the ":target" selector
            (node[PropertySymbol.ownerDocument] || node)[PropertySymbol.affectsCache].push(cachedItem);
        }
        for (const items of groups) {
            const matches = node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode
                ? this.findAll(node, [node], items, cachedItem)
                : this.findAll(null, node[PropertySymbol.elementArray], items, cachedItem);
            for (const match of matches) {
                if (!matchesMap.has(match.documentPosition)) {
                    matchesMap.set(match.documentPosition, match.element);
                    matchedPositions.push(match.documentPosition);
                }
            }
        }
        const keys = matchedPositions.sort();
        for (let i = 0, max = keys.length; i < max; i++) {
            items.push(matchesMap.get(keys[i]));
        }
        return nodeList;
    }
    /**
     * Finds an element based on a query selector.
     *
     * @param node Node to search in.
     * @param selector Selector.
     * @returns HTML element.
     */
    static querySelector(node, selector) {
        const window = node[PropertySymbol.window];
        if (selector === '') {
            throw new window.DOMException(`Failed to execute 'querySelector' on '${node.constructor.name}': The provided selector is empty.`);
        }
        if (typeof selector === 'function') {
            throw new window.DOMException(`Failed to execute 'querySelector' on '${node.constructor.name}': '${selector}' is not a valid selector.`);
        }
        if (typeof selector === 'symbol') {
            throw new window.TypeError(`Failed to execute 'querySelector' on '${node.constructor.name}': Cannot convert a Symbol value to a string`);
        }
        selector = String(selector);
        if (INVALID_SELECTOR_REGEXP.test(selector)) {
            throw new window.DOMException(`Failed to execute 'querySelector' on '${node.constructor.name}': '${selector}' is not a valid selector.`);
        }
        const cachedResult = node[PropertySymbol.cache].querySelector.get(selector);
        if (cachedResult) {
            if (cachedResult.result !== null) {
                if (!cachedResult.result.element) {
                    return null;
                }
                const result = cachedResult.result.element.deref();
                if (result) {
                    return result;
                }
            }
            node[PropertySymbol.cache].querySelector.delete(selector);
        }
        const cachedItem = {
            result: { element: null }
        };
        node[PropertySymbol.cache].querySelector.set(selector, cachedItem);
        if (node[PropertySymbol.isConnected]) {
            // Document is affected for the ":target" selector
            (node[PropertySymbol.ownerDocument] || node)[PropertySymbol.affectsCache].push(cachedItem);
        }
        let bestMatch = null;
        const matchesMap = new Map();
        const scope = node[PropertySymbol.nodeType] === NodeTypeEnum.documentNode
            ? node.documentElement
            : node;
        for (const items of new SelectorParser({ window, scope }).getSelectorGroups(selector)) {
            const match = node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode
                ? this.findFirst(node, [node], items, cachedItem)
                : this.findFirst(null, node[PropertySymbol.elementArray], items, cachedItem);
            if (match && !matchesMap.has(match.documentPosition)) {
                matchesMap.set(match.documentPosition, true);
                if (!bestMatch || match.documentPosition < bestMatch.documentPosition) {
                    bestMatch = match;
                }
            }
        }
        const element = bestMatch?.element || null;
        cachedItem.result = { element: element ? new WeakRef(element) : null };
        return element;
    }
    /**
     * Checks if an element matches a selector and returns priority weight.
     *
     * @param element Element to match.
     * @param selector Selector to match with.
     * @param [options] Options.
     * @param [options.scope] Scope.
     * @param [options.ignoreErrors] Ignores errors.
     * @returns Result.
     */
    static matches(element, selector, options) {
        const ignoreErrors = options?.ignoreErrors;
        const window = element[PropertySymbol.window];
        if (selector === '*') {
            return {
                priorityWeight: 1
            };
        }
        if (selector === '') {
            if (ignoreErrors) {
                return null;
            }
            throw new window.DOMException(`Failed to execute 'matches' on '${element.constructor.name}': The provided selector is empty.`);
        }
        if (typeof selector === 'function') {
            if (ignoreErrors) {
                return null;
            }
            throw new window.DOMException(`Failed to execute 'matches' on '${element.constructor.name}': '${selector}' is not a valid selector.`);
        }
        if (typeof selector === 'symbol') {
            if (ignoreErrors) {
                return null;
            }
            throw new window.TypeError(`Cannot convert a Symbol value to a string`);
        }
        selector = String(selector);
        if (INVALID_SELECTOR_REGEXP.test(selector)) {
            if (ignoreErrors) {
                return null;
            }
            throw new window.DOMException(`Failed to execute 'matches' on '${element.constructor.name}': '${selector}' is not a valid selector.`);
        }
        const cachedResult = element[PropertySymbol.cache].matches.get(selector);
        if (cachedResult) {
            if (cachedResult.result !== null) {
                return cachedResult.result.match;
            }
            element[PropertySymbol.cache].matches.delete(selector);
        }
        const cachedItem = {
            result: { match: null }
        };
        element[PropertySymbol.cache].matches.set(selector, cachedItem);
        if (element[PropertySymbol.isConnected]) {
            // Document is affected for the ":target" selector
            (element[PropertySymbol.ownerDocument] || element)[PropertySymbol.affectsCache].push(cachedItem);
        }
        const scopeOrElement = options?.scope || element;
        const scope = scopeOrElement[PropertySymbol.nodeType] === NodeTypeEnum.documentNode
            ? scopeOrElement.documentElement
            : scopeOrElement;
        for (const items of new SelectorParser({
            ignoreErrors: options?.ignoreErrors,
            window,
            scope
        }).getSelectorGroups(selector)) {
            const result = this.matchSelector(element, items.reverse(), cachedItem);
            if (result) {
                cachedItem.result.match = result;
                return result;
            }
        }
        return null;
    }
    /**
     * Checks if a node matches a selector.
     *
     * @param element Target element.
     * @param currentElement
     * @param selectorItems Selector items.
     * @param cachedItem Cached item.
     * @param [previousSelectorItem] Previous selector item.
     * @param [priorityWeight] Priority weight.
     * @returns Result.
     */
    static matchSelector(element, selectorItems, cachedItem, previousSelectorItem = null, priorityWeight = 0) {
        const selectorItem = selectorItems[0];
        if (!selectorItem) {
            return null;
        }
        const result = selectorItem.match(element);
        if (result) {
            if (selectorItems.length === 1) {
                return {
                    priorityWeight: priorityWeight + result.priorityWeight
                };
            }
            switch (selectorItem.combinator) {
                case SelectorCombinatorEnum.adjacentSibling:
                    const previousElementSibling = element.previousElementSibling;
                    if (previousElementSibling) {
                        previousElementSibling[PropertySymbol.affectsCache].push(cachedItem);
                        const match = this.matchSelector(previousElementSibling, selectorItems.slice(1), cachedItem, selectorItem, priorityWeight + result.priorityWeight);
                        if (match) {
                            return match;
                        }
                    }
                    break;
                case SelectorCombinatorEnum.none:
                case SelectorCombinatorEnum.child:
                case SelectorCombinatorEnum.descendant:
                    const parentElement = element.parentElement;
                    if (parentElement) {
                        parentElement[PropertySymbol.affectsCache].push(cachedItem);
                        const match = this.matchSelector(parentElement, selectorItems.slice(1), cachedItem, selectorItem, priorityWeight + result.priorityWeight);
                        if (match) {
                            return match;
                        }
                    }
                    break;
                case SelectorCombinatorEnum.subsequentSibling:
                    const siblingParentElement = element.parentElement;
                    if (siblingParentElement) {
                        const siblings = siblingParentElement[PropertySymbol.elementArray];
                        const index = siblings.indexOf(element);
                        siblingParentElement[PropertySymbol.affectsCache].push(cachedItem);
                        for (let i = index - 1; i >= 0; i--) {
                            const sibling = siblings[i];
                            sibling[PropertySymbol.affectsCache].push(cachedItem);
                            const match = this.matchSelector(sibling, selectorItems.slice(1), cachedItem, selectorItem, priorityWeight + result.priorityWeight);
                            if (match) {
                                return match;
                            }
                        }
                    }
                    break;
            }
        }
        if (previousSelectorItem?.combinator === SelectorCombinatorEnum.none ||
            previousSelectorItem?.combinator === SelectorCombinatorEnum.descendant) {
            const parentElement = element.parentElement;
            if (parentElement) {
                return this.matchSelector(parentElement, selectorItems, cachedItem, previousSelectorItem, priorityWeight);
            }
        }
        return null;
    }
    /**
     * Finds elements based on a query selector for a part of a list of selectors separated with comma.
     *
     * @param rootElement Root element.
     * @param children Child elements.
     * @param selectorItems Selector items.
     * @param cachedItem Cached item.
     * @param [documentPosition] Document position of the element.
     * @returns Document position and element map.
     */
    static findAll(rootElement, children, selectorItems, cachedItem, documentPosition) {
        const selectorItem = selectorItems[0];
        const nextSelectorItem = selectorItems[1];
        let matched = [];
        if (!selectorItem) {
            return [];
        }
        for (let i = 0, max = children.length; i < max; i++) {
            const child = children[i];
            const childrenOfChild = child[PropertySymbol.elementArray];
            const position = (documentPosition ? documentPosition + '>' : '') + String.fromCharCode(i);
            child[PropertySymbol.affectsCache].push(cachedItem);
            if (selectorItem.match(child)) {
                if (!nextSelectorItem) {
                    if (rootElement !== child) {
                        matched.push({
                            documentPosition: position,
                            element: child
                        });
                    }
                }
                else {
                    switch (nextSelectorItem.combinator) {
                        case SelectorCombinatorEnum.adjacentSibling:
                            const nextElementSibling = child.nextElementSibling;
                            if (nextElementSibling) {
                                matched = matched.concat(this.findAll(rootElement, [nextElementSibling], selectorItems.slice(1), cachedItem, position));
                            }
                            break;
                        case SelectorCombinatorEnum.none:
                        case SelectorCombinatorEnum.descendant:
                        case SelectorCombinatorEnum.child:
                            matched = matched.concat(this.findAll(rootElement, childrenOfChild, selectorItems.slice(1), cachedItem, position));
                            break;
                        case SelectorCombinatorEnum.subsequentSibling:
                            const index = children.indexOf(child);
                            for (let j = index + 1; j < children.length; j++) {
                                const sibling = children[j];
                                matched = matched.concat(this.findAll(rootElement, [sibling], selectorItems.slice(1), cachedItem, position));
                            }
                            break;
                    }
                }
            }
            if ((selectorItem.combinator === SelectorCombinatorEnum.none ||
                selectorItem.combinator === SelectorCombinatorEnum.descendant) &&
                childrenOfChild.length) {
                matched = matched.concat(this.findAll(rootElement, childrenOfChild, selectorItems, cachedItem, position));
            }
        }
        return matched;
    }
    /**
     * Finds an element based on a query selector for a part of a list of selectors separated with comma.
     *
     * @param rootElement Root element.
     * @param children Child elements.
     * @param selectorItems Selector items.
     * @param cachedItem Cached item.
     * @param [documentPosition] Document position of the element.
     * @returns Document position and element map.
     */
    static findFirst(rootElement, children, selectorItems, cachedItem, documentPosition) {
        const selectorItem = selectorItems[0];
        const nextSelectorItem = selectorItems[1];
        if (!selectorItem) {
            return null;
        }
        for (let i = 0, max = children.length; i < max; i++) {
            const child = children[i];
            const childrenOfChild = child[PropertySymbol.elementArray];
            const position = (documentPosition ? documentPosition + '>' : '') + String.fromCharCode(i);
            child[PropertySymbol.affectsCache].push(cachedItem);
            if (selectorItem.match(child)) {
                if (!nextSelectorItem) {
                    if (rootElement !== child) {
                        return { documentPosition: position, element: child };
                    }
                }
                else {
                    switch (nextSelectorItem.combinator) {
                        case SelectorCombinatorEnum.adjacentSibling:
                            const nextElementSibling = child.nextElementSibling;
                            if (nextElementSibling) {
                                const match = this.findFirst(rootElement, [nextElementSibling], selectorItems.slice(1), cachedItem, position);
                                if (match) {
                                    return match;
                                }
                            }
                            break;
                        case SelectorCombinatorEnum.none:
                        case SelectorCombinatorEnum.descendant:
                        case SelectorCombinatorEnum.child:
                            const match = this.findFirst(rootElement, childrenOfChild, selectorItems.slice(1), cachedItem, position);
                            if (match) {
                                return match;
                            }
                            break;
                        case SelectorCombinatorEnum.subsequentSibling:
                            const index = children.indexOf(child);
                            for (let i = index + 1; i < children.length; i++) {
                                const sibling = children[i];
                                const match = this.findFirst(rootElement, [sibling], selectorItems.slice(1), cachedItem, position);
                                if (match) {
                                    return match;
                                }
                            }
                            break;
                    }
                }
            }
            if ((selectorItem.combinator === SelectorCombinatorEnum.none ||
                selectorItem.combinator === SelectorCombinatorEnum.descendant) &&
                childrenOfChild.length) {
                const match = this.findFirst(rootElement, childrenOfChild, selectorItems, cachedItem, position);
                if (match) {
                    return match;
                }
            }
        }
        return null;
    }
}
//# sourceMappingURL=QuerySelector.js.map