// Platform detection, path helpers and shell selection.
//
// The host platform is resolved at *runtime* through the platform the async
// runtime reports, and every downstream decision is driven by a plain platform
// string. That keeps the Unix code path compiled - and unit-testable - on any
// host.
//
// Runtime detection is used rather than a compile-time `#cfg(platform=...)`
// because the compiler only documents "windows" as a platform value; the
// runtime enum knows Linux, macOS and Windows and also works under the Wasm
// backend, where the same binary can run on any operating system.

///|
/// Name of the platform the program is running on.
///
/// One of "windows", "linux", "macos" or "unknown".
pub fn host_platform() -> String {
  match @async_rt.platform {
    Linux => "linux"
    MacOS => "macos"
    Windows => "windows"
  }
}

///|
/// How a script must be executed on `platform`:
/// (interpreter, extra arguments, script file extension).
///
/// Windows batch scripts need `cmd /c`; POSIX shells need plain `sh`.
pub fn shell_for(platform : String) -> (String, Array[String], String) {
  if platform == "windows" {
    ("cmd", ["/c"], ".cmd")
  } else {
    ("sh", [], ".sh")
  }
}

///|
/// Set UNIX permission bits on `path`.
///
/// Windows has no UNIX permission bits and the async runtime's `chmod` is not
/// implemented there, so the call is skipped on Windows.
pub async fn set_permissions(
  platform : String,
  path : String,
  mode : Int32,
) -> Unit {
  if platform != "windows" {
    // The async runtime does not implement chmod on Windows at all, and some
    // filesystems reject it outright; permissions are best-effort.
    @fs.chmod(path, mode) catch {
      _ => ()
    }
  }
}

///|
/// Normalize path separators to '/' so host FS calls work uniformly on
/// Windows (which returns '\' from realpath) and inside the package format.
pub fn norm_sep(p : String) -> String {
  p.replace_all(old="\\", new="/")
}

///|
/// Map a machine-specific architecture name onto a portable one.
fn normalize_arch(raw : String) -> String {
  if raw == "AMD64" || raw == "amd64" || raw == "x86_64" || raw == "x64" {
    "x86_64"
  } else if raw == "ARM64" || raw == "arm64" || raw == "aarch64" {
    "aarch64"
  } else if raw == "x86" || raw == "i386" || raw == "i686" {
    "x86"
  } else {
    raw
  }
}

///|
/// Best-effort detection of the host CPU architecture.
/// Windows exposes PROCESSOR_ARCHITECTURE; some POSIX shells export HOSTTYPE.
pub fn default_arch() -> String {
  match @env.get_env_var("PROCESSOR_ARCHITECTURE") {
    Some(v) => normalize_arch(v)
    None =>
      match @env.get_env_var("PROCESSOR_ARCHITEW6432") {
        Some(v) => normalize_arch(v)
        None =>
          match @env.get_env_var("HOSTTYPE") {
            Some(v) => normalize_arch(v)
            None => "unknown"
          }
      }
  }
}

///|
/// The directory part of a path, or "" when the path has no directory.
pub fn dirname(p : String) -> String {
  let n = p.length()
  let mut i = n - 1
  while i >= 0 {
    let c = p[i]
    if c == '/' || c == '\\' {
      if i == 0 {
        return "/"
      }
      return p[:i].to_owned()
    }
    i = i - 1
  }
  ""
}

///|
/// Create the parent directory of `path` when it does not exist yet.
pub async fn ensure_parent_dir(path : String) -> Unit {
  let parent = dirname(path)
  // The parent of a path at the root of a drive is the drive itself, and
  // that exists already; creating it would fail.
  if parent != "" && !@fs.exists(parent) {
    @fs.mkdir(parent, recursive=true, allow_exist=true)
  }
}

///|
/// The base name of a path, without any directory part.
pub fn basename(p : String) -> String {
  let n = p.length()
  let mut i = n - 1
  while i >= 0 {
    let c = p[i]
    if c == '/' || c == '\\' {
      return p[i + 1:].to_owned()
    }
    i = i - 1
  }
  p
}

///|
/// Path of `full` relative to `base`.
pub fn relpath(base : String, full : String) -> String {
  let n = base.length()
  if full.length() > n && (full[n] == '/' || full[n] == '\\') {
    full[n + 1:].to_owned()
  } else if full.length() == n {
    basename(full)
  } else {
    full[n:].to_owned()
  }
}