// Execution of the optional pre_install / post_install scripts.
//
// The script body is stored verbatim inside the manifest together with the
// platform it targets. At unpack time the script is written into the
// destination directory and executed with the interpreter that matches the
// host; a script for a different platform is skipped.
//
// The script receives:
//   - the destination directory as its working directory
//   - $LUNAPACK_DEST pointing at the destination directory
//   - $LUNAPACK_PHASE set to "pre_install" or "post_install"

///|
/// Does a script targeting `script_platform` apply on `host`?
///
/// "any" always applies, "posix" covers Linux and macOS.
pub fn script_applies(script_platform : String, host : String) -> Bool {
  if script_platform == "any" {
    true
  } else if script_platform == host {
    true
  } else if script_platform == "posix" {
    host == "linux" || host == "macos"
  } else {
    false
  }
}

///|
/// Infer the target platform of a script from its file extension.
pub fn script_platform_of(path : String) -> String {
  let lower = path.to_lower()
  if lower.has_suffix(".cmd") || lower.has_suffix(".bat") {
    "windows"
  } else if lower.has_suffix(".sh") {
    "posix"
  } else if lower.has_suffix(".ps1") {
    "windows"
  } else {
    "any"
  }
}

///|
/// The name of the file the script of `kind` is written to.
///
/// The name is the same on every platform, and it is a name and not a path:
/// the script is written into the directory the interpreter runs in.
pub fn script_file_name(platform : String, kind : String) -> String {
  let (_, _, ext) = shell_for(platform)
  ".lunapack_" + kind + ext
}

///|
/// The absolute path of `dest`.
///
/// The script is told where it is going to work, and a relative path says
/// that only to the shell that happens to be in that directory. The path is
/// asked of the file system, which knows the answer even when the directory
/// was named relatively.
async fn absolute_dest(dest : String) -> String {
  let real = @fs.realpath(dest) catch { _ => return dest }
  norm_sep(real)
}

///|
/// Write `script` into `dest` as a runnable file for `platform`.
async fn write_script(
  platform : String,
  kind : String,
  script : Script,
  dest : String,
) -> String {
  let path = dest + "/" + script_file_name(platform, kind)
  @fs.write_binary(path, string_to_utf8_bytes(script.body))
  // Make the script executable on POSIX; a no-op on Windows.
  set_permissions(platform, path, 0o755)
  path
}

///|
/// Run the interpreter `interp` with `args` in `cwd` and return its exit status.
///
/// The interpreter is named in the error, because the spawn failure of the
/// runtime names no file: "The system cannot find the file specified." says
/// nothing about which interpreter was missing.
async fn run_interpreter(
  interp : String,
  args : Array[String],
  cwd : String,
) -> Int32 {
  @shell.Cmd(interp, args, cwd~).status() catch {
    err =>
      fail("cannot run the interpreter `" + interp + "`: " + err.to_string())
  }
}

///|
/// Write `script` into `dest`, run it with `interp`, and return its exit status.
///
/// The temporary file is removed on every path, including the one where the
/// interpreter could not be started: a script that was written into the
/// installation directory must not stay there.
async fn run_script(
  interp : String,
  extra : Array[String],
  platform : String,
  kind : String,
  script : Script,
  dest : String,
) -> Int32 {
  let path = write_script(platform, kind, script, dest)
  let args = []
  for a in extra {
    args.push(a)
  }
  // The interpreter is started in `dest`, where the script was written, so
  // the script is named by its name alone. A path would not do: the command
  // line of `cmd` reads a forward slash as the start of a switch, so
  // `cmd /c dir/script.cmd` does not run the script but complains that `.`
  // is not a command.
  args.push(script_file_name(platform, kind))
  @env.set_env_var("LUNAPACK_DEST", absolute_dest(dest))
  @env.set_env_var("LUNAPACK_PHASE", kind)
  // `status` inherits stdout/stderr so the user sees the script output.
  let status = run_interpreter(interp, args, dest) catch {
    err => {
      @fs.remove(path) catch {
        _ => ()
      }
      fail(err.to_string())
    }
  }
  @fs.remove(path) catch {
    _ => ()
  }
  status
}

///|
/// Run an install script of `kind` inside `dest` and return its exit status.
///
/// `platform` selects the interpreter, which makes the POSIX path testable on
/// any host.
pub async fn run_install_script(
  platform : String,
  kind : String,
  script : Script,
  dest : String,
) -> Int32 {
  let (interp, extra, _) = shell_for(platform)
  run_script(interp, extra, platform, kind, script, dest)
}