"use strict";
module.exports = TreeWalker;

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

var mapChild = {
  first: 'firstChild',
  last: 'lastChild',
  next: 'firstChild',
  previous: 'lastChild'
};

var mapSibling = {
  first: 'nextSibling',
  last: 'previousSibling',
  next: 'nextSibling',
  previous: 'previousSibling'
};

/* Private methods and helpers */

/**
 * @spec https://dom.spec.whatwg.org/#concept-traverse-children
 * @method
 * @access private
 * @param {TreeWalker} tw
 * @param {string} type One of 'first' or 'last'.
 * @return {Node|null}
 */
function traverseChildren(tw, type) {
  var child, node, parent, result, sibling;
  node = tw._currentNode[mapChild[type]];
  while (node !== null) {
    result = tw._internalFilter(node);
    if (result === NodeFilter.FILTER_ACCEPT) {
      tw._currentNode = node;
      return node;
    }
    if (result === NodeFilter.FILTER_SKIP) {
      child = node[mapChild[type]];
      if (child !== null) {
        node = child;
        continue;
      }
    }
    while (node !== null) {
      sibling = node[mapSibling[type]];
      if (sibling !== null) {
        node = sibling;
        break;
      }
      parent = node.parentNode;
      if (parent === null || parent === tw.root || parent === tw._currentNode) {
        return null;
      } else {
        node = parent;
      }
    }
  }
  return null;
}

/**
 * @spec https://dom.spec.whatwg.org/#concept-traverse-siblings
 * @method
 * @access private
 * @param {TreeWalker} tw
 * @param {TreeWalker} type One of 'next' or 'previous'.
 * @return {Node|nul}
 */
function traverseSiblings(tw, type) {
  var node, result, sibling;
  node = tw._currentNode;
  if (node === tw.root) {
    return null;
  }
  while (true) {
    sibling = node[mapSibling[type]];
    while (sibling !== null) {
      node = sibling;
      result = tw._internalFilter(node);
      if (result === NodeFilter.FILTER_ACCEPT) {
        tw._currentNode = node;
        return node;
      }
      sibling = node[mapChild[type]];
      if (result === NodeFilter.FILTER_REJECT || sibling === null) {
        sibling = node[mapSibling[type]];
      }
    }
    node = node.parentNode;
    if (node === null || node === tw.root) {
      return null;
    }
    if (tw._internalFilter(node) === NodeFilter.FILTER_ACCEPT) {
      return null;
    }
  }
}


/* Public API */

/**
 * Latest version: https://dom.spec.whatwg.org/#treewalker
 *
 * @constructor
 * @param {Node} root
 * @param {number} whatToShow [optional]
 * @param {Function|NodeFilter} filter [optional]
 * @throws Error
 */
function TreeWalker(root, whatToShow, filter) {
  if (!root || !root.nodeType) {
    utils.NotSupportedError();
  }

  // Read-only properties
  this._root = root;
  this._whatToShow = Number(whatToShow) || 0;
  this._filter = filter || null;
  this._active = false;
  // Read-write property
  this._currentNode = root;
}

Object.defineProperties(TreeWalker.prototype, {
  root: { get: function() { return this._root; } },
  whatToShow: { get: function() { return this._whatToShow; } },
  filter: { get: function() { return this._filter; } },

  currentNode: {
    get: function currentNode() {
      return this._currentNode;
    },
    set: function setCurrentNode(v) {
      if (!(v instanceof Node)) {
        throw new TypeError("Not a Node"); // `null` is also not a node
      }
      this._currentNode = v;
    },
  },

  /**
   * @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/#dom-treewalker-parentnode
   * @based on WebKit's TreeWalker::parentNode
   * https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/dom/TreeWalker.cpp?rev=220453#L50
   * @method
   * @return {Node|null}
   */
  parentNode: { value: function parentNode() {
    var node = this._currentNode;
    while (node !== this.root) {
      node = node.parentNode;
      if (node === null) {
        return null;
      }
      if (this._internalFilter(node) === NodeFilter.FILTER_ACCEPT) {
        this._currentNode = node;
        return node;
      }
    }
    return null;
  }},

  /**
   * @spec https://dom.spec.whatwg.org/#dom-treewalker-firstchild
   * @method
   * @return {Node|null}
   */
  firstChild: { value: function firstChild() {
    return traverseChildren(this, 'first');
  }},

  /**
   * @spec https://dom.spec.whatwg.org/#dom-treewalker-lastchild
   * @method
   * @return {Node|null}
   */
  lastChild: { value: function lastChild() {
    return traverseChildren(this, 'last');
  }},

  /**
   * @spec http://www.w3.org/TR/dom/#dom-treewalker-previoussibling
   * @method
   * @return {Node|null}
   */
  previousSibling: { value: function previousSibling() {
    return traverseSiblings(this, 'previous');
  }},

  /**
   * @spec http://www.w3.org/TR/dom/#dom-treewalker-nextsibling
   * @method
   * @return {Node|null}
   */
  nextSibling: { value: function nextSibling() {
    return traverseSiblings(this, 'next');
  }},

  /**
   * @spec https://dom.spec.whatwg.org/#dom-treewalker-previousnode
   * @based on WebKit's TreeWalker::previousNode
   * https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/dom/TreeWalker.cpp?rev=220453#L181
   * @method
   * @return {Node|null}
   */
  previousNode: { value: function previousNode() {
    var node, result, previousSibling, lastChild;
    node = this._currentNode;
    while (node !== this._root) {
      for (previousSibling = node.previousSibling;
           previousSibling;
           previousSibling = node.previousSibling) {
        node = previousSibling;
        result = this._internalFilter(node);
        if (result === NodeFilter.FILTER_REJECT) {
          continue;
        }
        for (lastChild = node.lastChild;
             lastChild;
             lastChild = node.lastChild) {
          node = lastChild;
          result = this._internalFilter(node);
          if (result === NodeFilter.FILTER_REJECT) {
            break;
          }
        }
        if (result === NodeFilter.FILTER_ACCEPT) {
          this._currentNode = node;
          return node;
        }
      }
      if (node === this.root || node.parentNode === null) {
        return null;
      }
      node = node.parentNode;
      if (this._internalFilter(node) === NodeFilter.FILTER_ACCEPT) {
        this._currentNode = node;
        return node;
      }
    }
    return null;
  }},

  /**
   * @spec https://dom.spec.whatwg.org/#dom-treewalker-nextnode
   * @based on WebKit's TreeWalker::nextNode
   * https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/dom/TreeWalker.cpp?rev=220453#L228
   * @method
   * @return {Node|null}
   */
  nextNode: { value: function nextNode() {
    var node, result, firstChild, nextSibling;
    node = this._currentNode;
    result = NodeFilter.FILTER_ACCEPT;

    CHILDREN:
    while (true) {
      for (firstChild = node.firstChild;
           firstChild;
           firstChild = node.firstChild) {
        node = firstChild;
        result = this._internalFilter(node);
        if (result === NodeFilter.FILTER_ACCEPT) {
          this._currentNode = node;
          return node;
        } else if (result === NodeFilter.FILTER_REJECT) {
          break;
        }
      }
      for (nextSibling = NodeTraversal.nextSkippingChildren(node, this.root);
           nextSibling;
           nextSibling = NodeTraversal.nextSkippingChildren(node, this.root)) {
        node = nextSibling;
        result = this._internalFilter(node);
        if (result === NodeFilter.FILTER_ACCEPT) {
          this._currentNode = node;
          return node;
        } else if (result === NodeFilter.FILTER_SKIP) {
          continue CHILDREN;
        }
      }
      return null;
    }
  }},

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