"use strict";
const attributes = require("./attributes");
const { cloningSteps, domSymbolTree } = require("./helpers/internal-constants");
const NODE_TYPE = require("./node-type");
const orderedSetParse = require("./helpers/ordered-set").parse;
const { asciiCaseInsensitiveMatch, asciiLowercase } = require("./helpers/strings");
const { HTML_NS, XMLNS_NS } = require("./helpers/namespaces");
const HTMLCollection = require("./generated/HTMLCollection");

module.exports.clone = function (node, document, cloneChildren) {
  if (document === undefined) {
    document = node._ownerDocument;
  }

  let copy;
  switch (node.nodeType) {
    case NODE_TYPE.DOCUMENT_NODE:
      // Can't use a simple Document.createImpl because of circular dependency issues :-/
      copy = document.implementation.createDocument(null, "", null);
      copy._encoding = node._encoding;
      copy.contentType = node.contentType;
      copy._URL = node._URL;
      copy.origin = node.origin;
      copy._parsingMode = node._parsingMode;
      break;

    case NODE_TYPE.DOCUMENT_TYPE_NODE:
      copy = document.implementation.createDocumentType(node.name, node.publicId, node.systemId);
      break;

    case NODE_TYPE.ELEMENT_NODE:
      copy = document._createElementWithCorrectElementInterface(node._localName, node._namespaceURI);
      copy._prefix = node._prefix;
      attributes.copyAttributeList(node, copy);
      break;

    case NODE_TYPE.TEXT_NODE:
      copy = document.createTextNode(node._data);
      break;

    case NODE_TYPE.CDATA_SECTION_NODE:
      copy = document.createCDATASection(node._data);
      break;

    case NODE_TYPE.COMMENT_NODE:
      copy = document.createComment(node._data);
      break;

    case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
      copy = document.createProcessingInstruction(node.target, node._data);
      break;

    case NODE_TYPE.DOCUMENT_FRAGMENT_NODE:
      copy = document.createDocumentFragment();
      break;
  }

  if (node[cloningSteps]) {
    node[cloningSteps](copy, node, document, cloneChildren);
  }

  if (cloneChildren) {
    for (const child of domSymbolTree.childrenIterator(node)) {
      const childCopy = module.exports.clone(child, document, true);
      copy._append(childCopy);
    }
  }

  return copy;
};

// For the following, memoization is not applied here since the memoized results are stored on `this`.

module.exports.listOfElementsWithClassNames = (classNames, root) => {
  // https://dom.spec.whatwg.org/#concept-getElementsByClassName

  const classes = orderedSetParse(classNames);

  if (classes.size === 0) {
    return HTMLCollection.createImpl([], { element: root, query: () => [] });
  }

  return HTMLCollection.createImpl([], {
    element: root,
    query: () => {
      const isQuirksMode = root._ownerDocument.compatMode === "BackCompat";

      return domSymbolTree.treeToArray(root, { filter(node) {
        if (node.nodeType !== NODE_TYPE.ELEMENT_NODE || node === root) {
          return false;
        }

        const { classList } = node;
        if (isQuirksMode) {
          for (const className of classes) {
            if (!classList.tokenSet.some(cur => asciiCaseInsensitiveMatch(cur, className))) {
              return false;
            }
          }
        } else {
          for (const className of classes) {
            if (!classList.tokenSet.contains(className)) {
              return false;
            }
          }
        }

        return true;
      } });
    }
  });
};

module.exports.listOfElementsWithQualifiedName = (qualifiedName, root) => {
  // https://dom.spec.whatwg.org/#concept-getelementsbytagname

  if (qualifiedName === "*") {
    return HTMLCollection.createImpl([], {
      element: root,
      query: () => domSymbolTree.treeToArray(root, {
        filter: node => node.nodeType === NODE_TYPE.ELEMENT_NODE && node !== root
      })
    });
  }

  if (root._ownerDocument._parsingMode === "html") {
    const lowerQualifiedName = asciiLowercase(qualifiedName);

    return HTMLCollection.createImpl([], {
      element: root,
      query: () => domSymbolTree.treeToArray(root, {
        filter(node) {
          if (node.nodeType !== NODE_TYPE.ELEMENT_NODE || node === root) {
            return false;
          }

          if (node._namespaceURI === HTML_NS) {
            return node._qualifiedName === lowerQualifiedName;
          }

          return node._qualifiedName === qualifiedName;
        }
      })
    });
  }

  return HTMLCollection.createImpl([], {
    element: root,
    query: () => domSymbolTree.treeToArray(root, {
      filter(node) {
        if (node.nodeType !== NODE_TYPE.ELEMENT_NODE || node === root) {
          return false;
        }

        return node._qualifiedName === qualifiedName;
      }
    })
  });
};

module.exports.listOfElementsWithNamespaceAndLocalName = (namespace, localName, root) => {
  // https://dom.spec.whatwg.org/#concept-getelementsbytagnamens

  if (namespace === "") {
    namespace = null;
  }

  if (namespace === "*" && localName === "*") {
    return HTMLCollection.createImpl([], {
      element: root,
      query: () => domSymbolTree.treeToArray(root, {
        filter: node => node.nodeType === NODE_TYPE.ELEMENT_NODE && node !== root
      })
    });
  }

  if (namespace === "*") {
    return HTMLCollection.createImpl([], {
      element: root,
      query: () => domSymbolTree.treeToArray(root, {
        filter(node) {
          if (node.nodeType !== NODE_TYPE.ELEMENT_NODE || node === root) {
            return false;
          }

          return node._localName === localName;
        }
      })
    });
  }

  if (localName === "*") {
    return HTMLCollection.createImpl([], {
      element: root,
      query: () => domSymbolTree.treeToArray(root, {
        filter(node) {
          if (node.nodeType !== NODE_TYPE.ELEMENT_NODE || node === root) {
            return false;
          }

          return node._namespaceURI === namespace;
        }
      })
    });
  }

  return HTMLCollection.createImpl([], {
    element: root,
    query: () => domSymbolTree.treeToArray(root, {
      filter(node) {
        if (node.nodeType !== NODE_TYPE.ELEMENT_NODE || node === root) {
          return false;
        }

        return node._localName === localName && node._namespaceURI === namespace;
      }
    })
  });
};

// https://dom.spec.whatwg.org/#converting-nodes-into-a-node
// create a fragment (or just return a node for one item)
exports.convertNodesIntoNode = (document, nodes) => {
  if (nodes.length === 1) { // note: I'd prefer to check instanceof Node rather than string
    return typeof nodes[0] === "string" ? document.createTextNode(nodes[0]) : nodes[0];
  }
  const fragment = document.createDocumentFragment();
  for (let i = 0; i < nodes.length; i++) {
    fragment._append(typeof nodes[i] === "string" ? document.createTextNode(nodes[i]) : nodes[i]);
  }
  return fragment;
};

// https://dom.spec.whatwg.org/#locate-a-namespace-prefix
exports.locateNamespacePrefix = (element, namespace) => {
  if (element._namespaceURI === namespace && element._prefix !== null) {
    return element._prefix;
  }

  for (const attribute of element._attributeList) {
    if (attribute._namespacePrefix === "xmlns" && attribute._value === namespace) {
      return attribute._localName;
    }
  }

  if (element.parentElement !== null) {
    return exports.locateNamespacePrefix(element.parentElement, namespace);
  }

  return null;
};

// https://dom.spec.whatwg.org/#locate-a-namespace
exports.locateNamespace = (node, prefix) => {
  switch (node.nodeType) {
    case NODE_TYPE.ELEMENT_NODE: {
      if (node._namespaceURI !== null && node._prefix === prefix) {
        return node._namespaceURI;
      }

      if (prefix === null) {
        for (const attribute of node._attributeList) {
          if (attribute._namespace === XMLNS_NS &&
              attribute._namespacePrefix === null &&
              attribute._localName === "xmlns") {
            return attribute._value !== "" ? attribute._value : null;
          }
        }
      } else {
        for (const attribute of node._attributeList) {
          if (attribute._namespace === XMLNS_NS &&
              attribute._namespacePrefix === "xmlns" &&
              attribute._localName === prefix) {
            return attribute._value !== "" ? attribute._value : null;
          }
        }
      }

      if (node.parentElement === null) {
        return null;
      }

      return exports.locateNamespace(node.parentElement, prefix);
    }

    case NODE_TYPE.DOCUMENT_NODE: {
      if (node.documentElement === null) {
        return null;
      }

      return exports.locateNamespace(node.documentElement, prefix);
    }

    case NODE_TYPE.DOCUMENT_TYPE_NODE:
    case NODE_TYPE.DOCUMENT_FRAGMENT_NODE: {
      return null;
    }

    case NODE_TYPE.ATTRIBUTE_NODE: {
      if (node._element === null) {
        return null;
      }

      return exports.locateNamespace(node._element, prefix);
    }

    default: {
      if (node.parentElement === null) {
        return null;
      }

      return exports.locateNamespace(node.parentElement, prefix);
    }
  }
};
