///|
/// CLDR-compatible plural categories used by message selection.
pub(all) enum PluralCategory {
  Zero
  One
  Two
  Few
  Many
  Other
} derive(Eq, Debug)

///|
pub fn PluralCategory::name(self : PluralCategory) -> String {
  match self {
    Zero => "zero"
    One => "one"
    Two => "two"
    Few => "few"
    Many => "many"
    Other => "other"
  }
}

///|
/// The kind of plural rule requested by an application.
pub(all) enum PluralKind {
  Cardinal
  Ordinal
} derive(Eq, Debug)

///|
/// Reusable plural selector bound to a locale.
pub struct PluralRules {
  locale : Locale
  kind : PluralKind
} derive(Eq, Debug)

///|
pub fn PluralRules::new(
  locale : Locale,
  kind? : PluralKind = Cardinal,
) -> PluralRules {
  { locale, kind }
}

///|
/// Creates rules for ordinal values such as `1st` and `23rd`.
pub fn PluralRules::ordinal(locale : Locale) -> PluralRules {
  { locale, kind: Ordinal }
}

///|
pub fn PluralRules::locale(self : PluralRules) -> Locale {
  self.locale
}

///|
pub fn PluralRules::kind(self : PluralRules) -> PluralKind {
  self.kind
}

///|
pub fn PluralRules::select(self : PluralRules, value : Int) -> PluralCategory {
  match self.kind {
    Cardinal => cardinal_plural(self.locale, value)
    Ordinal => ordinal_plural(self.locale, value)
  }
}

///|
fn absolute(value : Int) -> Int {
  if value < 0 {
    -value
  } else {
    value
  }
}

///|
fn between(value : Int, lower : Int, upper : Int) -> Bool {
  value >= lower && value <= upper
}

///|
fn is_language_in(language : String, languages : Array[String]) -> Bool {
  languages.any(fn(candidate) { candidate == language })
}

///|
fn cardinal_arabic(value : Int) -> PluralCategory {
  let n = absolute(value)
  let mod100 = n % 100
  if n == 0 {
    Zero
  } else if n == 1 {
    One
  } else if n == 2 {
    Two
  } else if between(mod100, 3, 10) {
    Few
  } else if between(mod100, 11, 99) {
    Many
  } else {
    Other
  }
}

///|
fn cardinal_east_slavic(value : Int) -> PluralCategory {
  let n = absolute(value)
  let mod10 = n % 10
  let mod100 = n % 100
  if mod10 == 1 && mod100 != 11 {
    One
  } else if between(mod10, 2, 4) && !between(mod100, 12, 14) {
    Few
  } else if mod10 == 0 || between(mod10, 5, 9) || between(mod100, 11, 14) {
    Many
  } else {
    Other
  }
}

///|
fn cardinal_polish(value : Int) -> PluralCategory {
  let n = absolute(value)
  let mod10 = n % 10
  let mod100 = n % 100
  if n == 1 {
    One
  } else if between(mod10, 2, 4) && !between(mod100, 12, 14) {
    Few
  } else if n != 1 &&
    (between(mod10, 0, 1) || between(mod10, 5, 9) || between(mod100, 12, 14)) {
    Many
  } else {
    Other
  }
}

///|
fn cardinal_lithuanian(value : Int) -> PluralCategory {
  let n = absolute(value)
  let mod10 = n % 10
  let mod100 = n % 100
  if mod10 == 1 && !between(mod100, 11, 19) {
    One
  } else if between(mod10, 2, 9) && !between(mod100, 11, 19) {
    Few
  } else if mod10 == 0 || between(mod100, 11, 19) {
    Many
  } else {
    Other
  }
}

///|
fn cardinal_latvian(value : Int) -> PluralCategory {
  let n = absolute(value)
  let mod10 = n % 10
  let mod100 = n % 100
  if mod10 == 0 || between(mod100, 11, 19) {
    Zero
  } else if mod10 == 1 && mod100 != 11 {
    One
  } else {
    Other
  }
}

///|
fn cardinal_slovenian(value : Int) -> PluralCategory {
  match absolute(value) % 100 {
    1 => One
    2 => Two
    3 | 4 => Few
    _ => Other
  }
}

///|
fn cardinal_romanian(value : Int) -> PluralCategory {
  let n = absolute(value)
  let mod100 = n % 100
  if n == 1 {
    One
  } else if n == 0 || between(mod100, 1, 19) {
    Few
  } else {
    Other
  }
}

///|
fn cardinal_hebrew(value : Int) -> PluralCategory {
  let n = absolute(value)
  if n == 1 {
    One
  } else if n == 2 {
    Two
  } else if n != 0 && n % 10 == 0 {
    Many
  } else {
    Other
  }
}

///|
/// Selects a cardinal category for an integer.
///
/// The implementation covers the major CLDR integer rule families. Languages
/// without grammatical integer plurals safely return `other`.
pub fn cardinal_plural(locale : Locale, value : Int) -> PluralCategory {
  let language = locale.language()
  if language == "ar" {
    cardinal_arabic(value)
  } else if is_language_in(language, ["ru", "uk", "be", "sr", "hr"]) {
    cardinal_east_slavic(value)
  } else if language == "pl" {
    cardinal_polish(value)
  } else if language == "lt" {
    cardinal_lithuanian(value)
  } else if language == "lv" {
    cardinal_latvian(value)
  } else if language == "sl" {
    cardinal_slovenian(value)
  } else if language == "ro" {
    cardinal_romanian(value)
  } else if language == "he" {
    cardinal_hebrew(value)
  } else if is_language_in(language, ["cs", "sk"]) {
    if value == 1 {
      One
    } else if between(absolute(value), 2, 4) {
      Few
    } else {
      Other
    }
  } else if is_language_in(language, ["fr", "pt"]) {
    if value == 0 || value == 1 {
      One
    } else {
      Other
    }
  } else if is_language_in(language, [
      "en", "de", "es", "it", "nl", "sv", "no", "da", "fi", "et", "el",
    ]) {
    if value == 1 {
      One
    } else {
      Other
    }
  } else {
    Other
  }
}

///|
fn ordinal_english(value : Int) -> PluralCategory {
  let n = absolute(value)
  let mod10 = n % 10
  let mod100 = n % 100
  if mod10 == 1 && mod100 != 11 {
    One
  } else if mod10 == 2 && mod100 != 12 {
    Two
  } else if mod10 == 3 && mod100 != 13 {
    Few
  } else {
    Other
  }
}

///|
fn ordinal_welsh(value : Int) -> PluralCategory {
  match absolute(value) {
    0 | 7 | 8 | 9 => Zero
    1 => One
    2 => Two
    3 | 4 => Few
    5 | 6 => Many
    _ => Other
  }
}

///|
/// Selects an ordinal category for an integer.
pub fn ordinal_plural(locale : Locale, value : Int) -> PluralCategory {
  match locale.language() {
    "en" => ordinal_english(value)
    "cy" => ordinal_welsh(value)
    "fr" => if value == 1 { One } else { Other }
    "it" =>
      if value == 8 || value == 11 || value == 80 || value == 800 {
        Many
      } else {
        Other
      }
    _ => Other
  }
}