var compilerbase = require("./base");

exports.attach = function (Handlebars) {

    compilerbase.attach(Handlebars);

// BEGIN(BROWSER)

    /*jshint eqnull:true*/
    var Compiler = Handlebars.Compiler = function () {
    };
    var JavaScriptCompiler = Handlebars.JavaScriptCompiler = function () {
    };

// 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,

        disassemble: function () {
            var opcodes = this.opcodes, opcode, out = [], params, param;

            for (var i = 0, l = opcodes.length; i < l; i++) {
                opcode = opcodes[i];

                if (opcode.opcode === 'DECLARE') {
                    out.push("DECLARE " + opcode.name + "=" + opcode.value);
                } else {
                    params = [];
                    for (var j = 0; j < opcode.args.length; j++) {
                        param = opcode.args[j];
                        if (typeof param === "string") {
                            param = "\"" + param.replace("\n", "\\n") + "\"";
                        }
                        params.push(param);
                    }
                    out.push(opcode.opcode + " " + params.join(" "));
                }
            }

            return out.join("\n");
        },
        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 || opcode.args.length !== otherOpcode.args.length) {
                    return false;
                }
                for (var j = 0; j < opcode.args.length; j++) {
                    if (opcode.args[j] !== otherOpcode.args[j]) {
                        return false;
                    }
                }
            }

            len = this.children.length;
            if (other.children.length !== len) {
                return false;
            }
            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.children = [];
            this.depths = {list: []};
            this.options = options;

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

            return this.program(program);
        },

        accept: function (node) {
            return this[node.type](node);
        },

        program: function (program) {
            var statements = program.statements, statement;
            this.opcodes = [];

            for (var i = 0, l = statements.length; i < l; i++) {
                statement = statements[i];
                this[statement.type](statement);
            }
            this.isSimple = l === 1;

            this.depths.list = this.depths.list.sort(function (a, b) {
                return a - b;
            });

            return this;
        },

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

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

            this.children[guid] = result;

            for (var i = 0, l = result.depths.list.length; i < l; i++) {
                depth = result.depths.list[i];

                if (depth < 2) {
                    continue;
                }
                else {
                    this.addDepth(depth - 1);
                }
            }

            return guid;
        },

        block: function (block) {
            var mustache = block.mustache,
                program = block.program,
                inverse = block.inverse;

            if (program) {
                program = this.compileProgram(program);
            }

            if (inverse) {
                inverse = this.compileProgram(inverse);
            }

            var type = this.classifyMustache(mustache);

            if (type === "helper") {
                this.helperMustache(mustache, program, inverse);
            } else if (type === "simple") {
                this.simpleMustache(mustache);

                // 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');
            } else {
                this.ambiguousMustache(mustache, 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');
        },

        hash: function (hash) {
            var pairs = hash.pairs, pair, val;

            this.opcode('pushHash');

            for (var i = 0, l = pairs.length; i < l; i++) {
                pair = pairs[i];
                val = pair[1];

                if (this.options.stringParams) {
                    if (val.depth) {
                        this.addDepth(val.depth);
                    }
                    this.opcode('getContext', val.depth || 0);
                    this.opcode('pushStringParam', val.stringModeValue, val.type);
                } else {
                    this.accept(val);
                }

                this.opcode('assignToHash', pair[0]);
            }
            this.opcode('popHash');
        },

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

            if (partial.context) {
                this.ID(partial.context);
            } else {
                this.opcode('push', 'depth0');
            }

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

        content: function (content) {
            this.opcode('appendContent', content.string);
        },

        mustache: function (mustache) {
            var options = this.options;
            var type = this.classifyMustache(mustache);

            if (type === "simple") {
                this.simpleMustache(mustache);
            } else if (type === "helper") {
                this.helperMustache(mustache);
            } else {
                this.ambiguousMustache(mustache);
            }

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

        ambiguousMustache: function (mustache, program, inverse) {
            var id = mustache.id,
                name = id.parts[0],
                isBlock = program != null || inverse != null;

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

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

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

        simpleMustache: function (mustache) {
            var id = mustache.id;

            if (id.type === 'DATA') {
                this.DATA(id);
            } else if (id.parts.length) {
                this.ID(id);
            } else {
                // Simplified ID for `this`
                this.addDepth(id.depth);
                this.opcode('getContext', id.depth);
                this.opcode('pushContext');
            }

            this.opcode('resolvePossibleLambda');
        },

        helperMustache: function (mustache, program, inverse) {
            var params = this.setupFullMustacheParams(mustache, program, inverse),
                name = mustache.id.parts[0];

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

        ID: function (id) {
            this.addDepth(id.depth);
            this.opcode('getContext', id.depth);

            var name = id.parts[0];
            if (!name) {
                this.opcode('pushContext');
            } else {
                this.opcode('lookupOnContext', id.parts[0]);
            }

            for (var i = 1, l = id.parts.length; i < l; i++) {
                this.opcode('lookup', id.parts[i]);
            }
        },

        DATA: function (data) {
            this.options.data = true;
            if (data.id.isScoped || data.id.depth) {
                throw new Handlebars.Exception('Scoped data references are not supported: ' + data.original);
            }

            this.opcode('lookupData');
            var parts = data.id.parts;
            for (var i = 0, l = parts.length; i < l; i++) {
                this.opcode('lookup', parts[i]);
            }
        },

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

        INTEGER: function (integer) {
            this.opcode('pushLiteral', integer.integer);
        },

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

        comment: function () {
        },

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

        declare: function (name, value) {
            this.opcodes.push({opcode: 'DECLARE', name: name, value: value});
        },

        addDepth: function (depth) {
            if (isNaN(depth)) {
                throw new Error("EWOT");
            }
            if (depth === 0) {
                return;
            }

            if (!this.depths[depth]) {
                this.depths[depth] = true;
                this.depths.list.push(depth);
            }
        },

        classifyMustache: function (mustache) {
            var isHelper = mustache.isHelper;
            var isEligible = mustache.eligibleHelper;
            var options = this.options;

            // if ambiguous, we can possibly resolve the ambiguity now
            if (isEligible && !isHelper) {
                var name = mustache.id.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) {
            var i = params.length, param;

            while (i--) {
                param = params[i];

                if (this.options.stringParams) {
                    if (param.depth) {
                        this.addDepth(param.depth);
                    }

                    this.opcode('getContext', param.depth || 0);
                    this.opcode('pushStringParam', param.stringModeValue, param.type);
                } else {
                    this[param.type](param);
                }
            }
        },

        setupMustacheParams: function (mustache) {
            var params = mustache.params;
            this.pushParams(params);

            if (mustache.hash) {
                this.hash(mustache.hash);
            } else {
                this.opcode('emptyHash');
            }

            return params;
        },

        // this will replace setupMustacheParams when we're done
        setupFullMustacheParams: function (mustache, program, inverse) {
            var params = mustache.params;
            this.pushParams(params);

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

            if (mustache.hash) {
                this.hash(mustache.hash);
            } else {
                this.opcode('emptyHash');
            }

            return params;
        }
    };

    var Literal = function (value) {
        this.value = value;
    };

    JavaScriptCompiler.prototype = {
        // PUBLIC API: You can override these methods in a subclass to provide
        // alternative compiled forms for name lookup and buffering semantics
        nameLookup: function (parent, name /* , type*/) {
            if (/^[0-9]+$/.test(name)) {
                return parent + "[" + name + "]";
            } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
                return parent + "." + name;
            }
            else {
                return parent + "['" + name + "']";
            }
        },

        appendToBuffer: function (string) {
            if (this.environment.isSimple) {
                return "return " + string + ";";
            } else {
                return {
                    appendToBuffer: true,
                    content: string,
                    toString: function () {
                        return "buffer += " + string + ";";
                    }
                };
            }
        },

        initializeBuffer: function () {
            return this.quotedString("");
        },

        namespace: "Handlebars",
        // END PUBLIC API

        compile: function (environment, options, context, asObject) {
            this.environment = environment;
            this.options = options || {};

            Handlebars.log(Handlebars.logger.DEBUG, this.environment.disassemble() + "\n\n");

            this.name = this.environment.name;
            this.isChild = !!context;
            this.context = context || {
                    programs: [],
                    environments: [],
                    aliases: {}
                };

            this.preamble();

            this.stackSlot = 0;
            this.stackVars = [];
            this.registers = {list: []};
            this.compileStack = [];
            this.inlineStack = [];

            this.compileChildren(environment, options);

            var opcodes = environment.opcodes, opcode;

            this.i = 0;

            for (l = opcodes.length; this.i < l; this.i++) {
                opcode = opcodes[this.i];

                if (opcode.opcode === 'DECLARE') {
                    this[opcode.name] = opcode.value;
                } else {
                    this[opcode.opcode].apply(this, opcode.args);
                }
            }

            return this.createFunctionContext(asObject);
        },

        nextOpcode: function () {
            var opcodes = this.environment.opcodes;
            return opcodes[this.i + 1];
        },

        eat: function () {
            this.i = this.i + 1;
        },

        preamble: function () {
            var out = [];

            if (!this.isChild) {
                var namespace = this.namespace;

                var copies = "helpers = this.merge(helpers, " + namespace + ".helpers);";
                if (this.environment.usePartial) {
                    copies = copies + " partials = this.merge(partials, " + namespace + ".partials);";
                }
                if (this.options.data) {
                    copies = copies + " data = data || {};";
                }
                out.push(copies);
            } else {
                out.push('');
            }

            if (!this.environment.isSimple) {
                out.push(", buffer = " + this.initializeBuffer());
            } else {
                out.push("");
            }

            // track the last context pushed into place to allow skipping the
            // getContext opcode when it would be a noop
            this.lastContext = 0;
            this.source = out;
        },

        createFunctionContext: function (asObject) {
            var locals = this.stackVars.concat(this.registers.list);

            if (locals.length > 0) {
                this.source[1] = this.source[1] + ", " + locals.join(", ");
            }

            // Generate minimizer alias mappings
            if (!this.isChild) {
                for (var alias in this.context.aliases) {
                    if (this.context.aliases.hasOwnProperty(alias)) {
                        this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias];
                    }
                }
            }

            if (this.source[1]) {
                this.source[1] = "var " + this.source[1].substring(2) + ";";
            }

            // Merge children
            if (!this.isChild) {
                this.source[1] += '\n' + this.context.programs.join('\n') + '\n';
            }

            if (!this.environment.isSimple) {
                this.source.push("return buffer;");
            }

            var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"];

            for (var i = 0, l = this.environment.depths.list.length; i < l; i++) {
                params.push("depth" + this.environment.depths.list[i]);
            }

            // Perform a second pass over the output to merge content when possible
            var source = this.mergeSource();

            if (!this.isChild) {
                var revision = Handlebars.COMPILER_REVISION,
                    versions = Handlebars.REVISION_CHANGES[revision];
                source = "this.compilerInfo = [" + revision + ",'" + versions + "'];\n" + source;
            }

            if (asObject) {
                params.push(source);

                return Function.apply(this, params);
            } else {
                var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n  ' + source
                    + '}';
                Handlebars.log(Handlebars.logger.DEBUG, functionSource + "\n\n");
                return functionSource;
            }
        },
        mergeSource: function () {
            // WARN: We are not handling the case where buffer is still populated as the source should
            // not have buffer append operations as their final action.
            var source = '',
                buffer;
            for (var i = 0, len = this.source.length; i < len; i++) {
                var line = this.source[i];
                if (line.appendToBuffer) {
                    if (buffer) {
                        buffer = buffer + '\n    + ' + line.content;
                    } else {
                        buffer = line.content;
                    }
                } else {
                    if (buffer) {
                        source += 'buffer += ' + buffer + ';\n  ';
                        buffer = undefined;
                    }
                    source += line + '\n  ';
                }
            }
            return source;
        },

        // [blockValue]
        //
        // On stack, before: hash, inverse, program, value
        // On stack, after: return value of blockHelperMissing
        //
        // The purpose of this opcode is to take a block of the form
        // `{{#foo}}...{{/foo}}`, resolve the value of `foo`, and
        // replace it on the stack with the result of properly
        // invoking blockHelperMissing.
        blockValue: function () {
            this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';

            var params = ["depth0"];
            this.setupParams(0, params);

            this.replaceStack(function (current) {
                params.splice(1, 0, current);
                return "blockHelperMissing.call(" + params.join(", ") + ")";
            });
        },

        // [ambiguousBlockValue]
        //
        // On stack, before: hash, inverse, program, value
        // Compiler value, before: lastHelper=value of last found helper, if any
        // On stack, after, if no lastHelper: same as [blockValue]
        // On stack, after, if lastHelper: value
        ambiguousBlockValue: function () {
            this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';

            var params = ["depth0"];
            this.setupParams(0, params);

            var current = this.topStack();
            params.splice(1, 0, current);

            // Use the options value generated from the invocation
            params[params.length - 1] = 'options';

            this.source.push(
                "if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ")
                + "); }");
        },

        // [appendContent]
        //
        // On stack, before: ...
        // On stack, after: ...
        //
        // Appends the string value of `content` to the current buffer
        appendContent: function (content) {
            this.source.push(this.appendToBuffer(this.quotedString(content)));
        },

        // [append]
        //
        // On stack, before: value, ...
        // On stack, after: ...
        //
        // Coerces `value` to a String and appends it to the current buffer.
        //
        // If `value` is truthy, or 0, it is coerced into a string and appended
        // Otherwise, the empty string is appended
        append: function () {
            // Force anything that is inlined onto the stack so we don't have duplication
            // when we examine local
            this.flushInline();
            var local = this.popStack();
            this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
            if (this.environment.isSimple) {
                this.source.push("else { " + this.appendToBuffer("''") + " }");
            }
        },

        // [appendEscaped]
        //
        // On stack, before: value, ...
        // On stack, after: ...
        //
        // Escape `value` and append it to the buffer
        appendEscaped: function () {
            this.context.aliases.escapeExpression = 'this.escapeExpression';

            this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")"));
        },

        // [getContext]
        //
        // On stack, before: ...
        // On stack, after: ...
        // Compiler value, after: lastContext=depth
        //
        // Set the value of the `lastContext` compiler value to the depth
        getContext: function (depth) {
            if (this.lastContext !== depth) {
                this.lastContext = depth;
            }
        },

        // [lookupOnContext]
        //
        // On stack, before: ...
        // On stack, after: currentContext[name], ...
        //
        // Looks up the value of `name` on the current context and pushes
        // it onto the stack.
        lookupOnContext: function (name) {
            this.push(this.nameLookup('depth' + this.lastContext, name, 'context'));
        },

        // [pushContext]
        //
        // On stack, before: ...
        // On stack, after: currentContext, ...
        //
        // Pushes the value of the current context onto the stack.
        pushContext: function () {
            this.pushStackLiteral('depth' + this.lastContext);
        },

        // [resolvePossibleLambda]
        //
        // On stack, before: value, ...
        // On stack, after: resolved value, ...
        //
        // If the `value` is a lambda, replace it on the stack by
        // the return value of the lambda
        resolvePossibleLambda: function () {
            this.context.aliases.functionType = '"function"';

            this.replaceStack(function (current) {
                return "typeof " + current + " === functionType ? " + current + ".apply(depth0) : " + current;
            });
        },

        // [lookup]
        //
        // On stack, before: value, ...
        // On stack, after: value[name], ...
        //
        // Replace the value on the stack with the result of looking
        // up `name` on `value`
        lookup: function (name) {
            this.replaceStack(function (current) {
                return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current,
                        name, 'context');
            });
        },

        // [lookupData]
        //
        // On stack, before: ...
        // On stack, after: data[id], ...
        //
        // Push the result of looking up `id` on the current data
        lookupData: function (id) {
            this.push('data');
        },

        // [pushStringParam]
        //
        // On stack, before: ...
        // On stack, after: string, currentContext, ...
        //
        // This opcode is designed for use in string mode, which
        // provides the string value of a parameter along with its
        // depth rather than resolving it immediately.
        pushStringParam: function (string, type) {
            this.pushStackLiteral('depth' + this.lastContext);

            this.pushString(type);

            if (typeof string === 'string') {
                this.pushString(string);
            } else {
                this.pushStackLiteral(string);
            }
        },

        emptyHash: function () {
            this.pushStackLiteral('{}');

            if (this.options.stringParams) {
                this.register('hashTypes', '{}');
                this.register('hashContexts', '{}');
            }
        },
        pushHash: function () {
            this.hash = {values: [], types: [], contexts: []};
        },
        popHash: function () {
            var hash = this.hash;
            this.hash = undefined;

            if (this.options.stringParams) {
                this.register('hashContexts', '{' + hash.contexts.join(',') + '}');
                this.register('hashTypes', '{' + hash.types.join(',') + '}');
            }
            this.push('{\n    ' + hash.values.join(',\n    ') + '\n  }');
        },

        // [pushString]
        //
        // On stack, before: ...
        // On stack, after: quotedString(string), ...
        //
        // Push a quoted version of `string` onto the stack
        pushString: function (string) {
            this.pushStackLiteral(this.quotedString(string));
        },

        // [push]
        //
        // On stack, before: ...
        // On stack, after: expr, ...
        //
        // Push an expression onto the stack
        push: function (expr) {
            this.inlineStack.push(expr);
            return expr;
        },

        // [pushLiteral]
        //
        // On stack, before: ...
        // On stack, after: value, ...
        //
        // Pushes a value onto the stack. This operation prevents
        // the compiler from creating a temporary variable to hold
        // it.
        pushLiteral: function (value) {
            this.pushStackLiteral(value);
        },

        // [pushProgram]
        //
        // On stack, before: ...
        // On stack, after: program(guid), ...
        //
        // Push a program expression onto the stack. This takes
        // a compile-time guid and converts it into a runtime-accessible
        // expression.
        pushProgram: function (guid) {
            if (guid != null) {
                this.pushStackLiteral(this.programExpression(guid));
            } else {
                this.pushStackLiteral(null);
            }
        },

        // [invokeHelper]
        //
        // On stack, before: hash, inverse, program, params..., ...
        // On stack, after: result of helper invocation
        //
        // Pops off the helper's parameters, invokes the helper,
        // and pushes the helper's return value onto the stack.
        //
        // If the helper is not found, `helperMissing` is called.
        invokeHelper: function (paramSize, name) {
            this.context.aliases.helperMissing = 'helpers.helperMissing';

            var helper = this.lastHelper = this.setupHelper(paramSize, name, true);
            var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');

            this.push(helper.name + ' || ' + nonHelper);
            this.replaceStack(function (name) {
                return name + ' ? ' + name + '.call(' +
                    helper.callParams + ") " + ": helperMissing.call(" +
                    helper.helperMissingParams + ")";
            });
        },

        // [invokeKnownHelper]
        //
        // On stack, before: hash, inverse, program, params..., ...
        // On stack, after: result of helper invocation
        //
        // This operation is used when the helper is known to exist,
        // so a `helperMissing` fallback is not required.
        invokeKnownHelper: function (paramSize, name) {
            var helper = this.setupHelper(paramSize, name);
            this.push(helper.name + ".call(" + helper.callParams + ")");
        },

        // [invokeAmbiguous]
        //
        // On stack, before: hash, inverse, program, params..., ...
        // On stack, after: result of disambiguation
        //
        // This operation is used when an expression like `{{foo}}`
        // is provided, but we don't know at compile-time whether it
        // is a helper or a path.
        //
        // This operation emits more code than the other options,
        // and can be avoided by passing the `knownHelpers` and
        // `knownHelpersOnly` flags at compile-time.
        invokeAmbiguous: function (name, helperCall) {
            this.context.aliases.functionType = '"function"';

            this.pushStackLiteral('{}');    // Hash value
            var helper = this.setupHelper(0, name, helperCall);

            var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');

            var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
            var nextStack = this.nextStack();

            this.source.push('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call('
                + helper.callParams + '); }');
            this.source.push('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack
                + ' === functionType ? ' + nextStack + '.apply(depth0) : ' + nextStack + '; }');
        },

        // [invokePartial]
        //
        // On stack, before: context, ...
        // On stack after: result of partial invocation
        //
        // This operation pops off a context, invokes a partial with that context,
        // and pushes the result of the invocation back.
        invokePartial: function (name) {
            var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), "helpers",
                "partials"];

            if (this.options.data) {
                params.push("data");
            }

            this.context.aliases.self = "this";
            this.push("self.invokePartial(" + params.join(", ") + ")");
        },

        // [assignToHash]
        //
        // On stack, before: value, hash, ...
        // On stack, after: hash, ...
        //
        // Pops a value and hash off the stack, assigns `hash[key] = value`
        // and pushes the hash back onto the stack.
        assignToHash: function (key) {
            var value = this.popStack(),
                context,
                type;

            if (this.options.stringParams) {
                type = this.popStack();
                context = this.popStack();
            }

            var hash = this.hash;
            if (context) {
                hash.contexts.push("'" + key + "': " + context);
            }
            if (type) {
                hash.types.push("'" + key + "': " + type);
            }
            hash.values.push("'" + key + "': (" + value + ")");
        },

        // HELPERS

        compiler: JavaScriptCompiler,

        compileChildren: function (environment, options) {
            var children = environment.children, child, compiler;

            for (var i = 0, l = children.length; i < l; i++) {
                child = children[i];
                compiler = new this.compiler();

                var index = this.matchExistingProgram(child);

                if (index == null) {
                    this.context.programs.push('');     // Placeholder to prevent name conflicts for nested children
                    index = this.context.programs.length;
                    child.index = index;
                    child.name = 'program' + index;
                    this.context.programs[index] = compiler.compile(child, options, this.context);
                    this.context.environments[index] = child;
                } else {
                    child.index = index;
                    child.name = 'program' + index;
                }
            }
        },
        matchExistingProgram: function (child) {
            for (var i = 0, len = this.context.environments.length; i < len; i++) {
                var environment = this.context.environments[i];
                if (environment && environment.equals(child)) {
                    return i;
                }
            }
        },

        programExpression: function (guid) {
            this.context.aliases.self = "this";

            if (guid == null) {
                return "self.noop";
            }

            var child = this.environment.children[guid],
                depths = child.depths.list, depth;

            var programParams = [child.index, child.name, "data"];

            for (var i = 0, l = depths.length; i < l; i++) {
                depth = depths[i];

                if (depth === 1) {
                    programParams.push("depth0");
                }
                else {
                    programParams.push("depth" + (depth - 1));
                }
            }

            return (depths.length === 0 ? "self.program(" : "self.programWithDepth(") + programParams.join(", ") + ")";
        },

        register: function (name, val) {
            this.useRegister(name);
            this.source.push(name + " = " + val + ";");
        },

        useRegister: function (name) {
            if (!this.registers[name]) {
                this.registers[name] = true;
                this.registers.list.push(name);
            }
        },

        pushStackLiteral: function (item) {
            return this.push(new Literal(item));
        },

        pushStack: function (item) {
            this.flushInline();

            var stack = this.incrStack();
            if (item) {
                this.source.push(stack + " = " + item + ";");
            }
            this.compileStack.push(stack);
            return stack;
        },

        replaceStack: function (callback) {
            var prefix = '',
                inline = this.isInline(),
                stack;

            // If we are currently inline then we want to merge the inline statement into the
            // replacement statement via ','
            if (inline) {
                var top = this.popStack(true);

                if (top instanceof Literal) {
                    // Literals do not need to be inlined
                    stack = top.value;
                } else {
                    // Get or create the current stack name for use by the inline
                    var name = this.stackSlot ? this.topStackName() : this.incrStack();

                    prefix = '(' + this.push(name) + ' = ' + top + '),';
                    stack = this.topStack();
                }
            } else {
                stack = this.topStack();
            }

            var item = callback.call(this, stack);

            if (inline) {
                if (this.inlineStack.length || this.compileStack.length) {
                    this.popStack();
                }
                this.push('(' + prefix + item + ')');
            } else {
                // Prevent modification of the context depth variable. Through replaceStack
                if (!/^stack/.test(stack)) {
                    stack = this.nextStack();
                }

                this.source.push(stack + " = (" + prefix + item + ");");
            }
            return stack;
        },

        nextStack: function () {
            return this.pushStack();
        },

        incrStack: function () {
            this.stackSlot++;
            if (this.stackSlot > this.stackVars.length) {
                this.stackVars.push("stack" + this.stackSlot);
            }
            return this.topStackName();
        },
        topStackName: function () {
            return "stack" + this.stackSlot;
        },
        flushInline: function () {
            var inlineStack = this.inlineStack;
            if (inlineStack.length) {
                this.inlineStack = [];
                for (var i = 0, len = inlineStack.length; i < len; i++) {
                    var entry = inlineStack[i];
                    if (entry instanceof Literal) {
                        this.compileStack.push(entry);
                    } else {
                        this.pushStack(entry);
                    }
                }
            }
        },
        isInline: function () {
            return this.inlineStack.length;
        },

        popStack: function (wrapped) {
            var inline = this.isInline(),
                item = (inline ? this.inlineStack : this.compileStack).pop();

            if (!wrapped && (item instanceof Literal)) {
                return item.value;
            } else {
                if (!inline) {
                    this.stackSlot--;
                }
                return item;
            }
        },

        topStack: function (wrapped) {
            var stack = (this.isInline() ? this.inlineStack : this.compileStack),
                item = stack[stack.length - 1];

            if (!wrapped && (item instanceof Literal)) {
                return item.value;
            } else {
                return item;
            }
        },

        quotedString: function (str) {
            return '"' + str
                .replace(/\\/g, '\\\\')
                .replace(/"/g, '\\"')
                .replace(/\n/g, '\\n')
                .replace(/\r/g, '\\r')
                .replace(/\u2028/g, '\\u2028')   // Per Ecma-262 7.3 + 7.8.4
                .replace(/\u2029/g, '\\u2029') + '"';
        },

        setupHelper: function (paramSize, name, missingParams) {
            var params = [];
            this.setupParams(paramSize, params, missingParams);
            var foundHelper = this.nameLookup('helpers', name, 'helper');

            return {
                params: params,
                name: foundHelper,
                callParams: ["depth0"].concat(params).join(", "),
                helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ")
            };
        },

        // the params and contexts arguments are passed in arrays
        // to fill in
        setupParams: function (paramSize, params, useRegister) {
            var options = [], contexts = [], types = [], param, inverse, program;

            options.push("hash:" + this.popStack());

            inverse = this.popStack();
            program = this.popStack();

            // Avoid setting fn and inverse if neither are set. This allows
            // helpers to do a check for `if (options.fn)`
            if (program || inverse) {
                if (!program) {
                    this.context.aliases.self = "this";
                    program = "self.noop";
                }

                if (!inverse) {
                    this.context.aliases.self = "this";
                    inverse = "self.noop";
                }

                options.push("inverse:" + inverse);
                options.push("fn:" + program);
            }

            for (var i = 0; i < paramSize; i++) {
                param = this.popStack();
                params.push(param);

                if (this.options.stringParams) {
                    types.push(this.popStack());
                    contexts.push(this.popStack());
                }
            }

            if (this.options.stringParams) {
                options.push("contexts:[" + contexts.join(",") + "]");
                options.push("types:[" + types.join(",") + "]");
                options.push("hashContexts:hashContexts");
                options.push("hashTypes:hashTypes");
            }

            if (this.options.data) {
                options.push("data:data");
            }

            options = "{" + options.join(",") + "}";
            if (useRegister) {
                this.register('options', options);
                params.push('options');
            } else {
                params.push(options);
            }
            return params.join(", ");
        }
    };

    var reservedWords = (
        "break else new var" +
        " case finally return void" +
        " catch for switch while" +
        " continue function this with" +
        " default if throw" +
        " delete in try" +
        " do instanceof typeof" +
        " abstract enum int short" +
        " boolean export interface static" +
        " byte extends long super" +
        " char final native synchronized" +
        " class float package throws" +
        " const goto private transient" +
        " debugger implements protected volatile" +
        " double import public let yield"
    ).split(" ");

    var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};

    for (var i = 0, l = reservedWords.length; i < l; i++) {
        compilerWords[reservedWords[i]] = true;
    }

    JavaScriptCompiler.isValidJavaScriptVariableName = function (name) {
        if (!JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(name)) {
            return true;
        }
        return false;
    };

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

        options = options || {};
        if (!('data' in options)) {
            options.data = true;
        }
        var ast = Handlebars.parse(input);
        var environment = new Compiler().compile(ast, options);
        return new JavaScriptCompiler().compile(environment, options);
    };

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

        options = options || {};
        if (!('data' in options)) {
            options.data = true;
        }
        var compiled;

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

        // Template is only compiled on first use and cached after that point.
        return function (context, options) {
            if (!compiled) {
                compiled = compile();
            }
            return compiled.call(this, context, options);
        };
    };

// END(BROWSER)

    return Handlebars;

};


