"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]";
  } },
});
