///|
fn join(left : String, right : String) -> String {
  left + "/" + right
}

///|
/// moon runs `MOON_CC` and `MOON_AR` from other directories, so the paths must be absolute.
fn absolute(path : String) -> String {
  if path.has_prefix("/") {
    return path
  }
  match @env.current_dir() {
    Some(cwd) => join(cwd, path)
    None => path
  }
}

///|
async fn fail(message : String) -> Unit {
  @stdio.stderr.write("❌ zig-cross: \{message}\n")
  @sys.exit(1)
}

///|
async fn write_script(path : String, text : String) -> Unit {
  @fs.write_file(path, text, permission=0o755)
  // write_file applies the permission only when it creates the file.
  @fs.chmod(path, 0o755)
}

///|
/// Write the `cc` and `ar` wrappers of one target to `/zig/`, and return the
/// environment that makes moon use them.
async fn prepare(
  target : Target,
  target_dir : String,
  detected : Environment,
) -> Map[String, String] {
  let dir = absolute(join(join(target_dir, "zig"), target.name()))
  @fs.mkdir(dir, recursive=true, allow_exist=true)
  let cc = join(dir, "cc")
  let ar = join(dir, "ar")
  let flags = header_flags(target, detected, dir)
  if flags.contains("-idirafter") {
    @fs.mkdir(join(dir, "include/sys"), recursive=true, allow_exist=true)
    @fs.write_file(join(dir, "include/sys/vnode.h"), vnode_shim)
  }
  write_script(cc, cc_wrapper(target, header_fix=flags))
  write_script(ar, ar_wrapper(target))
  { "MOON_CC": cc, "MOON_AR": ar }
}

///|
/// One line of the summary: an executable that a build produced, or why there is none.
priv struct Row {
  target : Target
  path : String
  size : Int64
  problem : String?
}

///|
async fn check_executable(target : Target, path : String) -> Row {
  let size = @fs.file_size(path)
  let file = @fs.open(path, mode=ReadOnly)
  defer file.close()
  let header = file.read_some(max_len=4096).unwrap_or(b"")
  { target, path, size, problem: check_format(target, header), }
}

///|
/// Build one target. The rows are the executables found, or one row with the problem.
async fn build_target(
  target : Target,
  target_dir : String,
  paths : Array[String],
  view : String?,
  detected : Environment,
) -> Array[Row] {
  let env = prepare(target, target_dir, detected)
  if view is Some(path) {
    env["MOON_TOOLCHAIN_ROOT"] = path
  }
  let out = join(target_dir, target.name())
  let args = ["build", "--target", "native", "--release", "--target-dir", out]
  args.append(paths)
  println("🔨 Building for \{target.name()}...")
  let code = @shell.Cmd("moon", args, env~, stdout=Inherit, stderr=Inherit).status() catch {
    error => {
      @stdio.stderr.write("zig-cross: moon launch failed: \{error}\n")
      1
    }
  }
  if code != 0 {
    return [
      {
        target,
        path: out,
        size: 0L,
        problem: Some(
          "\{target.name()}: moon build failed with exit code \{code}",
        ),
      },
    ]
  }
  let found = @shell.glob(join(out, "native/release/build/**/*.exe"))
  if found.is_empty() {
    return [
      {
        target,
        path: out,
        size: 0L,
        problem: Some(
          "\{target.name()}: no executable under \{out}/native/release/build",
        ),
      },
    ]
  }
  found.sort()
  let rows = []
  for path in found {
    rows.push(check_executable(target, path))
  }
  rows
}

///|
fn max_int(a : Int32, b : Int32) -> Int32 {
  if a > b {
    a
  } else {
    b
  }
}

///|
fn pad(text : String, width : Int32) -> String {
  let mut result = text
  while result.length() < width {
    result += " "
  }
  result
}

///|
/// Print the table (target, path, size, result) and the problems. Return the number of failures.
fn print_summary(rows : Array[Row]) -> Int32 {
  let mut target_width = 6
  let mut path_width = 4
  for row in rows {
    target_width = max_int(target_width, row.target.name().length())
    path_width = max_int(path_width, row.path.length())
  }
  println(
    "\{pad("target", target_width)}  \{pad("path", path_width)}  size (bytes)  result",
  )
  let mut failures = 0
  for row in rows {
    let mark = if row.problem is None { "✅" } else { "❌" }
    println(
      "\{pad(row.target.name(), target_width)}  \{pad(row.path, path_width)}  \{pad(row.size.to_string(), 12)}  \{mark}",
    )
    if row.problem is Some(message) {
      failures += 1
      println("  \{message}")
    }
  }
  failures
}

///|
async fn run_build(
  target_dir : String,
  targets : Array[Target],
  paths : Array[String],
) -> Int32 {
  let detected = detect_environment()
  emit_notices(report(detected, targets))
  if !execution_allowed(detected) {
    return 1
  }
  let view = runtime_view(detected)
  defer (if view is Some(path) { @fs.rmdir(path, recursive=true) })
  let rows = []
  for target in targets {
    rows.append(build_target(target, target_dir, paths, view, detected))
  }
  if print_summary(rows) > 0 {
    let hinted : Array[Target] = []
    for row in rows {
      if row.problem is Some(_) && !hinted.contains(row.target) {
        hinted.push(row.target)
        @stdio.stderr.write(
          "zig-cross: run zig-cross doctor \{row.target.name()} for target-specific guidance.\n",
        )
      }
    }
    1
  } else {
    0
  }
}

///|
/// Run a command with `MOON_CC` and `MOON_AR` set to the wrappers of `target`.
async fn run_exec(target : Target, argv : Array[String]) -> Int32 {
  let detected = detect_environment()
  emit_notices(report(detected, [target]))
  if !execution_allowed(detected) {
    return 1
  }
  let view = runtime_view(detected)
  defer (if view is Some(path) { @fs.rmdir(path, recursive=true) })
  let env = prepare(target, default_target_dir, detected)
  if view is Some(path) {
    env["MOON_TOOLCHAIN_ROOT"] = path
  }
  let code = @shell.Cmd(
    argv[0],
    argv[1:].to_owned(),
    env~,
    stdin=Inherit,
    stdout=Inherit,
    stderr=Inherit,
  ).status()
  if code > 0 {
    code
  } else if code == 0 {
    0
  } else {
    1
  }
}

///|
async fn main {
  if @env.get_env_var("OS") == Some("Windows_NT") {
    fail("zig-cross runs on Linux or macOS only")
    return
  }
  let args = @env.args()[1:].to_owned()
  match parse_args(args) {
    Err(message) => {
      if !args.is_empty() && args[0] == "doctor" && args.contains("--json") {
        let empty : Environment = {
          host_os: None,
          host_arch: None,
          zig_cross: version_text(),
          moon: None,
          compiler: None,
          zig: None,
          toolchain_root: None,
          runtime_fingerprint: None,
          runtime_mode: requested_mode(),
          darwin_sdk: None,
          toolchain_overrides: [],
        }
        println(@json.to_json(report(empty, [], usage=message)).stringify())
      } else {
        @stdio.stderr.write("❌ zig-cross: \{message}\n\n\{help_text()}\n")
      }
      @sys.exit(2)
    }
    Ok(Doctor(targets~, json~)) => {
      let result = report(detect_environment(), targets)
      if json {
        println(@json.to_json(result).stringify())
      } else {
        @stdio.stdout.write(report_text(result))
      }
      if result.status == "error" {
        @sys.exit(1)
      }
    }
    Ok(Help) => @stdio.stdout.write(help_text() + "\n")
    Ok(Version) => @stdio.stdout.write(version_text() + "\n")
    Ok(Build(target_dir~, targets~, paths~)) => {
      let code = run_build(target_dir, targets, paths)
      if code != 0 {
        @sys.exit(code)
      }
    }
    Ok(Exec(target~, argv~)) => {
      let code = run_exec(target, argv)
      if code != 0 {
        @sys.exit(code)
      }
    }
  }
}