/* eslint-disable max-classes-per-file,no-bitwise */
/**
 * nodeB precedes nodeA in either a pre-order depth-first traversal of a tree containing both
 * (e.g., as a descendant or preceding sibling or a descendant of a preceding sibling or
 * preceding sibling of an ancestor) or (if they are disconnected) in an arbitrary but
 * consistent ordering.
 *
 * @param nodeA - The first node to compare.
 * @param nodeB - The second node to compare.
 * @returns - True if nodeA is after nodeB, false otherwise.
 *
 * @see [compareDocumentPosition](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition)
 */
export const isAfter = (nodeA, nodeB) => !!(nodeA.compareDocumentPosition(nodeB) & Node.DOCUMENT_POSITION_PRECEDING);
/**
 * nodeB follows nodeA in either a pre-order depth-first traversal of a tree containing both
 * (e.g., as a descendant or following sibling or a descendant of a following sibling or
 * following sibling of an ancestor) or (if they are disconnected) in an arbitrary but
 * consistent ordering.
 *
 * @param nodeA - The first node to compare.
 * @param nodeB - The second node to compare.
 * @returns - True if nodeA is before nodeB, false otherwise.
 *
 * @see [compareDocumentPosition](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition)
 */
export const isBefore = (nodeA, nodeB) => !!(nodeA.compareDocumentPosition(nodeB) & Node.DOCUMENT_POSITION_FOLLOWING);
/**
 * Checks if the element has no client rectangles (not visible in the viewport).
 *
 * @param element - The element to check.
 * @returns True if the element has no client rectangles.
 */
export const hasNoClientRects = (element) => element.getClientRects().length === 0;
/**
 * Checks if the element has zero dimensions (width and height are both 0).
 *
 * @param element - The element to check.
 * @returns True if the element has zero dimensions.
 */
export const hasZeroDimensions = (element) => {
    const { width, height } = element.getBoundingClientRect();
    const { offsetWidth, offsetHeight } = element;
    return offsetWidth + offsetHeight + height + width === 0;
};
/**
 * Checks if the element is scrollable (has overflow content).
 *
 * @param element - The element to check.
 * @returns True if the element is scrollable.
 */
export const isScrollable = (element) => {
    if (!(element instanceof Element))
        return false;
    const computedStyle = getComputedStyle(element);
    const { overflowX, overflowY } = computedStyle;
    // Check if overflow is set to scrollable values
    return overflowX === 'auto' || overflowX === 'scroll' || overflowY === 'auto' || overflowY === 'scroll';
};
/**
 * Determines if the element is not visible in the DOM.
 *
 * @param element - The element to check.
 * @returns True if the element is not visible.
 */
export const isNotVisible = (element) => hasZeroDimensions(element) || hasNoClientRects(element);
/**
 * Checks if the element has inline styles that make it hidden.
 *
 * @param element - The element to check.
 * @returns True if the element has inline styles that make it hidden.
 */
export const hasHiddenStyle = (element) => {
    const { display, opacity, visibility } = element.style;
    return display === 'none' || opacity === '0' || visibility === 'hidden' || visibility === 'collapse';
};
/**
 * Checks if the element is hidden by a computed style.
 *
 * @param element - The element to check.
 * @returns True if the element is hidden by a computed style.
 */
export const hasComputedHidden = (element) => {
    const computedStyle = getComputedStyle(element);
    return computedStyle.visibility === 'hidden' || computedStyle.height === '0' || computedStyle.display === 'none';
};
const isDisplayContents = (element) => {
    const computedStyle = getComputedStyle(element);
    return computedStyle.display === 'contents';
};
/**
 * Checks if the element is hidden from the user.
 *
 * @param element - The element to check.
 * @returns True if the element is hidden.
 */
export const isHidden = (element) => element.hasAttribute('hidden') ||
    element.getAttribute('aria-hidden') === 'true' ||
    hasHiddenStyle(element) ||
    (!isDisplayContents(element) && (hasComputedHidden(element) || isNotVisible(element)));
/**
 * Checks if the element is disabled.
 *
 * @param element - The element to check.
 * @returns True if the element is disabled.
 */
export const isDisabled = (element) => element.disabled;
/**
 * Checks if the element is not tabbable.
 *
 * @param element - The element to check.
 * @returns True if the element is not tabbable.
 */
export const isTabbable = (element) => element.getAttribute('tabindex') !== '-1';
/**
 * Checks if the element is interactive.
 *
 * @param element - The element to check.
 * @returns True if the element is interactive.
 */
export const isInteractiveElement = (element) => {
    const interactiveTags = new Set(['BUTTON', 'DETAILS', 'EMBED', 'IFRAME', 'SELECT', 'TEXTAREA']);
    if (interactiveTags.has(element.tagName)) {
        return true;
    }
    if (element instanceof HTMLAnchorElement && element.hasAttribute('href')) {
        return true;
    }
    if (element instanceof HTMLInputElement && element.type !== 'hidden') {
        return true;
    }
    if ((element instanceof HTMLAudioElement || element instanceof HTMLVideoElement) &&
        element.hasAttribute('controls')) {
        return true;
    }
    if ((element instanceof HTMLImageElement || element instanceof HTMLObjectElement) && element.hasAttribute('usemap')) {
        return true;
    }
    if (element.hasAttribute('tabindex') && element.tabIndex > -1) {
        return true;
    }
    return false;
};
/**
 * Checks if the element matches any of the given selectors.
 *
 * @param element - The element to check.
 * @param selectors - The list of selectors to match against.
 * @returns True if the element matches any of the selectors.
 */
export const isMatchAny = (element, selectors) => selectors.some(selector => element.matches(selector));
/**
 * Checks if the element is focusable.
 *
 * @param element - The element to check.
 * @returns True if the element is focusable.
 */
export const isFocusable = (element) => !isDisabled(element) && isTabbable(element) && !isHidden(element) && isInteractiveElement(element);
/**
 * Recursively finds all focusable elements within the given root and its descendants.
 *
 * Make sure this is performant, as it will be called multiple times.
 *
 * @param root - The root element to search for focusable elements.
 * @param options - Options to customize the search.
 * @returns The list of focusable elements.
 */
export const findFocusable = (root, options = {}) => {
    var _a, _b, _c, _d, _e;
    if (!root) {
        return [];
    }
    const excludesSet = new Set((_a = options === null || options === void 0 ? void 0 : options.excludedElements) !== null && _a !== void 0 ? _a : []);
    const includeSelectors = (_b = options === null || options === void 0 ? void 0 : options.includeSelectors) !== null && _b !== void 0 ? _b : [];
    const excludeSelectors = (_c = options === null || options === void 0 ? void 0 : options.excludeSelectors) !== null && _c !== void 0 ? _c : [];
    const stopAtNonTabbable = (_d = options === null || options === void 0 ? void 0 : options.stopAtNonTabbable) !== null && _d !== void 0 ? _d : false;
    const matches = new Set((_e = options.includeElements) !== null && _e !== void 0 ? _e : []);
    const focusableCheck = (element) => {
        if (!(element instanceof HTMLSlotElement) &&
            (isHidden(element) || isDisabled(element) || isMatchAny(element, excludeSelectors))) {
            return 'stop';
        }
        if (stopAtNonTabbable && !(element instanceof HTMLSlotElement) && element.getAttribute('tabindex') === '-1') {
            // AI-Assisted
            // Do not stop traversal for elements inside a shadow root with delegatesFocus: true.
            // Components like mdc-searchfield use delegatesFocus and have internal container divs
            // with tabindex="-1" that should not block finding the actual focusable element (e.g., input).
            const rootNode = element.getRootNode();
            if (!(rootNode instanceof ShadowRoot && rootNode.delegatesFocus)) {
                return 'stop';
            }
            // End AI-Assisted
        }
        return isMatchAny(element, includeSelectors) || (isTabbable(element) && isInteractiveElement(element))
            ? 'focusable'
            : 'continue';
    };
    const finder = (root) => {
        if (excludesSet.has(root) || (root instanceof HTMLElement && isMatchAny(root, excludeSelectors))) {
            return;
        }
        if (root instanceof HTMLElement && focusableCheck(root) === 'focusable') {
            matches.add(root);
        }
        let children = [];
        if (root instanceof HTMLElement && root.shadowRoot) {
            children = Array.from(root.shadowRoot.children);
        }
        else if (root.children.length) {
            children = Array.from(root.children);
        }
        children.forEach((child) => {
            const element = child;
            const isFocusableResult = focusableCheck(element);
            if (isFocusableResult === 'focusable') {
                matches.add(element);
            }
            if (isFocusableResult === 'stop') {
                return;
            }
            if (element.shadowRoot) {
                finder(element.shadowRoot);
            }
            else if (element.tagName === 'SLOT') {
                const assignedNodes = element.assignedElements({ flatten: true });
                assignedNodes.forEach(node => {
                    if (node instanceof HTMLElement) {
                        // When stopAtNonTabbable is enabled, skip non-tabbable slotted elements and their
                        // subtrees to support composite widget patterns (e.g., roving tabindex in lists)
                        if (stopAtNonTabbable && !(node instanceof HTMLSlotElement) && node.getAttribute('tabindex') === '-1') {
                            return;
                        }
                        finder(node);
                    }
                });
            }
            else {
                finder(element);
            }
        });
    };
    finder(root);
    return [...matches];
};
/**
 * Get the active element from the DOM, including shadow DOMs
 *
 * @param root - The root document or element to start the search from. Defaults to the global document.
 * @returns The active element, or null if there is none.
 */
export const getDomActiveElement = (root = document) => {
    var _a;
    let activeElement = root instanceof Document ? root.activeElement : root;
    while ((_a = activeElement === null || activeElement === void 0 ? void 0 : activeElement.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement)
        activeElement = activeElement.shadowRoot.activeElement;
    return activeElement;
};
/**
 * Type guard to check if an element inherits the OverflowMixin.
 *
 * @param element - The element to check
 * @returns True if the element has the OverflowMixin methods
 */
export const hasOverflowMixin = (element) => 'isWidthOverflowing' in element && typeof element.isWidthOverflowing === 'function';
/**
 * Recursively gets the host element if the provided element is inside a shadow DOM.
 *
 * @param element - The element to check
 * @returns The host element if inside shadow DOM, otherwise the original element
 */
export const getElementOrHost = (element) => {
    const rootNode = element.getRootNode();
    if (rootNode !== document && rootNode instanceof ShadowRoot) {
        return getElementOrHost(rootNode.host);
    }
    return element;
};
/**
 * Recursively gets all root elements for the provided element, including shadow DOM hosts.
 * First item is the element itself, followed by its shadow host, then the host's shadow host, and so on.
 * The last item is the top-level document.
 *
 * @param element - The element to check
 * @returns An array of elements starting with the original element and followed by its shadow hosts up the tree
 */
export const getHostComposePath = (element) => {
    const roots = [element];
    let currentElement = element;
    while (currentElement) {
        const rootNode = currentElement.getRootNode();
        if (rootNode instanceof ShadowRoot) {
            currentElement = rootNode.host;
            roots.push(currentElement);
        }
        else {
            break;
        }
    }
    return roots;
};
