// `main` and the run loop around `mtsc_compile_entry`.
//
// Split out of `main.mbt` because that file is the compile pipeline and
// this is the command line: the dispatch between the tsc-compatible
// compiler and the four verbs, resolving entries from `-p` or the
// argument list, running one entry per input, and the `--watch` loop
// over all of them.

///|
/// Name every file in the program, once each.
///
/// The loader keys `files` by BOTH the resolved path and the
/// extension-less form a specifier spelled (`./sub` -> `/dir/sub`), so
/// an unfiltered dump names most modules twice. `mtsc_is_module_path`
/// is the same filter the graph type-check applies for the same reason.
fn mtsc_list_program_files(files : Map[String, String]) -> Unit {
  let names : Array[String] = []
  for path in files.keys() {
    if mtsc_is_module_path(path) || path.has_suffix(".d.ts") {
      names.push(path)
    }
  }
  names.sort()
  for name in names {
    println(name)
  }
}

///|
/// Replace a source extension with `.js`.
fn mtsc_js_output_name(path : String) -> String {
  for ext in [".tsx", ".ts", ".mts", ".cts", ".jsx", ".mjs", ".cjs"] {
    if path.has_suffix(ext) {
      return path[0:path.length() - ext.length()].to_owned() + ".js"
    }
  }
  path + ".js"
}

///|
/// Where one entry's JavaScript goes.
///
/// `None` means stdout, which is mtsc's existing default and the one
/// `tsc` does not have. `--outDir` mirrors the input's basename under
/// the directory; the full `rootDir`-relative tree `tsc` reproduces
/// needs a `rootDir` model, and flattening is the honest subset — which
/// is why `--rootDir` is in the accepted-no-effect tier rather than
/// looking supported.
fn mtsc_entry_output_path(opts : MtscOptions, input_path : String) -> String? {
  match opts.out_dir {
    Some(dir) =>
      Some(
        mtsc_normalize(
          mtsc_join_path(dir, mtsc_js_output_name(mtsc_basename(input_path))),
        ),
      )
    None => opts.out
  }
}

///|
/// Compile every entry. Returns the number that failed.
///
/// All of them are attempted even after one fails, because `tsc` reports
/// every file's diagnostics rather than stopping at the first, and a
/// caller fixing a build wants the whole list.
async fn mtsc_compile_all(opts : MtscOptions, entries : Array[String]) -> Int {
  let mut failed = 0
  for entry in entries {
    // `--out` names ONE file, so it cannot serve several entries. Using
    // it for each in turn would leave only the last, silently.
    let out_override = if entries.length() > 1 && opts.out_dir is None {
      match opts.out {
        Some(_) => {
          println(
            "mtsc: --out names one file but \{entries.length()} inputs were given; use --outDir",
          )
          return entries.length()
        }
        None => None
      }
    } else {
      mtsc_entry_output_path(opts, entry)
    }
    if !mtsc_compile_entry(opts, entry, out_override) {
      failed = failed + 1
    }
  }
  failed
}

///|
/// Resolve the entries for this run: the command line's inputs, or the
/// project a `-p` (or a bare invocation) points at.
///
/// Returns `None` when there is nothing to compile and the reason has
/// already been reported.
async fn mtsc_resolve_entries(opts : MtscOptions) -> (Array[String], String?)? {
  // Explicit inputs win over a project, matching `tsc`: naming files on
  // the command line ignores `tsconfig.json` entirely.
  if opts.inputs.length() > 0 {
    if opts.project is Some(_) {
      println(
        "mtsc: -p is ignored when input files are named (tsc does the same)",
      )
    }
    return Some((opts.inputs, None))
  }
  let config_path = match mtsc_resolve_project_config(opts.project) {
    Some(p) => p
    None =>
      match opts.project {
        Some(p) => {
          println("mtsc: cannot find a tsconfig.json at \{p}")
          return None
        }
        None => {
          println(
            "mtsc: no input files, and no tsconfig.json in this directory",
          )
          return None
        }
      }
  }
  let project = match @parser.load_tsconfig_project(config_path) {
    Some(p) => p
    None => {
      println("mtsc: cannot read \{config_path}")
      return None
    }
  }
  mtsc_apply_project_compiler_options(opts, config_path)
  let entries = mtsc_project_entries(project, opts.out_dir)
  if entries.length() == 0 {
    println(
      "mtsc: \{config_path} matched no input files (check its `files` / `include`)",
    )
    return None
  }
  Some((entries, Some(config_path)))
}

///|
/// The four verbs, so the dispatch and the "is this a verb" test cannot
/// disagree about the set.
fn mtsc_is_verb(token : String) -> Bool {
  match token {
    "check" | "conformance" | "bridge" | "pkg" => true
    _ => false
  }
}

///|
async fn main {
  let argv = mtsc_argv()
  if argv.length() < 2 {
    // A bare `mtsc` in a directory with a tsconfig.json is a compile,
    // the way a bare `tsc` is. With no config it is a help request,
    // which is what mtsc has always done with no arguments.
    if mtsc_resolve_project_config(None) is Some(_) {
      mtsc_run_compile(mtsc_options_new())
      return
    }
    mtsc_print_usage()
    return
  }
  // A verb wins over a same-named file. See the note at the top of
  // `main.mbt`: `mtsc ./check` still names the file.
  if mtsc_is_verb(argv[1]) {
    match argv[1] {
      "check" => mtsc_run_check(argv, 2)
      "conformance" => mtsc_run_conformance(argv, 2)
      "bridge" => @ts.run_bridge_cli(argv, 2)
      "pkg" => @ts.run_pkg_cli(argv, 2)
      _ => ()
    }
    return
  }
  let opts = match mtsc_parse_args(argv, 1) {
    Ok(o) => o
    Err(e) => {
      println("mtsc: \{e}")
      // `tsc` reports a bad command line as a diagnostic and exits
      // non-zero. mtsc used to print this and exit 0, so a typo in a
      // build script was invisible.
      mtsc_exit(MTSC_EXIT_DIAGNOSTICS)
      return
    }
  }
  if opts.help {
    mtsc_print_usage()
    return
  }
  if opts.version {
    println("Version \{@ts.CLI_VERSION} (mtsc, mizchi/ts)")
    return
  }
  if opts.init {
    let path = "tsconfig.json"
    if mtsc_file_exists(path) {
      println("mtsc: \{path} already exists")
      mtsc_exit(MTSC_EXIT_DIAGNOSTICS)
      return
    }
    if write_text(path, mtsc_init_tsconfig_text()) {
      println("Created a new tsconfig.json with the recommended settings")
    } else {
      mtsc_exit(MTSC_EXIT_DIAGNOSTICS)
    }
    return
  }
  mtsc_report_ignored(opts)
  mtsc_run_compile(opts)
}

///|
/// Resolve the entries and compile them, once or on every change.
async fn mtsc_run_compile(opts : MtscOptions) -> Unit {
  let (entries, config_path) = match mtsc_resolve_entries(opts) {
    Some(v) => v
    None => {
      mtsc_exit(MTSC_EXIT_DIAGNOSTICS)
      return
    }
  }
  if opts.show_config {
    mtsc_print_show_config(opts, config_path, entries)
    return
  }
  if opts.watch {
    mtsc_watch_loop(opts, entries)
    return
  }
  let failed = mtsc_compile_all(opts, entries)
  if failed > 0 {
    // Diagnostics were reported and nothing was written for the entries
    // that failed — `tsc`'s exit code 1. Its code 2 is the other case,
    // diagnostics WITH output, which this pipeline cannot reach: a
    // failing check stops the emit, and `--noCheck` produces no
    // diagnostics to report. Falling through is exit 0.
    mtsc_exit(MTSC_EXIT_DIAGNOSTICS)
  }
}