///|
/// Convert a relative path to absolute using `base` as the anchor. Absolute
/// paths are returned unchanged.
fn make_absolute(path : String, base : String) -> String {
  if path == "" {
    base
  } else if path_is_absolute(path) {
    path
  } else {
    join_path(base, path)
  }
}

///|
/// Return the local upstream `window` package root used by embedded-runtime templates
/// and native glue.
fn window_package_root(workspace_root : String) -> String {
  join_path(workspace_root, "window/modules/window")
}

///|
/// Return the local upstream `window` package root for `platform`.
fn window_platform_root(workspace_root : String, platform : String) -> String {
  join_path(window_package_root(workspace_root), platform)
}

///|
/// Return the local upstream `window` build template for `platform`.
fn window_template_root(workspace_root : String, platform : String) -> String {
  join_path(window_platform_root(workspace_root, platform), "template")
}

///|
/// Return the first existing path among `paths`. Empty strings are skipped.
fn first_existing(paths : Array[String]) -> String? {
  for path in paths {
    if path != "" && path_is_file(path) {
      return Some(path)
    }
  }
  None
}

///|
/// Return the first existing non-empty executable file among `paths`.
fn first_executable(paths : Array[String]) -> String? {
  for path in paths {
    if path != "" && path_is_nonempty_file(path) && path_is_executable(path) {
      return Some(path)
    }
  }
  None
}

///|
/// Split the PATH environment variable using the platform-specific separator
/// (`;` on Windows, `:` elsewhere). Returns an empty array when PATH is unset.
fn path_entries(host : HostPlatform) -> Array[String] {
  match @env.get_env_var("PATH") {
    Some(value) => {
      let separator = if host == Windows { ";" } else { ":" }
      value.split(separator).map(part => part.to_owned()).to_array()
    }
    None => []
  }
}

///|
/// Return the candidate executable file names for `name` on `host`. On
/// Windows, also tries `.exe`, `.cmd`, and `.bat` suffixes.
fn executable_names(name : String, host : HostPlatform) -> Array[String] {
  if host == Windows {
    if name.to_lower().has_suffix(".exe") {
      [name]
    } else {
      [name + ".exe", name + ".cmd", name + ".bat", name]
    }
  } else {
    [name]
  }
}

///|
/// Search for an executable named `name` in every PATH directory. Returns the
/// full path to the first match, or `None` if not found.
fn find_in_path(name : String, host : HostPlatform) -> String? {
  for directory in path_entries(host) {
    let root = if directory == "" { "." } else { directory }
    for candidate in executable_names(name, host) {
      let path = join_path(root, candidate)
      if path_is_nonempty_file(path) && path_is_executable(path) {
        return Some(path)
      }
    }
  }
  None
}

///|
/// Look up `name` in `home/bin` first, then fall back to PATH lookup. `home`
/// may be `None`, in which case this is equivalent to `find_in_path`.
fn find_executable_in_home(
  name : String,
  home : String?,
  host : HostPlatform,
) -> String? {
  match home {
    Some(root) => {
      let candidates : Array[String] = []
      for executable in executable_names(name, host) {
        candidates.push(join_path(root, "bin/" + executable))
      }
      match first_executable(candidates) {
        Some(path) => return Some(path)
        None => ()
      }
    }
    None => ()
  }
  find_in_path(name, host)
}