///|
/// Search-path variables: values that are ordered lists of directories.
///
/// A search path is stored in the manifest with `:` between directories,
/// whatever shell it came from, and marked with the `list` flag. Rendering
/// then uses the family's own separator — PowerShell joins with `;` — and
/// `shrc ls PATH` can show one directory per row in search order.

///|
/// The separator used in the stored, canonical form.
pub let path_sep : String = ":"

///|
/// The search-path variables that are not simply named `*PATH`. Names ending
/// in `PATH` are recognised without being listed.
pub fn search_path_vars() -> Array[String] {
  [
    "CDPATH", "MANPATH", "INFOPATH", "FPATH", "LOCPATH", "NLSPATH", "PERL5LIB", "PKG_CONFIG_PATH",
    "LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH", "DYLD_FALLBACK_LIBRARY_PATH", "LIBRARY_PATH",
    "CPATH", "C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH", "CLASSPATH", "RUBYLIB", "GEM_PATH",
    "XDG_DATA_DIRS", "TERMINFO_DIRS",
  ]
}

///|
/// Whether `name` holds a search path. Any name ending in `PATH` counts, so
/// `PATH`, `PYTHONPATH`, `LD_LIBRARY_PATH` and a project's own `MYPATH` all
/// behave the same.
pub fn is_search_path_name(name : String) -> Bool {
  if name.view().has_suffix("PATH") {
    return true
  }
  for known in search_path_vars() {
    if known == name {
      return true
    }
  }
  false
}

///|
/// Whether an entry should be treated as a search path: an environment
/// variable whose stored flag or name says so.
pub fn is_search_path_entry(entry : Entry) -> Bool {
  kind_eq(entry.kind, Env) && (entry.list || is_search_path_name(entry.name))
}

///|
/// The separator `family` uses between search-path directories.
pub fn family_path_sep(family : ShellFamily) -> String {
  match family {
    PowerShell => ";"
    _ => ":"
  }
}

///|
/// Split a stored search-path value into its directories, in order. An empty
/// value has no directories.
pub fn split_path(value : String) -> Array[String] {
  if value.length() == 0 {
    return []
  }
  value.split(path_sep).map(part => part.to_owned()).to_array()
}

///|
/// Join directories back into the stored form.
pub fn join_path(parts : Array[String]) -> String {
  parts.join(path_sep)
}

///|
/// A directory as it is compared: a trailing `/` is insignificant, `/` itself
/// is kept.
pub fn normalize_dir(dir : String) -> String {
  let mut text = dir
  while text.length() > 1 && text.view().has_suffix("/") {
    text = text.view(end_offset=text.length() - 1).to_owned()
  }
  text
}

///|
/// Whether two directories name the same place.
pub fn same_dir(a : String, b : String) -> Bool {
  normalize_dir(a) == normalize_dir(b)
}

///|
/// Add `dir` to a search path, first removing an identical directory so
/// repeating the command never duplicates it. `at_end` adds it after the
/// existing directories instead of before them.
pub fn path_insert(value : String, dir : String, at_end : Bool) -> String {
  let kept = []
  for part in split_path(value) {
    if !same_dir(part, dir) {
      kept.push(part)
    }
  }
  if at_end {
    kept.push(dir)
  } else {
    kept.insert(0, dir)
  }
  join_path(kept)
}

///|
/// Remove every occurrence of `dir` from a search path. Returns the new value
/// and whether a directory was removed.
pub fn path_remove(value : String, dir : String) -> (String, Bool) {
  let kept = []
  let mut removed = false
  for part in split_path(value) {
    if same_dir(part, dir) {
      removed = true
    } else {
      kept.push(part)
    }
  }
  (join_path(kept), removed)
}

///|
/// The canonical self-reference of a variable: `$NAME` — so a search path
/// shrc creates keeps whatever the shell already had.
pub fn self_ref(name : String) -> String {
  "$" + name
}

///|
/// A value adopted from PowerShell, where search paths are joined by `;`:
/// stored canonically with `:` so every family can render it.
pub fn canonical_list(name : String, value : String) -> String {
  if is_search_path_name(name) && value.contains(";") {
    value.replace_all(old=";", new=":")
  } else {
    value
  }
}