"use strict";

var arrays  = require("../../utils/arrays"),
    objects = require("../../utils/objects"),
    asts    = require("../asts"),
    op      = require("../opcodes"),
    js      = require("../js");

/* Generates parser JavaScript code. */
function generateJS(ast, options) {
  /* These only indent non-empty lines to avoid trailing whitespace. */
  function indent2(code)  { return code.replace(/^(.+)$/gm, '  $1');         }
  function indent6(code)  { return code.replace(/^(.+)$/gm, '      $1');     }
  function indent10(code) { return code.replace(/^(.+)$/gm, '          $1'); }

  function generateTables() {
    if (options.optimize === "size") {
      return [
        'peg$consts = [',
           indent2(ast.consts.join(',\n')),
        '],',
        '',
        'peg$bytecode = [',
           indent2(arrays.map(ast.rules, function(rule) {
             return 'peg$decode("'
                   + js.stringEscape(arrays.map(
                       rule.bytecode,
                       function(b) { return String.fromCharCode(b + 32); }
                     ).join(''))
                   + '")';
           }).join(',\n')),
        '],'
      ].join('\n');
    } else {
      return arrays.map(
        ast.consts,
        function(c, i) { return 'peg$c' + i + ' = ' + c + ','; }
      ).join('\n');
    }
  }

  function generateRuleHeader(ruleNameCode, ruleIndexCode) {
    var parts = [];

    parts.push('');

    if (options.trace) {
      parts.push([
        'peg$tracer.trace({',
        '  type:     "rule.enter",',
        '  rule:     ' + ruleNameCode + ',',
        '  location: peg$computeLocation(startPos, startPos)',
        '});',
        ''
      ].join('\n'));
    }

    if (options.cache) {
      parts.push([
        'var key    = peg$currPos * ' + ast.rules.length + ' + ' + ruleIndexCode + ',',
        '    cached = peg$resultsCache[key];',
        '',
        'if (cached) {',
        '  peg$currPos = cached.nextPos;',
        ''
      ].join('\n'));

      if (options.trace) {
        parts.push([
          'if (cached.result !== peg$FAILED) {',
          '  peg$tracer.trace({',
          '    type:   "rule.match",',
          '    rule:   ' + ruleNameCode + ',',
          '    result: cached.result,',
          '    location: peg$computeLocation(startPos, peg$currPos)',
          '  });',
          '} else {',
          '  peg$tracer.trace({',
          '    type: "rule.fail",',
          '    rule: ' + ruleNameCode + ',',
          '    location: peg$computeLocation(startPos, startPos)',
          '  });',
          '}',
          ''
        ].join('\n'));
      }

      parts.push([
        '  return cached.result;',
        '}',
        ''
      ].join('\n'));
    }

    return parts.join('\n');
  }

  function generateRuleFooter(ruleNameCode, resultCode) {
    var parts = [];

    if (options.cache) {
      parts.push([
        '',
        'peg$resultsCache[key] = { nextPos: peg$currPos, result: ' + resultCode + ' };'
      ].join('\n'));
    }

    if (options.trace) {
      parts.push([
          '',
          'if (' + resultCode + ' !== peg$FAILED) {',
          '  peg$tracer.trace({',
          '    type:   "rule.match",',
          '    rule:   ' + ruleNameCode + ',',
          '    result: ' + resultCode + ',',
          '    location: peg$computeLocation(startPos, peg$currPos)',
          '  });',
          '} else {',
          '  peg$tracer.trace({',
          '    type: "rule.fail",',
          '    rule: ' + ruleNameCode + ',',
          '    location: peg$computeLocation(startPos, startPos)',
          '  });',
          '}'
      ].join('\n'));
    }

    parts.push([
      '',
      'return ' + resultCode + ';'
    ].join('\n'));

    return parts.join('\n');
  }

  function generateInterpreter() {
    var parts = [];

    function generateCondition(cond, argsLength) {
      var baseLength      = argsLength + 3,
          thenLengthCode = 'bc[ip + ' + (baseLength - 2) + ']',
          elseLengthCode = 'bc[ip + ' + (baseLength - 1) + ']';

      return [
        'ends.push(end);',
        'ips.push(ip + ' + baseLength + ' + ' + thenLengthCode + ' + ' + elseLengthCode + ');',
        '',
        'if (' + cond + ') {',
        '  end = ip + ' + baseLength + ' + ' + thenLengthCode + ';',
        '  ip += ' + baseLength + ';',
        '} else {',
        '  end = ip + ' + baseLength + ' + ' + thenLengthCode + ' + ' + elseLengthCode + ';',
        '  ip += ' + baseLength + ' + ' + thenLengthCode + ';',
        '}',
        '',
        'break;'
      ].join('\n');
    }

    function generateLoop(cond) {
      var baseLength     = 2,
          bodyLengthCode = 'bc[ip + ' + (baseLength - 1) + ']';

      return [
        'if (' + cond + ') {',
        '  ends.push(end);',
        '  ips.push(ip);',
        '',
        '  end = ip + ' + baseLength + ' + ' + bodyLengthCode + ';',
        '  ip += ' + baseLength + ';',
        '} else {',
        '  ip += ' + baseLength + ' + ' + bodyLengthCode + ';',
        '}',
        '',
        'break;'
      ].join('\n');
    }

    function generateCall() {
      var baseLength       = 4,
          paramsLengthCode = 'bc[ip + ' + (baseLength - 1) + ']';

      return [
        'params = bc.slice(ip + ' + baseLength + ', ip + ' + baseLength + ' + ' + paramsLengthCode + ');',
        'for (i = 0; i < ' + paramsLengthCode + '; i++) {',
        '  params[i] = stack[stack.length - 1 - params[i]];',
        '}',
        '',
        'stack.splice(',
        '  stack.length - bc[ip + 2],',
        '  bc[ip + 2],',
        '  peg$consts[bc[ip + 1]].apply(null, params)',
        ');',
        '',
        'ip += ' + baseLength + ' + ' + paramsLengthCode + ';',
        'break;'
      ].join('\n');
    }

    parts.push([
      'function peg$decode(s) {',
      '  var bc = new Array(s.length), i;',
      '',
      '  for (i = 0; i < s.length; i++) {',
      '    bc[i] = s.charCodeAt(i) - 32;',
      '  }',
      '',
      '  return bc;',
      '}',
      '',
      'function peg$parseRule(index) {'
    ].join('\n'));

    if (options.trace) {
      parts.push([
        '  var bc       = peg$bytecode[index],',
        '      ip       = 0,',
        '      ips      = [],',
        '      end      = bc.length,',
        '      ends     = [],',
        '      stack    = [],',
        '      startPos = peg$currPos,',
        '      params, i;'
      ].join('\n'));
    } else {
      parts.push([
        '  var bc    = peg$bytecode[index],',
        '      ip    = 0,',
        '      ips   = [],',
        '      end   = bc.length,',
        '      ends  = [],',
        '      stack = [],',
        '      params, i;'
      ].join('\n'));
    }

    parts.push(indent2(generateRuleHeader('peg$ruleNames[index]', 'index')));

    parts.push([
      /*
       * The point of the outer loop and the |ips| & |ends| stacks is to avoid
       * recursive calls for interpreting parts of bytecode. In other words, we
       * implement the |interpret| operation of the abstract machine without
       * function calls. Such calls would likely slow the parser down and more
       * importantly cause stack overflows for complex grammars.
       */
      '  while (true) {',
      '    while (ip < end) {',
      '      switch (bc[ip]) {',
      '        case ' + op.PUSH + ':',               // PUSH c
      '          stack.push(peg$consts[bc[ip + 1]]);',
      '          ip += 2;',
      '          break;',
      '',
      '        case ' + op.PUSH_UNDEFINED + ':',     // PUSH_UNDEFINED
      '          stack.push(void 0);',
      '          ip++;',
      '          break;',
      '',
      '        case ' + op.PUSH_NULL + ':',          // PUSH_NULL
      '          stack.push(null);',
      '          ip++;',
      '          break;',
      '',
      '        case ' + op.PUSH_FAILED + ':',        // PUSH_FAILED
      '          stack.push(peg$FAILED);',
      '          ip++;',
      '          break;',
      '',
      '        case ' + op.PUSH_EMPTY_ARRAY + ':',   // PUSH_EMPTY_ARRAY
      '          stack.push([]);',
      '          ip++;',
      '          break;',
      '',
      '        case ' + op.PUSH_CURR_POS + ':',      // PUSH_CURR_POS
      '          stack.push(peg$currPos);',
      '          ip++;',
      '          break;',
      '',
      '        case ' + op.POP + ':',                // POP
      '          stack.pop();',
      '          ip++;',
      '          break;',
      '',
      '        case ' + op.POP_CURR_POS + ':',       // POP_CURR_POS
      '          peg$currPos = stack.pop();',
      '          ip++;',
      '          break;',
      '',
      '        case ' + op.POP_N + ':',              // POP_N n
      '          stack.length -= bc[ip + 1];',
      '          ip += 2;',
      '          break;',
      '',
      '        case ' + op.NIP + ':',                // NIP
      '          stack.splice(-2, 1);',
      '          ip++;',
      '          break;',
      '',
      '        case ' + op.APPEND + ':',             // APPEND
      '          stack[stack.length - 2].push(stack.pop());',
      '          ip++;',
      '          break;',
      '',
      '        case ' + op.WRAP + ':',               // WRAP n
      '          stack.push(stack.splice(stack.length - bc[ip + 1], bc[ip + 1]));',
      '          ip += 2;',
      '          break;',
      '',
      '        case ' + op.TEXT + ':',               // TEXT
      '          stack.push(input.substring(stack.pop(), peg$currPos));',
      '          ip++;',
      '          break;',
      '',
      '        case ' + op.IF + ':',                 // IF t, f
                 indent10(generateCondition('stack[stack.length - 1]', 0)),
      '',
      '        case ' + op.IF_ERROR + ':',           // IF_ERROR t, f
                 indent10(generateCondition(
                   'stack[stack.length - 1] === peg$FAILED',
                   0
                 )),
      '',
      '        case ' + op.IF_NOT_ERROR + ':',       // IF_NOT_ERROR t, f
                 indent10(
                   generateCondition('stack[stack.length - 1] !== peg$FAILED',
                   0
                 )),
      '',
      '        case ' + op.WHILE_NOT_ERROR + ':',    // WHILE_NOT_ERROR b
                 indent10(generateLoop('stack[stack.length - 1] !== peg$FAILED')),
      '',
      '        case ' + op.MATCH_ANY + ':',          // MATCH_ANY a, f, ...
                 indent10(generateCondition('input.length > peg$currPos', 0)),
      '',
      '        case ' + op.MATCH_STRING + ':',       // MATCH_STRING s, a, f, ...
                 indent10(generateCondition(
                   'input.substr(peg$currPos, peg$consts[bc[ip + 1]].length) === peg$consts[bc[ip + 1]]',
                   1
                 )),
      '',
      '        case ' + op.MATCH_STRING_IC + ':',    // MATCH_STRING_IC s, a, f, ...
                 indent10(generateCondition(
                   'input.substr(peg$currPos, peg$consts[bc[ip + 1]].length).toLowerCase() === peg$consts[bc[ip + 1]]',
                   1
                 )),
      '',
      '        case ' + op.MATCH_REGEXP + ':',       // MATCH_REGEXP r, a, f, ...
                 indent10(generateCondition(
                   'peg$consts[bc[ip + 1]].test(input.charAt(peg$currPos))',
                   1
                 )),
      '',
      '        case ' + op.ACCEPT_N + ':',           // ACCEPT_N n
      '          stack.push(input.substr(peg$currPos, bc[ip + 1]));',
      '          peg$currPos += bc[ip + 1];',
      '          ip += 2;',
      '          break;',
      '',
      '        case ' + op.ACCEPT_STRING + ':',      // ACCEPT_STRING s
      '          stack.push(peg$consts[bc[ip + 1]]);',
      '          peg$currPos += peg$consts[bc[ip + 1]].length;',
      '          ip += 2;',
      '          break;',
      '',
      '        case ' + op.FAIL + ':',               // FAIL e
      '          stack.push(peg$FAILED);',
      '          if (peg$silentFails === 0) {',
      '            peg$fail(peg$consts[bc[ip + 1]]);',
      '          }',
      '          ip += 2;',
      '          break;',
      '',
      '        case ' + op.LOAD_SAVED_POS + ':',     // LOAD_SAVED_POS p
      '          peg$savedPos = stack[stack.length - 1 - bc[ip + 1]];',
      '          ip += 2;',
      '          break;',
      '',
      '        case ' + op.UPDATE_SAVED_POS + ':',   // UPDATE_SAVED_POS
      '          peg$savedPos = peg$currPos;',
      '          ip++;',
      '          break;',
      '',
      '        case ' + op.CALL + ':',               // CALL f, n, pc, p1, p2, ..., pN
                 indent10(generateCall()),
      '',
      '        case ' + op.RULE + ':',               // RULE r
      '          stack.push(peg$parseRule(bc[ip + 1]));',
      '          ip += 2;',
      '          break;',
      '',
      '        case ' + op.SILENT_FAILS_ON + ':',    // SILENT_FAILS_ON
      '          peg$silentFails++;',
      '          ip++;',
      '          break;',
      '',
      '        case ' + op.SILENT_FAILS_OFF + ':',   // SILENT_FAILS_OFF
      '          peg$silentFails--;',
      '          ip++;',
      '          break;',
      '',
      '        default:',
      '          throw new Error("Invalid opcode: " + bc[ip] + ".");',
      '      }',
      '    }',
      '',
      '    if (ends.length > 0) {',
      '      end = ends.pop();',
      '      ip = ips.pop();',
      '    } else {',
      '      break;',
      '    }',
      '  }'
    ].join('\n'));

    parts.push(indent2(generateRuleFooter('peg$ruleNames[index]', 'stack[0]')));
    parts.push('}');

    return parts.join('\n');
  }

  function generateRuleFunction(rule) {
    var parts = [], code;

    function c(i) { return "peg$c" + i; } // |consts[i]| of the abstract machine
    function s(i) { return "s"     + i; } // |stack[i]| of the abstract machine

    var stack = {
          sp:    -1,
          maxSp: -1,

          push: function(exprCode) {
            var code = s(++this.sp) + ' = ' + exprCode + ';';

            if (this.sp > this.maxSp) { this.maxSp = this.sp; }

            return code;
          },

          pop: function(n) {
            var values;

            if (n === void 0) {
              return s(this.sp--);
            } else {
              values = arrays.map(arrays.range(this.sp - n + 1, this.sp + 1), s);
              this.sp -= n;

              return values;
            }
          },

          top: function() {
            return s(this.sp);
          },

          index: function(i) {
            return s(this.sp - i);
          }
        };

    function compile(bc) {
      var ip    = 0,
          end   = bc.length,
          parts = [],
          value;

      function compileCondition(cond, argCount) {
        var baseLength = argCount + 3,
            thenLength = bc[ip + baseLength - 2],
            elseLength = bc[ip + baseLength - 1],
            baseSp     = stack.sp,
            thenCode, elseCode, thenSp, elseSp;

        ip += baseLength;
        thenCode = compile(bc.slice(ip, ip + thenLength));
        thenSp = stack.sp;
        ip += thenLength;

        if (elseLength > 0) {
          stack.sp = baseSp;
          elseCode = compile(bc.slice(ip, ip + elseLength));
          elseSp = stack.sp;
          ip += elseLength;

          if (thenSp !== elseSp) {
            throw new Error(
              "Branches of a condition must move the stack pointer in the same way."
            );
          }
        }

        parts.push('if (' + cond + ') {');
        parts.push(indent2(thenCode));
        if (elseLength > 0) {
          parts.push('} else {');
          parts.push(indent2(elseCode));
        }
        parts.push('}');
      }

      function compileLoop(cond) {
        var baseLength = 2,
            bodyLength = bc[ip + baseLength - 1],
            baseSp     = stack.sp,
            bodyCode, bodySp;

        ip += baseLength;
        bodyCode = compile(bc.slice(ip, ip + bodyLength));
        bodySp = stack.sp;
        ip += bodyLength;

        if (bodySp !== baseSp) {
          throw new Error("Body of a loop can't move the stack pointer.");
        }

        parts.push('while (' + cond + ') {');
        parts.push(indent2(bodyCode));
        parts.push('}');
      }

      function compileCall() {
        var baseLength   = 4,
            paramsLength = bc[ip + baseLength - 1];

        var value = c(bc[ip + 1]) + '('
              + arrays.map(
                  bc.slice(ip + baseLength, ip + baseLength + paramsLength),
                  function(p) { return stack.index(p); }
                ).join(', ')
              + ')';
        stack.pop(bc[ip + 2]);
        parts.push(stack.push(value));
        ip += baseLength + paramsLength;
      }

      while (ip < end) {
        switch (bc[ip]) {
          case op.PUSH:               // PUSH c
            parts.push(stack.push(c(bc[ip + 1])));
            ip += 2;
            break;

          case op.PUSH_CURR_POS:      // PUSH_CURR_POS
            parts.push(stack.push('peg$currPos'));
            ip++;
            break;

          case op.PUSH_UNDEFINED:      // PUSH_UNDEFINED
            parts.push(stack.push('void 0'));
            ip++;
            break;

          case op.PUSH_NULL:          // PUSH_NULL
            parts.push(stack.push('null'));
            ip++;
            break;

          case op.PUSH_FAILED:        // PUSH_FAILED
            parts.push(stack.push('peg$FAILED'));
            ip++;
            break;

          case op.PUSH_EMPTY_ARRAY:   // PUSH_EMPTY_ARRAY
            parts.push(stack.push('[]'));
            ip++;
            break;

          case op.POP:                // POP
            stack.pop();
            ip++;
            break;

          case op.POP_CURR_POS:       // POP_CURR_POS
            parts.push('peg$currPos = ' + stack.pop() + ';');
            ip++;
            break;

          case op.POP_N:              // POP_N n
            stack.pop(bc[ip + 1]);
            ip += 2;
            break;

          case op.NIP:                // NIP
            value = stack.pop();
            stack.pop();
            parts.push(stack.push(value));
            ip++;
            break;

          case op.APPEND:             // APPEND
            value = stack.pop();
            parts.push(stack.top() + '.push(' + value + ');');
            ip++;
            break;

          case op.WRAP:               // WRAP n
            parts.push(
              stack.push('[' + stack.pop(bc[ip + 1]).join(', ') + ']')
            );
            ip += 2;
            break;

          case op.TEXT:               // TEXT
            parts.push(
              stack.push('input.substring(' + stack.pop() + ', peg$currPos)')
            );
            ip++;
            break;

          case op.IF:                 // IF t, f
            compileCondition(stack.top(), 0);
            break;

          case op.IF_ERROR:           // IF_ERROR t, f
            compileCondition(stack.top() + ' === peg$FAILED', 0);
            break;

          case op.IF_NOT_ERROR:       // IF_NOT_ERROR t, f
            compileCondition(stack.top() + ' !== peg$FAILED', 0);
            break;

          case op.WHILE_NOT_ERROR:    // WHILE_NOT_ERROR b
            compileLoop(stack.top() + ' !== peg$FAILED', 0);
            break;

          case op.MATCH_ANY:          // MATCH_ANY a, f, ...
            compileCondition('input.length > peg$currPos', 0);
            break;

          case op.MATCH_STRING:       // MATCH_STRING s, a, f, ...
            compileCondition(
              eval(ast.consts[bc[ip + 1]]).length > 1
                ? 'input.substr(peg$currPos, '
                    + eval(ast.consts[bc[ip + 1]]).length
                    + ') === '
                    + c(bc[ip + 1])
                : 'input.charCodeAt(peg$currPos) === '
                    + eval(ast.consts[bc[ip + 1]]).charCodeAt(0),
              1
            );
            break;

          case op.MATCH_STRING_IC:    // MATCH_STRING_IC s, a, f, ...
            compileCondition(
              'input.substr(peg$currPos, '
                + eval(ast.consts[bc[ip + 1]]).length
                + ').toLowerCase() === '
                + c(bc[ip + 1]),
              1
            );
            break;

          case op.MATCH_REGEXP:       // MATCH_REGEXP r, a, f, ...
            compileCondition(
              c(bc[ip + 1]) + '.test(input.charAt(peg$currPos))',
              1
            );
            break;

          case op.ACCEPT_N:           // ACCEPT_N n
            parts.push(stack.push(
              bc[ip + 1] > 1
                ? 'input.substr(peg$currPos, ' + bc[ip + 1] + ')'
                : 'input.charAt(peg$currPos)'
            ));
            parts.push(
              bc[ip + 1] > 1
                ? 'peg$currPos += ' + bc[ip + 1] + ';'
                : 'peg$currPos++;'
            );
            ip += 2;
            break;

          case op.ACCEPT_STRING:      // ACCEPT_STRING s
            parts.push(stack.push(c(bc[ip + 1])));
            parts.push(
              eval(ast.consts[bc[ip + 1]]).length > 1
                ? 'peg$currPos += ' + eval(ast.consts[bc[ip + 1]]).length + ';'
                : 'peg$currPos++;'
            );
            ip += 2;
            break;

          case op.FAIL:               // FAIL e
            parts.push(stack.push('peg$FAILED'));
            parts.push('if (peg$silentFails === 0) { peg$fail(' + c(bc[ip + 1]) + '); }');
            ip += 2;
            break;

          case op.LOAD_SAVED_POS:     // LOAD_SAVED_POS p
            parts.push('peg$savedPos = ' + stack.index(bc[ip + 1]) + ';');
            ip += 2;
            break;

          case op.UPDATE_SAVED_POS:   // UPDATE_SAVED_POS
            parts.push('peg$savedPos = peg$currPos;');
            ip++;
            break;

          case op.CALL:               // CALL f, n, pc, p1, p2, ..., pN
            compileCall();
            break;

          case op.RULE:               // RULE r
            parts.push(stack.push("peg$parse" + ast.rules[bc[ip + 1]].name + "()"));
            ip += 2;
            break;

          case op.SILENT_FAILS_ON:    // SILENT_FAILS_ON
            parts.push('peg$silentFails++;');
            ip++;
            break;

          case op.SILENT_FAILS_OFF:   // SILENT_FAILS_OFF
            parts.push('peg$silentFails--;');
            ip++;
            break;

          default:
            throw new Error("Invalid opcode: " + bc[ip] + ".");
        }
      }

      return parts.join('\n');
    }

    code = compile(rule.bytecode);

    parts.push('function peg$parse' + rule.name + '() {');

    if (options.trace) {
      parts.push([
        '  var ' + arrays.map(arrays.range(0, stack.maxSp + 1), s).join(', ') + ',',
        '      startPos = peg$currPos;'
      ].join('\n'));
    } else {
      parts.push(
        '  var ' + arrays.map(arrays.range(0, stack.maxSp + 1), s).join(', ') + ';'
      );
    }

    parts.push(indent2(generateRuleHeader(
      '"' + js.stringEscape(rule.name) + '"',
      asts.indexOfRule(ast, rule.name)
    )));
    parts.push(indent2(code));
    parts.push(indent2(generateRuleFooter(
      '"' + js.stringEscape(rule.name) + '"',
      s(0)
    )));

    parts.push('}');

    return parts.join('\n');
  }

  function generateToplevel() {
    var parts = [],
        startRuleIndices,   startRuleIndex,
        startRuleFunctions, startRuleFunction,
        ruleNames;

    parts.push([
      'function peg$subclass(child, parent) {',
      '  function ctor() { this.constructor = child; }',
      '  ctor.prototype = parent.prototype;',
      '  child.prototype = new ctor();',
      '}',
      '',
      'function peg$SyntaxError(message, expected, found, location) {',
      '  this.message  = message;',
      '  this.expected = expected;',
      '  this.found    = found;',
      '  this.location = location;',
      '  this.name     = "SyntaxError";',
      '',
      '  if (typeof Error.captureStackTrace === "function") {',
      '    Error.captureStackTrace(this, peg$SyntaxError);',
      '  }',
      '}',
      '',
      'peg$subclass(peg$SyntaxError, Error);',
      '',
      'peg$SyntaxError.buildMessage = function(expected, found) {',
      '  var DESCRIBE_EXPECTATION_FNS = {',
      '        literal: function(expectation) {',
      '          return "\\\"" + literalEscape(expectation.text) + "\\\"";',
      '        },',
      '',
      '        "class": function(expectation) {',
      '          var escapedParts = "",',
      '              i;',
      '',
      '          for (i = 0; i < expectation.parts.length; i++) {',
      '            escapedParts += expectation.parts[i] instanceof Array',
      '              ? classEscape(expectation.parts[i][0]) + "-" + classEscape(expectation.parts[i][1])',
      '              : classEscape(expectation.parts[i]);',
      '          }',
      '',
      '          return "[" + (expectation.inverted ? "^" : "") + escapedParts + "]";',
      '        },',
      '',
      '        any: function(expectation) {',
      '          return "any character";',
      '        },',
      '',
      '        end: function(expectation) {',
      '          return "end of input";',
      '        },',
      '',
      '        other: function(expectation) {',
      '          return expectation.description;',
      '        }',
      '      };',
      '',
      '  function hex(ch) {',
      '    return ch.charCodeAt(0).toString(16).toUpperCase();',
      '  }',
      '',
      '  function literalEscape(s) {',
      '    return s',
      '      .replace(/\\\\/g, \'\\\\\\\\\')',   // backslash
      '      .replace(/"/g,  \'\\\\"\')',        // closing double quote
      '      .replace(/\\0/g, \'\\\\0\')',       // null
      '      .replace(/\\t/g, \'\\\\t\')',       // horizontal tab
      '      .replace(/\\n/g, \'\\\\n\')',       // line feed
      '      .replace(/\\r/g, \'\\\\r\')',       // carriage return
      '      .replace(/[\\x00-\\x0F]/g,          function(ch) { return \'\\\\x0\' + hex(ch); })',
      '      .replace(/[\\x10-\\x1F\\x7F-\\x9F]/g, function(ch) { return \'\\\\x\'  + hex(ch); });',
      '  }',
      '',
      '  function classEscape(s) {',
      '    return s',
      '      .replace(/\\\\/g, \'\\\\\\\\\')',   // backslash
      '      .replace(/\\]/g, \'\\\\]\')',       // closing bracket
      '      .replace(/\\^/g, \'\\\\^\')',       // caret
      '      .replace(/-/g,  \'\\\\-\')',        // dash
      '      .replace(/\\0/g, \'\\\\0\')',       // null
      '      .replace(/\\t/g, \'\\\\t\')',       // horizontal tab
      '      .replace(/\\n/g, \'\\\\n\')',       // line feed
      '      .replace(/\\r/g, \'\\\\r\')',       // carriage return
      '      .replace(/[\\x00-\\x0F]/g,          function(ch) { return \'\\\\x0\' + hex(ch); })',
      '      .replace(/[\\x10-\\x1F\\x7F-\\x9F]/g, function(ch) { return \'\\\\x\'  + hex(ch); });',
      '  }',
      '',
      '  function describeExpectation(expectation) {',
      '    return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);',
      '  }',
      '',
      '  function describeExpected(expected) {',
      '    var descriptions = new Array(expected.length),',
      '        i, j;',
      '',
      '    for (i = 0; i < expected.length; i++) {',
      '      descriptions[i] = describeExpectation(expected[i]);',
      '    }',
      '',
      '    descriptions.sort();',
      '',
      '    if (descriptions.length > 0) {',
      '      for (i = 1, j = 1; i < descriptions.length; i++) {',
      '        if (descriptions[i - 1] !== descriptions[i]) {',
      '          descriptions[j] = descriptions[i];',
      '          j++;',
      '        }',
      '      }',
      '      descriptions.length = j;',
      '    }',
      '',
      '    switch (descriptions.length) {',
      '      case 1:',
      '        return descriptions[0];',
      '',
      '      case 2:',
      '        return descriptions[0] + " or " + descriptions[1];',
      '',
      '      default:',
      '        return descriptions.slice(0, -1).join(", ")',
      '          + ", or "',
      '          + descriptions[descriptions.length - 1];',
      '    }',
      '  }',
      '',
      '  function describeFound(found) {',
      '    return found ? "\\"" + literalEscape(found) + "\\"" : "end of input";',
      '  }',
      '',
      '  return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found.";',
      '};',
      ''
    ].join('\n'));

    if (options.trace) {
      parts.push([
        'function peg$DefaultTracer() {',
        '  this.indentLevel = 0;',
        '}',
        '',
        'peg$DefaultTracer.prototype.trace = function(event) {',
        '  var that = this;',
        '',
        '  function log(event) {',
        '    function repeat(string, n) {',
        '       var result = "", i;',
        '',
        '       for (i = 0; i < n; i++) {',
        '         result += string;',
        '       }',
        '',
        '       return result;',
        '    }',
        '',
        '    function pad(string, length) {',
        '      return string + repeat(" ", length - string.length);',
        '    }',
        '',
        '    if (typeof console === "object") {',   // IE 8-10
        '      console.log(',
        '        event.location.start.line + ":" + event.location.start.column + "-"',
        '          + event.location.end.line + ":" + event.location.end.column + " "',
        '          + pad(event.type, 10) + " "',
        '          + repeat("  ", that.indentLevel) + event.rule',
        '      );',
        '    }',
        '  }',
        '',
        '  switch (event.type) {',
        '    case "rule.enter":',
        '      log(event);',
        '      this.indentLevel++;',
        '      break;',
        '',
        '    case "rule.match":',
        '      this.indentLevel--;',
        '      log(event);',
        '      break;',
        '',
        '    case "rule.fail":',
        '      this.indentLevel--;',
        '      log(event);',
        '      break;',
        '',
        '    default:',
        '      throw new Error("Invalid event type: " + event.type + ".");',
        '  }',
        '};',
        ''
      ].join('\n'));
    }

    parts.push([
      'function peg$parse(input, options) {',
      '  options = options !== void 0 ? options : {};',
      '',
      '  var peg$FAILED = {},',
      ''
    ].join('\n'));

    if (options.optimize === "size") {
      startRuleIndices = '{ '
                       + arrays.map(
                           options.allowedStartRules,
                           function(r) { return r + ': ' + asts.indexOfRule(ast, r); }
                         ).join(', ')
                       + ' }';
      startRuleIndex = asts.indexOfRule(ast, options.allowedStartRules[0]);

      parts.push([
        '      peg$startRuleIndices = ' + startRuleIndices + ',',
        '      peg$startRuleIndex   = ' + startRuleIndex + ','
      ].join('\n'));
    } else {
      startRuleFunctions = '{ '
                       + arrays.map(
                           options.allowedStartRules,
                           function(r) { return r + ': peg$parse' + r; }
                         ).join(', ')
                       + ' }';
      startRuleFunction = 'peg$parse' + options.allowedStartRules[0];

      parts.push([
        '      peg$startRuleFunctions = ' + startRuleFunctions + ',',
        '      peg$startRuleFunction  = ' + startRuleFunction + ','
      ].join('\n'));
    }

    parts.push('');

    parts.push(indent6(generateTables()));

    parts.push([
      '',
      '      peg$currPos          = 0,',
      '      peg$savedPos         = 0,',
      '      peg$posDetailsCache  = [{ line: 1, column: 1 }],',
      '      peg$maxFailPos       = 0,',
      '      peg$maxFailExpected  = [],',
      '      peg$silentFails      = 0,',   // 0 = report failures, > 0 = silence failures
      ''
    ].join('\n'));

    if (options.cache) {
      parts.push([
        '      peg$resultsCache = {},',
        ''
      ].join('\n'));
    }

    if (options.trace) {
      if (options.optimize === "size") {
        ruleNames = '['
                  + arrays.map(
                      ast.rules,
                      function(r) { return '"' + js.stringEscape(r.name) + '"'; }
                    ).join(', ')
                  + ']';

        parts.push([
          '      peg$ruleNames = ' + ruleNames + ',',
          ''
        ].join('\n'));
      }

      parts.push([
        '      peg$tracer = "tracer" in options ? options.tracer : new peg$DefaultTracer(),',
        ''
      ].join('\n'));
    }

    parts.push([
      '      peg$result;',
      ''
    ].join('\n'));

    if (options.optimize === "size") {
      parts.push([
        '  if ("startRule" in options) {',
        '    if (!(options.startRule in peg$startRuleIndices)) {',
        '      throw new Error("Can\'t start parsing from rule \\"" + options.startRule + "\\".");',
        '    }',
        '',
        '    peg$startRuleIndex = peg$startRuleIndices[options.startRule];',
        '  }'
      ].join('\n'));
    } else {
      parts.push([
        '  if ("startRule" in options) {',
        '    if (!(options.startRule in peg$startRuleFunctions)) {',
        '      throw new Error("Can\'t start parsing from rule \\"" + options.startRule + "\\".");',
        '    }',
        '',
        '    peg$startRuleFunction = peg$startRuleFunctions[options.startRule];',
        '  }'
      ].join('\n'));
    }

    parts.push([
      '',
      '  function text() {',
      '    return input.substring(peg$savedPos, peg$currPos);',
      '  }',
      '',
      '  function location() {',
      '    return peg$computeLocation(peg$savedPos, peg$currPos);',
      '  }',
      '',
      '  function expected(description, location) {',
      '    location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos)',
      '',
      '    throw peg$buildStructuredError(',
      '      [peg$otherExpectation(description)],',
      '      input.substring(peg$savedPos, peg$currPos),',
      '      location',
      '    );',
      '  }',
      '',
      '  function error(message, location) {',
      '    location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos)',
      '',
      '    throw peg$buildSimpleError(message, location);',
      '  }',
      '',
      '  function peg$literalExpectation(text, ignoreCase) {',
      '    return { type: "literal", text: text, ignoreCase: ignoreCase };',
      '  }',
      '',
      '  function peg$classExpectation(parts, inverted, ignoreCase) {',
      '    return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase };',
      '  }',
      '',
      '  function peg$anyExpectation() {',
      '    return { type: "any" };',
      '  }',
      '',
      '  function peg$endExpectation() {',
      '    return { type: "end" };',
      '  }',
      '',
      '  function peg$otherExpectation(description) {',
      '    return { type: "other", description: description };',
      '  }',
      '',
      '  function peg$computePosDetails(pos) {',
      '    var details = peg$posDetailsCache[pos], p;',
      '',
      '    if (details) {',
      '      return details;',
      '    } else {',
      '      p = pos - 1;',
      '      while (!peg$posDetailsCache[p]) {',
      '        p--;',
      '      }',
      '',
      '      details = peg$posDetailsCache[p];',
      '      details = {',
      '        line:   details.line,',
      '        column: details.column',
      '      };',
      '',
      '      while (p < pos) {',
      '        if (input.charCodeAt(p) === 10) {',
      '          details.line++;',
      '          details.column = 1;',
      '        } else {',
      '          details.column++;',
      '        }',
      '',
      '        p++;',
      '      }',
      '',
      '      peg$posDetailsCache[pos] = details;',
      '      return details;',
      '    }',
      '  }',
      '',
      '  function peg$computeLocation(startPos, endPos) {',
      '    var startPosDetails = peg$computePosDetails(startPos),',
      '        endPosDetails   = peg$computePosDetails(endPos);',
      '',
      '    return {',
      '      start: {',
      '        offset: startPos,',
      '        line:   startPosDetails.line,',
      '        column: startPosDetails.column',
      '      },',
      '      end: {',
      '        offset: endPos,',
      '        line:   endPosDetails.line,',
      '        column: endPosDetails.column',
      '      }',
      '    };',
      '  }',
      '',
      '  function peg$fail(expected) {',
      '    if (peg$currPos < peg$maxFailPos) { return; }',
      '',
      '    if (peg$currPos > peg$maxFailPos) {',
      '      peg$maxFailPos = peg$currPos;',
      '      peg$maxFailExpected = [];',
      '    }',
      '',
      '    peg$maxFailExpected.push(expected);',
      '  }',
      '',
      '  function peg$buildSimpleError(message, location) {',
      '    return new peg$SyntaxError(message, null, null, location);',
      '  }',
      '',
      '  function peg$buildStructuredError(expected, found, location) {',
      '    return new peg$SyntaxError(',
      '      peg$SyntaxError.buildMessage(expected, found),',
      '      expected,',
      '      found,',
      '      location',
      '    );',
      '  }',
      ''
    ].join('\n'));

    if (options.optimize === "size") {
      parts.push(indent2(generateInterpreter()));
      parts.push('');
    } else {
      arrays.each(ast.rules, function(rule) {
        parts.push(indent2(generateRuleFunction(rule)));
        parts.push('');
      });
    }

    if (ast.initializer) {
      parts.push(indent2(ast.initializer.code));
      parts.push('');
    }

    if (options.optimize === "size") {
      parts.push('  peg$result = peg$parseRule(peg$startRuleIndex);');
    } else {
      parts.push('  peg$result = peg$startRuleFunction();');
    }

    parts.push([
      '',
      '  if (peg$result !== peg$FAILED && peg$currPos === input.length) {',
      '    return peg$result;',
      '  } else {',
      '    if (peg$result !== peg$FAILED && peg$currPos < input.length) {',
      '      peg$fail(peg$endExpectation());',
      '    }',
      '',
      '    throw peg$buildStructuredError(',
      '      peg$maxFailExpected,',
      '      peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,',
      '      peg$maxFailPos < input.length',
      '        ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)',
      '        : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)',
      '    );',
      '  }',
      '}'
    ].join('\n'));

    return parts.join('\n');
  }

  function generateWrapper(toplevelCode) {
    function generateGeneratedByComment() {
      return [
        '/*',
        ' * Generated by PEG.js 0.10.0.',
        ' *',
        ' * http://pegjs.org/',
        ' */'
      ].join('\n');
    }

    function generateParserObject() {
      return options.trace
        ? [
            '{',
            '  SyntaxError:   peg$SyntaxError,',
            '  DefaultTracer: peg$DefaultTracer,',
            '  parse:         peg$parse',
            '}'
          ].join('\n')
        : [
            '{',
            '  SyntaxError: peg$SyntaxError,',
            '  parse:       peg$parse',
            '}'
          ].join('\n');
    }

    var generators = {
      bare: function() {
        return [
          generateGeneratedByComment(),
          '(function() {',
          '  "use strict";',
          '',
             indent2(toplevelCode),
          '',
             indent2('return ' + generateParserObject() + ';'),
          '})()'
        ].join('\n');
      },

      commonjs: function() {
        var parts          = [],
            dependencyVars = objects.keys(options.dependencies),
            requires       = arrays.map(
              dependencyVars,
              function(variable) {
                return variable
                  + ' = require("'
                  + js.stringEscape(options.dependencies[variable])
                  + '")';
              }
            );

        parts.push([
          generateGeneratedByComment(),
          '',
          '"use strict";',
          ''
        ].join('\n'));

        if (requires.length > 0) {
          parts.push('var ' + requires.join(', ') + ';');
          parts.push('');
        }

        parts.push([
          toplevelCode,
          '',
          'module.exports = ' + generateParserObject() + ';',
          ''
        ].join('\n'));

        return parts.join('\n');
      },

      amd: function() {
        var dependencyIds  = objects.values(options.dependencies),
            dependencyVars = objects.keys(options.dependencies),
            dependencies   = '['
              + arrays.map(
                  dependencyIds,
                  function(id) { return '"' + js.stringEscape(id) + '"'; }
                ).join(', ')
              + ']',
            params         = dependencyVars.join(', ');

        return [
          generateGeneratedByComment(),
          'define(' + dependencies + ', function(' + params + ') {',
          '  "use strict";',
          '',
             indent2(toplevelCode),
          '',
             indent2('return ' + generateParserObject() + ';'),
          '});',
          ''
        ].join('\n');
      },

      globals: function() {
        return [
          generateGeneratedByComment(),
          '(function(root) {',
          '  "use strict";',
          '',
             indent2(toplevelCode),
          '',
             indent2('root.' + options.exportVar + ' = ' + generateParserObject() + ';'),
          '})(this);',
          ''
        ].join('\n');
      },

      umd: function() {
        var parts          = [],
            dependencyIds  = objects.values(options.dependencies),
            dependencyVars = objects.keys(options.dependencies),
            dependencies   = '['
              + arrays.map(
                  dependencyIds,
                  function(id) { return '"' + js.stringEscape(id) + '"'; }
                ).join(', ')
              + ']',
            requires       = arrays.map(
              dependencyIds,
              function(id) { return 'require("' + js.stringEscape(id) + '")'; }
            ).join(', '),
            params         = dependencyVars.join(', ');

        parts.push([
          generateGeneratedByComment(),
          '(function(root, factory) {',
          '  if (typeof define === "function" && define.amd) {',
          '    define(' + dependencies + ', factory);',
          '  } else if (typeof module === "object" && module.exports) {',
          '    module.exports = factory(' + requires + ');'
        ].join('\n'));

        if (options.exportVar !== null) {
          parts.push([
            '  } else {',
            '    root.' + options.exportVar + ' = factory();'
          ].join('\n'));
        }

        parts.push([
          '  }',
          '})(this, function(' + params + ') {',
          '  "use strict";',
          '',
             indent2(toplevelCode),
          '',
             indent2('return ' + generateParserObject() + ';'),
          '});',
          ''
        ].join('\n'));

        return parts.join('\n');
      }
    };

    return generators[options.format]();
  }

  ast.code = generateWrapper(generateToplevel());
}

module.exports = generateJS;
