"use strict";

const domSymbolTree = require("./helpers/internal-constants").domSymbolTree;
const defineGetter = require("../utils").defineGetter;
const INTERNAL = Symbol("NodeIterator internal");

module.exports = function (core) {
  // https://dom.spec.whatwg.org/#interface-nodeiterator

  function NodeIteratorInternal(document, root, whatToShow, filter) {
    this.active = true;
    this.document = document;
    this.root = root;
    this.referenceNode = root;
    this.pointerBeforeReferenceNode = true;
    this.whatToShow = whatToShow;
    this.filter = filter;
  }

  NodeIteratorInternal.prototype.throwIfNotActive = function () {
    // (only thrown for getters/methods that are affected by removing steps)
    if (!this.active) {
      throw Error("This NodeIterator is no longer active. " +
                  "More than " + this.document._activeNodeIteratorsMax +
                  " iterators are being used concurrently. " +
                  "You can increase the 'concurrentNodeIterators' option to " +
                  "make this error go away."
      );
      // Alternatively, you can pester Ecma to add support for weak references,
      // the DOM standard assumes the implementor has control over object life cycles.
    }
  };

  NodeIteratorInternal.prototype.traverse = function (next) {
    let node = this.referenceNode;
    let beforeNode = this.pointerBeforeReferenceNode;

    do {
      if (next) {
        if (!beforeNode) {
          node = domSymbolTree.following(node, { root: this.root });

          if (!node) {
            return null;
          }
        }

        beforeNode = false;
      } else { // previous
        if (beforeNode) {
          node = domSymbolTree.preceding(node, { root: this.root });

          if (!node) {
            return null;
          }
        }

        beforeNode = true;
      }
    }
    while (this.filterNode(node) !== core.NodeFilter.FILTER_ACCEPT);

    this.pointerBeforeReferenceNode = beforeNode;
    this.referenceNode = node;
    return node;
  };

  NodeIteratorInternal.prototype.filterNode = function (node) {
    const n = node.nodeType - 1;
    if (!(this.whatToShow & (1 << n))) {
      return core.NodeFilter.FILTER_SKIP;
    }

    let ret = core.NodeFilter.FILTER_ACCEPT;
    const filter = this.filter;
    if (typeof filter === "function") {
      ret = filter(node);
    } else if (filter && typeof filter.acceptNode === "function") {
      ret = filter.acceptNode(node);
    }

    if (ret === true) {
      return core.NodeFilter.FILTER_ACCEPT;
    } else if (ret === false) {
      return core.NodeFilter.FILTER_REJECT;
    }

    return ret;
  };

  NodeIteratorInternal.prototype.runRemovingSteps = function (oldNode, oldParent, oldPreviousSibling) {
    if (oldNode.contains(this.root)) {
      return;
    }

    // If oldNode is not an inclusive ancestor of the referenceNode
    // attribute value, terminate these steps.
    if (!oldNode.contains(this.referenceNode)) {
      return;
    }

    if (this.pointerBeforeReferenceNode) {
      // Let nextSibling be oldPreviousSibling’s next sibling, if oldPreviousSibling is non-null,
      // and oldParent’s first child otherwise.
      const nextSibling = oldPreviousSibling ?
                          oldPreviousSibling.nextSibling :
                          oldParent.firstChild;

      // If nextSibling is non-null, set the referenceNode attribute to nextSibling
      // and terminate these steps.
      if (nextSibling) {
        this.referenceNode = nextSibling;
        return;
      }

      // Let next be the first node following oldParent (excluding any children of oldParent).
      const next = domSymbolTree.following(oldParent, { skipChildren: true });

      // If root is an inclusive ancestor of next, set the referenceNode
      // attribute to next and terminate these steps.
      if (this.root.contains(next)) {
        this.referenceNode = next;
        return;
      }

      // Otherwise, set the pointerBeforeReferenceNode attribute to false.
      this.pointerBeforeReferenceNode = false;

      // Note: Steps are not terminated here.
    }

    // Set the referenceNode attribute to the last inclusive descendant in tree order of oldPreviousSibling,
    // if oldPreviousSibling is non-null, and to oldParent otherwise.
    this.referenceNode = oldPreviousSibling ?
                             domSymbolTree.lastInclusiveDescendant(oldPreviousSibling) :
                             oldParent;
  };

  core.Document._removingSteps.push((document, oldNode, oldParent, oldPreviousSibling) => {
    for (let i = 0; i < document._activeNodeIterators.length; ++i) {
      const internal = document._activeNodeIterators[i];
      internal.runRemovingSteps(oldNode, oldParent, oldPreviousSibling);
    }
  });

  core.Document.prototype.createNodeIterator = function (root, whatToShow, filter) {
    if (!root) {
      throw new TypeError("Not enough arguments to Document.createNodeIterator.");
    }

    if (filter === undefined) {
      filter = null;
    }

    if (filter !== null &&
        typeof filter !== "function" &&
        typeof filter.acceptNode !== "function") {
      throw new TypeError("Argument 3 of Document.createNodeIterator should be a function or implement NodeFilter.");
    }

    const document = root._ownerDocument;

    whatToShow = whatToShow === undefined ?
      core.NodeFilter.SHOW_ALL :
      (whatToShow & core.NodeFilter.SHOW_ALL) >>> 0; // >>> makes sure the result is unsigned

    filter = filter || null;

    const it = Object.create(core.NodeIterator.prototype);
    const internal = new NodeIteratorInternal(document, root, whatToShow, filter);
    it[INTERNAL] = internal;

    document._activeNodeIterators.push(internal);
    while (document._activeNodeIterators.length > document._activeNodeIteratorsMax) {
      const internalOther = document._activeNodeIterators.shift();
      internalOther.active = false;
    }

    return it;
  };

  core.NodeIterator = function NodeIterator() {
    throw new TypeError("Illegal constructor");
  };

  defineGetter(core.NodeIterator.prototype, "root", function () {
    return this[INTERNAL].root;
  });

  defineGetter(core.NodeIterator.prototype, "referenceNode", function () {
    const internal = this[INTERNAL];
    internal.throwIfNotActive();
    return internal.referenceNode;
  });

  defineGetter(core.NodeIterator.prototype, "pointerBeforeReferenceNode", function () {
    const internal = this[INTERNAL];
    internal.throwIfNotActive();
    return internal.pointerBeforeReferenceNode;
  });

  defineGetter(core.NodeIterator.prototype, "whatToShow", function () {
    return this[INTERNAL].whatToShow;
  });

  defineGetter(core.NodeIterator.prototype, "filter", function () {
    return this[INTERNAL].filter;
  });

  core.NodeIterator.prototype.previousNode = function () {
    const internal = this[INTERNAL];
    internal.throwIfNotActive();
    return internal.traverse(false);
  };

  core.NodeIterator.prototype.nextNode = function () {
    const internal = this[INTERNAL];
    internal.throwIfNotActive();
    return internal.traverse(true);
  };

  core.NodeIterator.prototype.detach = function () {
    // noop
  };

  core.NodeIterator.prototype.toString = function () {
    return "[object NodeIterator]";
  };
};
