///|
/// A word and an initialism remain distinct in the semantic name.
#derive(Eq, Compare, Hash, Debug)
pub(all) enum NameSegment {
  Word(String)
  Initialism(String)
}

///|
/// Immutable storage keeps names safe as map keys.
#derive(Eq, Compare, Hash, Debug)
pub struct Name {
  segments : @list.List[NameSegment]
}

///|
#derive(Eq, Debug)
pub(all) suberror NameError {
  InvalidName(String)
}

///|
/// Decode legacy words using the upstream maximal-single-letter-run rule.
pub fn Name::from_words(words : Array[String]) -> Name {
  let segments : Array[NameSegment] = []
  let mut i = 0
  while i < words.length() {
    let start = i
    while i < words.length() &&
          words[i].length() == 1 &&
          words[i] >= "a" &&
          words[i] <= "z" {
      i += 1
    }
    if i - start > 1 {
      segments.push(
        Initialism(words[start:i].iter().collect().join("").to_upper()),
      )
    } else if i > start {
      segments.push(Word(words[start]))
    } else {
      segments.push(Word(words[i]))
      i += 1
    }
  }
  { segments: @list.List::from_iter(segments.iter()), }
}

///|
/// Parse the v4 canonical name grammar without guessing from camel case.
pub fn Name::parse(text : String) -> Name raise NameError {
  let segments : Array[NameSegment] = []
  for part in text.split("-") {
    if part.is_empty() {
      raise InvalidName(text)
    }
    let mut lower = true
    let mut upper = true
    let mut has_upper = false
    for character in part {
      let digit = character >= '0' && character <= '9'
      lower = lower && (digit || (character >= 'a' && character <= 'z'))
      upper = upper && (digit || (character >= 'A' && character <= 'Z'))
      has_upper = has_upper || (character >= 'A' && character <= 'Z')
    }
    if !lower && !upper {
      raise InvalidName(text)
    }
    segments.push(
      if has_upper {
        Initialism(part.to_owned())
      } else {
        Word(part.to_owned())
      },
    )
  }
  { segments: @list.List::from_iter(segments.iter()), }
}

///|
/// A fresh array; changing it cannot change the name.
pub fn Name::to_segments(self : Name) -> Array[NameSegment] {
  self.segments.iter().collect()
}

///|
/// Legacy spelling. Some canonical v4 names have no lossless legacy spelling;
/// version migration must check the round trip instead of silently collapsing them.
pub fn Name::to_words(self : Name) -> Array[String] {
  let words : Array[String] = []
  for segment in self.segments {
    match segment {
      Word(word) => words.push(word)
      Initialism(initialism) =>
        for character in initialism.to_lower() {
          words.push(character.to_string())
        }
    }
  }
  words
}

///|
pub fn Name::to_canonical(self : Name) -> String {
  self.segments
  .iter()
  .map(segment => {
    match segment {
      Word(text) | Initialism(text) => text
    }
  })
  .collect()
  .join("-")
}

///|
#derive(Eq, Compare, Hash, Debug)
pub struct Path {
  segments : @list.List[Name]
}

///|
pub fn Path::from_names(segments : Array[Name]) -> Path {
  { segments: @list.List::from_iter(segments.iter()), }
}

///|
pub fn Path::parse(text : String) -> Path raise NameError {
  let segments : Array[Name] = []
  for segment in text.split("/") {
    segments.push(Name::parse(segment.to_owned()))
  }
  Path::from_names(segments)
}

///|
pub fn Path::to_names(self : Path) -> Array[Name] {
  self.segments.iter().collect()
}

///|
pub fn Path::to_canonical(self : Path) -> String {
  self.segments.iter().map(Name::to_canonical).collect().join("/")
}

///|
pub type PackageName = Path

///|
pub type ModuleName = Path

///|
#derive(Eq, Compare, Hash, Debug)
pub(all) struct FQName {
  package_name : PackageName
  module_name : ModuleName
  local_name : Name
}

///|
pub fn FQName::parse(text : String) -> FQName raise NameError {
  let package_and_local = text.split(":").collect()
  if package_and_local.length() != 2 {
    raise InvalidName(text)
  }
  let module_and_name = package_and_local[1].split("#").collect()
  if module_and_name.length() != 2 {
    raise InvalidName(text)
  }
  {
    package_name: Path::parse(package_and_local[0].to_owned()),
    module_name: Path::parse(module_and_name[0].to_owned()),
    local_name: Name::parse(module_and_name[1].to_owned()),
  }
}

///|
pub fn FQName::to_canonical(self : FQName) -> String {
  self.package_name.to_canonical() +
  ":" +
  self.module_name.to_canonical() +
  "#" +
  self.local_name.to_canonical()
}