///|
pub(all) enum ListType {
  And
  Or
  Unit
} derive(Eq, Debug)

///|
pub(all) enum ListWidth {
  Long
  Short
  Narrow
} derive(Eq, Debug)

///|
pub struct ListFormatter {
  locale : Locale
  list_type : ListType
  width : ListWidth
} derive(Eq, Debug)

///|
pub fn ListFormatter::new(locale : Locale) -> ListFormatter {
  { locale, list_type: And, width: Long }
}

///|
pub fn ListFormatter::with_type(
  self : ListFormatter,
  list_type : ListType,
) -> ListFormatter {
  { ..self, list_type, }
}

///|
pub fn ListFormatter::with_width(
  self : ListFormatter,
  width : ListWidth,
) -> ListFormatter {
  { ..self, width, }
}

///|
fn list_word(
  locale : Locale,
  list_type : ListType,
  width : ListWidth,
) -> String {
  match (locale.language(), list_type, width) {
    ("zh", And, _) => "\u{548c}"
    ("zh", Or, _) => "\u{6216}"
    ("ja", And, _) => "\u{3068}"
    ("ja", Or, _) => "\u{307e}\u{305f}\u{306f}"
    ("fr", And, _) => "et"
    ("fr", Or, _) => "ou"
    ("de", And, _) => "und"
    ("de", Or, _) => "oder"
    ("es", And, _) => "y"
    ("es", Or, _) => "o"
    ("it", And, _) => "e"
    ("it", Or, _) => "o"
    (_, And, Short) => "&"
    (_, And, _) => "and"
    (_, Or, _) => "or"
    (_, Unit, _) => ""
  }
}

///|
fn list_separator(locale : Locale, width : ListWidth) -> String {
  if locale.language() == "zh" || locale.language() == "ja" {
    "\u{3001}"
  } else {
    match width {
      Narrow => " "
      _ => ", "
    }
  }
}

///|
fn list_pair_separator(
  locale : Locale,
  list_type : ListType,
  width : ListWidth,
) -> String {
  if list_type == Unit {
    match width {
      Narrow => " "
      _ => ", "
    }
  } else {
    let word = list_word(locale, list_type, width)
    if locale.language() == "zh" || locale.language() == "ja" {
      word
    } else {
      " \{word} "
    }
  }
}

///|
fn list_final_separator(
  locale : Locale,
  list_type : ListType,
  width : ListWidth,
) -> String {
  if list_type == Unit {
    return list_pair_separator(locale, list_type, width)
  }
  let word = list_word(locale, list_type, width)
  if locale.language() == "zh" || locale.language() == "ja" {
    word
  } else if locale.language() == "en" && width != Narrow {
    ", \{word} "
  } else {
    " \{word} "
  }
}

///|
/// Joins already-rendered values using locale punctuation and conjunctions.
pub fn ListFormatter::format(
  self : ListFormatter,
  values : Array[String],
) -> String {
  match values.length() {
    0 => ""
    1 => values[0]
    2 =>
      values[0] +
      list_pair_separator(self.locale, self.list_type, self.width) +
      values[1]
    length => {
      let output = StringBuilder::new()
      for index = 0; index < length; index = index + 1 {
        if index > 0 {
          if index == length - 1 {
            output.write_string(
              list_final_separator(self.locale, self.list_type, self.width),
            )
          } else {
            output.write_string(list_separator(self.locale, self.width))
          }
        }
        output.write_string(values[index])
      }
      output.to_string()
    }
  }
}

///|
pub fn format_list(locale : Locale, values : Array[String]) -> String {
  ListFormatter::new(locale).format(values)
}

///|
pub(all) enum RelativeUnit {
  Second
  Minute
  Hour
  Day
  Week
  Month
  Quarter
  Year
} derive(Eq, Debug)

///|
pub(all) enum RelativeNumeric {
  Always
  Auto
} derive(Eq, Debug)

///|
pub struct RelativeTimeFormatter {
  locale : Locale
  numeric : RelativeNumeric
  width : ListWidth
} derive(Eq, Debug)

///|
pub fn RelativeTimeFormatter::new(locale : Locale) -> RelativeTimeFormatter {
  { locale, numeric: Auto, width: Long }
}

///|
pub fn RelativeTimeFormatter::with_numeric(
  self : RelativeTimeFormatter,
  numeric : RelativeNumeric,
) -> RelativeTimeFormatter {
  { ..self, numeric, }
}

///|
pub fn RelativeTimeFormatter::with_width(
  self : RelativeTimeFormatter,
  width : ListWidth,
) -> RelativeTimeFormatter {
  { ..self, width, }
}

///|
fn relative_special(
  locale : Locale,
  value : Int,
  unit : RelativeUnit,
) -> String? {
  let language = locale.language()
  match (language, unit, value) {
    ("zh", Second, 0) => Some("\u{73b0}\u{5728}")
    ("zh", Day, -1) => Some("\u{6628}\u{5929}")
    ("zh", Day, 0) => Some("\u{4eca}\u{5929}")
    ("zh", Day, 1) => Some("\u{660e}\u{5929}")
    ("zh", Week, -1) => Some("\u{4e0a}\u{5468}")
    ("zh", Week, 0) => Some("\u{672c}\u{5468}")
    ("zh", Week, 1) => Some("\u{4e0b}\u{5468}")
    ("fr", Second, 0) => Some("maintenant")
    ("fr", Day, -1) => Some("hier")
    ("fr", Day, 0) => Some("aujourd'hui")
    ("fr", Day, 1) => Some("demain")
    ("de", Second, 0) => Some("jetzt")
    ("de", Day, -1) => Some("gestern")
    ("de", Day, 0) => Some("heute")
    ("de", Day, 1) => Some("morgen")
    (_, Second, 0) => Some("now")
    (_, Day, -1) => Some("yesterday")
    (_, Day, 0) => Some("today")
    (_, Day, 1) => Some("tomorrow")
    (_, Week, -1) => Some("last week")
    (_, Week, 0) => Some("this week")
    (_, Week, 1) => Some("next week")
    (_, Month, -1) => Some("last month")
    (_, Month, 0) => Some("this month")
    (_, Month, 1) => Some("next month")
    (_, Year, -1) => Some("last year")
    (_, Year, 0) => Some("this year")
    (_, Year, 1) => Some("next year")
    _ => None
  }
}

///|
fn english_unit(
  unit : RelativeUnit,
  width : ListWidth,
  plural : Bool,
) -> String {
  match width {
    Narrow =>
      match unit {
        Second => "s"
        Minute => "m"
        Hour => "h"
        Day => "d"
        Week => "w"
        Month => "mo"
        Quarter => "q"
        Year => "y"
      }
    Short =>
      match unit {
        Second => "sec."
        Minute => "min."
        Hour => "hr."
        Day => "day"
        Week => "wk."
        Month => "mo."
        Quarter => "qtr."
        Year => "yr."
      }
    Long => {
      let root = match unit {
        Second => "second"
        Minute => "minute"
        Hour => "hour"
        Day => "day"
        Week => "week"
        Month => "month"
        Quarter => "quarter"
        Year => "year"
      }
      if plural {
        root + "s"
      } else {
        root
      }
    }
  }
}

///|
fn localized_unit(
  locale : Locale,
  unit : RelativeUnit,
  width : ListWidth,
  amount : Int,
) -> String {
  if width != Long {
    return english_unit(unit, width, amount != 1)
  }
  match locale.language() {
    "zh" =>
      match unit {
        Second => "\u{79d2}"
        Minute => "\u{5206}\u{949f}"
        Hour => "\u{5c0f}\u{65f6}"
        Day => "\u{5929}"
        Week => "\u{5468}"
        Month => "\u{4e2a}\u{6708}"
        Quarter => "\u{4e2a}\u{5b63}\u{5ea6}"
        Year => "\u{5e74}"
      }
    "fr" =>
      match unit {
        Second => if amount == 1 { "seconde" } else { "secondes" }
        Minute => if amount == 1 { "minute" } else { "minutes" }
        Hour => if amount == 1 { "heure" } else { "heures" }
        Day => if amount == 1 { "jour" } else { "jours" }
        Week => if amount == 1 { "semaine" } else { "semaines" }
        Month => "mois"
        Quarter => if amount == 1 { "trimestre" } else { "trimestres" }
        Year => if amount == 1 { "an" } else { "ans" }
      }
    "de" =>
      match unit {
        Second => if amount == 1 { "Sekunde" } else { "Sekunden" }
        Minute => if amount == 1 { "Minute" } else { "Minuten" }
        Hour => if amount == 1 { "Stunde" } else { "Stunden" }
        Day => if amount == 1 { "Tag" } else { "Tagen" }
        Week => if amount == 1 { "Woche" } else { "Wochen" }
        Month => if amount == 1 { "Monat" } else { "Monaten" }
        Quarter => if amount == 1 { "Quartal" } else { "Quartalen" }
        Year => if amount == 1 { "Jahr" } else { "Jahren" }
      }
    _ => english_unit(unit, width, amount != 1)
  }
}

///|
fn relative_number(locale : Locale, amount : Int) -> String {
  NumberFormatter::new(locale).with_grouping(false).format_int(amount)
}

///|
/// Formats a signed offset, where negative values are in the past.
pub fn RelativeTimeFormatter::format(
  self : RelativeTimeFormatter,
  value : Int,
  unit : RelativeUnit,
) -> String {
  if self.numeric == Auto {
    match relative_special(self.locale, value, unit) {
      Some(rendered) => return rendered
      None => ()
    }
  }
  let amount = positive_int(value)
  let number = relative_number(self.locale, amount)
  let unit_name = localized_unit(self.locale, unit, self.width, amount)
  match self.locale.language() {
    "zh" =>
      if value < 0 {
        "\{number}\{unit_name}\u{524d}"
      } else {
        "\{number}\{unit_name}\u{540e}"
      }
    "fr" =>
      if value < 0 {
        "il y a \{number} \{unit_name}"
      } else {
        "dans \{number} \{unit_name}"
      }
    "de" =>
      if value < 0 {
        "vor \{number} \{unit_name}"
      } else {
        "in \{number} \{unit_name}"
      }
    _ =>
      if value < 0 {
        "\{number} \{unit_name} ago"
      } else {
        "in \{number} \{unit_name}"
      }
  }
}