///|
/// A normalized locale identifier consisting of a language and optional script
/// and region subtags.
pub struct Locale {
  language : String
  script : String?
  region : String?
}

///|
/// Parses a locale identifier with a language and optional script and region.
/// Both `-` and `_` separators are accepted. Canonical locale casing uses a
/// lowercase language, titlecase script, and uppercase region.
pub fn Locale::parse(raw : String) -> Locale? {
  let raw = raw.trim()
  if raw == "" {
    return None
  }
  let parts : Array[String] = raw
    .to_owned()
    .replace_all(old="_", new="-")
    .split("-")
    .map(part => part.to_owned())
    .collect()
  guard parts.length() >= 1 else { return None }
  guard is_language(parts[0]) else { return None }
  let language = parts[0].to_lower()
  let mut script : String? = None
  let mut region : String? = None
  for index, part in parts {
    if index == 0 {
      continue
    }
    if part == "" {
      return None
    }
    if script is None && is_script(part) {
      script = Some(canonical_script(part))
      continue
    }
    if region is None && is_region(part) {
      region = Some(canonical_region(part))
      continue
    }
    return None
  }
  Some({ language, script, region, })
}

///|
/// Returns the canonical language subtag.
pub fn Locale::language(self : Locale) -> String {
  self.language
}

///|
/// Returns the canonical script subtag when supplied.
pub fn Locale::script(self : Locale) -> String? {
  self.script
}

///|
/// Returns the canonical region subtag when supplied.
pub fn Locale::region(self : Locale) -> String? {
  self.region
}

///|
/// Returns the canonical locale string, such as `zh-Hans-CN`.
pub fn Locale::canonical(self : Locale) -> String {
  let result = self.language
  let result = match self.script {
    Some(script) => result + "-" + script
    None => result
  }
  match self.region {
    Some(region) => result + "-" + region
    None => result
  }
}

///|
/// Returns deterministic lookup candidates from most to least specific.
/// A locale with both script and region falls back through `language-script`
/// and then `language`.
pub fn Locale::fallback_candidates(self : Locale) -> Array[String] {
  let candidates : Array[String] = []
  candidates.push(self.canonical())
  match (self.script, self.region) {
    (Some(script), Some(_)) => candidates.push(self.language + "-" + script)
    _ => ()
  }
  if candidates.last() != Some(self.language) {
    candidates.push(self.language)
  }
  candidates
}

///|
fn is_language(value : String) -> Bool {
  let length = value.char_length()
  if length < 2 || length > 8 {
    return false
  }
  for char in value {
    if !char.is_ascii_alphabetic() {
      return false
    }
  }
  true
}

///|
fn is_script(value : String) -> Bool {
  if value.char_length() != 4 {
    return false
  }
  for char in value {
    if !char.is_ascii_alphabetic() {
      return false
    }
  }
  true
}

///|
fn is_region(value : String) -> Bool {
  if value.char_length() == 2 {
    for char in value {
      if !char.is_ascii_alphabetic() {
        return false
      }
    }
    return true
  }
  if value.char_length() == 3 {
    for char in value {
      if !char.is_ascii_digit() {
        return false
      }
    }
    return true
  }
  false
}

///|
fn canonical_script(value : String) -> String {
  let chars = value.to_lower().to_array()
  let result : Array[Char] = []
  for index, char in chars {
    if index == 0 {
      result.push(char.to_ascii_uppercase())
    } else {
      result.push(char)
    }
  }
  String::from_array(result)
}

///|
fn canonical_region(value : String) -> String {
  value.to_upper()
}