// @ts-check
"use strict";

const asts = require("../asts");
const op = require("../opcodes");
const Stack = require("../stack");
const { version } = require("../../version");
const { stringEscape, regexpClassEscape } = require("../utils");
const { SourceNode } = require("source-map-generator");
const GrammarLocation = require("../../grammar-location");
const { parse } = require("../../parser.js");

/**
 * @typedef {import("../../peg")} PEG
 */

/**
 * Converts source text from the grammar into the `source-map` object
 *
 * @param {string} code Multiline string with source code
 * @param {PEG.LocationRange} location
 *        Location that represents code block in the grammar
 * @param {string} [name] Name of the code chunk
 *
 * @returns {SourceNode} New node that represents code chunk.
 *          Code will be splitted by lines if necessary
 */
function toSourceNode(code, location, name) {
  const start = GrammarLocation.offsetStart(location);
  const line = start.line;
  // `source-map` columns are 0-based, peggy columns is 1-based
  const column = start.column - 1;
  const lines = code.split("\n");

  if (lines.length === 1) {
    return new SourceNode(
      line, column, String(location.source), code, name
    );
  }

  return new SourceNode(
    null, null, String(location.source), lines.map((l, i) => new SourceNode(
      line + i,
      i === 0 ? column : 0,
      String(location.source),
      i === lines.length - 1 ? l : [l, "\n"],
      name
    ))
  );
}

/**
 * Wraps code line that consists from three parts into `SourceNode`.
 *
 * @param {string} prefix String that will be prepended before mapped chunk
 * @param {string} chunk Chunk for mapping (possible multiline)
 * @param {PEG.LocationRange} location
 *        Location that represents chunk in the grammar
 * @param {string} suffix String that will be appended after mapped chunk
 * @param {string} [name] Name of the code chunk
 *
 * @returns {SourceNode} New node that represents code chunk.
 *          Code will be splitted by lines if necessary
 */
function wrapInSourceNode(prefix, chunk, location, suffix, name) {
  // If location is not defined (for example, AST node was replaced
  // by a plugin and does not provide location information, see
  // plugin-api.spec.js/"can replace parser") returns original chunk
  if (location) {
    const end = GrammarLocation.offsetEnd(location);
    return new SourceNode(null, null, String(location.source), [
      prefix,
      toSourceNode(chunk, location, name),
      // Mark end location with column information otherwise
      // mapping will be always continue to the end of line
      new SourceNode(
        end.line,
        // `source-map` columns are 0-based, peggy columns is 1-based
        end.column - 1,
        String(location.source),
        suffix
      ),
    ]);
  }

  return new SourceNode(null, null, null, [prefix, chunk, suffix]);
}

/**
 * @typedef {(string|SourceNode)[]} SourceArray
 *
 * @typedef {PEG.SourceBuildOptions<PEG.SourceOutputs>} SourceBuildOptions
 * @typedef {object} ExtraOptions
 * @property {PEG.Dependencies} [dependencies]
 * @property {string} [exportVar]
 * @typedef {SourceBuildOptions & ExtraOptions} Options
 */
/**
 * Generates parser JavaScript code.
 *
 * @param {PEG.ast.Grammar} ast
 * @param {Options} options
 */
function generateJS(ast, options) {
  if (!ast.literals || !ast.locations || !ast.classes
      || !ast.expectations || !ast.functions || !ast.importedNames) {
    throw new Error(
      "generateJS: generate bytecode was not called."
    );
  }
  const {
    literals, locations, classes, expectations, functions, importedNames,
  } = ast;
  if (!options.allowedStartRules) {
    throw new Error(
      "generateJS: options.allowedStartRules was not set."
    );
  }
  const { allowedStartRules } = options;
  /** @type {PEG.Dependencies} */
  const dependencies = options.dependencies || {};

  /**
   * @overload
   * @param {string} code
   * @returns {string}
   */
  /**
   * @overload
   * @param {SourceArray} code
   * @returns {SourceArray}
   */
  /**
   * These only indent non-empty lines to avoid trailing whitespace.
   * @param {SourceArray} code
   * @returns {SourceArray}
   */
  function indent2(code) {
    /*
     * - raw lines (outside of SourceNodes) have implict newlines
     *   that get inserted at the end of processing, so indent
     *   should always be applied to the next string.
     *
     * - chunks inside SourceNodes are assumed to have explict
     *   new lines, and often have several chunks on one line.
     *   we therefore shouldn't indent them, unless we've seen
     *   an explicit new line, or the previous line was raw.
     *
     * So eg:
     * [
     *   SourceNode(["a ", "b", "\nfoo "]),
     *   "x",
     *   "y",
     * ]
     *
     * Should end up as
     * [
     *   SourceNode(["  a ", "b", "\n  foo "]),
     *   "x",
     *   "  y",
     * ]
     *
     * sawEol, and inSourceNode are used to keep track of when
     * we should apply the indent.
     */
    let sawEol = true;
    let inSourceNode = 0;
    /**
     * @overload
     * @param {string | SourceNode} code
     * @returns {string | SourceNode}
     */
    /**
     * @overload
     * @param {SourceNode} code
     * @returns {SourceNode}
     */
    /**
     * @overload
     * @param {SourceNode[]} code
     * @returns {SourceNode[]}
     */
    /**
     * @overload
     * @param {SourceArray} code
     * @returns {SourceArray}
     */
    /**
     * @param {SourceArray | string | SourceNode} code
     * @returns {SourceArray | string | SourceNode}
     */
    function helper(code) {
      if (Array.isArray(code)) {
        return code.map(s => helper(s));
      }
      if (code instanceof SourceNode) {
        inSourceNode++;
        code.children = helper(code.children);
        inSourceNode--;
        return code;
      }
      if (sawEol) {
        // There was an immediately prior newline, so
        // indent at the start of every line
        code = code.replace(/^(.+)$/gm, "  $1");
      } else {
        // This line will be appended directly to
        // the end of the previous one, so only indent
        // after each contained newline (and only if
        // there's non-whitespace following the newline)
        code = code.replace(/\n(\s*\S)/g, "\n  $1");
      }
      sawEol = !inSourceNode || code.endsWith("\n");
      return code;
    }
    return helper(code);
  }
  /** @param {number} i */
  function l(i) { return "peg$c" + i; } // |literals[i]| of the abstract machine
  /** @param {number} i */
  function r(i) { return "peg$r" + i; } // |classes[i]| of the abstract machine
  /** @param {number} i */
  function e(i) { return "peg$e" + i; } // |expectations[i]| of the abstract machine
  /** @param {number} i */
  function f(i) { return "peg$f" + i; } // |actions[i]| of the abstract machine
  /** @param {number} i */
  function gi(i) { return "peg$import" + i; } // |grammar_import[i]|

  /**
   * Generates name of the function that parses specified rule.
   * @param {string} name
   */
  function name(name) { return "peg$parse" + name; }

  function generateTables() {
    /** @param {string} literal */
    function buildLiteral(literal) {
      return "\"" + stringEscape(literal) + "\"";
    }

    /** @param {PEG.ast.GrammarCharacterClass} cls */
    function buildRegexp(cls) {
      return "/^["
        + (cls.inverted ? "^" : "")
        + cls.value.map(part => (Array.isArray(part)
          ? regexpClassEscape(part[0])
            + "-"
            + regexpClassEscape(part[1])
          : regexpClassEscape(part))).join("")
        + "]/" + (cls.ignoreCase ? "i" : "");
    }

    /** @param {PEG.ast.GrammarExpectation} e */
    function buildExpectation(e) {
      switch (e.type) {
        case "rule": {
          return "peg$otherExpectation(\"" + stringEscape(e.value) + "\")";
        }
        case "literal": {
          return "peg$literalExpectation(\""
                  + stringEscape(e.value)
                  + "\", "
                  + e.ignoreCase
                  + ")";
        }
        case "class": {
          const parts = e.value.map(part => (Array.isArray(part)
            ? "[\"" + stringEscape(part[0]) + "\", \"" + stringEscape(part[1]) + "\"]"
            : "\""  + stringEscape(part) + "\"")).join(", ");

          return "peg$classExpectation(["
                  + parts + "], "
                  + e.inverted + ", "
                  + e.ignoreCase
                  + ")";
        }
        case "any": return "peg$anyExpectation()";
        // istanbul ignore next Because we never generate expectation type we cannot reach this branch
        default: throw new Error("Unknown expectation type (" + JSON.stringify(e) + ")");
      }
    }

    /**
     * @param {PEG.ast.FunctionConst} a
     * @param {number} i
     */
    function buildFunc(a, i) {
      return wrapInSourceNode(
        `\n  var ${f(i)} = function(${a.params.join(", ")}) {`,
        a.body,
        a.location,
        "};"
      );
    }

    return new SourceNode(
      null, null, options.grammarSource, [
        literals.map(
          (c, i) => "  var " + l(i) + " = " + buildLiteral(c) + ";"
        ).concat("", classes.map(
          (c, i) => "  var " + r(i) + " = " + buildRegexp(c) + ";"
        )).concat("", expectations.map(
          (c, i) => "  var " + e(i) + " = " + buildExpectation(c) + ";"
        )).concat("").join("\n"),
        ...functions.map(buildFunc),
      ]
    );
  }

  /**
   * @param {string} ruleNameCode
   * @param {number} ruleIndexCode
   */
  function generateRuleHeader(ruleNameCode, ruleIndexCode) {
    /** @type {string[]} */
    const parts = [];

    parts.push("");

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

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

      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, true)",
          "  });",
          "} else {",
          "  peg$tracer.trace({",
          "    type: \"rule.fail\",",
          "    rule: " + ruleNameCode + ",",
          "    location: peg$computeLocation(startPos, startPos, true)",
          "  });",
          "}",
          ""
        );
      }

      parts.push(
        "  return cached.result;",
        "}",
        ""
      );
    }

    return parts;
  }

  /**
   * @param {string} ruleNameCode
   * @param {string} resultCode
   */
  function generateRuleFooter(ruleNameCode, resultCode) {
    /** @type {string[]} */
    const parts = [];

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

    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, true)",
        "  });",
        "} else {",
        "  peg$tracer.trace({",
        "    type: \"rule.fail\",",
        "    rule: " + ruleNameCode + ",",
        "    location: peg$computeLocation(startPos, startPos, true)",
        "  });",
        "}"
      );
    }

    parts.push(
      "",
      "return " + resultCode + ";"
    );

    return parts;
  }

  /** @param {PEG.ast.Rule} rule */
  function generateRuleFunction(rule) {
    /** @type {SourceArray} */
    const parts = [];
    const bytecode = /** @type {number[]} */(rule.bytecode);
    const stack = new Stack(rule.name, "s", "var", bytecode);

    /** @param {number[]} bc */
    function compile(bc) {
      let ip = 0;
      const end = bc.length;
      const parts = [];
      // eslint-disable-next-line no-useless-assignment
      let value = undefined;

      /**
       * @param {string} cond
       * @param {number} argCount
       * @param {((bc: number[])=>SourceArray) | null} [thenFn]
       */
      function compileCondition(cond, argCount, thenFn) {
        const baseLength = argCount + 3;
        const thenLength = bc[ip + baseLength - 2];
        const elseLength = bc[ip + baseLength - 1];

        const [thenCode, elseCode] = stack.checkedIf(
          ip,
          () => {
            ip += baseLength + thenLength;
            return (thenFn || compile)(bc.slice(ip - thenLength, ip));
          },
          (elseLength > 0)
            ? () => {
                ip += elseLength;
                return compile(bc.slice(ip - elseLength, ip));
              }
            : null
        );

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

      /**
        MATCH_* opcodes typically do something like

          if (<test>(input.substr(peg$currPos, length))) {
            sN = input.substr(peg$currPos, length);
            ...
          } else {
            sN = peg$FAILED;
            ...
          }

        compileInputChunkCondition will convert that to

          sN = input.substr(peg$currPos, length);
          if (<test>(sN)) {
            ...
          } else {
            sN = peg$FAILED;
            ...
          }

          and avoid extracting the sub string twice.
        @param {(chunk:string, optimized:boolean)=>string} condFn
        @param {number} argCount
        @param {number} inputChunkLength
      */
      function compileInputChunkCondition(
        condFn, argCount, inputChunkLength
      ) {
        const baseLength = argCount + 3;
        let inputChunk = inputChunkLength === 1
          ? "input.charAt(peg$currPos)"
          : "input.substr(peg$currPos, " + inputChunkLength + ")";
        let thenFn = null;
        if (bc[ip + baseLength] === op.ACCEPT_N
              && bc[ip + baseLength + 1] === inputChunkLength) {
          // Push the assignment to the next available variable.
          parts.push(stack.push(inputChunk));
          inputChunk = stack.pop();
          /** @param {number[]} bc */
          thenFn = bc => {
            // The bc[0] is an ACCEPT_N, and bc[1] is the N. We've already done
            // the assignment (before the if), so we just need to bump the
            // stack, and increment peg$currPos appropriately.
            stack.sp++;
            const code = compile(bc.slice(2));
            code.unshift(
              inputChunkLength === 1
                ? "peg$currPos++;"
                : "peg$currPos += " + inputChunkLength + ";"
            );
            return code;
          };
        }
        compileCondition(condFn(inputChunk, thenFn !== null), argCount, thenFn);
      }

      /** @param {string} cond */
      function compileLoop(cond) {
        const baseLength = 2;
        const bodyLength = bc[ip + baseLength - 1];

        const bodyCode = stack.checkedLoop(ip, () => {
          ip += baseLength + bodyLength;
          return compile(bc.slice(ip - bodyLength, ip));
        });

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

      /** @param {number} baseLength */
      function compileCall(baseLength) {
        const paramsLength = bc[ip + baseLength - 1];

        return f(bc[ip + 1]) + "("
          + bc.slice(ip + baseLength, ip + baseLength + paramsLength).map(
            p => stack.index(p)
          ).join(", ")
          + ")";
      }

      while (ip < end) {
        switch (bc[ip]) {
          case op.PUSH_EMPTY_STRING:  // PUSH_EMPTY_STRING
            parts.push(stack.push("''"));
            ip++;
            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("undefined"));
            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.PLUCK: {            // PLUCK n, k, p1, ..., pK
            const baseLength = 3;
            const paramsLength = bc[ip + baseLength - 1];
            const n = baseLength + paramsLength;
            value = bc.slice(ip + baseLength, ip + n);
            value = paramsLength === 1
              ? stack.index(value[0])
              : `[ ${
                value.map(p => stack.index(p)).join(", ")
              } ]`;
            stack.pop(bc[ip + 1]);
            parts.push(stack.push(value));
            ip += n;
            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.IF_LT:              // IF_LT min, t, f
            compileCondition(stack.top() + ".length < " + bc[ip + 1], 1);
            break;

          case op.IF_GE:              // IF_GE max, t, f
            compileCondition(stack.top() + ".length >= " + bc[ip + 1], 1);
            break;

          case op.IF_LT_DYNAMIC:      // IF_LT_DYNAMIC min, t, f
            compileCondition(stack.top() + ".length < (" + stack.index(bc[ip + 1]) + "|0)", 1);
            break;

          case op.IF_GE_DYNAMIC:      // IF_GE_DYNAMIC max, t, f
            compileCondition(stack.top() + ".length >= (" + stack.index(bc[ip + 1]) + "|0)", 1);
            break;

          case op.WHILE_NOT_ERROR:    // WHILE_NOT_ERROR b
            compileLoop(stack.top() + " !== peg$FAILED");
            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, ...
            const litNum = bc[ip + 1];
            const literal = literals[litNum];
            compileInputChunkCondition(
              (inputChunk, optimized) => {
                if (literal.length > 1) {
                  return `${inputChunk} === ${l(litNum)}`;
                }
                inputChunk = !optimized
                  ? "input.charCodeAt(peg$currPos)"
                  : `${inputChunk}.charCodeAt(0)`;
                return `${inputChunk} === ${literal.charCodeAt(0)}`;
              },
              1,
              literal.length
            );
            break;
          }

          case op.MATCH_STRING_IC: {  // MATCH_STRING_IC s, a, f, ...
            const litNum = bc[ip + 1];
            compileInputChunkCondition(
              inputChunk => `${inputChunk}.toLowerCase() === ${l(litNum)}`,
              1,
              literals[litNum].length
            );
            break;
          }

          case op.MATCH_CHAR_CLASS: { // MATCH_CHAR_CLASS c, a, f, ...
            const regNum = bc[ip + 1];
            compileInputChunkCondition(
              inputChunk => `${r(regNum)}.test(${inputChunk})`, 1, 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(l(bc[ip + 1])));
            parts.push(
              literals[bc[ip + 1]].length > 1
                ? "peg$currPos += " + literals[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(" + e(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
            value = compileCall(4);
            stack.pop(bc[ip + 2]);
            parts.push(stack.push(value));
            ip += 4 + bc[ip + 3];
            break;

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

          case op.LIBRARY_RULE: {       // LIBRARY_RULE module, name
            const nm = bc[ip + 2];
            const cnm = (nm === -1) ? "" : ", \"" + importedNames[nm] + "\"";
            parts.push(stack.push("peg$callLibrary("
              + gi(bc[ip + 1])
              + cnm
              + ")"));
            ip += 3;
            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;

          case op.SOURCE_MAP_PUSH:
            stack.sourceMapPush(
              parts,
              locations[bc[ip + 1]]
            );
            ip += 2;
            break;

          case op.SOURCE_MAP_POP: {
            stack.sourceMapPop();
            ip++;
            break;
          }

          case op.SOURCE_MAP_LABEL_PUSH:
            stack.labels[bc[ip + 1]] = {
              label: literals[bc[ip + 2]],
              location: locations[bc[ip + 3]],
            };
            ip += 4;
            break;

          case op.SOURCE_MAP_LABEL_POP:
            delete stack.labels[bc[ip + 1]];
            ip += 2;
            break;

          // istanbul ignore next Because we never generate invalid bytecode we cannot reach this branch
          default:
            throw new Error("Invalid opcode: " + bc[ip] + ".");
        }
      }

      return parts;
    }

    const code = compile(bytecode);

    parts.push(wrapInSourceNode(
      "function ",
      name(rule.name),
      rule.nameLocation,
      "() {\n",
      rule.name
    ));

    if (options.trace) {
      parts.push("  var startPos = peg$currPos;");
    }

    parts.push(indent2(stack.defines()));

    parts.push(...indent2(generateRuleHeader(
      "\"" + stringEscape(rule.name) + "\"",
      asts.indexOfRule(ast, rule.name)
    )));
    parts.push(...indent2(code));
    parts.push(...indent2(generateRuleFooter(
      "\"" + stringEscape(rule.name) + "\"",
      stack.result()
    )));

    parts.push("}");

    return parts;
  }

  /**
   * @template {string} T
   * @param {PEG.ast.CodeBlock<T>} node
   */
  function ast2SourceNode(node) {
    // If location is not defined (for example, AST node was replaced
    // by a plugin and does not provide location information, see
    // plugin-api.spec.js/"can replace parser") returns initializer code
    if (node.codeLocation) {
      // Append "$" to the name to create an impossible rule name
      // so that names will not collide with rule names
      return toSourceNode(node.code, node.codeLocation, "$" + node.type);
    }

    return node.code;
  }

  function generateToplevel() {
    const parts = [];

    let topLevel = ast.topLevelInitializer;
    if (topLevel) {
      if (Array.isArray(topLevel)) {
        if (options.format === "es") {
          const imps = [];
          const codes = [];
          for (const tli of topLevel) {
            const [
              imports,
              code,
            ] = /** @type {PEG.ast.TopLevelInitializer[]} */ (
              parse(tli.code, {
                startRule: "ImportsAndSource",
                grammarSource: new GrammarLocation(
                  tli.codeLocation.source,
                  tli.codeLocation.start
                ),
              })
            );
            if (imports.code) {
              imps.push(imports);
              codes.push(code);
            } else {
              // Prefer the original
              codes.push(tli);
            }
          }
          // Imports go at the end so that when reversed, they end up in front.
          topLevel = codes.concat(imps);
        }
        // Put library code before code using it.
        const reversed = topLevel.slice(0).reverse();
        for (const tli of reversed) {
          parts.push(ast2SourceNode(tli));
          parts.push("");
        }
      } else {
        parts.push(ast2SourceNode(topLevel));
        parts.push("");
      }
    }

    parts.push(
      "function peg$subclass(child, parent) {",
      "  function C() { this.constructor = child; }",
      "  C.prototype = parent.prototype;",
      "  child.prototype = new C();",
      "}",
      "",
      "function peg$SyntaxError(message, expected, found, location) {",
      "  var self = Error.call(this, message);",
      "  // istanbul ignore next Check is a necessary evil to support older environments",
      "  if (Object.setPrototypeOf) {",
      "    Object.setPrototypeOf(self, peg$SyntaxError.prototype);",
      "  }",
      "  self.expected = expected;",
      "  self.found = found;",
      "  self.location = location;",
      "  self.name = \"SyntaxError\";",
      "  return self;",
      "}",
      "",
      "peg$subclass(peg$SyntaxError, Error);",
      "",
      "function peg$padEnd(str, targetLength, padString) {",
      "  padString = padString || \" \";",
      "  if (str.length > targetLength) { return str; }",
      "  targetLength -= str.length;",
      "  padString += padString.repeat(targetLength);",
      "  return str + padString.slice(0, targetLength);",
      "}",
      "",
      "peg$SyntaxError.prototype.format = function(sources) {",
      "  var str = \"Error: \" + this.message;",
      "  if (this.location) {",
      "    var src = null;",
      "    var k;",
      "    for (k = 0; k < sources.length; k++) {",
      "      if (sources[k].source === this.location.source) {",
      "        src = sources[k].text.split(/\\r\\n|\\n|\\r/g);",
      "        break;",
      "      }",
      "    }",
      "    var s = this.location.start;",
      "    var offset_s = (this.location.source && (typeof this.location.source.offset === \"function\"))",
      "      ? this.location.source.offset(s)",
      "      : s;",
      "    var loc = this.location.source + \":\" + offset_s.line + \":\" + offset_s.column;",
      "    if (src) {",
      "      var e = this.location.end;",
      "      var filler = peg$padEnd(\"\", offset_s.line.toString().length, ' ');",
      "      var line = src[s.line - 1];",
      "      var last = s.line === e.line ? e.column : line.length + 1;",
      "      var hatLen = (last - s.column) || 1;",
      "      str += \"\\n --> \" + loc + \"\\n\"",
      "          + filler + \" |\\n\"",
      "          + offset_s.line + \" | \" + line + \"\\n\"",
      "          + filler + \" | \" + peg$padEnd(\"\", s.column - 1, ' ')",
      "          + peg$padEnd(\"\", hatLen, \"^\");",
      "    } else {",
      "      str += \"\\n at \" + loc;",
      "    }",
      "  }",
      "  return str;",
      "};",
      "",
      "peg$SyntaxError.buildMessage = function(expected, found) {",
      "  var DESCRIBE_EXPECTATION_FNS = {",
      "    literal: function(expectation) {",
      "      return \"\\\"\" + literalEscape(expectation.text) + \"\\\"\";",
      "    },",
      "",
      "    class: function(expectation) {",
      "      var escapedParts = expectation.parts.map(function(part) {",
      "        return Array.isArray(part)",
      "          ? classEscape(part[0]) + \"-\" + classEscape(part[1])",
      "          : classEscape(part);",
      "      });",
      "",
      "      return \"[\" + (expectation.inverted ? \"^\" : \"\") + escapedParts.join(\"\") + \"]\";",
      "    },",
      "",
      "    any: function() {",
      "      return \"any character\";",
      "    },",
      "",
      "    end: function() {",
      "      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 = expected.map(describeExpectation);",
      "    var i, j;",
      "",
      "    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.\";",
      "};",
      ""
    );

    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 + \".\");",
        "  }",
        "};",
        ""
      );
    }

    const startRuleFunctions = "{ "
      + allowedStartRules.map(
        r => r + ": " + name(r)
      ).join(", ")
      + " }";
    const startRuleFunction = name(allowedStartRules[0]);

    parts.push(
      "function peg$parse(input, options) {",
      "  options = options !== undefined ? options : {};",
      "",
      "  var peg$FAILED = {};",
      "  var peg$source = options.grammarSource;",
      "",
      "  var peg$startRuleFunctions = " + startRuleFunctions + ";",
      "  var peg$startRuleFunction = " + startRuleFunction + ";",
      "",
      generateTables(),
      "",
      "  var peg$currPos = options.peg$currPos | 0;",
      "  var peg$savedPos = peg$currPos;",
      "  var peg$posDetailsCache = [{ line: 1, column: 1 }];",
      "  var peg$maxFailPos = peg$currPos;",
      "  var peg$maxFailExpected = options.peg$maxFailExpected || [];",
      "  var peg$silentFails = options.peg$silentFails | 0;",   // 0 = report failures, > 0 = silence failures
      ""
    );

    if (options.cache) {
      parts.push(
        "  var peg$resultsCache = {};",
        ""
      );
    }

    if (options.trace) {
      parts.push(
        "  var peg$tracer = \"tracer\" in options ? options.tracer : new peg$DefaultTracer();",
        ""
      );
    }

    parts.push(
      "  var peg$result;",
      "",
      "  if (options.startRule) {",
      "    if (!(options.startRule in peg$startRuleFunctions)) {",
      "      throw new Error(\"Can't start parsing from rule \\\"\" + options.startRule + \"\\\".\");",
      "    }",
      "",
      "    peg$startRuleFunction = peg$startRuleFunctions[options.startRule];",
      "  }",
      "",
      "  function text() {",
      "    return input.substring(peg$savedPos, peg$currPos);",
      "  }",
      "",
      "  function offset() {",
      "    return peg$savedPos;",
      "  }",
      "",
      "  function range() {",
      "    return {",
      "      source: peg$source,",
      "      start: peg$savedPos,",
      "      end: peg$currPos",
      "    };",
      "  }",
      "",
      "  function location() {",
      "    return peg$computeLocation(peg$savedPos, peg$currPos);",
      "  }",
      "",
      "  function expected(description, location) {",
      "    location = location !== undefined",
      "      ? 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 !== undefined",
      "      ? 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];",
      "    var p;",
      "",
      "    if (details) {",
      "      return details;",
      "    } else {",
      "      if (pos >= peg$posDetailsCache.length) {",
      "        p = peg$posDetailsCache.length - 1;",
      "      } else {",
      "        p = pos;",
      "        while (!peg$posDetailsCache[--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, offset) {",
      "    var startPosDetails = peg$computePosDetails(startPos);",
      "    var endPosDetails = peg$computePosDetails(endPos);",
      "",
      "    var res = {",
      "      source: peg$source,",
      "      start: {",
      "        offset: startPos,",
      "        line: startPosDetails.line,",
      "        column: startPosDetails.column",
      "      },",
      "      end: {",
      "        offset: endPos,",
      "        line: endPosDetails.line,",
      "        column: endPosDetails.column",
      "      }",
      "    };",
      "    if (offset && peg$source && (typeof peg$source.offset === \"function\")) {",
      "      res.start = peg$source.offset(res.start);",
      "      res.end = peg$source.offset(res.end);",
      "    }",
      "    return res;",
      "  }",
      "",
      "  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",
      "    );",
      "  }",
      ""
    );

    if (ast.imports.length > 0) {
      parts.push(
        "  var peg$assign = Object.assign || function(t) {",
        "    var i, s;",
        "    for (i = 1; i < arguments.length; i++) {",
        "      s = arguments[i];",
        "      for (var p in s) {",
        "        if (Object.prototype.hasOwnProperty.call(s, p)) {",
        "          t[p] = s[p];",
        "        }",
        "      }",
        "    }",
        "    return t;",
        "  };",
        "",
        "  function peg$callLibrary(lib, startRule) {",
        "    const opts = peg$assign({}, options, {",
        "      startRule: startRule,",
        "      peg$currPos: peg$currPos,",
        "      peg$silentFails: peg$silentFails,",
        "      peg$library: true,",
        "      peg$maxFailExpected: peg$maxFailExpected",
        "    });",
        "    const res = lib.parse(input, opts);",
        "    peg$currPos = res.peg$currPos;",
        "    peg$maxFailPos = res.peg$maxFailPos;",
        "    peg$maxFailExpected = res.peg$maxFailExpected;",
        "    return (res.peg$result === res.peg$FAILED) ? peg$FAILED : res.peg$result;",
        "  }",
        ""
      );
    }

    ast.rules.forEach(rule => {
      parts.push(...indent2(generateRuleFunction(rule)));
      parts.push("");
    });

    if (ast.initializer) {
      if (Array.isArray(ast.initializer)) {
        for (const init of ast.initializer) {
          parts.push(ast2SourceNode(init));
          parts.push("");
        }
      } else {
        parts.push(ast2SourceNode(ast.initializer));
        parts.push("");
      }
    }

    parts.push(
      "  peg$result = peg$startRuleFunction();",
      "",
      "  if (options.peg$library) {",
      // Hide this from TypeScript.  It's internal-only.
      "    return /** @type {any} */ ({",
      "      peg$result,",
      "      peg$currPos,",
      "      peg$FAILED,",
      "      peg$maxFailExpected,",
      "      peg$maxFailPos",
      "    });",
      "  }",
      "  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)",
      "    );",
      "  }",
      "}"
    );

    return new SourceNode(
      // This expression has a better readability when on two lines
      // eslint-disable-next-line @stylistic/function-call-argument-newline
      null, null, options.grammarSource,
      parts.map(s => (s instanceof SourceNode ? s : s + "\n"))
    );
  }

  /** @param {SourceNode} toplevelCode */
  function generateWrapper(toplevelCode) {
    /** @return {(string|SourceNode)[]} */
    function generateGeneratedByComment() {
      return [
        `// @generated by Peggy ${version}.`,
        "//",
        "// https://peggyjs.org/",
      ];
    }

    function generateParserObject() {
      const res = ["{"];
      if (options.trace) {
        res.push("  DefaultTracer: peg$DefaultTracer,");
      }

      if (options.allowedStartRules) {
        res.push("  StartRules: [" + options.allowedStartRules.map(r => '"' + r + '"').join(", ") + "],");
      }

      res.push(
        "  SyntaxError: peg$SyntaxError,",
        "  parse: peg$parse"
      );

      res.push("}");
      return res.join("\n");
    }

    const generators = {
      bare() {
        if ((Object.keys(dependencies).length > 0)
            || (ast.imports.length > 0)) {
          throw new Error("Dependencies not supported in format 'bare'.");
        }
        return [
          ...generateGeneratedByComment(),
          "(function() {",
          "  \"use strict\";",
          "",
          toplevelCode,
          "",
          indent2("return " + generateParserObject() + ";"),
          "})()",
        ];
      },

      commonjs() {
        const dependencyVars = Object.keys(dependencies);

        const parts = generateGeneratedByComment();
        parts.push(
          "",
          "\"use strict\";",
          ""
        );

        if (dependencyVars.length > 0) {
          dependencyVars.forEach(variable => {
            parts.push(
              "var " + variable
              + " = require(\""
              + stringEscape(dependencies[variable])
              + "\");"
            );
          });
          parts.push("");
        }

        const impLen = ast.imports.length;
        for (let i = 0; i < impLen; i++) {
          parts.push(
            "var " + gi(i)
            + " = require(\""
            + stringEscape(ast.imports[i].from.module)
            + "\");"
          );
        }

        parts.push(
          "",
          toplevelCode,
          "",
          "module.exports = " + generateParserObject() + ";"
        );

        return parts;
      },

      es() {
        const dependencyVars = Object.keys(dependencies);

        const parts = generateGeneratedByComment();
        parts.push("");

        if (dependencyVars.length > 0) {
          dependencyVars.forEach(variable => {
            parts.push(
              "import " + variable
              + " from \""
              + stringEscape(dependencies[variable])
              + "\";"
            );
          });
          parts.push("");
        }

        for (let i = 0; i < ast.imports.length; i++) {
          parts.push(
            "import * as " + gi(i)
            + " from \""
            + stringEscape(ast.imports[i].from.module)
            + "\";"
          );
        }

        parts.push(
          "",
          toplevelCode,
          ""
        );

        parts.push(
          "const peg$allowedStartRules = [",
          "  " + (options.allowedStartRules ? options.allowedStartRules.map(r => '"' + r + '"').join(",\n  ") : ""),
          "];",
          ""
        );

        parts.push(
          "export {"
        );

        if (options.trace) {
          parts.push("  peg$DefaultTracer as DefaultTracer,");
        }

        parts.push(
          "  peg$allowedStartRules as StartRules,",
          "  peg$SyntaxError as SyntaxError,",
          "  peg$parse as parse",
          "};"
        );

        return parts;
      },

      amd() {
        if (ast.imports.length > 0) {
          throw new Error("Imports are not supported in format 'amd'.");
        }

        const dependencyVars = Object.keys(dependencies);
        const dependencyIds = dependencyVars.map(v => dependencies[v]);
        const deps = "["
          + dependencyIds.map(
            id => "\"" + stringEscape(id) + "\""
          ).join(", ")
          + "]";
        const params = dependencyVars.join(", ");

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

      globals() {
        if ((Object.keys(dependencies).length > 0)
            || (ast.imports.length > 0)) {
          throw new Error("Dependencies not supported in format 'globals'.");
        }
        if (!options.exportVar) {
          throw new Error("No export variable defined for format 'globals'.");
        }

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

      umd() {
        if (ast.imports.length > 0) {
          throw new Error("Imports are not supported in format 'umd'.");
        }

        const dependencyVars = Object.keys(dependencies);
        const dependencyIds = dependencyVars.map(v => dependencies[v]);
        const deps = "["
          + dependencyIds.map(
            id => "\"" + stringEscape(id) + "\""
          ).join(", ")
          + "]";
        const requires = dependencyIds.map(
          id => "require(\"" + stringEscape(id) + "\")"
        ).join(", ");
        const params = dependencyVars.join(", ");

        const parts = generateGeneratedByComment();
        parts.push(
          "(function(root, factory) {",
          "  if (typeof define === \"function\" && define.amd) {",
          "    define(" + deps + ", factory);",
          "  } else if (typeof module === \"object\" && module.exports) {",
          "    module.exports = factory(" + requires + ");"
        );

        if (options.exportVar) {
          parts.push(
            "  } else {",
            "    root." + options.exportVar + " = factory();"
          );
        }

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

        return parts;
      },
    };

    const parts = generators[options.format || "bare"]();

    return new SourceNode(
      // eslint-disable-next-line @stylistic/function-call-argument-newline -- This expression has a better readability when on two lines
      null, null, options.grammarSource,
      parts.map(s => (s instanceof SourceNode ? s : s + "\n"))
    );
  }

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

module.exports = generateJS;
