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

    var slice = [].slice;


    function Compiler() {}

    __exports__.Compiler = Compiler;// the foundHelper register will disambiguate helper lookup from finding a
    // function in a context. This is necessary for mustache compatibility, which
    // requires that context functions in blocks are evaluated by blockHelperMissing,
    // and then proceed as if the resulting value was provided to blockHelperMissing.

    Compiler.prototype = {
      compiler: Compiler,

      equals: function(other) {
        var len = this.opcodes.length;
        if (other.opcodes.length !== len) {
          return false;
        }

        for (var i = 0; i < len; i++) {
          var opcode = this.opcodes[i],
              otherOpcode = other.opcodes[i];
          if (opcode.opcode !== otherOpcode.opcode || !argEquals(opcode.args, otherOpcode.args)) {
            return false;
          }
        }

        // We know that length is the same between the two arrays because they are directly tied
        // to the opcode behavior above.
        len = this.children.length;
        for (i = 0; i < len; i++) {
          if (!this.children[i].equals(other.children[i])) {
            return false;
          }
        }

        return true;
      },

      guid: 0,

      compile: function(program, options) {
        this.sourceNode = [];
        this.opcodes = [];
        this.children = [];
        this.options = options;
        this.stringParams = options.stringParams;
        this.trackIds = options.trackIds;

        options.blockParams = options.blockParams || [];

        // These changes will propagate to the other compiler components
        var knownHelpers = options.knownHelpers;
        options.knownHelpers = {
          'helperMissing': true,
          'blockHelperMissing': true,
          'each': true,
          'if': true,
          'unless': true,
          'with': true,
          'log': true,
          'lookup': true
        };
        if (knownHelpers) {
          for (var name in knownHelpers) {
            options.knownHelpers[name] = knownHelpers[name];
          }
        }

        return this.accept(program);
      },

      compileProgram: function(program) {
        var result = new this.compiler().compile(program, this.options);
        var guid = this.guid++;

        this.usePartial = this.usePartial || result.usePartial;

        this.children[guid] = result;
        this.useDepths = this.useDepths || result.useDepths;

        return guid;
      },

      accept: function(node) {
        this.sourceNode.unshift(node);
        var ret = this[node.type](node);
        this.sourceNode.shift();
        return ret;
      },

      Program: function(program) {
        this.options.blockParams.unshift(program.blockParams);

        var body = program.body;
        for(var i=0, l=body.length; i<l; i++) {
          this.accept(body[i]);
        }

        this.options.blockParams.shift();

        this.isSimple = l === 1;
        this.blockParams = program.blockParams ? program.blockParams.length : 0;

        return this;
      },

      BlockStatement: function(block) {
        transformLiteralToPath(block);

        var program = block.program,
            inverse = block.inverse;

        program = program && this.compileProgram(program);
        inverse = inverse && this.compileProgram(inverse);

        var type = this.classifySexpr(block);

        if (type === 'helper') {
          this.helperSexpr(block, program, inverse);
        } else if (type === 'simple') {
          this.simpleSexpr(block);

          // now that the simple mustache is resolved, we need to
          // evaluate it by executing `blockHelperMissing`
          this.opcode('pushProgram', program);
          this.opcode('pushProgram', inverse);
          this.opcode('emptyHash');
          this.opcode('blockValue', block.path.original);
        } else {
          this.ambiguousSexpr(block, program, inverse);

          // now that the simple mustache is resolved, we need to
          // evaluate it by executing `blockHelperMissing`
          this.opcode('pushProgram', program);
          this.opcode('pushProgram', inverse);
          this.opcode('emptyHash');
          this.opcode('ambiguousBlockValue');
        }

        this.opcode('append');
      },

      PartialStatement: function(partial) {
        this.usePartial = true;

        var params = partial.params;
        if (params.length > 1) {
          throw new Exception('Unsupported number of partial arguments: ' + params.length, partial);
        } else if (!params.length) {
          params.push({type: 'PathExpression', parts: [], depth: 0});
        }

        var partialName = partial.name.original,
            isDynamic = partial.name.type === 'SubExpression';
        if (isDynamic) {
          this.accept(partial.name);
        }

        this.setupFullMustacheParams(partial, undefined, undefined, true);

        var indent = partial.indent || '';
        if (this.options.preventIndent && indent) {
          this.opcode('appendContent', indent);
          indent = '';
        }

        this.opcode('invokePartial', isDynamic, partialName, indent);
        this.opcode('append');
      },

      MustacheStatement: function(mustache) {
        this.SubExpression(mustache);

        if(mustache.escaped && !this.options.noEscape) {
          this.opcode('appendEscaped');
        } else {
          this.opcode('append');
        }
      },

      ContentStatement: function(content) {
        if (content.value) {
          this.opcode('appendContent', content.value);
        }
      },

      CommentStatement: function() {},

      SubExpression: function(sexpr) {
        transformLiteralToPath(sexpr);
        var type = this.classifySexpr(sexpr);

        if (type === 'simple') {
          this.simpleSexpr(sexpr);
        } else if (type === 'helper') {
          this.helperSexpr(sexpr);
        } else {
          this.ambiguousSexpr(sexpr);
        }
      },
      ambiguousSexpr: function(sexpr, program, inverse) {
        var path = sexpr.path,
            name = path.parts[0],
            isBlock = program != null || inverse != null;

        this.opcode('getContext', path.depth);

        this.opcode('pushProgram', program);
        this.opcode('pushProgram', inverse);

        this.accept(path);

        this.opcode('invokeAmbiguous', name, isBlock);
      },

      simpleSexpr: function(sexpr) {
        this.accept(sexpr.path);
        this.opcode('resolvePossibleLambda');
      },

      helperSexpr: function(sexpr, program, inverse) {
        var params = this.setupFullMustacheParams(sexpr, program, inverse),
            path = sexpr.path,
            name = path.parts[0];

        if (this.options.knownHelpers[name]) {
          this.opcode('invokeKnownHelper', params.length, name);
        } else if (this.options.knownHelpersOnly) {
          throw new Exception("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
        } else {
          path.falsy = true;

          this.accept(path);
          this.opcode('invokeHelper', params.length, path.original, AST.helpers.simpleId(path));
        }
      },

      PathExpression: function(path) {
        this.addDepth(path.depth);
        this.opcode('getContext', path.depth);

        var name = path.parts[0],
            scoped = AST.helpers.scopedId(path),
            blockParamId = !path.depth && !scoped && this.blockParamIndex(name);

        if (blockParamId) {
          this.opcode('lookupBlockParam', blockParamId, path.parts);
        } else  if (!name) {
          // Context reference, i.e. `{{foo .}}` or `{{foo ..}}`
          this.opcode('pushContext');
        } else if (path.data) {
          this.options.data = true;
          this.opcode('lookupData', path.depth, path.parts);
        } else {
          this.opcode('lookupOnContext', path.parts, path.falsy, scoped);
        }
      },

      StringLiteral: function(string) {
        this.opcode('pushString', string.value);
      },

      NumberLiteral: function(number) {
        this.opcode('pushLiteral', number.value);
      },

      BooleanLiteral: function(bool) {
        this.opcode('pushLiteral', bool.value);
      },

      Hash: function(hash) {
        var pairs = hash.pairs, i, l;

        this.opcode('pushHash');

        for (i=0, l=pairs.length; i<l; i++) {
          this.pushParam(pairs[i].value);
        }
        while (i--) {
          this.opcode('assignToHash', pairs[i].key);
        }
        this.opcode('popHash');
      },

      // HELPERS
      opcode: function(name) {
        this.opcodes.push({ opcode: name, args: slice.call(arguments, 1), loc: this.sourceNode[0].loc });
      },

      addDepth: function(depth) {
        if (!depth) {
          return;
        }

        this.useDepths = true;
      },

      classifySexpr: function(sexpr) {
        var isSimple = AST.helpers.simpleId(sexpr.path);

        var isBlockParam = isSimple && !!this.blockParamIndex(sexpr.path.parts[0]);

        // a mustache is an eligible helper if:
        // * its id is simple (a single part, not `this` or `..`)
        var isHelper = !isBlockParam && AST.helpers.helperExpression(sexpr);

        // if a mustache is an eligible helper but not a definite
        // helper, it is ambiguous, and will be resolved in a later
        // pass or at runtime.
        var isEligible = !isBlockParam && (isHelper || isSimple);

        var options = this.options;

        // if ambiguous, we can possibly resolve the ambiguity now
        // An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc.
        if (isEligible && !isHelper) {
          var name = sexpr.path.parts[0];

          if (options.knownHelpers[name]) {
            isHelper = true;
          } else if (options.knownHelpersOnly) {
            isEligible = false;
          }
        }

        if (isHelper) { return 'helper'; }
        else if (isEligible) { return 'ambiguous'; }
        else { return 'simple'; }
      },

      pushParams: function(params) {
        for(var i=0, l=params.length; i<l; i++) {
          this.pushParam(params[i]);
        }
      },

      pushParam: function(val) {
        var value = val.value != null ? val.value : val.original || '';

        if (this.stringParams) {
          if (value.replace) {
            value = value
                .replace(/^(\.?\.\/)*/g, '')
                .replace(/\//g, '.');
          }

          if(val.depth) {
            this.addDepth(val.depth);
          }
          this.opcode('getContext', val.depth || 0);
          this.opcode('pushStringParam', value, val.type);

          if (val.type === 'SubExpression') {
            // SubExpressions get evaluated and passed in
            // in string params mode.
            this.accept(val);
          }
        } else {
          if (this.trackIds) {
            var blockParamIndex;
            if (val.parts && !AST.helpers.scopedId(val) && !val.depth) {
               blockParamIndex = this.blockParamIndex(val.parts[0]);
            }
            if (blockParamIndex) {
              var blockParamChild = val.parts.slice(1).join('.');
              this.opcode('pushId', 'BlockParam', blockParamIndex, blockParamChild);
            } else {
              value = val.original || value;
              if (value.replace) {
                value = value
                    .replace(/^\.\//g, '')
                    .replace(/^\.$/g, '');
              }

              this.opcode('pushId', val.type, value);
            }
          }
          this.accept(val);
        }
      },

      setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) {
        var params = sexpr.params;
        this.pushParams(params);

        this.opcode('pushProgram', program);
        this.opcode('pushProgram', inverse);

        if (sexpr.hash) {
          this.accept(sexpr.hash);
        } else {
          this.opcode('emptyHash', omitEmpty);
        }

        return params;
      },

      blockParamIndex: function(name) {
        for (var depth = 0, len = this.options.blockParams.length; depth < len; depth++) {
          var blockParams = this.options.blockParams[depth],
              param = blockParams && indexOf(blockParams, name);
          if (blockParams && param >= 0) {
            return [depth, param];
          }
        }
      }
    };

    function precompile(input, options, env) {
      if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
        throw new Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input);
      }

      options = options || {};
      if (!('data' in options)) {
        options.data = true;
      }
      if (options.compat) {
        options.useDepths = true;
      }

      var ast = env.parse(input, options);
      var environment = new env.Compiler().compile(ast, options);
      return new env.JavaScriptCompiler().compile(environment, options);
    }

    __exports__.precompile = precompile;function compile(input, options, env) {
      if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
        throw new Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
      }

      options = options || {};

      if (!('data' in options)) {
        options.data = true;
      }
      if (options.compat) {
        options.useDepths = true;
      }

      var compiled;

      function compileInput() {
        var ast = env.parse(input, options);
        var environment = new env.Compiler().compile(ast, options);
        var templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true);
        return env.template(templateSpec);
      }

      // Template is only compiled on first use and cached after that point.
      var ret = function(context, options) {
        if (!compiled) {
          compiled = compileInput();
        }
        return compiled.call(this, context, options);
      };
      ret._setup = function(options) {
        if (!compiled) {
          compiled = compileInput();
        }
        return compiled._setup(options);
      };
      ret._child = function(i, data, blockParams, depths) {
        if (!compiled) {
          compiled = compileInput();
        }
        return compiled._child(i, data, blockParams, depths);
      };
      return ret;
    }

    __exports__.compile = compile;function argEquals(a, b) {
      if (a === b) {
        return true;
      }

      if (isArray(a) && isArray(b) && a.length === b.length) {
        for (var i = 0; i < a.length; i++) {
          if (!argEquals(a[i], b[i])) {
            return false;
          }
        }
        return true;
      }
    }

    function transformLiteralToPath(sexpr) {
      if (!sexpr.path.parts) {
        var literal = sexpr.path;
        // Casting to string here to make false and 0 literal values play nicely with the rest
        // of the system.
        sexpr.path = new AST.PathExpression(false, 0, [literal.original+''], literal.original+'', literal.log);
      }
    }
  });