module.exports = traverse;

var Scope = require("./scope");
var t     = require("../types");
var _     = require("lodash");

function traverse(parent, callbacks, opts) {
  // falsy node
  if (!parent) return;

  // array of nodes
  if (_.isArray(parent)) {
    _.each(parent, function (node) {
      traverse(node, callbacks, opts);
    });
    return;
  }

  // unknown node type to traverse
  var keys = t.VISITOR_KEYS[parent.type];
  if (!keys) return;

  opts = opts || {};
  if (_.isArray(opts)) opts = { blacklist: opts };

  // blacklist these node types from being traversed
  var blacklistTypes = opts.blacklist || [];

  // normalise callbacks
  if (_.isFunction(callbacks)) callbacks = { enter: callbacks };

  _.each(keys, function (key) {
    var nodes = parent[key];
    if (!nodes) return;

    var handle = function (obj, key) {
      var node = obj[key];
      if (!node) return;

      // type is blacklisted
      if (_.contains(blacklistTypes, node.type)) return;

      // replace node
      var maybeReplace = function (result) {
        if (result === false) return;
        if (result != null) node = obj[key] = result;
      };

      //
      var opts2 = _.clone(opts);
      if (t.isScope(node)) opts2.scope = new Scope(opts.scope, node);

      // enter
      if (callbacks.enter) {
        var result = callbacks.enter(node, parent, opts2.scope);
        maybeReplace(result);

        // stop iteration
        if (result === false) return;
      }

      // traverse node
      traverse(node, callbacks, opts2);

      // exit
      if (callbacks.exit) {
        maybeReplace(callbacks.exit(node, parent, opts2.scope));
      }
    };

    if (_.isArray(nodes)) {
      _.each(nodes, function (node, i) {
        handle(nodes, i);
      });

      parent[key] = _.flatten(parent[key]);
    } else {
      handle(parent, key);
    }
  });
}

traverse.removeProperties = function (tree) {
  var clear = function (node) {
    delete node.extendedRange;
    delete node._scopeIds;
    delete node._parent;
    delete node.tokens;
    delete node.range;
    delete node.start;
    delete node.end;
    delete node.loc;
    delete node.raw;

    clearComments(node.trailingComments);
    clearComments(node.leadingComments);
  };

  var clearComments = function (comments) {
    _.each(comments, clear);
  };

  clear(tree);
  traverse(tree, clear);

  return tree;
};

traverse.hasType = function (tree, type, blacklistTypes) {
  blacklistTypes = [].concat(blacklistTypes || []);

  var has = false;

  // the node we're searching in is blacklisted
  if (_.contains(blacklistTypes, tree.type)) return false;

  // the type we're looking for is the same as the passed node
  if (tree.type === type) return true;

  traverse(tree, function (node) {
    if (node.type === type) {
      has = true;
      return false;
    }
  }, { blacklist: blacklistTypes });

  return has;
};

@@ -441,22 +441,62 @@
 (!keys) 
-return
+throw new Error(%22unknown node %22 + parent.type)
 ;%0A%0A  opt
@@ -759,37 +759,52 @@
 %0A%0A  
-_.each(keys, function (key) %7B
+for (var i in keys) %7B%0A    var key = keys%5Bi%5D;
 %0A   
@@ -845,22 +845,50 @@
 !nodes) 
-return
+continue;%0A%0A    var updated = false
 ;%0A%0A    v
@@ -1008,35 +1008,24 @@
 ed%0A      if 
-(_.contains
 (blacklistTy
@@ -1027,18 +1027,25 @@
 istTypes
-, 
+.indexOf(
 node.typ
@@ -1042,24 +1042,29 @@
 f(node.type)
+ %3E -1
 ) return;%0A%0A 
@@ -1163,24 +1163,25 @@
 se) return;%0A
+%0A
         if (
@@ -1195,16 +1195,54 @@
 != null)
+ %7B%0A          updated = true;%0A         
  node = 
@@ -1260,16 +1260,26 @@
 result;%0A
+        %7D%0A
       %7D;
@@ -1311,21 +1311,56 @@
 2 = 
-_.clone(opts)
+%7B scope: opts.scope, blacklist: opts.blacklist %7D
 ;%0A  
@@ -1866,39 +1866,27 @@
     
-_.each(nodes, functio
+for (var i i
 n 
-(
 node
-, i
+s
 ) %7B%0A
@@ -1918,25 +1918,36 @@
 %0A      %7D
-);
 %0A%0A     
+ if (updated)
  parent%5B
@@ -2026,18 +2026,16 @@
    %7D%0A  %7D
-);
 %0A%7D%0A%0Atrav

optimise traverse, improves traversal speed by 50%