// base.mbt — core engine for num2words-moonbit
//
// An independent reimplementation in MoonBit of the ideas behind the Python
// `num2words` library (https://github.com/savoirfairelinux/num2words, LGPL-2.1).
// It spells integers / years / ordinals / currency amounts in multiple
// languages. The upstream project is acknowledged in README.md.
//
// Design:
//   - `BigInt` is used throughout so arbitrarily large integers are supported.
//   - Each language is described by a `Lang` value (word tables + a `Variant`
//     tag). The heavy lifting (splitting into 3-digit groups) is shared; the
//     per-language quirks live in the cardinal_* helpers.

///|
pub enum Variant {
  En
  Fr
  Es
  De
  It
  Pt
  PtPt
  Ru
}

///|
/// Word tables + behaviour flags for one language.
pub struct Lang {
  variant : Variant
  zero : String
  ones : Array[String] // 0..9
  teens : Array[String] // 10..19  (index = value - 10)
  tens : Array[String] // 10,20,..90 (index = d)
  hundred : String // prefix base, e.g. "hundred" / "cent" / "hundert"
  thousands : Array[String] // index = group power: 1->"thousand", 2->"million"...
  negative : String
  decimal : String
  comma_sep : Bool
}

///|
/// Split `n` (>0) into 3-digit groups from low to high, return high-to-low.
pub fn group3(n : BigInt) -> Array[(BigInt, Int)] {
  let low : Array[(BigInt, Int)] = []
  let mut x = n
  let mut power = 0
  while x != BigInt::from_int(0) {
    low.push((x % BigInt::from_int(1000), power))
    x = x / BigInt::from_int(1000)
    power = power + 1
  }
  let res : Array[(BigInt, Int)] = []
  for i = low.length() - 1; i >= 0; i = i - 1 {
    res.push(low[i])
  }
  res
}

///|
fn join_parts(arr : Array[String], sep : String) -> String {
  let mut s = ""
  for i = 0; i < arr.length(); i = i + 1 {
    if i > 0 {
      s = s + sep
    }
    s = s + arr[i]
  }
  s
}

///|
fn split_to_array(s : String, sep : String) -> Array[String] {
  s.split(sep).map(fn(w) { w.to_owned() }).collect()
}

///|
fn split_hyphen(word : String) -> (String, String) {
  let n = word.length()
  for i = 0; i < n; i = i + 1 {
    if word[i:i + 1].to_owned() == "-" {
      return (word[0:i].to_owned(), word[i + 1:n].to_owned())
    }
  }
  (word, "")
}

// =====================================================================
// English
// =====================================================================

///|
pub fn lang_en() -> Lang {
  {
    variant: En,
    zero: "zero",
    ones: [
      "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
    ],
    teens: [
      "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen",
      "eighteen", "nineteen",
    ],
    tens: [
      "", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty",
      "ninety",
    ],
    hundred: "hundred",
    thousands: [
      "", "thousand", "million", "billion", "trillion", "quadrillion", "quintillion",
      "sextillion", "septillion", "octillion", "nonillion", "decillion",
    ],
    negative: "minus",
    decimal: "point",
    comma_sep: true,
  }
}

///|
fn Lang::below100_en(self : Lang, r : Int) -> String {
  if r < 10 {
    return self.ones[r]
  }
  if r < 20 {
    return self.teens[r - 10]
  }
  let d = r / 10
  let u = r % 10
  if u == 0 {
    return self.tens[d]
  }
  self.tens[d] + "-" + self.ones[u]
}

///|
fn Lang::triple_en(self : Lang, t : Int) -> String {
  if t == 0 {
    return ""
  }
  let h = t / 100
  let r = t % 100
  if h == 0 {
    return self.below100_en(r)
  }
  let hs = (if h == 1 { "one" } else { self.ones[h] }) + " " + self.hundred
  if r == 0 {
    return hs
  }
  hs + " and " + self.below100_en(r)
}

///|
fn Lang::cardinal_en(self : Lang, n : BigInt) -> String {
  if n == BigInt::from_int(0) {
    return self.zero
  }
  if !self.supports(n) {
    return ""
  }
  let neg = n < BigInt::from_int(0)
  let n = if neg { -n } else { n }
  let groups = group3(n)
  let mut joined = ""
  for i = 0; i < groups.length(); i = i + 1 {
    let (triple, power) = groups[i]
    let t = triple.to_int()
    if t == 0 {
      continue
    }
    let mut s = self.triple_en(t)
    if power > 0 {
      s = s + " " + self.thousands[power]
    }
    if joined != "" {
      // Only the final units group is joined with "and" ("one thousand and
      // one", "one million, one thousand"); everything else uses a comma.
      joined = joined + (if power == 0 && t < 100 { " and " } else { ", " })
    }
    joined = joined + s
  }
  if neg {
    self.negative + " " + joined
  } else {
    joined
  }
}

// ---------------------------------------------------------------------
// English ordinal ("first", "second", ...)
// ---------------------------------------------------------------------

///|
pub fn ordinal_en(n : BigInt) -> String {
  if n < BigInt::from_int(0) {
    // Upstream raises TypeError for negative ordinals.
    return ""
  }
  if n == BigInt::from_int(0) {
    return "zeroth"
  }
  let card = lang_en().cardinal_en(n)
  let words = split_to_array(card, " ")
  let last_idx = words.length() - 1
  words[last_idx] = ordinalize(words[last_idx])
  join_parts(words, " ")
}

///|
fn ordinalize(word : String) -> String {
  let (left, right) = split_hyphen(word)
  if right == "" {
    ordinalize_simple(word)
  } else {
    left + "-" + ordinalize_simple(right)
  }
}

///|
fn ordinalize_simple(w : String) -> String {
  match w {
    "one" => "first"
    "two" => "second"
    "three" => "third"
    "five" => "fifth"
    "eight" => "eighth"
    "nine" => "ninth"
    "twelve" => "twelfth"
    "twenty" => "twentieth"
    "thirty" => "thirtieth"
    "forty" => "fortieth"
    "fifty" => "fiftieth"
    "sixty" => "sixtieth"
    "seventy" => "seventieth"
    "eighty" => "eightieth"
    "ninety" => "ninetieth"
    "hundred" => "hundredth"
    "thousand" => "thousandth"
    "million" => "millionth"
    "billion" => "billionth"
    "trillion" => "trillionth"
    "quadrillion" => "quadrillionth"
    "quintillion" => "quintillionth"
    "sextillion" => "sextillionth"
    "septillion" => "septillionth"
    "octillion" => "octillionth"
    "nonillion" => "nonillionth"
    "decillion" => "decillionth"
    _ => w + "th"
  }
}

///|
/// Ordinal number with numeric suffix: 1 -> "1st", 2 -> "2nd", 12 -> "12th".
pub fn ordinal_num_en(n : BigInt) -> String {
  if n < BigInt::from_int(0) {
    // Upstream raises TypeError for negative ordinal_num.
    return ""
  }
  let s = n.to_string()
  let last = if s.length() >= 2 {
    s[s.length() - 2:s.length()].to_owned()
  } else {
    s
  }
  let suffix = if last == "11" || last == "12" || last == "13" {
    "th"
  } else {
    match s[s.length() - 1:s.length()].to_owned() {
      "1" => "st"
      "2" => "nd"
      "3" => "rd"
      _ => "th"
    }
  }
  let base = n.to_string() + suffix
  base
}

// ---------------------------------------------------------------------
// English year ("2024" -> "twenty twenty-four")
// ---------------------------------------------------------------------

///|
pub fn year_en(n : BigInt) -> String {
  if n < BigInt::from_int(1000) || n > BigInt::from_int(9999) {
    return lang_en().cardinal_en(n)
  }
  let neg = n < BigInt::from_int(0)
  let n = if neg { -n } else { n }
  // 2001 -> "two thousand and one", not "twenty one"; likewise 1001, 3001.
  if n % BigInt::from_int(1000) < BigInt::from_int(10) {
    return lang_en().cardinal_en(n)
  }
  if n % BigInt::from_int(1000) == BigInt::from_int(0) {
    // 2000 -> "two thousand"
    return lang_en().cardinal_en(n)
  }
  let s = n.to_string()
  let first = BigInt::from_string(s[0:2].to_owned()).to_int()
  let second = BigInt::from_string(s[2:4].to_owned()).to_int()
  if second == 0 {
    // 1900 -> "nineteen hundred"
    return lang_en().cardinal_en(BigInt::from_int(first)) + " hundred"
  }
  let a = lang_en().cardinal_en(BigInt::from_int(first))
  let b = lang_en().cardinal_en(BigInt::from_int(second))
  if neg {
    "minus " + a + " " + b
  } else {
    a + " " + b
  }
}

// ---------------------------------------------------------------------
// Dispatch
// ---------------------------------------------------------------------

///|
/// True when the magnitude of `n` fits inside the language's scale-word
/// table. Scale tables are finite, so every language has a maximum supported
/// power; values beyond it are reported as unsupported instead of indexing
/// past the table (which would abort).
pub fn Lang::supports(self : Lang, n : BigInt) -> Bool {
  let x = if n < BigInt::from_int(0) { -n } else { n }
  if x == BigInt::from_int(0) {
    return true
  }
  let groups = group3(x)
  let (_, top_power) = groups[0]
  top_power < self.thousands.length()
}

///|
/// Highest power of 1000 the language can spell (e.g. English: 11 -> 10^33).
pub fn Lang::max_power(self : Lang) -> Int {
  self.thousands.length() - 1
}

///|
pub fn Lang::cardinal(self : Lang, n : BigInt) -> String {
  match self.variant {
    En => self.cardinal_en(n)
    Fr => self.cardinal_fr(n)
    Es => self.cardinal_es(n)
    De => self.cardinal_de(n)
    It => self.cardinal_it(n)
    Pt => self.cardinal_pt(n)
    PtPt => self.cardinal_pt_pt(n)
    Ru => self.cardinal_ru(n)
  }
}