// Number humanization (faithful port of python-humanize number.py).
// See docs/specs/port-to-moonbit.md.
//
// Functions: clamp, metric, intcomma, intword, apnumber, fractional,
// scientific, ordinal.
//
// All outputs are aligned to the real python-humanize `number.py` semantics
// (golden values verified against the Python reference).

/// Clamp `value` into [floor, ceil], formatting with `format` and prepending
/// `floor_token`/`ceil_token` when clamped. Mirrors python-humanize `clamp`.
/// None / non-finite inputs are not representable as a String here; non-finite
/// values return their `Inf` / `NaN` string form (Python returns None/Inf).
pub fn clamp(
  value : Double,
  floor~ : Double? = None,
  ceil~ : Double? = None,
  floor_token~ : String = "<",
  ceil_token~ : String = ">",
  format~ : (Double) -> String = default_clamp_fmt,
) -> String {
  if value.is_nan() {
    return "NaN"
  }
  if value.is_inf() {
    return if value < 0.0 { "-Inf" } else { "+Inf" }
  }
  let mut v = value
  let mut token = ""
  match floor {
    Some(f) => if value < f {
      v = f
      token = floor_token
    }
    None => ()
  }
  match ceil {
    Some(c) => if v > c {
      v = c
      token = ceil_token
    }
    None => ()
  }
  token + format(v)
}

pub fn default_clamp_fmt(x : Double) -> String {
  x.to_string()
}

/// Floor division by 3 (MoonBit `/` truncates toward zero).
fn floor_div3(e : Int) -> Int {
  let q = e / 3
  if e < 0 && e % 3 != 0 {
    q - 1
  } else {
    q
  }
}

/// Largest multiple of 3 that is <= e (floor to 3-bucket), matching Python
/// `int(e / 3) * 3` with floor division.
fn floor_mul3(e : Int) -> Int {
  floor_div3(e) * 3
}

/// Non-negative remainder modulo 3 (0, 1, or 2), matching Python `%`.
fn mod3(e : Int) -> Int {
  let r = e % 3
  if r < 0 {
    r + 3
  } else {
    r
  }
}

/// Format a magnitude with an SI metric prefix. Mirrors python-humanize
/// `metric(value, unit="", precision=3)`.
pub fn metric(value : Double, unit~ : String = "", precision~ : Int = 3) -> String {
  if value.is_nan() {
    return "NaN"
  }
  if value.is_inf() {
    return if value < 0.0 { "-Inf" } else { "+Inf" }
  }
  let exponent = if value == 0.0 {
    0
  } else {
    ilog10(value.abs())
  }

  if exponent >= 33 || exponent < -30 {
    return scientific(value.to_string(), precision = precision - 1) + unit
  }

  let old_bucket = floor_mul3(exponent)
  let mut v = value / pow10_double(old_bucket)
  let mut exp = exponent
  let mut digits = if 0 > precision - mod3(exp) - 1 { 0 } else { precision - mod3(exp) - 1 }
  let rounded = round_half_up(v.abs(), digits)
  if exp < 30 && rounded >= 1000.0 {
    let new_exponent = exp + 3 - mod3(exp)
    let new_bucket = floor_mul3(new_exponent)
    v = v / pow10_double(new_bucket - old_bucket)
    exp = new_exponent
    digits = if 0 > precision - mod3(exp) - 1 { 0 } else { precision - mod3(exp) - 1 }
  }

  let mut ordinal_ : String = ""
  if exp >= 3 {
    ordinal_ = positive_prefix[(floor_div3(exp) - 1)]
  } else if exp < 0 {
    ordinal_ = negative_prefix[((-exp - 1) / 3)]
  } else {
    ordinal_ = ""
  }
  let value_ = format_fixed(v, digits)
  let space = if !(unit != "" || ordinal_ != "") || unit == "°" || unit == "′" || unit == "″" {
    ""
  } else {
    " "
  }
  value_ + space + ordinal_ + unit
}

/// Integer base-10 logarithm (floor), matching `int(math.floor(log10(x)))` for
/// positive `x`. Implemented with a loop to avoid floating-point edge cases.
fn ilog10(x : Double) -> Int {
  // floor(log10(x)) with a tiny epsilon to avoid floating-point errors at
  // exact powers of ten (e.g. 1e-14 would otherwise drift to -15).
  @math.floor(@math.log10(x) + 1.0e-9).to_int()
}

/// Positive SI prefixes: k, M, G, T, P, E, Z, Y, R, Q (index = exp/3 - 1).
let positive_prefix : Array[String] = [
  "k", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q",
]

/// Negative SI prefixes: m, µ, n, p, f, a, z, y, r, q (index = (-exp-1)/3).
let negative_prefix : Array[String] = [
  "m", "μ", "n", "p", "f", "a", "z", "y", "r", "q",
]

fn round_half_up(x : Double, digits : Int) -> Double {
  let r = format_fixed(x, digits)
  try {
    @string.parse_double(r)
  } catch {
    _ => x
  }
}

/// Add thousands separators to a numeric string, optionally rounding to
/// `ndigits` decimals. Mirrors python-humanize `intcomma(value, ndigits=None)`.
pub fn intcomma(value : String, ndigits~ : Int? = None) -> String {
  let neg = value.has_prefix("-")
  let body = if neg { value[1:].to_owned() } else { value }
  let formatted = try {
    let f = @string.parse_double(body)
    match ndigits {
      Some(d) => format_fixed(f, d)
      None => f.to_string()
    }
  } catch {
    _ => body
  }
  let grouped = group_number_string(formatted)
  if neg {
    "-" + grouped
  } else {
    grouped
  }
}

fn group_number_string(s : String) -> String {
  match s.find(".") {
    Some(idx) => group_thousands(s[0:idx].to_owned()) + s[idx:].to_owned()
    None => group_thousands(s)
  }
}

fn group_thousands(int_part : String) -> String {
  if int_part.length() <= 3 {
    return int_part
  }
  // Use the active locale's thousands separator (e.g. "." for de_DE),
  // defaulting to "," when no locale is active.
  let sep = thousands_separator()
  let mut buf = ""
  let len = int_part.length()
  let mut count = 0
  for i = len - 1; i >= 0; i = i - 1 {
    if count == 3 {
      buf = sep + buf
      count = 0
    }
    buf = int_part[i:i + 1].to_owned() + buf
    count = count + 1
  }
  buf
}

/// Powers of ten thresholds for `intword` (matches python-humanize: the last
/// entry is the googol, 10**100).
let intword_powers : Array[Double] = [
  1.0e3, 1.0e6, 1.0e9, 1.0e12, 1.0e15, 1.0e18, 1.0e21, 1.0e24, 1.0e27, 1.0e30,
  1.0e33, 1.0e100,
]

let intword_units : Array[String] = [
  "thousand", "million", "billion", "trillion", "quadrillion", "quintillion",
  "sextillion", "septillion", "octillion", "nonillion", "decillion", "googol",
]

/// Convert a large integer (given as String) to a friendly magnitude, e.g.
/// "1.0 million". Mirrors python-humanize `intword(value, format="%.1f")`.
pub fn intword(value : String, format~ : String = "%.1f") -> String {
  let neg = value.has_prefix("-")
  let body = if neg { value[1:].to_owned() } else { value }
  match bisect_intword_powers(body) {
    Some((ordinal, power)) => {
      let v = try {
        @string.parse_double(body)
      } catch {
        _ => return value
      }
      let mut ordinal = ordinal
      let chopped = v / power
      let mut number = format_float_with(format, chopped)
      // python 4.16.0: rounding can land the mantissa exactly on the next
      // power (999999 -> "1000.0 thousand"); bump up to "1.0 million".
      // `rounded` is the mantissa rounded to an integer (1.0e3 -> 1000).
      let rounded = @math.round(chopped)
      if ordinal < intword_powers.length() - 1 &&
        rounded * power == intword_powers[ordinal + 1] {
        ordinal = ordinal + 1
        number = format_float_with(format, 1.0)
      }
      let unit = gettext(intword_units[ordinal])
      (if neg { "-" } else { "" }) + number + " " + unit
    }
    None => {
      // below the smallest power: return the (possibly negative) integer string
      if neg {
        "-" + body
      } else {
        body
      }
    }
  }
}

/// Find the ordinal index into `intword_powers`/`intword_units` for a numeric
/// string, using bisect_right semantics. Returns None if the value is smaller
/// than the smallest power (10**3).
fn bisect_intword_powers(body : String) -> Option[(Int, Double)] {
  let v = try {
    @string.parse_double(body)
  } catch {
    _ => return None
  }
  if v < 1.0e3 {
    return None
  }
  let mut lo = 0
  let mut hi = intword_powers.length()
  while lo < hi {
    let mid = (lo + hi) / 2
    if intword_powers[mid] <= v {
      lo = mid + 1
    } else {
      hi = mid
    }
  }
  // lo == bisect_right; ordinal index is lo-1
  let idx = lo - 1
  // The googol entry may be out of Double range; treat index 11 as the cap.
  Some((idx, intword_powers[idx]))
}

/// Apply a Python-style `"%.Nf"` format string to a Double using `format_fixed`.
fn format_float_with(format : String, x : Double) -> String {
  let n = try {
    let idx = format.find(".")
    match idx {
      Some(i) => @string.parse_int(format[i + 1:format.length() - 1].to_owned(), base=10)
      None => 1
    }
  } catch {
    _ => 1
  }
  format_fixed(x, n)
}

/// Convert an integer (given as String) to AP-style words for 0-9, else the
/// decimal string. Mirrors python-humanize `apnumber`.
pub fn apnumber(value : String) -> String {
  let n = try {
    @string.parse_int(value, base=10)
  } catch {
    _ => return value
  }
  if n < 0 || n > 9 {
    return value
  }
  let words = [
    "zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
    "nine",
  ]
  // User-visible word passes through gettext so locale catalogs (e.g. zh_CN,
  // fr_FR) can translate it; English / unknown locales fall back to the word.
  gettext(words[n])
}

/// Convert a float/number (given as String) to a human-readable fraction or
/// mixed fraction. Mirrors python-humanize `fractional`.
pub fn fractional(value : String) -> String {
  let num = try {
    @string.parse_double(value)
  } catch {
    _ => return value
  }
  if num.is_nan() {
    return "NaN"
  }
  if num.is_inf() {
    return if num < 0.0 { "-Inf" } else { "+Inf" }
  }
  let whole = @math.trunc(num).to_int()
  let frac = num - whole.to_double()
  let r = Rational::from_float_approx(frac, 1000)
  let numer = r.numerator()
  let denom = r.denominator()
  if whole != 0 && numer == 0 && denom == 1 {
    // an integer (or integer variant like 1.0000)
    return format_fixed(whole.to_double(), 0)
  }
  if whole == 0 {
    return gettext(numer.to_string() + "/" + denom.to_string())
  }
  // mixed fraction; sign rides on whole part, abs on numerator
  gettext(format_fixed(whole.to_double(), 0) + " " + numer.abs().to_string() + "/" + denom.to_string())
}

/// Render a number (given as String) in scientific notation "z.wq x 10ⁿ".
/// Mirrors python-humanize `scientific(value, precision=2)`.
pub fn scientific(value : String, precision~ : Int = 2) -> String {
  let num = try {
    @string.parse_double(value)
  } catch {
    _ => return value
  }
  if num.is_nan() {
    return "NaN"
  }
  if num.is_inf() {
    return if num < 0.0 { "-Inf" } else { "+Inf" }
  }
  let neg = num < 0.0
  let abs = if neg { -num } else { num }
  let exp = if abs == 0.0 {
    0
  } else {
    ilog10(abs)
  }
  let mantissa = abs / pow10_double(exp)
  let mant_str = format_fixed(mantissa, precision)
  let exp_str = to_superscript(exp.to_string())
  let sign = if neg { "-" } else { "" }
  // Whole expression passes through gettext so locale catalogs can translate
  // the coefficient words / connector; English / unknown locales fall back.
  gettext(sign + mant_str + " x 10" + exp_str)
}

// ---------------------------------------------------------------------------
// Feature 1: locale-aware scientific notation + decimal/word helpers
// ---------------------------------------------------------------------------

/// Trait for locale-aware number formatting. Concrete number types can
/// implement `scientific` to produce a localized scientific-notation string.
/// The free `scientific(value : Double, ~precision, ~exponent, ~locale)`
/// function below is the default, generic implementation.
pub trait NumberFormatter {
  scientific_localized(value : Double, precision~ : Int, exponent~ : Int) -> String
}

/// Convert a real number to scientific notation as a plain (locale-neutral)
/// string, e.g. `to_scientific(3.14159, 2, 3) == "3.14 x 10^3"`.
///
/// This is the building block used by the localized `scientific` overload.
pub fn to_scientific(value : Double, precision~ : Int = 6, exponent~ : Int = 3) -> String {
  let mantissa = value / pow10_double(exponent)
  let m = format_fixed(mantissa, precision)
  m + " x 10^" + exponent.to_string()
}

/// Format a number with grouped thousands and a fixed number of `decimals`,
/// e.g. `to_decimal(1234567.891, decimals=2) == "1,234,567.89"`.
pub fn to_decimal(value : Double, decimals~ : Int = 2) -> String {
  let neg = value < 0.0
  let v = if neg { -value } else { value }
  let factor = pow10_double(decimals)
  let x = v * factor
  let scaled = if x >= 0.0 {
    (x + 0.5).floor().to_int()
  } else {
    (x - 0.5).ceil().to_int()
  }
  let denom = pow10_int(decimals)
  let int_part = scaled / denom
  let frac_part = scaled % denom
  let int_str = group_thousands(int_part.to_string())
  // decimals == 0: render the integer only, no trailing "." + empty fraction.
  if decimals == 0 {
    if neg { "-" + int_str } else { int_str }
  } else {
    let frac_str = pad_left(frac_part.to_string(), decimals, '0')
    let body = int_str + "." + frac_str
    if neg { "-" + body } else { body }
  }
}

/// Spell out an integer in English cardinal words, e.g. `123` -> `"one hundred and twenty-three"`.
///
/// Negative numbers are prefixed with `"negative"`. Locale-specific spelling
/// (e.g. Chinese 读数) is out of scope for the initial implementation; this
/// returns English regardless of the active locale.
pub fn integer_to_words(n : Int) -> String {
  if n == 0 {
    return "zero"
  }
  let neg = n < 0
  let x = if neg { -n } else { n }
  let words = int_to_words_positive(x)
  if neg { "negative " + words } else { words }
}

fn int_to_words_positive(n : Int) -> String {
  let scales = [
    "", " thousand", " million", " billion", " trillion", " quadrillion",
    " quintillion",
  ]
  if n == 0 {
    return ""
  }
  let groups : Array[Int] = []
  let mut rem = n
  while rem > 0 {
    groups.push(rem % 1000)
    rem = rem / 1000
  }
  let parts : Array[String] = []
  let mut i = groups.length() - 1
  while i >= 0 {
    let g = groups[i]
    if g != 0 {
      let w = under1000_words(g)
      let scale = scales[i]
      parts.push(if scale == "" { w } else { w + scale })
    }
    i = i - 1
  }
  parts.join(" ")
}

fn under1000_words(n : Int) -> String {
  let ones = [
    "", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
    "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
    "seventeen", "eighteen", "nineteen",
  ]
  let tens = [
    "", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty",
    "ninety",
  ]
  let h = n / 100
  let t = n % 100
  let mut s = ""
  if h > 0 {
    s = s + ones[h] + " hundred"
  }
  if t > 0 {
    if h > 0 {
      s = s + " and "
    }
    if t < 20 {
      s = s + ones[t]
    } else {
      let d = t / 10
      let u = t % 10
      s = s + tens[d]
      if u > 0 {
        s = s + "-" + ones[u]
      }
    }
  }
  s
}

fn pow10_int(n : Int) -> Int {
  let mut r = 1
  for i = 0; i < n; i = i + 1 {
    r = r * 10
  }
  r
}

fn pad_left(s : String, width : Int, ch : Char) -> String {
  let len = s.length()
  if len >= width {
    return s
  }
  let mut prefix = ""
  for i = 0; i < width - len; i = i + 1 {
    prefix = prefix + ch.to_string()
  }
  prefix + s
}

/// Format a real number in scientific notation, localized via the i18n catalog.
///
/// Uses the `scientific_tmpl` message (`"%s x 10^%s"` for en, `"%s × 10^%s"`
/// for ru_RU, `"%s×10^%s"` for zh/zh_CN) and substitutes the mantissa and the
/// exponent (as a plain decimal number) into the localized connector. The
/// exponent is always rendered as digits (e.g. `10^3`); only the connector
/// between mantissa and `10^` is localized.
pub fn scientific_localized(
  value : Double,
  precision~ : Int = 6,
  exponent~ : Int = 3,
  locale~ : Locale = match current_locale() {
    Some(l) => l
    None => En
  }
) -> String {
  let mantissa = value / pow10_double(exponent)
  let m = format_fixed(mantissa, precision)
  let exp_repr = exponent.to_string()
  msg_str(locale, SCItmpl).format([m, exp_repr])
}

/// Convert an integer (given as String) to its English ordinal, e.g. "1st".
/// Mirrors python-humanize `ordinal` (gender ignored, uses male form).
pub fn ordinal(value : String) -> String {
  let n = try {
    @string.parse_int(value, base=10)
  } catch {
    _ => return value
  }
  let abs_n = n.abs()
  let mod100 = abs_n % 100
  let digit = if mod100 == 11 || mod100 == 12 || mod100 == 13 {
    0
  } else {
    abs_n % 10
  }
  let suffixes = ["th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"]
  let suffix = suffixes[digit]
  // Localize the ordinal suffix via pgettext using the digit context, exactly
  // like python-humanize ("{digit} (male)" ctx + English suffix word). This lets
  // catalogs such as ru_RU map e.g. digit 2 / "nd" -> "ой". English / unknown
  // locales fall back to the English suffix; the number is always ASCII digits.
  let localized = pgettext(digit.to_string() + " (male)", suffix)
  n.to_string() + localized
}