'use strict';
module.exports = NodeIterator;

var NodeFilter = require('./NodeFilter');
var NodeTraversal = require('./NodeTraversal');
var utils = require('./utils');

/* Private methods and helpers */

/**
 * @based on WebKit's NodeIterator::moveToNext and NodeIterator::moveToPrevious
 * https://trac.webkit.org/browser/trunk/Source/WebCore/dom/NodeIterator.cpp?rev=186279#L51
 */
function move(node, stayWithin, directionIsNext) {
  if (directionIsNext) {
    return NodeTraversal.next(node, stayWithin);
  } else {
    if (node === stayWithin) {
      return null;
    }
    return NodeTraversal.previous(node, null);
  }
}

function isInclusiveAncestor(node, possibleChild) {
  for (; possibleChild; possibleChild = possibleChild.parentNode) {
    if (node === possibleChild) {
      return true;
    }
  }
  return false;
}

/**
 * @spec http://www.w3.org/TR/dom/#concept-nodeiterator-traverse
 * @method
 * @access private
 * @param {NodeIterator} ni
 * @param {string} direction One of 'next' or 'previous'.
 * @return {Node|null}
 */
function traverse(ni, directionIsNext) {
  var node, beforeNode;
  node = ni._referenceNode;
  beforeNode = ni._pointerBeforeReferenceNode;
  while (true) {
    if (beforeNode === directionIsNext) {
      beforeNode = !beforeNode;
    } else {
      node = move(node, ni._root, directionIsNext);
      if (node === null) {
        return null;
      }
    }
    var result = ni._internalFilter(node);
    if (result === NodeFilter.FILTER_ACCEPT) {
      break;
    }
  }
  ni._referenceNode = node;
  ni._pointerBeforeReferenceNode = beforeNode;
  return node;
}

/* Public API */

/**
 * Implemented version: http://www.w3.org/TR/2015/WD-dom-20150618/#nodeiterator
 * Latest version: http://www.w3.org/TR/dom/#nodeiterator
 *
 * @constructor
 * @param {Node} root
 * @param {number} whatToShow [optional]
 * @param {Function|NodeFilter} filter [optional]
 * @throws Error
 */
function NodeIterator(root, whatToShow, filter) {
  if (!root || !root.nodeType) {
    utils.NotSupportedError();
  }

  // Read-only properties
  this._root = root;
  this._referenceNode = root;
  this._pointerBeforeReferenceNode = true;
  this._whatToShow = Number(whatToShow) || 0;
  this._filter = filter || null;
  this._active = false;
  // Record active node iterators in the document, in order to perform
  // "node iterator pre-removal steps".
  root.doc._attachNodeIterator(this);
}

Object.defineProperties(NodeIterator.prototype, {
  root: {
    get: function root() {
      return this._root;
    },
  },
  referenceNode: {
    get: function referenceNode() {
      return this._referenceNode;
    },
  },
  pointerBeforeReferenceNode: {
    get: function pointerBeforeReferenceNode() {
      return this._pointerBeforeReferenceNode;
    },
  },
  whatToShow: {
    get: function whatToShow() {
      return this._whatToShow;
    },
  },
  filter: {
    get: function filter() {
      return this._filter;
    },
  },

  /**
   * @method
   * @param {Node} node
   * @return {Number} Constant NodeFilter.FILTER_ACCEPT,
   *  NodeFilter.FILTER_REJECT or NodeFilter.FILTER_SKIP.
   */
  _internalFilter: {
    value: function _internalFilter(node) {
      /* jshint bitwise: false */
      var result, filter;
      if (this._active) {
        utils.InvalidStateError();
      }

      // Maps nodeType to whatToShow
      if (!((1 << (node.nodeType - 1)) & this._whatToShow)) {
        return NodeFilter.FILTER_SKIP;
      }

      filter = this._filter;
      if (filter === null) {
        result = NodeFilter.FILTER_ACCEPT;
      } else {
        this._active = true;
        try {
          if (typeof filter === 'function') {
            result = filter(node);
          } else {
            result = filter.acceptNode(node);
          }
        } finally {
          this._active = false;
        }
      }

      // Note that coercing to a number means that
      //  `true` becomes `1` (which is NodeFilter.FILTER_ACCEPT)
      //  `false` becomes `0` (neither accept, reject, or skip)
      return +result;
    },
  },

  /**
   * @spec https://dom.spec.whatwg.org/#nodeiterator-pre-removing-steps
   * @method
   * @return void
   */
  _preremove: {
    value: function _preremove(toBeRemovedNode) {
      if (isInclusiveAncestor(toBeRemovedNode, this._root)) {
        return;
      }
      if (!isInclusiveAncestor(toBeRemovedNode, this._referenceNode)) {
        return;
      }
      if (this._pointerBeforeReferenceNode) {
        var next = toBeRemovedNode;
        while (next.lastChild) {
          next = next.lastChild;
        }
        next = NodeTraversal.next(next, this.root);
        if (next) {
          this._referenceNode = next;
          return;
        }
        this._pointerBeforeReferenceNode = false;
        // fall through
      }
      if (toBeRemovedNode.previousSibling === null) {
        this._referenceNode = toBeRemovedNode.parentNode;
      } else {
        this._referenceNode = toBeRemovedNode.previousSibling;
        var lastChild;
        for (
          lastChild = this._referenceNode.lastChild;
          lastChild;
          lastChild = this._referenceNode.lastChild
        ) {
          this._referenceNode = lastChild;
        }
      }
    },
  },

  /**
   * @spec http://www.w3.org/TR/dom/#dom-nodeiterator-nextnode
   * @method
   * @return {Node|null}
   */
  nextNode: {
    value: function nextNode() {
      return traverse(this, true);
    },
  },

  /**
   * @spec http://www.w3.org/TR/dom/#dom-nodeiterator-previousnode
   * @method
   * @return {Node|null}
   */
  previousNode: {
    value: function previousNode() {
      return traverse(this, false);
    },
  },

  /**
   * @spec http://www.w3.org/TR/dom/#dom-nodeiterator-detach
   * @method
   * @return void
   */
  detach: {
    value: function detach() {
      /* "The detach() method must do nothing.
       * Its functionality (disabling a NodeIterator object) was removed,
       * but the method itself is preserved for compatibility.
       */
    },
  },

  /** For compatibility with web-platform-tests. */
  toString: {
    value: function toString() {
      return '[object NodeIterator]';
    },
  },
});
