define(
  ["../exception","./ast","exports"],
  function(__dependency1__, __dependency2__, __exports__) {
    "use strict";
    var Exception = __dependency1__["default"];
    var AST = __dependency2__["default"];

    function Visitor() {
      this.parents = [];
    }

    Visitor.prototype = {
      constructor: Visitor,
      mutating: false,

      // Visits a given value. If mutating, will replace the value if necessary.
      acceptKey: function(node, name) {
        var value = this.accept(node[name]);
        if (this.mutating) {
          // Hacky sanity check:
          if (value && (!value.type || !AST[value.type])) {
            throw new Exception('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type);
          }
          node[name] = value;
        }
      },

      // Performs an accept operation with added sanity check to ensure
      // required keys are not removed.
      acceptRequired: function(node, name) {
        this.acceptKey(node, name);

        if (!node[name]) {
          throw new Exception(node.type + ' requires ' + name);
        }
      },

      // Traverses a given array. If mutating, empty respnses will be removed
      // for child elements.
      acceptArray: function(array) {
        for (var i = 0, l = array.length; i < l; i++) {
          this.acceptKey(array, i);

          if (!array[i]) {
            array.splice(i, 1);
            i--;
            l--;
          }
        }
      },

      accept: function(object) {
        if (!object) {
          return;
        }

        if (this.current) {
          this.parents.unshift(this.current);
        }
        this.current = object;

        var ret = this[object.type](object);

        this.current = this.parents.shift();

        if (!this.mutating || ret) {
          return ret;
        } else if (ret !== false) {
          return object;
        }
      },

      Program: function(program) {
        this.acceptArray(program.body);
      },

      MustacheStatement: function(mustache) {
        this.acceptRequired(mustache, 'path');
        this.acceptArray(mustache.params);
        this.acceptKey(mustache, 'hash');
      },

      BlockStatement: function(block) {
        this.acceptRequired(block, 'path');
        this.acceptArray(block.params);
        this.acceptKey(block, 'hash');

        this.acceptKey(block, 'program');
        this.acceptKey(block, 'inverse');
      },

      PartialStatement: function(partial) {
        this.acceptRequired(partial, 'name');
        this.acceptArray(partial.params);
        this.acceptKey(partial, 'hash');
      },

      ContentStatement: function(/* content */) {},
      CommentStatement: function(/* comment */) {},

      SubExpression: function(sexpr) {
        this.acceptRequired(sexpr, 'path');
        this.acceptArray(sexpr.params);
        this.acceptKey(sexpr, 'hash');
      },
      PartialExpression: function(partial) {
        this.acceptRequired(partial, 'name');
        this.acceptArray(partial.params);
        this.acceptKey(partial, 'hash');
      },

      PathExpression: function(/* path */) {},

      StringLiteral: function(/* string */) {},
      NumberLiteral: function(/* number */) {},
      BooleanLiteral: function(/* bool */) {},

      Hash: function(hash) {
        this.acceptArray(hash.pairs);
      },
      HashPair: function(pair) {
        this.acceptRequired(pair, 'value');
      }
    };

    __exports__["default"] = Visitor;
  });