/**
 * Compiler frontend for node.js
 *
 * Uses the low-level API exported from src/index.ts so it works with the compiler compiled to
 * JavaScript as well as the compiler compiled to WebAssembly (eventually). Runs the sources
 * directly through ts-node if distribution files are not present (indicated by a `-dev` version).
 *
 * Can also be packaged as a bundle suitable for in-browser use with the standard library injected
 * in the build step. See dist/asc.js for the bundle and webpack.config.js for building details.
 *
 * @module cli/asc
 */

// Use "." instead of "/" as cwd in browsers
if (process.browser) process.cwd = function() { return "."; };

const fs = require("fs");
const path = require("path");
const utf8 = require("@protobufjs/utf8");
const colorsUtil = require("./util/colors");
const optionsUtil = require("./util/options");
const mkdirp = require("./util/mkdirp");
const EOL = process.platform === "win32" ? "\r\n" : "\n";

// Emscripten adds an `uncaughtException` listener to Binaryen that results in an additional
// useless code fragment on top of an actual error. suppress this:
if (process.removeAllListeners) process.removeAllListeners("uncaughtException");

// Use distribution files if present, otherwise run the sources directly
var assemblyscript, isDev = false;
(() => {
  try { // `asc` on the command line
    assemblyscript = require("../dist/assemblyscript.js");
  } catch (e) {
    try { // `asc` on the command line without dist files
      require("ts-node").register({ project: path.join(__dirname, "..", "src", "tsconfig.json") });
      require("../src/glue/js");
      assemblyscript = require("../src");
      isDev = true;
    } catch (e_ts) {
      try { // `require("dist/asc.js")` in explicit browser tests
        assemblyscript = eval("require('./assemblyscript')");
      } catch (e) {
        // combine both errors that lead us here
        e.stack = e_ts.stack + "\n---\n" + e.stack;
        throw e;
      }
    }
  }
})();

/** Whether this is a webpack bundle or not. */
exports.isBundle = typeof BUNDLE_VERSION === "string";

/** Whether asc runs the sources directly or not. */
exports.isDev = isDev;

/** AssemblyScript version. */
exports.version = exports.isBundle ? BUNDLE_VERSION : require("../package.json").version;

/** Available CLI options. */
exports.options = require("./asc.json");

/** Common root used in source maps. */
exports.sourceMapRoot = "assemblyscript:///";

/** Prefix used for library files. */
exports.libraryPrefix = assemblyscript.LIBRARY_PREFIX;

/** Default Binaryen optimization level. */
exports.defaultOptimizeLevel = 2;

/** Default Binaryen shrink level. */
exports.defaultShrinkLevel = 1;

/** Bundled library files. */
exports.libraryFiles = exports.isBundle ? BUNDLE_LIBRARY : (() => { // set up if not a bundle
  const libDir = path.join(__dirname, "..", "std", "assembly");
  const libFiles = require("glob").sync("**/!(*.d).ts", { cwd: libDir });
  const bundled = {};
  libFiles.forEach(file => bundled[file.replace(/\.ts$/, "")] = fs.readFileSync(path.join(libDir, file), "utf8" ));
  return bundled;
})();

/** Bundled definition files. */
exports.definitionFiles = exports.isBundle ? BUNDLE_DEFINITIONS : (() => { // set up if not a bundle
  const stdDir = path.join(__dirname, "..", "std");
  return {
    "assembly": fs.readFileSync(path.join(stdDir, "assembly", "index.d.ts"), "utf8"),
    "portable": fs.readFileSync(path.join(stdDir, "portable", "index.d.ts"), "utf8")
  };
})();

/** Convenience function that parses and compiles source strings directly. */
exports.compileString = (sources, options) => {
  if (typeof sources === "string") sources = { "input.ts": sources };
  const output = Object.create({
    stdout: createMemoryStream(),
    stderr: createMemoryStream(),
    binary: null,
    text: null
  });
  var argv = [
    "--binaryFile", "binary",
    "--textFile", "text",
  ];
  Object.keys(options || {}).forEach(key => {
    var val = options[key];
    if (Array.isArray(val)) val.forEach(val => argv.push("--" + key, String(val)));
    else argv.push("--" + key, String(val));
  });
  exports.main(argv.concat(Object.keys(sources)), {
    stdout: output.stdout,
    stderr: output.stderr,
    readFile: name => sources.hasOwnProperty(name) ? sources[name] : null,
    writeFile: (name, contents) => output[name] = contents,
    listFiles: () => []
  });
  return output;
}

/** Runs the command line utility using the specified arguments array. */
exports.main = function main(argv, options, callback) {
  if (typeof options === "function") {
    callback = options;
    options = {};
  } else if (!options) {
    options = {};
  }

  const stdout = options.stdout || process.stdout;
  const stderr = options.stderr || process.stderr;
  const readFile = options.readFile || readFileNode;
  const writeFile = options.writeFile || writeFileNode;
  const listFiles = options.listFiles || listFilesNode;
  const stats = options.stats || createStats();

  // Output must be specified if not present in the environment
  if (!stdout) throw Error("'options.stdout' must be specified");
  if (!stderr) throw Error("'options.stderr' must be specified");

  const opts = optionsUtil.parse(argv, exports.options);
  const args = opts.options;
  argv = opts.arguments;
  if (args.noColors) {
    colorsUtil.stdout.supported =
    colorsUtil.stderr.supported = false;
  } else {
    colorsUtil.stdout = colorsUtil.from(stdout);
    colorsUtil.stderr = colorsUtil.from(stderr);
  }

  // Check for unknown arguments
  if (opts.unknown.length) {
    opts.unknown.forEach(arg => {
      stderr.write(colorsUtil.stderr.yellow("WARN: ") + "Unknown option '" + arg + "'" + EOL);
    });
  }

  // Check for trailing arguments
  if (opts.trailing.length) {
    stderr.write(colorsUtil.stderr.yellow("WARN: ") + "Unsupported trailing arguments: " + opts.trailing.join(" ") + EOL);
  }

  // Use default callback if none is provided
  if (!callback) callback = function defaultCallback(err) {
    var code = 0;
    if (err) {
      stderr.write(colorsUtil.stderr.red("ERROR: ") + err.stack.replace(/^ERROR: /i, "") + EOL);
      code = 1;
    }
    return code;
  };

  // Just print the version if requested
  if (args.version) {
    stdout.write("Version " + exports.version + (isDev ? "-dev" : "") + EOL);
    return callback(null);
  }
  // Print the help message if requested or no source files are provided
  if (args.help || !argv.length) {
    var out = args.help ? stdout : stderr;
    var color = args.help ? colorsUtil.stdout : colorsUtil.stderr;
    out.write([
      color.white("SYNTAX"),
      "  " + color.cyan("asc") + " [entryFile ...] [options]",
      "",
      color.white("EXAMPLES"),
      "  " + color.cyan("asc") + " hello.ts",
      "  " + color.cyan("asc") + " hello.ts -b hello.wasm -t hello.wat",
      "  " + color.cyan("asc") + " hello1.ts hello2.ts -b -O > hello.wasm",
      "",
      color.white("OPTIONS"),
    ].concat(
      optionsUtil.help(exports.options, 24, EOL)
    ).join(EOL) + EOL);
    return callback(null);
  }

  // I/O must be specified if not present in the environment
  if (!fs.readFileSync) {
    if (readFile === readFileNode) throw Error("'options.readFile' must be specified");
    if (writeFile === writeFileNode) throw Error("'options.writeFile' must be specified");
    if (listFiles === listFilesNode) throw Error("'options.listFiles' must be specified");
  }

  // Set up base directory
  const baseDir = args.baseDir ? path.resolve(args.baseDir) : ".";

  // Set up transforms
  const transforms = [];
  if (args.transform) {
    args.transform.forEach(transform =>
      transforms.push(
        require(
          path.isAbsolute(transform = transform.trim())
            ? transform
            : path.join(process.cwd(), transform)
        )
      )
    );
  }
  function applyTransform(name, ...args) {
    transforms.forEach(transform => {
      if (typeof transform[name] === "function") transform[name](...args);
    });
  }

  // Begin parsing
  var parser = null;

  // Include library files
  if (!args.noLib) {
    Object.keys(exports.libraryFiles).forEach(libPath => {
      if (libPath.indexOf("/") >= 0) return; // in sub-directory: imported on demand
      stats.parseCount++;
      stats.parseTime += measure(() => {
        parser = assemblyscript.parseFile(
          exports.libraryFiles[libPath],
          exports.libraryPrefix + libPath + ".ts",
          false,
          parser
        );
      });
    });
  } else { // always include builtins
    stats.parseCount++;
    stats.parseTime += measure(() => {
      parser = assemblyscript.parseFile(
        exports.libraryFiles["builtins"],
        exports.libraryPrefix + "builtins.ts",
        false,
        parser
      );
    });
  }
  const customLibDirs = [];
  if (args.lib) {
    let lib = args.lib;
    if (typeof lib === "string") lib = lib.split(",");
    Array.prototype.push.apply(customLibDirs, lib.map(lib => lib.trim()));
    for (let i = 0, k = customLibDirs.length; i < k; ++i) { // custom
      let libDir = customLibDirs[i];
      let libFiles;
      if (libDir.endsWith(".ts")) {
        libFiles = [ path.basename(libDir) ];
        libDir = path.dirname(libDir);
      } else {
        libFiles = listFiles(libDir);
      }
      for (let j = 0, l = libFiles.length; j < l; ++j) {
        let libPath = libFiles[j];
        let libText = readFile(path.join(libDir, libPath));
        if (libText === null) return callback(Error("Library file '" + libPath + "' not found."));
        stats.parseCount++;
        stats.parseTime += measure(() => {
          parser = assemblyscript.parseFile(
            libText,
            exports.libraryPrefix + libPath,
            false,
            parser
          );
        });
      }
    }
  }

  // Include entry files
  for (let i = 0, k = argv.length; i < k; ++i) {
    const filename = argv[i];

    let sourcePath = String(filename).replace(/\\/g, "/").replace(/(\.ts|\/)$/, "");

    // Try entryPath.ts, then entryPath/index.ts
    let sourceText = readFile(path.join(baseDir, sourcePath) + ".ts");
    if (sourceText === null) {
      sourceText = readFile(path.join(baseDir, sourcePath, "index.ts"));
      if (sourceText === null) {
        return callback(Error("Entry file '" + sourcePath + ".ts' not found."));
      } else {
        sourcePath += "/index.ts";
      }
    } else {
      sourcePath += ".ts";
    }

    stats.parseCount++;
    stats.parseTime += measure(() => {
      parser = assemblyscript.parseFile(sourceText, sourcePath, true, parser);
    });

    // Process backlog
    while ((sourcePath = parser.nextFile()) != null) {
      let found = false;

      // Load library file if explicitly requested
      if (sourcePath.startsWith(exports.libraryPrefix)) {
        const plainName = sourcePath.substring(exports.libraryPrefix.length);
        const indexName = sourcePath.substring(exports.libraryPrefix.length) + "/index";
        if (exports.libraryFiles.hasOwnProperty(plainName)) {
          sourceText = exports.libraryFiles[plainName];
          sourcePath = exports.libraryPrefix + plainName + ".ts";
        } else if (exports.libraryFiles.hasOwnProperty(indexName)) {
          sourceText = exports.libraryFiles[indexName];
          sourcePath = exports.libraryPrefix + indexName + ".ts";
        } else {
          for (let i = 0, k = customLibDirs.length; i < k; ++i) {
            const dir = customLibDirs[i];
            sourceText = readFile(path.join(dir, plainName + ".ts"));
            if (sourceText !== null) {
              sourcePath = exports.libraryPrefix + plainName + ".ts";
              break;
            } else {
              sourceText = readFile(path.join(dir, indexName + ".ts"));
              if (sourceText !== null) {
                sourcePath = exports.libraryPrefix + indexName + ".ts";
                break;
              }
            }
          }
        }

      // Otherwise try nextFile.ts, nextFile/index.ts, ~lib/nextFile.ts, ~lib/nextFile/index.ts
      } else {
        const plainName = sourcePath;
        const indexName = sourcePath + "/index";
        sourceText = readFile(path.join(baseDir, plainName + ".ts"));
        if (sourceText !== null) {
          sourcePath = plainName + ".ts";
        } else {
          sourceText = readFile(path.join(baseDir, indexName + ".ts"));
          if (sourceText !== null) {
            sourcePath = indexName + ".ts";
          } else if (!plainName.startsWith(".")) {
            if (exports.libraryFiles.hasOwnProperty(plainName)) {
              sourceText = exports.libraryFiles[plainName];
              sourcePath = exports.libraryPrefix + plainName + ".ts";
            } else if (exports.libraryFiles.hasOwnProperty(indexName)) {
              sourceText = exports.libraryFiles[indexName];
              sourcePath = exports.libraryPrefix + indexName + ".ts";
            } else {
              for (let i = 0, k = customLibDirs.length; i < k; ++i) {
                const dir = customLibDirs[i];
                sourceText = readFile(path.join(dir, plainName + ".ts"));
                if (sourceText !== null) {
                  sourcePath = exports.libraryPrefix + plainName + ".ts";
                  break;
                } else {
                  sourceText = readFile(path.join(dir, indexName + ".ts"));
                  if (sourceText !== null) {
                    sourcePath = exports.libraryPrefix + indexName + ".ts";
                    break;
                  }
                }
              }
            }
          }
        }
      }
      if (sourceText == null) {
        return callback(Error("Import file '" + sourcePath + ".ts' not found."));
      }
      stats.parseCount++;
      stats.parseTime += measure(() => {
        assemblyscript.parseFile(sourceText, sourcePath, false, parser);
      });
    }
    if (checkDiagnostics(parser, stderr)) {
      return callback(Error("Parse error"));
    }
  }

  applyTransform("afterParse", parser);

  // Finish parsing
  const program = assemblyscript.finishParsing(parser);

  // Set up optimization levels
  var optimizeLevel = 0;
  var shrinkLevel = 0;
  if (args.optimize) {
    optimizeLevel = exports.defaultOptimizeLevel;
    shrinkLevel = exports.defaultShrinkLevel;
  }
  if (typeof args.optimizeLevel === "number") {
    optimizeLevel = args.optimizeLevel;
  }
  if (typeof args.shrinkLevel === "number") {
    shrinkLevel = args.shrinkLevel;
  }
  optimizeLevel = Math.min(Math.max(optimizeLevel, 0), 3);
  shrinkLevel = Math.min(Math.max(shrinkLevel, 0), 2);

  // Begin compilation
  const compilerOptions = assemblyscript.createOptions();
  assemblyscript.setTarget(compilerOptions, 0);
  assemblyscript.setNoTreeShaking(compilerOptions, args.noTreeShaking);
  assemblyscript.setNoAssert(compilerOptions, args.noAssert);
  assemblyscript.setImportMemory(compilerOptions, args.importMemory);
  assemblyscript.setImportTable(compilerOptions, args.importTable);
  assemblyscript.setMemoryBase(compilerOptions, args.memoryBase >>> 0);
  assemblyscript.setSourceMap(compilerOptions, args.sourceMap != null);
  assemblyscript.setOptimizeLevelHints(compilerOptions, optimizeLevel, shrinkLevel);

  if (!args.noLib) {
    // Initialize default aliases
    assemblyscript.setGlobalAlias(compilerOptions, "Math", "NativeMath");
    assemblyscript.setGlobalAlias(compilerOptions, "Mathf", "NativeMathf");
    assemblyscript.setGlobalAlias(compilerOptions, "abort", "~lib/env/abort");
    assemblyscript.setGlobalAlias(compilerOptions, "trace", "~lib/env/trace");
  }

  // Add or override aliases if specified
  if (args.use) {
    let aliases = args.use;
    for (let i = 0, k = aliases.length; i < k; ++i) {
      let part = aliases[i];
      let p = part.indexOf("=");
      if (p < 0) return callback(Error("Global alias '" + part + "' is invalid."));
      let name = part.substring(0, p).trim();
      let alias = part.substring(p + 1).trim();
      if (!name.length) return callback(Error("Global alias '" + part + "' is invalid."));
      assemblyscript.setGlobalAlias(compilerOptions, name, alias);
    }
  }

  // Enable additional features if specified
  var features = args.enable;
  if (features != null) {
    if (typeof features === "string") features = features.split(",");
    for (let i = 0, k = features.length; i < k; ++i) {
      let name = features[i].trim();
      let flag = assemblyscript["FEATURE_" + name.replace(/\-/g, "_").toUpperCase()];
      if (!flag) return callback(Error("Feature '" + name + "' is unknown."));
      assemblyscript.enableFeature(compilerOptions, flag);
    }
  }

  var module;
  stats.compileCount++;
  (() => {
    try {
      stats.compileTime += measure(() => {
        module = assemblyscript.compileProgram(program, compilerOptions);
      });
    } catch (e) {
      return callback(e);
    }
  })();
  if (checkDiagnostics(parser, stderr)) {
    if (module) module.dispose();
    return callback(Error("Compile error"));
  }

  // Validate the module if requested
  if (args.validate) {
    stats.validateCount++;
    stats.validateTime += measure(() => {
      if (!module.validate()) {
        module.dispose();
        return callback(Error("Validate error"));
      }
    });
  }

  // Set Binaryen-specific options
  if (args.trapMode === "clamp") {
    stats.optimizeCount++;
    stats.optimizeTime += measure(() => {
      module.runPasses([ "trap-mode-clamp" ]);
    });
  } else if (args.trapMode === "js") {
    stats.optimizeCount++;
    stats.optimizeTime += measure(() => {
      module.runPasses([ "trap-mode-js" ]);
    });
  } else if (args.trapMode !== "allow") {
    module.dispose();
    return callback(Error("Unsupported trap mode"));
  }

  // Implicitly run costly non-LLVM optimizations on -O3 or -Oz
  // see: https://github.com/WebAssembly/binaryen/pull/1596
  if (optimizeLevel >= 3 || shrinkLevel >= 2) optimizeLevel = 4;

  module.setOptimizeLevel(optimizeLevel);
  module.setShrinkLevel(shrinkLevel);
  module.setDebugInfo(args.debug);

  var runPasses = [];
  if (args.runPasses) {
    if (typeof args.runPasses === "string") {
      args.runPasses = args.runPasses.split(",");
    }
    if (args.runPasses.length) {
      args.runPasses.forEach(pass => {
        if (runPasses.indexOf(pass) < 0)
          runPasses.push(pass);
      });
    }
  }

  // Optimize the module if requested
  if (optimizeLevel > 0 || shrinkLevel > 0) {
    stats.optimizeCount++;
    stats.optimizeTime += measure(() => {
      module.optimize();
    });
  }

  // Run additional passes if requested
  if (runPasses.length) {
    stats.optimizeCount++;
    stats.optimizeTime += measure(() => {
      module.runPasses(runPasses.map(pass => pass.trim()));
    });
  }

  // Prepare output
  if (!args.noEmit) {
    let hasStdout = false;
    let hasOutput = false;

    if (args.outFile != null) {
      if (/\.was?t$/.test(args.outFile) && args.textFile == null) {
        args.textFile = args.outFile;
      } else if (/\.js$/.test(args.outFile) && args.asmjsFile == null) {
        args.asmjsFile = args.outFile;
      } else if (args.binaryFile == null) {
        args.binaryFile = args.outFile;
      }
    }

    // Write binary
    if (args.binaryFile != null) {
      let sourceMapURL = args.sourceMap != null
        ? args.sourceMap.length
          ? args.sourceMap
          : path.basename(args.binaryFile) + ".map"
        : null;

      let wasm;
      stats.emitCount++;
      stats.emitTime += measure(() => {
        wasm = module.toBinary(sourceMapURL)
      });

      if (args.binaryFile.length) {
        writeFile(path.join(baseDir, args.binaryFile), wasm.output);
      } else {
        writeStdout(wasm.output);
        hasStdout = true;
      }
      hasOutput = true;

      // Post-process source map
      if (wasm.sourceMap != null) {
        if (args.binaryFile.length) {
          let sourceMap = JSON.parse(wasm.sourceMap);
          sourceMap.sourceRoot = exports.sourceMapRoot;
          sourceMap.sources.forEach((name, index) => {
            let text = null;
            if (name.startsWith(exports.libraryPrefix)) {
              let stdName = name.substring(exports.libraryPrefix.length).replace(/\.ts$/, "");
              if (exports.libraryFiles.hasOwnProperty(stdName)) {
                text = exports.libraryFiles[stdName];
              } else {
                for (let i = 0, k = customLibDirs.length; i < k; ++i) {
                  text = readFile(path.join(
                    customLibDirs[i],
                    name.substring(exports.libraryPrefix.length))
                  );
                  if (text !== null) break;
                }
              }
            } else {
              text = readFile(path.join(baseDir, name));
            }
            if (text === null) {
              return callback(Error("Source file '" + name + "' not found."));
            }
            if (!sourceMap.sourceContents) sourceMap.sourceContents = [];
            sourceMap.sourceContents[index] = text;
          });
          writeFile(path.join(
            baseDir,
            path.dirname(args.binaryFile),
            path.basename(sourceMapURL)
          ), JSON.stringify(sourceMap));
        } else {
          stderr.write("Skipped source map (stdout already occupied)" + EOL);
        }
      }
    }

    // Write asm.js
    if (args.asmjsFile != null) {
      let asm;
      if (args.asmjsFile.length) {
        stats.emitCount++;
        stats.emitTime += measure(() => {
          asm = module.toAsmjs();
        });
        writeFile(path.join(baseDir, args.asmjsFile), asm);
      } else if (!hasStdout) {
        stats.emitCount++;
        stats.emitTime += measure(() => {
          asm = module.toAsmjs();
        });
        writeStdout(asm);
        hasStdout = true;
      }
      hasOutput = true;
    }

    // Write WebIDL
    if (args.idlFile != null) {
      let idl;
      if (args.idlFile.length) {
        stats.emitCount++;
        stats.emitTime += measure(() => {
          idl = assemblyscript.buildIDL(program);
        });
        writeFile(path.join(baseDir, args.idlFile), idl);
      } else if (!hasStdout) {
        stats.emitCount++;
        stats.emitTime += measure(() => {
          idl = assemblyscript.buildIDL(program);
        });
        writeStdout(idl);
        hasStdout = true;
      }
      hasOutput = true;
    }

    // Write TypeScript definition
    if (args.tsdFile != null) {
      let tsd;
      if (args.tsdFile.length) {
        stats.emitCount++;
        stats.emitTime += measure(() => {
          tsd = assemblyscript.buildTSD(program);
        });
        writeFile(path.join(baseDir, args.tsdFile), tsd);
      } else if (!hasStdout) {
        stats.emitCount++;
        stats.emitTime += measure(() => {
          tsd = assemblyscript.buildTSD(program);
        });
        writeStdout(tsd);
        hasStdout = true;
      }
      hasOutput = true;
    }

    // Write text (must be last)
    if (args.textFile != null || !hasOutput) {
      let wat;
      if (args.textFile && args.textFile.length) {
        stats.emitCount++;
        stats.emitTime += measure(() => {
          wat = module.toText();
        });
        writeFile(path.join(baseDir, args.textFile), wat);
      } else if (!hasStdout) {
        stats.emitCount++;
        stats.emitTime += measure(() => {
          wat = module.toText()
        });
        writeStdout(wat);
      }
    }
  }

  module.dispose();
  if (args.measure) {
    printStats(stats, stderr);
  }
  return callback(null);

  function readFileNode(filename) {
    try {
      let text;
      stats.readCount++;
      stats.readTime += measure(() => {
        text = fs.readFileSync(filename, { encoding: "utf8" });
      });
      return text;
    } catch (e) {
      return null;
    }
  }

  function writeFileNode(filename, contents) {
    try {
      stats.writeCount++;
      stats.writeTime += measure(() => {
        mkdirp(path.dirname(filename));
        if (typeof contents === "string") {
          fs.writeFileSync(filename, contents, { encoding: "utf8" } );
        } else {
          fs.writeFileSync(filename, contents);
        }
      });
      return true;
    } catch (e) {
      return false;
    }
  }

  function listFilesNode(dirname) {
    var files;
    try {
      stats.readTime += measure(() => {
        files = fs.readdirSync(dirname).filter(file => /^(?!.*\.d\.ts$).*\.ts$/.test(file));
      });
      return files;
    } catch (e) {
      return [];
    }
  }

  function writeStdout(contents) {
    if (!writeStdout.used) {
      stats.writeCount++;
      writeStdout.used = true;
    }
    stats.writeTime += measure(() => {
      if (typeof contents === "string") {
        stdout.write(contents, { encoding: "utf8" });
      } else {
        stdout.write(contents);
      }
    });
  }
}

var argumentSubstitutions = {
  "-O"  : [ "--optimize" ],
  "-Os" : [ "--optimize", "--shrinkLevel", "1" ],
  "-Oz" : [ "--optimize", "--shrinkLevel", "2" ],
  "-O0" : [ "--optimizeLevel", "0", "--shrinkLevel", "0" ],
  "-O0s": [ "--optimizeLevel", "0", "--shrinkLevel", "1" ],
  "-O0z": [ "--optimizeLevel", "0", "--shrinkLevel", "2" ],
  "-O1" : [ "--optimizeLevel", "1", "--shrinkLevel", "0" ],
  "-O1s": [ "--optimizeLevel", "1", "--shrinkLevel", "1" ],
  "-O1z": [ "--optimizeLevel", "1", "--shrinkLevel", "2" ],
  "-O2" : [ "--optimizeLevel", "2", "--shrinkLevel", "0" ],
  "-O2s": [ "--optimizeLevel", "2", "--shrinkLevel", "1" ],
  "-O2z": [ "--optimizeLevel", "2", "--shrinkLevel", "2" ],
  "-O3" : [ "--optimizeLevel", "3", "--shrinkLevel", "0" ],
  "-O3s": [ "--optimizeLevel", "3", "--shrinkLevel", "1" ],
  "-O3z": [ "--optimizeLevel", "3", "--shrinkLevel", "2" ],
};

/** Checks diagnostics emitted so far for errors. */
function checkDiagnostics(emitter, stderr) {
  var diagnostic;
  var hasErrors = false;
  while ((diagnostic = assemblyscript.nextDiagnostic(emitter)) != null) {
    if (stderr) {
      stderr.write(
        assemblyscript.formatDiagnostic(diagnostic, stderr.isTTY, true) +
        EOL + EOL
      );
    }
    if (assemblyscript.isError(diagnostic)) hasErrors = true;
  }
  return hasErrors;
}

exports.checkDiagnostics = checkDiagnostics;

/** Creates an empty set of stats. */
function createStats() {
  return {
    readTime: 0,
    readCount: 0,
    writeTime: 0,
    writeCount: 0,
    parseTime: 0,
    parseCount: 0,
    compileTime: 0,
    compileCount: 0,
    emitTime: 0,
    emitCount: 0,
    validateTime: 0,
    validateCount: 0,
    optimizeTime: 0,
    optimizeCount: 0
  };
}

exports.createStats = createStats;

if (!process.hrtime) process.hrtime = require("browser-process-hrtime");

/** Measures the execution time of the specified function.  */
function measure(fn) {
  const start = process.hrtime();
  fn();
  const times = process.hrtime(start);
  return times[0] * 1e9 + times[1];
}

exports.measure = measure;

/** Formats a high resolution time to a human readable string. */
function formatTime(time) {
  return time ? (time / 1e6).toFixed(3) + " ms" : "N/A";
}

exports.formatTime = formatTime;

/** Formats and prints out the contents of a set of stats. */
function printStats(stats, output) {
  function format(time, count) {
    return formatTime(time);
  }
  (output || process.stdout).write([
    "I/O Read  : " + format(stats.readTime, stats.readCount),
    "I/O Write : " + format(stats.writeTime, stats.writeCount),
    "Parse     : " + format(stats.parseTime, stats.parseCount),
    "Compile   : " + format(stats.compileTime, stats.compileCount),
    "Emit      : " + format(stats.emitTime, stats.emitCount),
    "Validate  : " + format(stats.validateTime, stats.validateCount),
    "Optimize  : " + format(stats.optimizeTime, stats.optimizeCount)
  ].join(EOL) + EOL);
}

exports.printStats = printStats;

var allocBuffer = typeof global !== "undefined" && global.Buffer
  ? global.Buffer.allocUnsafe || function(len) { return new global.Buffer(len); }
  : function(len) { return new Uint8Array(len) };

/** Creates a memory stream that can be used in place of stdout/stderr. */
function createMemoryStream(fn) {
  var stream = [];
  stream.write = function(chunk) {
    if (fn) fn(chunk);
    if (typeof chunk === "string") {
      let buffer = allocBuffer(utf8.length(chunk));
      utf8.write(chunk, buffer, 0);
      chunk = buffer;
    }
    this.push(chunk);
  };
  stream.reset = function() {
    stream.length = 0;
  };
  stream.toBuffer = function() {
    var offset = 0, i = 0, k = this.length;
    while (i < k) offset += this[i++].length;
    var buffer = allocBuffer(offset);
    offset = i = 0;
    while (i < k) {
      buffer.set(this[i], offset);
      offset += this[i].length;
      ++i;
    }
    return buffer;
  };
  stream.toString = function() {
    var buffer = this.toBuffer();
    return utf8.read(buffer, 0, buffer.length);
  };
  return stream;
}

exports.createMemoryStream = createMemoryStream;

/** Compatible TypeScript compiler options for syntax highlighting etc. */
exports.tscOptions = {
  alwaysStrict: true,
  noImplicitAny: true,
  noImplicitReturns: true,
  noImplicitThis: true,
  noEmitOnError: true,
  strictNullChecks: true,
  experimentalDecorators: true,
  target: "esnext",
  module: "commonjs",
  noLib: true,
  types: [],
  allowJs: false
};