///|
/// A rule of an absolute path.
pub enum AbsolutePathRule {
  /// The path is empty.
  Empty
  /// The path has no absolute root.
  NotAbsolute
  /// A UNC root needs both a server and a share.
  MissingUncServerOrShare
} derive(Eq, Debug)

///|
pub extend AbsolutePathRule with Eq::{equal, not_equal}

///|
pub extend AbsolutePathRule with @debug.Debug::{to_repr}

///|
/// A path that cannot identify an absolute location.
pub suberror AbsolutePathError {
  /// The input text breaks the named rule.
  InvalidAbsolutePath(text~ : String, rule~ : AbsolutePathRule)
} derive(Debug)

///|
pub extend AbsolutePathError with @debug.Debug::{to_repr}

///|
/// Describe the input and the broken rule for people.
pub fn AbsolutePathError::message(self : AbsolutePathError) -> String {
  let InvalidAbsolutePath(text~, rule~) = self
  let reason = match rule {
    Empty => "the path is empty"
    NotAbsolute => "the path must be absolute"
    MissingUncServerOrShare => "a UNC root needs a server and a share"
  }
  "Invalid absolute path \{text.escape()}: \{reason}"
}

///|
/// An absolute location, with `/` separators for Windows forms. A Windows drive
/// root and a UNC root are accepted too. This type suits both working
/// directories and content roots without claiming that the location exists.
pub struct AbsolutePath {
  priv text : String
} derive(Eq, Debug)

///|
pub extend AbsolutePath with Eq::{equal, not_equal}

///|
pub extend AbsolutePath with @debug.Debug::{to_repr}

///|
/// Parse at a path boundary, converting Windows separators once.
pub fn AbsolutePath::parse(text : String) -> AbsolutePath raise AbsolutePathError {
  guard !text.is_empty() else { raise InvalidAbsolutePath(text~, rule=Empty) }
  let chars = Array::from_iter(text.iter())
  let drive = chars.length() >= 3 && is_ascii_letter(chars[0]) && chars[1] == ':' && (chars[2] == '/' || chars[2] == '\\')
  let normalized = if drive || text.has_prefix("\\\\") {
    text.replace_all(old="\\", new="/")
  } else { text }
  guard text.has_prefix("/") || text.has_prefix("\\\\") || drive else {
    raise InvalidAbsolutePath(text~, rule=NotAbsolute)
  }
  if normalized.has_prefix("//") {
    let parts = normalized.split("/").to_array()
    guard parts.length() >= 4 && !parts[2].is_empty() && !parts[3].is_empty() else {
      raise InvalidAbsolutePath(text~, rule=MissingUncServerOrShare)
    }
  }
  { text: normalized }
}

///|
/// The root used by hosts without a process working directory.
pub fn AbsolutePath::root() -> AbsolutePath {
  { text: "/" }
}

///|
/// The path with `/` separators for Windows forms; the text is kept as written.
/// `parse` reads it back to an equal path.
pub impl Show for AbsolutePath with output(self, logger) {
  logger.write_string(self.text)
}

///|
pub extend AbsolutePath with Show::{output, to_string}