/**
 * utility.js
 */

/* import */
import nwsapi from '@asamuzakjp/nwsapi';
import bidiFactory from 'bidi-js';
import * as cssTree from 'css-tree';
import isCustomElementName from 'is-potential-custom-element-name';

/* constants */
import {
  ATRULE,
  COMBO,
  COMPOUND_I,
  DESCEND,
  DOCUMENT_FRAGMENT_NODE,
  DOCUMENT_NODE,
  DOCUMENT_POSITION_CONTAINS,
  DOCUMENT_POSITION_PRECEDING,
  ELEMENT_NODE,
  HAS_COMPOUND,
  INPUT_BUTTON,
  INPUT_EDIT,
  INPUT_LTR,
  INPUT_TEXT,
  KEYS_LOGICAL,
  LOGIC_COMPLEX,
  LOGIC_COMPOUND,
  N_TH,
  PSEUDO_CLASS,
  RULE,
  SCOPE,
  SELECTOR_LIST,
  SIBLING,
  TARGET_ALL,
  TARGET_FIRST,
  TEXT_NODE,
  TYPE_FROM,
  TYPE_TO
} from './constant.js';
const KEYS_DIR_AUTO = new Set([...INPUT_BUTTON, ...INPUT_TEXT, 'hidden']);
const KEYS_DIR_LTR = new Set(INPUT_LTR);
const KEYS_INPUT_EDIT = new Set(INPUT_EDIT);
const KEYS_NODE_DIR_EXCLUDE = new Set(['bdi', 'script', 'style', 'textarea']);
const KEYS_NODE_FOCUSABLE = new Set(['button', 'select', 'textarea']);
const KEYS_NODE_FOCUSABLE_SVG = new Set([
  'clipPath',
  'defs',
  'desc',
  'linearGradient',
  'marker',
  'mask',
  'metadata',
  'pattern',
  'radialGradient',
  'script',
  'style',
  'symbol',
  'title'
]);
const REG_EXCLUDE_BASIC =
  /[|\\]|::|[^\u0021-\u007F\s]|\[\s*[\w$*=^|~-]+(?:(?:"[\w$*=^|~\s'-]+"|'[\w$*=^|~\s"-]+')?(?:\s+[\w$*=^|~-]+)+|"[^"\]]{1,255}|'[^'\]]{1,255})\s*\]|:(?:is|where)\(\s*\)/;
const REG_COMPLEX = new RegExp(`${COMPOUND_I}${COMBO}${COMPOUND_I}`, 'i');
const REG_DESCEND = new RegExp(`${COMPOUND_I}${DESCEND}${COMPOUND_I}`, 'i');
const REG_SIBLING = new RegExp(`${COMPOUND_I}${SIBLING}${COMPOUND_I}`, 'i');
const REG_LOGIC_COMPLEX = new RegExp(
  `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPLEX})`
);
const REG_LOGIC_COMPOUND = new RegExp(
  `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND})`
);
const REG_LOGIC_HAS_COMPOUND = new RegExp(
  `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND}|${HAS_COMPOUND})`
);
const REG_END_WITH_HAS = new RegExp(`:${HAS_COMPOUND}$`);
const REG_WO_LOGICAL = new RegExp(`:(?!${PSEUDO_CLASS}|${N_TH})`);
const REG_IS_HTML = /^(?:application\/xhtml\+x|text\/ht)ml$/;
const REG_IS_XML =
  /^(?:application\/(?:[\w\-.]+\+)?|image\/[\w\-.]+\+|text\/)xml$/;

/**
 * Manages state for extracting nested selectors from a CSS AST.
 */
class SelectorExtractor {
  constructor() {
    this.selectors = [];
    this.isScoped = false;
  }

  /**
   * Walker enter function.
   * @param {object} node - The AST node.
   */
  enter(node) {
    switch (node.type) {
      case ATRULE: {
        if (node.name === 'scope') {
          this.isScoped = true;
        }
        break;
      }
      case SCOPE: {
        const { children, type } = node.root;
        const arr = [];
        if (type === SELECTOR_LIST) {
          for (const child of children) {
            const selector = cssTree.generate(child);
            arr.push(selector);
          }
          this.selectors.push(arr);
        }
        break;
      }
      case RULE: {
        const { children, type } = node.prelude;
        const arr = [];
        if (type === SELECTOR_LIST) {
          let hasAmp = false;
          for (const child of children) {
            const selector = cssTree.generate(child);
            if (this.isScoped && !hasAmp) {
              hasAmp = /\x26/.test(selector);
            }
            arr.push(selector);
          }
          if (this.isScoped) {
            if (hasAmp) {
              this.selectors.push(arr);
              /* FIXME:
              } else {
                this.selectors = arr;
                this.isScoped = false;
              */
            }
          } else {
            this.selectors.push(arr);
          }
        }
      }
    }
  }

  /**
   * Walker leave function.
   * @param {object} node - The AST node.
   */
  leave(node) {
    if (node.type === ATRULE) {
      if (node.name === 'scope') {
        this.isScoped = false;
      }
    }
  }
}

/**
 * Get type of an object.
 * @param {object} o - Object to check.
 * @returns {string} - Type of the object.
 */
export const getType = o =>
  Object.prototype.toString.call(o).slice(TYPE_FROM, TYPE_TO);

/**
 * Verify array contents.
 * @param {Array} arr - The array.
 * @param {string} type - Expected type, e.g. 'String'.
 * @throws {TypeError} - Throws if array or its items are of unexpected type.
 * @returns {Array} - The verified array.
 */
export const verifyArray = (arr, type) => {
  if (!Array.isArray(arr)) {
    throw new TypeError(`Unexpected type ${getType(arr)}`);
  }
  if (typeof type !== 'string') {
    throw new TypeError(`Unexpected type ${getType(type)}`);
  }
  for (const item of arr) {
    if (getType(item) !== type) {
      throw new TypeError(`Unexpected type ${getType(item)}`);
    }
  }
  return arr;
};

/**
 * Generate a DOMException.
 * @param {string} msg - The error message.
 * @param {string} name - The error name.
 * @param {object} globalObject - The global object (e.g., window).
 * @returns {DOMException} The generated DOMException object.
 */
export const generateException = (msg, name, globalObject = globalThis) => {
  return new globalObject.DOMException(msg, name);
};

/**
 * Find a nested :has() pseudo-class.
 * @param {object} leaf - The AST leaf to check.
 * @returns {?object} The leaf if it's :has, otherwise null.
 */
export const findNestedHas = leaf => {
  return leaf.name === 'has';
};

/**
 * Find a logical pseudo-class that contains a nested :has().
 * @param {object} leaf - The AST leaf to check.
 * @returns {?object} The leaf if it matches, otherwise null.
 */
export const findLogicalWithNestedHas = leaf => {
  if (KEYS_LOGICAL.has(leaf.name) && cssTree.find(leaf, findNestedHas)) {
    return leaf;
  }
  return null;
};

/**
 * Filter a list of nodes based on An+B logic
 * @param {Array.<object>} nodes - array of nodes to filter
 * @param {object} anb - An+B options
 * @param {number} anb.a - a
 * @param {number} anb.b - b
 * @param {boolean} [anb.reverse] - reverse order
 * @returns {Array.<object>} - array of matched nodes
 */
export const filterNodesByAnB = (nodes, anb) => {
  const { a, b, reverse } = anb;
  const processedNodes = reverse ? [...nodes].reverse() : nodes;
  const l = nodes.length;
  const matched = [];
  if (a === 0) {
    if (b > 0 && b <= l) {
      matched.push(processedNodes[b - 1]);
    }
    return matched;
  }
  let startIndex = b - 1;
  if (a > 0) {
    while (startIndex < 0) {
      startIndex += a;
    }
    for (let i = startIndex; i < l; i += a) {
      matched.push(processedNodes[i]);
    }
  } else if (startIndex >= 0) {
    for (let i = startIndex; i >= 0; i += a) {
      matched.push(processedNodes[i]);
    }
    return matched.reverse();
  }
  return matched;
};

/**
 * Resolve content document, root node, and check if it's in a shadow DOM.
 * @param {object} node - Document, DocumentFragment, or Element node.
 * @returns {Array.<object|boolean>} - [document, root, isInShadow].
 */
export const resolveContent = node => {
  if (!node?.nodeType) {
    throw new TypeError(`Unexpected type ${getType(node)}`);
  }
  let document;
  let root;
  let shadow;
  switch (node.nodeType) {
    case DOCUMENT_NODE: {
      document = node;
      root = node;
      break;
    }
    case DOCUMENT_FRAGMENT_NODE: {
      const { host, mode, ownerDocument } = node;
      document = ownerDocument;
      root = node;
      shadow = host && (mode === 'close' || mode === 'open');
      break;
    }
    case ELEMENT_NODE: {
      document = node.ownerDocument;
      let refNode = node;
      while (refNode) {
        const { host, mode, nodeType, parentNode } = refNode;
        if (nodeType === DOCUMENT_FRAGMENT_NODE) {
          shadow = host && (mode === 'close' || mode === 'open');
          break;
        } else if (parentNode) {
          refNode = parentNode;
        } else {
          break;
        }
      }
      root = refNode;
      break;
    }
    default: {
      throw new TypeError(`Unexpected node ${node.nodeName}`);
    }
  }
  return [document, root, !!shadow];
};

/**
 * Traverse node tree with a TreeWalker.
 * @param {object} node - The target node.
 * @param {object} walker - The TreeWalker instance.
 * @param {boolean} [force] - Traverse only to the next node.
 * @returns {?object} - The current node if found, otherwise null.
 */
export const traverseNode = (node, walker, force = false) => {
  if (!node?.nodeType) {
    throw new TypeError(`Unexpected type ${getType(node)}`);
  }
  if (!walker) {
    return null;
  }
  let refNode = walker.currentNode;
  if (refNode === node) {
    return refNode;
  } else if (force || refNode.contains(node)) {
    refNode = walker.nextNode();
    while (refNode) {
      if (refNode === node) {
        break;
      }
      refNode = walker.nextNode();
    }
    return refNode;
  } else {
    if (refNode !== walker.root) {
      let bool;
      while (refNode) {
        if (refNode === node) {
          bool = true;
          break;
        } else if (refNode === walker.root || refNode.contains(node)) {
          break;
        }
        refNode = walker.parentNode();
      }
      if (bool) {
        return refNode;
      }
    }
    if (node.nodeType === ELEMENT_NODE) {
      let bool;
      while (refNode) {
        if (refNode === node) {
          bool = true;
          break;
        }
        refNode = walker.nextNode();
      }
      if (bool) {
        return refNode;
      }
    }
  }
  return null;
};

/**
 * Check if a node is a custom element.
 * @param {object} node - The Element node.
 * @param {object} [opt] - Options.
 * @returns {boolean} - True if it's a custom element.
 */
export const isCustomElement = (node, opt = {}) => {
  if (!node?.nodeType) {
    throw new TypeError(`Unexpected type ${getType(node)}`);
  }
  if (node.nodeType !== ELEMENT_NODE) {
    return false;
  }
  const { localName, ownerDocument } = node;
  const { formAssociated } = opt;
  const window = ownerDocument.defaultView;
  let elmConstructor;
  const attr = node.getAttribute('is');
  if (attr) {
    elmConstructor =
      isCustomElementName(attr) && window.customElements.get(attr);
  } else {
    elmConstructor =
      isCustomElementName(localName) && window.customElements.get(localName);
  }
  if (elmConstructor) {
    if (formAssociated) {
      return !!elmConstructor.formAssociated;
    }
    return true;
  }
  return false;
};

/**
 * Get slotted text content.
 * @param {object} node - The Element node (likely a <slot>).
 * @returns {?string} - The text content.
 */
export const getSlottedTextContent = node => {
  if (!node?.nodeType) {
    throw new TypeError(`Unexpected type ${getType(node)}`);
  }
  if (typeof node.assignedNodes !== 'function') {
    return null;
  }
  const nodes = node.assignedNodes();
  if (nodes.length) {
    let text = '';
    const l = nodes.length;
    for (let i = 0; i < l; i++) {
      const item = nodes[i];
      text = item.textContent.trim();
      if (text) {
        break;
      }
    }
    return text;
  }
  return node.textContent.trim();
};

/**
 * Get directionality of a node.
 * @see https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute
 * @param {object} node - The Element node.
 * @returns {?string} - 'ltr' or 'rtl'.
 */
export const getDirectionality = node => {
  if (!node?.nodeType) {
    throw new TypeError(`Unexpected type ${getType(node)}`);
  }
  if (node.nodeType !== ELEMENT_NODE) {
    return null;
  }
  const { dir: dirAttr, localName, parentNode } = node;
  const { getEmbeddingLevels } = bidiFactory();
  if (dirAttr === 'ltr' || dirAttr === 'rtl') {
    return dirAttr;
  } else if (dirAttr === 'auto') {
    let text = '';
    switch (localName) {
      case 'input': {
        if (!node.type || KEYS_DIR_AUTO.has(node.type)) {
          text = node.value;
        } else if (KEYS_DIR_LTR.has(node.type)) {
          return 'ltr';
        }
        break;
      }
      case 'slot': {
        text = getSlottedTextContent(node);
        break;
      }
      case 'textarea': {
        text = node.value;
        break;
      }
      default: {
        const items = [].slice.call(node.childNodes);
        for (const item of items) {
          const {
            dir: itemDir,
            localName: itemLocalName,
            nodeType: itemNodeType,
            textContent: itemTextContent
          } = item;
          if (itemNodeType === TEXT_NODE) {
            text = itemTextContent.trim();
          } else if (
            itemNodeType === ELEMENT_NODE &&
            !KEYS_NODE_DIR_EXCLUDE.has(itemLocalName) &&
            (!itemDir || (itemDir !== 'ltr' && itemDir !== 'rtl'))
          ) {
            if (itemLocalName === 'slot') {
              text = getSlottedTextContent(item);
            } else {
              text = itemTextContent.trim();
            }
          }
          if (text) {
            break;
          }
        }
      }
    }
    if (text) {
      const {
        paragraphs: [{ level }]
      } = getEmbeddingLevels(text);
      if (level % 2 === 1) {
        return 'rtl';
      }
    } else if (parentNode) {
      const { nodeType: parentNodeType } = parentNode;
      if (parentNodeType === ELEMENT_NODE) {
        return getDirectionality(parentNode);
      }
    }
  } else if (localName === 'input' && node.type === 'tel') {
    return 'ltr';
  } else if (localName === 'bdi') {
    const text = node.textContent.trim();
    if (text) {
      const {
        paragraphs: [{ level }]
      } = getEmbeddingLevels(text);
      if (level % 2 === 1) {
        return 'rtl';
      }
    }
  } else if (parentNode) {
    if (localName === 'slot') {
      const text = getSlottedTextContent(node);
      if (text) {
        const {
          paragraphs: [{ level }]
        } = getEmbeddingLevels(text);
        if (level % 2 === 1) {
          return 'rtl';
        }
        return 'ltr';
      }
    }
    const { nodeType: parentNodeType } = parentNode;
    if (parentNodeType === ELEMENT_NODE) {
      return getDirectionality(parentNode);
    }
  }
  return 'ltr';
};

/**
 * Traverses up the DOM tree to find the language attribute for a node.
 * It checks for 'lang' in HTML and 'xml:lang' in XML contexts.
 * @param {object} node - The starting element node.
 * @returns {string|null} The language attribute value, or null if not found.
 */
export const getLanguageAttribute = node => {
  if (!node?.nodeType) {
    throw new TypeError(`Unexpected type ${getType(node)}`);
  }
  if (node.nodeType !== ELEMENT_NODE) {
    return null;
  }
  const { contentType } = node.ownerDocument;
  const isHtml = REG_IS_HTML.test(contentType);
  const isXml = REG_IS_XML.test(contentType);
  let isShadow = false;
  // Traverse up from the current node to the root.
  let current = node;
  while (current) {
    // Check if the current node is an element.
    switch (current.nodeType) {
      case ELEMENT_NODE: {
        // Check for and return the language attribute if present.
        if (isHtml && current.hasAttribute('lang')) {
          return current.getAttribute('lang');
        } else if (isXml && current.hasAttribute('xml:lang')) {
          return current.getAttribute('xml:lang');
        }
        break;
      }
      case DOCUMENT_FRAGMENT_NODE: {
        // Continue traversal if the current node is a shadow root.
        if (current.host) {
          isShadow = true;
        }
        break;
      }
      case DOCUMENT_NODE:
      default: {
        // Stop if we reach the root document node.
        return null;
      }
    }
    if (isShadow) {
      current = current.host;
      isShadow = false;
    } else if (current.parentNode) {
      current = current.parentNode;
    } else {
      break;
    }
  }
  // No language attribute was found in the hierarchy.
  return null;
};

/**
 * Check if content is editable.
 * NOTE: Not implemented in jsdom https://github.com/jsdom/jsdom/issues/1670
 * @param {object} node - The Element node.
 * @returns {boolean} - True if content is editable.
 */
export const isContentEditable = node => {
  if (!node?.nodeType) {
    throw new TypeError(`Unexpected type ${getType(node)}`);
  }
  if (node.nodeType !== ELEMENT_NODE) {
    return false;
  }
  if (typeof node.isContentEditable === 'boolean') {
    return node.isContentEditable;
  } else if (node.ownerDocument.designMode === 'on') {
    return true;
  } else {
    let attr;
    if (node.hasAttribute('contenteditable')) {
      attr = node.getAttribute('contenteditable');
    } else {
      attr = 'inherit';
    }
    switch (attr) {
      case '':
      case 'true': {
        return true;
      }
      case 'plaintext-only': {
        // FIXME:
        // @see https://github.com/w3c/editing/issues/470
        // @see https://github.com/whatwg/html/issues/10651
        return true;
      }
      case 'false': {
        return false;
      }
      default: {
        if (node?.parentNode?.nodeType === ELEMENT_NODE) {
          return isContentEditable(node.parentNode);
        }
        return false;
      }
    }
  }
};

/**
 * Check if a node is visible.
 * @param {object} node - The Element node.
 * @returns {boolean} - True if the node is visible.
 */
export const isVisible = node => {
  if (node?.nodeType !== ELEMENT_NODE) {
    return false;
  }
  const window = node.ownerDocument.defaultView;
  const { display, visibility } = window.getComputedStyle(node);
  if (display !== 'none' && visibility === 'visible') {
    return true;
  }
  return false;
};

/**
 * Check if focus is visible on the node.
 * @param {object} node - The Element node.
 * @returns {boolean} - True if focus is visible.
 */
export const isFocusVisible = node => {
  if (node?.nodeType !== ELEMENT_NODE) {
    return false;
  }
  const { localName, type } = node;
  switch (localName) {
    case 'input': {
      if (!type || KEYS_INPUT_EDIT.has(type)) {
        return true;
      }
      return false;
    }
    case 'textarea': {
      return true;
    }
    default: {
      return isContentEditable(node);
    }
  }
};

/**
 * Check if an area is focusable.
 * @param {object} node - The Element node.
 * @returns {boolean} - True if the area is focusable.
 */
export const isFocusableArea = node => {
  if (node?.nodeType !== ELEMENT_NODE) {
    return false;
  }
  if (!node.isConnected) {
    return false;
  }
  const window = node.ownerDocument.defaultView;
  if (node instanceof window.HTMLElement) {
    if (Number.isInteger(parseInt(node.getAttribute('tabindex')))) {
      return true;
    }
    if (isContentEditable(node)) {
      return true;
    }
    const { localName, parentNode } = node;
    switch (localName) {
      case 'a': {
        if (node.href || node.hasAttribute('href')) {
          return true;
        }
        return false;
      }
      case 'iframe': {
        return true;
      }
      case 'input': {
        if (
          node.disabled ||
          node.hasAttribute('disabled') ||
          node.hidden ||
          node.hasAttribute('hidden')
        ) {
          return false;
        }
        return true;
      }
      case 'summary': {
        if (parentNode.localName === 'details') {
          let child = parentNode.firstElementChild;
          let bool = false;
          while (child) {
            if (child.localName === 'summary') {
              bool = child === node;
              break;
            }
            child = child.nextElementSibling;
          }
          return bool;
        }
        return false;
      }
      default: {
        if (
          KEYS_NODE_FOCUSABLE.has(localName) &&
          !(node.disabled || node.hasAttribute('disabled'))
        ) {
          return true;
        }
      }
    }
  } else if (node instanceof window.SVGElement) {
    if (Number.isInteger(parseInt(node.getAttributeNS(null, 'tabindex')))) {
      const ns = 'http://www.w3.org/2000/svg';
      let bool;
      let refNode = node;
      while (refNode.namespaceURI === ns) {
        bool = KEYS_NODE_FOCUSABLE_SVG.has(refNode.localName);
        if (bool) {
          break;
        }
        if (refNode?.parentNode?.namespaceURI === ns) {
          refNode = refNode.parentNode;
        } else {
          break;
        }
      }
      if (bool) {
        return false;
      }
      return true;
    }
    if (
      node.localName === 'a' &&
      (node.href || node.hasAttributeNS(null, 'href'))
    ) {
      return true;
    }
  }
  return false;
};

/**
 * Check if a node is focusable.
 * NOTE: Not applied, needs fix in jsdom itself.
 * @see https://github.com/whatwg/html/pull/8392
 * @see https://phabricator.services.mozilla.com/D156219
 * @see https://github.com/jsdom/jsdom/issues/3029
 * @see https://github.com/jsdom/jsdom/issues/3464
 * @param {object} node - The Element node.
 * @returns {boolean} - True if the node is focusable.
 */
export const isFocusable = node => {
  if (node?.nodeType !== ELEMENT_NODE) {
    return false;
  }
  const window = node.ownerDocument.defaultView;
  let refNode = node;
  let res = true;
  while (refNode) {
    if (refNode.disabled || refNode.hasAttribute('disabled')) {
      res = false;
      break;
    }
    if (refNode.hidden || refNode.hasAttribute('hidden')) {
      res = false;
    }
    const { contentVisibility, display, visibility } =
      window.getComputedStyle(refNode);
    if (
      display === 'none' ||
      visibility !== 'visible' ||
      (contentVisibility === 'hidden' && refNode !== node)
    ) {
      res = false;
    } else {
      res = true;
    }
    if (res && refNode?.parentNode?.nodeType === ELEMENT_NODE) {
      refNode = refNode.parentNode;
    } else {
      break;
    }
  }
  return res;
};

/**
 * Get namespace URI.
 * @param {string} ns - The namespace prefix.
 * @param {object} node - The Element node.
 * @returns {?string} - The namespace URI.
 */
export const getNamespaceURI = (ns, node) => {
  if (typeof ns !== 'string') {
    throw new TypeError(`Unexpected type ${getType(ns)}`);
  } else if (!node?.nodeType) {
    throw new TypeError(`Unexpected type ${getType(node)}`);
  }
  if (!ns || node.nodeType !== ELEMENT_NODE) {
    return null;
  }
  const { attributes } = node;
  let res;
  for (const attr of attributes) {
    const { name, namespaceURI, prefix, value } = attr;
    if (name === `xmlns:${ns}`) {
      res = value;
    } else if (prefix === ns) {
      res = namespaceURI;
    }
    if (res) {
      break;
    }
  }
  return res ?? null;
};

/**
 * Check if a namespace is declared.
 * @param {string} ns - The namespace.
 * @param {object} node - The Element node.
 * @returns {boolean} - True if the namespace is declared.
 */
export const isNamespaceDeclared = (ns = '', node = {}) => {
  if (!ns || typeof ns !== 'string' || node?.nodeType !== ELEMENT_NODE) {
    return false;
  }
  if (node.lookupNamespaceURI(ns)) {
    return true;
  }
  const root = node.ownerDocument.documentElement;
  let parent = node;
  let res;
  while (parent) {
    res = getNamespaceURI(ns, parent);
    if (res || parent === root) {
      break;
    }
    parent = parent.parentNode;
  }
  return !!res;
};

/**
 * Check if nodeA precedes and/or contains nodeB.
 * @param {object} nodeA - The first Element node.
 * @param {object} nodeB - The second Element node.
 * @returns {boolean} - True if nodeA precedes nodeB.
 */
export const isPreceding = (nodeA, nodeB) => {
  if (!nodeA?.nodeType) {
    throw new TypeError(`Unexpected type ${getType(nodeA)}`);
  } else if (!nodeB?.nodeType) {
    throw new TypeError(`Unexpected type ${getType(nodeB)}`);
  }
  if (nodeA.nodeType !== ELEMENT_NODE || nodeB.nodeType !== ELEMENT_NODE) {
    return false;
  }
  const posBit = nodeB.compareDocumentPosition(nodeA);
  const res =
    posBit & DOCUMENT_POSITION_PRECEDING || posBit & DOCUMENT_POSITION_CONTAINS;
  return !!res;
};

/**
 * Comparison function for sorting nodes based on document position.
 * @param {object} a - The first node.
 * @param {object} b - The second node.
 * @returns {number} - Sort order.
 */
export const compareNodes = (a, b) => {
  if (isPreceding(b, a)) {
    return 1;
  }
  return -1;
};

/**
 * Sort a collection of nodes.
 * @param {Array.<object>|Set.<object>} nodes - Collection of nodes.
 * @returns {Array.<object>} - Collection of sorted nodes.
 */
export const sortNodes = (nodes = []) => {
  const arr = [...nodes];
  if (arr.length > 1) {
    arr.sort(compareNodes);
  }
  return arr;
};

/**
 * Concat an array of nested selectors into an equivalent single selector.
 * @param {Array.<Array.<string>>} selectors - [parents, children, ...].
 * @returns {string} - The concatenated selector.
 */
export const concatNestedSelectors = selectors => {
  if (!Array.isArray(selectors)) {
    throw new TypeError(`Unexpected type ${getType(selectors)}`);
  }
  let selector = '';
  if (selectors.length) {
    const revSelectors = selectors.toReversed();
    let child = verifyArray(revSelectors.shift(), 'String');
    if (child.length === 1) {
      [child] = child;
    }
    while (revSelectors.length) {
      const parentArr = verifyArray(revSelectors.shift(), 'String');
      if (!parentArr.length) {
        continue;
      }
      let parent;
      if (parentArr.length === 1) {
        [parent] = parentArr;
        if (!/^[>~+]/.test(parent) && /[\s>~+]/.test(parent)) {
          parent = `:is(${parent})`;
        }
      } else {
        parent = `:is(${parentArr.join(', ')})`;
      }
      if (selector.includes('\x26')) {
        selector = selector.replace(/\x26/g, parent);
      }
      if (Array.isArray(child)) {
        const items = [];
        for (let item of child) {
          if (item.includes('\x26')) {
            if (/^[>~+]/.test(item)) {
              item = `${parent} ${item.replace(/\x26/g, parent)} ${selector}`;
            } else {
              item = `${item.replace(/\x26/g, parent)} ${selector}`;
            }
          } else {
            item = `${parent} ${item} ${selector}`;
          }
          items.push(item.trim());
        }
        selector = items.join(', ');
      } else if (revSelectors.length) {
        selector = `${child} ${selector}`;
      } else {
        if (child.includes('\x26')) {
          if (/^[>~+]/.test(child)) {
            selector = `${parent} ${child.replace(/\x26/g, parent)} ${selector}`;
          } else {
            selector = `${child.replace(/\x26/g, parent)} ${selector}`;
          }
        } else {
          selector = `${parent} ${child} ${selector}`;
        }
      }
      selector = selector.trim();
      if (revSelectors.length) {
        child = parentArr.length > 1 ? parentArr : parent;
      } else {
        break;
      }
    }
    selector = selector.replace(/\x26/g, ':scope').trim();
  }
  return selector;
};

/**
 * Extract nested selectors from CSSRule.cssText.
 * @param {string} css - CSSRule.cssText.
 * @returns {Array.<Array.<string>>} - Array of nested selectors.
 */
export const extractNestedSelectors = css => {
  const ast = cssTree.parse(css, {
    context: 'rule'
  });
  const extractor = new SelectorExtractor();
  cssTree.walk(ast, {
    enter: extractor.enter.bind(extractor),
    leave: extractor.leave.bind(extractor)
  });
  return extractor.selectors;
};

/**
 * Initialize nwsapi.
 * @param {object} window - The Window object.
 * @param {object} document - The Document object.
 * @returns {object} - The nwsapi instance.
 */
export const initNwsapi = (window, document) => {
  if (!window?.DOMException) {
    throw new TypeError(`Unexpected global object ${getType(window)}`);
  }
  if (document?.nodeType !== DOCUMENT_NODE) {
    document = window.document;
  }
  const nw = nwsapi({
    document,
    DOMException: window.DOMException
  });
  nw.configure({
    LOGERRORS: false
  });
  return nw;
};

/**
 * Filter a selector for use with nwsapi.
 * @param {string} selector - The selector string.
 * @param {string} target - The target type.
 * @returns {boolean} - True if the selector is valid for nwsapi.
 */
export const filterSelector = (selector, target) => {
  const isQuerySelectorType = target === TARGET_FIRST || target === TARGET_ALL;
  if (
    !selector ||
    typeof selector !== 'string' ||
    /null|undefined/.test(selector)
  ) {
    return false;
  }
  // Exclude missing close square bracket.
  if (selector.includes('[')) {
    const index = selector.lastIndexOf('[');
    const sel = selector.substring(index);
    if (sel.indexOf(']') < 0) {
      return false;
    }
  }
  // Exclude various complex or unsupported selectors.
  // - selectors containing '/'
  // - namespaced selectors
  // - escaped selectors
  // - pseudo-element selectors
  // - selectors containing non-ASCII
  // - selectors containing control character other than whitespace
  // - attribute selectors with case flag, e.g. [attr i]
  // - attribute selectors with unclosed quotes
  // - empty :is() or :where()
  if (selector.includes('/') || REG_EXCLUDE_BASIC.test(selector)) {
    return false;
  }
  // Include pseudo-classes that are known to work correctly.
  if (selector.includes(':')) {
    let complex = false;
    if (target !== isQuerySelectorType) {
      complex = REG_COMPLEX.test(selector);
    }
    if (
      isQuerySelectorType &&
      REG_DESCEND.test(selector) &&
      !REG_SIBLING.test(selector)
    ) {
      return false;
    } else if (!isQuerySelectorType && /:has\(/.test(selector)) {
      if (!complex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
        return false;
      }
      return REG_END_WITH_HAS.test(selector);
    } else if (/:(?:is|not)\(/.test(selector)) {
      if (complex) {
        return !REG_LOGIC_COMPLEX.test(selector);
      } else {
        return !REG_LOGIC_COMPOUND.test(selector);
      }
    } else {
      return !REG_WO_LOGICAL.test(selector);
    }
  }
  return true;
};
