//@ts-check
var arg = require("./rescript_arg.js");
var format_usage = `Usage: rescript format <options> [files]

\`rescript format\` formats the current directory
`;
var child_process = require("child_process");
var util = require("util");
var asyncExecFile = util.promisify(child_process.execFile);
var path = require("path");
var fs = require("fs");
var asyncFs = fs.promises;
/**
 * @type {arg.stringref}
 */
var stdin = { val: undefined };

/**
 * @type {arg.boolref}
 */
var format = { val: undefined };

/**
 * @type {arg.boolref}
 */
var check = { val: undefined };

/**
 * @type{arg.specs}
 */
var specs = [
  [
    "-stdin",
    { kind: "String", data: { kind: "String_set", data: stdin } },
    `[.res|.resi|.ml|.mli] Read the code from stdin and print
the formatted code to stdout in ReScript syntax`,
  ],
  [
    "-all",
    { kind: "Unit", data: { kind: "Unit_set", data: format } },
    "Format the whole project ",
  ],
  [
    "-check",
    { kind: "Unit", data: { kind: "Unit_set", data: check } },
    "Check formatting for file or the whole project. Use `-all` to check the whole project",
  ],
];
var formattedStdExtensions = [".res", ".resi", ".ml", ".mli"];
var formattedFileExtensions = [".res", ".resi"];

/**
 *
 * @param {string[]} extensions
 */
function hasExtension(extensions) {
  /**
   * @param {string} x
   */
  var pred = x => extensions.some(ext => x.endsWith(ext));
  return pred;
}
async function readStdin() {
  var stream = process.stdin;
  const chunks = [];
  for await (const chunk of stream) chunks.push(chunk);
  return Buffer.concat(chunks).toString("utf8");
}

/**
 * @param {string[]} files
 * @param {string} bsc_exe
 * @param {(x: string) => boolean} isSupportedFile
 * @param {boolean} checkFormatting
 */
async function formatFiles(files, bsc_exe, isSupportedFile, checkFormatting) {
  var incorrectlyFormattedFiles = 0;
  try {
    const _promises = await Promise.all(
      files.map(async file => {
        if (isSupportedFile(file)) {
          const flags = checkFormatting
            ? ["-format", file]
            : ["-o", file, "-format", file];
          const { stdout } = await asyncExecFile(bsc_exe, flags);
          if (check.val) {
            const original = await asyncFs.readFile(file, "utf-8");
            if (original != stdout) {
              console.error("[format check]", file);
              incorrectlyFormattedFiles++;
            }
          }
        }
        return null;
      })
    );
  } catch (err) {
    console.error(err);
    process.exit(2);
  }
  if (incorrectlyFormattedFiles > 0) {
    if (incorrectlyFormattedFiles == 1) {
      console.error("The file listed above needs formatting");
    } else {
      console.error(
        `The ${incorrectlyFormattedFiles} files listed above need formatting`
      );
    }
    process.exit(3);
  }
}

/**
 * @param {string[]} argv
 * @param {string} rescript_exe
 * @param {string} bsc_exe
 */
async function main(argv, rescript_exe, bsc_exe) {
  var isSupportedFile = hasExtension(formattedFileExtensions);
  var isSupportedStd = hasExtension(formattedStdExtensions);

  try {
    /**
     * @type {string[]}
     */
    var files = [];
    arg.parse_exn(format_usage, argv, specs, xs => {
      files = xs;
    });

    var format_project = format.val;
    var use_stdin = stdin.val;

    // Only -check arg
    // Require: -all or path to a file
    if (check.val && !format_project && files.length == 0) {
      console.error(
        "format check require path to a file or use `-all` to check the whole project"
      );
      process.exit(2);
    }

    if (format_project) {
      if (use_stdin || files.length !== 0) {
        console.error("format -all can not be in use with other flags");
        process.exit(2);
      }
      // -all
      // TODO: check the rest arguments
      var output = child_process.spawnSync(
        rescript_exe,
        ["info", "-list-files"],
        {
          encoding: "utf-8",
        }
      );
      if (output.status !== 0) {
        console.error(output.stdout);
        console.error(output.stderr);
        process.exit(2);
      }
      files = output.stdout.split("\n").map(x => x.trim());
      await formatFiles(files, bsc_exe, isSupportedFile, check.val);
    } else if (use_stdin) {
      if (check.val) {
        console.error("format -stdin cannot be used with -check flag");
        process.exit(2);
      }
      if (isSupportedStd(use_stdin)) {
        var crypto = require("crypto");
        var os = require("os");
        var filename = path.join(
          os.tmpdir(),
          "rescript_" +
            crypto.randomBytes(8).toString("hex") +
            path.parse(use_stdin).base
        );
        (async function () {
          var content = await readStdin();
          var fd = fs.openSync(filename, "wx", 0o600); // Avoid overwriting existing file
          fs.writeFileSync(fd, content, "utf8");
          fs.closeSync(fd);
          process.addListener("exit", () => fs.unlinkSync(filename));
          child_process.execFile(
            bsc_exe,
            ["-format", filename],
            (error, stdout, stderr) => {
              if (error === null) {
                process.stdout.write(stdout);
              } else {
                console.error(stderr);
                process.exit(2);
              }
            }
          );
        })();
      } else {
        console.error(`Unsupported extension ${use_stdin}`);
        console.error(`Supported extensions: ${formattedStdExtensions} `);
        process.exit(2);
      }
    } else {
      if (files.length === 0) {
        // none of argumets set
        // format the current directory
        files = fs.readdirSync(process.cwd()).filter(isSupportedFile);
      }

      for (let i = 0; i < files.length; ++i) {
        let file = files[i];
        if (!isSupportedStd(file)) {
          console.error(`Don't know what do with ${file}`);
          console.error(`Supported extensions: ${formattedFileExtensions}`);
          process.exit(2);
        }
      }
      await formatFiles(files, bsc_exe, isSupportedFile, check.val);
    }
  } catch (e) {
    if (e instanceof arg.ArgError) {
      console.error(e.message);
      process.exit(2);
    } else {
      throw e;
    }
  }
}
exports.main = main;