///|
fn is_ascii_letter(ch : Char) -> Bool {
  (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
}

///|
fn is_ascii_number(ch : Char) -> Bool {
  ch >= '0' && ch <= '9'
}

///|
fn is_ascii_alphanumeric(ch : Char) -> Bool {
  is_ascii_letter(ch) || is_ascii_number(ch)
}

///|
fn all_ascii_letters(value : String) -> Bool {
  if value == "" {
    return false
  }
  for ch in value {
    if !is_ascii_letter(ch) {
      return false
    }
  }
  true
}

///|
fn all_ascii_numbers(value : String) -> Bool {
  if value == "" {
    return false
  }
  for ch in value {
    if !is_ascii_number(ch) {
      return false
    }
  }
  true
}

///|
fn all_ascii_alphanumeric(value : String) -> Bool {
  if value == "" {
    return false
  }
  for ch in value {
    if !is_ascii_alphanumeric(ch) {
      return false
    }
  }
  true
}

///|
fn valid_language_subtag(value : String) -> Bool {
  value.length() >= 2 && value.length() <= 8 && all_ascii_letters(value)
}

///|
fn valid_script_subtag(value : String) -> Bool {
  value.length() == 4 && all_ascii_letters(value)
}

///|
fn valid_region_subtag(value : String) -> Bool {
  (value.length() == 2 && all_ascii_letters(value)) ||
  (value.length() == 3 && all_ascii_numbers(value))
}

///|
fn valid_variant_subtag(value : String) -> Bool {
  value.length() >= 4 && value.length() <= 8 && all_ascii_alphanumeric(value)
}

///|
fn title_case_script(value : String) -> String {
  let lower = value.to_lower()
  let output = StringBuilder::new()
  for index, ch in lower {
    if index == 0 {
      output.write_char(ch.to_ascii_uppercase())
    } else {
      output.write_char(ch)
    }
  }
  output.to_string()
}

///|
fn owned_locale_parts(value : String) -> Array[String] {
  let normalized = value.replace_all(old="_", new="-")
  let parts : Array[String] = []
  for part in normalized.split("-") {
    parts.push("\{part}")
  }
  parts
}

///|
fn join_locale_parts(parts : Array[String]) -> String {
  parts.join("-")
}

///|
fn canonical_parts(
  language : String,
  script : String?,
  region : String?,
  variants : Array[String],
) -> Array[String] {
  let parts : Array[String] = [language]
  match script {
    Some(value) => parts.push(value)
    None => ()
  }
  match region {
    Some(value) => parts.push(value)
    None => ()
  }
  for variant in variants {
    parts.push(variant)
  }
  parts
}

///|
/// Parse and canonicalize a compact BCP-47-style locale identifier.
pub fn LocaleId::parse(value : String) -> LocaleId raise MessageError {
  let parts = owned_locale_parts(value)
  if parts.length() == 0 || parts[0] == "" {
    raise InvalidLocale("Locale identifier cannot be empty.")
  }
  let language = parts[0].to_lower()
  if !valid_language_subtag(language) {
    raise InvalidLocale("Invalid language subtag '\{parts[0]}'.")
  }
  let mut index = 1
  let mut script : String? = None
  let mut region : String? = None
  let variants : Array[String] = []
  if index < parts.length() && valid_script_subtag(parts[index]) {
    script = Some(title_case_script(parts[index]))
    index += 1
  }
  if index < parts.length() && valid_region_subtag(parts[index]) {
    region = Some(parts[index].to_upper())
    index += 1
  }
  while index < parts.length() {
    let variant = parts[index]
    if !valid_variant_subtag(variant) {
      raise InvalidLocale("Invalid locale variant '\{variant}'.")
    }
    variants.push(variant.to_lower())
    index += 1
  }
  let canonical = join_locale_parts(
    canonical_parts(language, script, region, variants),
  )
  { original: value, canonical, language, script, region, variants }
}

///|
/// Canonicalize a locale identifier.
pub fn canonicalize_locale(value : String) -> String raise MessageError {
  LocaleId::parse(value).canonical
}

///|
fn canonicalize_locale_lossy(value : String) -> String {
  canonicalize_locale(value) catch {
    _ => value.replace_all(old="_", new="-")
  }
}

///|
pub fn LocaleId::original(self : LocaleId) -> String {
  self.original
}

///|
pub fn LocaleId::canonical(self : LocaleId) -> String {
  self.canonical
}

///|
pub fn LocaleId::language(self : LocaleId) -> String {
  self.language
}

///|
pub fn LocaleId::script(self : LocaleId) -> String? {
  self.script
}

///|
pub fn LocaleId::region(self : LocaleId) -> String? {
  self.region
}

///|
pub fn LocaleId::variants(self : LocaleId) -> Array[String] {
  self.variants
}

///|
pub fn LocaleId::has_script(self : LocaleId) -> Bool {
  self.script is Some(_)
}

///|
pub fn LocaleId::has_region(self : LocaleId) -> Bool {
  self.region is Some(_)
}

///|
pub fn LocaleId::is_language(self : LocaleId, language : String) -> Bool {
  self.language == language.to_lower()
}

///|
fn push_unique_locale(
  output : Array[String],
  seen : Map[String, Unit],
  value : String,
) -> Unit {
  if value != "" && !seen.contains(value) {
    seen[value] = ()
    output.push(value)
  }
}

///|
/// Return the exact locale followed by progressively broader parent locales.
pub fn LocaleId::fallback_chain(self : LocaleId) -> Array[String] {
  let output : Array[String] = []
  let seen : Map[String, Unit] = Map([])
  let variants = self.variants.copy()
  while variants.length() > 0 {
    push_unique_locale(
      output,
      seen,
      join_locale_parts(
        canonical_parts(self.language, self.script, self.region, variants),
      ),
    )
    ignore(variants.pop())
  }
  push_unique_locale(
    output,
    seen,
    join_locale_parts(
      canonical_parts(self.language, self.script, self.region, []),
    ),
  )
  match self.region {
    Some(_) =>
      push_unique_locale(
        output,
        seen,
        join_locale_parts(canonical_parts(self.language, self.script, None, [])),
      )
    None => ()
  }
  match self.script {
    Some(_) => push_unique_locale(output, seen, self.language)
    None => ()
  }
  output
}

///|
/// Build a fallback chain for a locale, tolerating non-canonical input.
pub fn locale_fallback_chain(locale : String) -> Array[String] {
  try LocaleId::parse(locale) catch {
    _ => [canonicalize_locale_lossy(locale)]
  } noraise {
    parsed => parsed.fallback_chain()
  }
}

///|
fn supported_language(language : String) -> Bool {
  language == "zh" ||
  language == "en" ||
  language == "ja" ||
  language == "ru" ||
  language == "ar"
}

///|
/// Return whether MoonL10n has an explicit plural rule for the locale.
pub fn is_supported_locale(locale : String) -> Bool {
  try LocaleId::parse(locale) catch {
    _ => false
  } noraise {
    parsed => supported_language(parsed.language)
  }
}

///|
fn locale_language_lossy(locale : String) -> String {
  try LocaleId::parse(locale) catch {
    _ => {
      let normalized = canonicalize_locale_lossy(locale).to_lower()
      let parts = normalized.split("-").to_array()
      match parts {
        [first, ..] => "\{first}"
        [] => normalized
      }
    }
  } noraise {
    parsed => parsed.language
  }
}

///|
fn cardinal_family_for_language(language : String) -> PluralRuleFamily {
  match language {
    "zh" | "ja" => OtherOnly
    "ru" => Russian
    "ar" => Arabic
    _ => English
  }
}

///|
fn ordinal_family_for_language(language : String) -> PluralRuleFamily {
  if language == "en" {
    English
  } else {
    OtherOnly
  }
}

///|
fn categories_for_family(family : PluralRuleFamily) -> Array[String] {
  match family {
    OtherOnly => ["other"]
    English => ["one", "other"]
    Russian => ["one", "few", "many", "other"]
    Arabic => ["zero", "one", "two", "few", "many", "other"]
  }
}

///|
fn ordinal_categories_for_language(language : String) -> Array[String] {
  if language == "en" {
    ["one", "two", "few", "other"]
  } else {
    ["other"]
  }
}

///|
fn direction_for_language(language : String) -> TextDirection {
  if language == "ar" {
    RightToLeft
  } else {
    LeftToRight
  }
}

///|
/// Return plural metadata for a locale.
pub fn locale_profile(locale : String) -> LocaleProfile raise MessageError {
  let parsed = LocaleId::parse(locale)
  let cardinal_family = cardinal_family_for_language(parsed.language)
  let ordinal_family = ordinal_family_for_language(parsed.language)
  {
    locale: parsed,
    direction: direction_for_language(parsed.language),
    cardinal_family,
    ordinal_family,
    cardinal_categories: categories_for_family(cardinal_family),
    ordinal_categories: ordinal_categories_for_language(parsed.language),
  }
}

///|
pub fn LocaleProfile::locale(self : LocaleProfile) -> LocaleId {
  self.locale
}

///|
pub fn LocaleProfile::direction(self : LocaleProfile) -> TextDirection {
  self.direction
}

///|
pub fn LocaleProfile::cardinal_family(self : LocaleProfile) -> PluralRuleFamily {
  self.cardinal_family
}

///|
pub fn LocaleProfile::ordinal_family(self : LocaleProfile) -> PluralRuleFamily {
  self.ordinal_family
}

///|
pub fn LocaleProfile::cardinal_categories(
  self : LocaleProfile,
) -> Array[String] {
  self.cardinal_categories
}

///|
pub fn LocaleProfile::ordinal_categories(self : LocaleProfile) -> Array[String] {
  self.ordinal_categories
}

///|
pub fn TextDirection::name(self : TextDirection) -> String {
  match self {
    LeftToRight => "ltr"
    RightToLeft => "rtl"
  }
}

///|
pub fn PluralRuleFamily::name(self : PluralRuleFamily) -> String {
  match self {
    OtherOnly => "other-only"
    English => "english"
    Russian => "russian"
    Arabic => "arabic"
  }
}

///|
/// Return the cardinal categories expected by the linter.
pub fn required_cardinal_categories(locale : String) -> Array[String] {
  let language = locale_language_lossy(locale)
  categories_for_family(cardinal_family_for_language(language))
}

///|
/// Return the ordinal categories expected by the linter.
pub fn required_ordinal_categories(locale : String) -> Array[String] {
  ordinal_categories_for_language(locale_language_lossy(locale))
}

///|
/// Return all locale identifiers with explicit v0.2 plural support.
pub fn supported_locales() -> Array[String] {
  ["zh", "zh-CN", "en", "ja", "ru", "ar"]
}

///|
/// Return all canonical base languages with explicit plural support.
pub fn supported_languages() -> Array[String] {
  ["zh", "en", "ja", "ru", "ar"]
}