///|
/// Python's `repr` of a float.
///
/// MoonBit's `Double::to_string` already produces the shortest digits that
/// round-trip, and agrees with CPython on the digits for every value tested.
/// What it does not agree on is the LAYOUT, in six ways: `1` for `1.0`,
/// `10000000000000000` for `1e16`, `0` for `-0.0`, `Infinity`, `NaN`, and
/// `2.5e-7` where Python writes `2.5e-07`. So the digits are taken from
/// MoonBit and laid out again here, by CPython's rules
/// (`Python/pystrtod.c`, `format_float_short` with mode `r`):
///
///   * `nan`, `inf`, `-inf`, and `-0.0` with its sign from the bit pattern.
///   * With the value written as `0.D × 10^decpt`: exponent notation when
///     `decpt <= -4 || decpt > 16`, and positional otherwise. That is why
///     `1e15` prints in full and `1e16` does not.
///   * Positional always carries a point and at least one digit after it.
///   * The exponent always carries a sign and at least two digits.
///
/// The same layout is what `str()` produces: Python 3 has one float format.
pub fn py_float_repr(d : Double) -> String {
  py_float_layout(d, dot_zero=true)
}

///|
/// The same layout without the forced `.0`, which is how CPython writes the
/// imaginary part of a complex: `repr(4j)` is `4j` and not `4.0j`. The flag is
/// CPython's own `Py_DTSF_ADD_DOT_0`, set for a float and clear for a complex.
pub fn py_float_layout(d : Double, dot_zero~ : Bool) -> String {
  if d != d {
    return "nan"
  }
  if d == @double.infinity {
    return "inf"
  }
  if d == @double.neg_infinity {
    return "-inf"
  }
  let text = d.to_string()
  let signed_text = text.has_prefix("-")
  // The sign of a zero is in the bit pattern, not in the digits: MoonBit
  // renders negative zero as `0`, and Python writes `-0.0`.
  let negative = signed_text || (d == 0.0 && d.reinterpret_as_uint64() != 0UL)
  let body = if signed_text { text[1:].to_owned() } else { text }
  let (digits, exp10) = decompose(body)
  let out = StringBuilder()
  if negative {
    out.write_char('-')
  }
  if digits == "0" {
    out.write_string(if dot_zero { "0.0" } else { "0" })
    return out.to_string()
  }
  // `exp10` is the power of ten the first digit stands for; CPython's `decpt`
  // is one more than that.
  let decpt = exp10 + 1
  if decpt <= -4 || decpt > 16 {
    write_exponential(out, digits, exp10)
  } else {
    write_positional(out, digits, exp10, dot_zero)
  }
  out.to_string()
}

///|
/// The significant digits, and the power of ten the first one stands for.
///
/// `body` is MoonBit's rendering of a non-negative finite double, in any of
/// the forms it produces: `3.14`, `0.000001`, `100000000000000000000`,
/// `1e+100`.
fn decompose(body : String) -> (String, Int) {
  let mut mant = body
  let mut exp = 0
  match find_char(body, 'e') {
    Some(i) => {
      mant = body[:i].to_owned()
      exp = parse_signed_int(body[i + 1:].to_owned())
    }
    None => ()
  }
  let point = match find_char(mant, '.') {
    Some(i) => i
    None => mant.length()
  }
  let all = StringBuilder()
  for c in mant {
    if c != '.' {
      all.write_char(c)
    }
  }
  let mut digits = all.to_string()
  let mut before = point
  // Leading zeros are not significant, and each one moves the point.
  let mut lead = 0
  while lead < digits.length() - 1 && digits.get_char(lead) is Some('0') {
    lead = lead + 1
    before = before - 1
  }
  digits = digits[lead:].to_owned()
  // Trailing zeros are not significant either.
  let mut last = digits.length()
  while last > 1 && digits.get_char(last - 1) is Some('0') {
    last = last - 1
  }
  digits = digits[:last].to_owned()
  if digits == "0" {
    return ("0", 0)
  }
  (digits, before - 1 + exp)
}

///|
fn find_char(s : String, c : Char) -> Int? {
  for i in 0.. Int {
  let mut i = 0
  let mut neg = false
  if s.get_char(0) is Some('-') {
    neg = true
    i = 1
  } else if s.get_char(0) is Some('+') {
    i = 1
  }
  let mut v = 0
  while i < s.length() {
    match s.get_char(i) {
      Some(c) if c >= '0' && c <= '9' => v = v * 10 + (c.to_int() - 48)
      _ => break
    }
    i = i + 1
  }
  if neg {
    -v
  } else {
    v
  }
}

///|
/// `1.0`, `10.0`, `0.0001` -- always with a point and a digit after it.
fn write_positional(
  out : StringBuilder,
  digits : String,
  exp10 : Int,
  dot_zero : Bool,
) -> Unit {
  if exp10 >= 0 {
    let int_len = exp10 + 1
    for i in 0.. int_len {
      out.write_char('.')
      out.write_string(digits[int_len:].to_owned())
    } else if dot_zero {
      out.write_string(".0")
    }
  } else {
    out.write_string("0.")
    for _ in 0..<(-exp10 - 1) {
      out.write_char('0')
    }
    out.write_string(digits)
  }
}

///|
/// `1e+16`, `2.5e-07` -- a point only when there is more than one digit, a
/// sign always, and at least two exponent digits.
fn write_exponential(out : StringBuilder, digits : String, exp10 : Int) -> Unit {
  out.write_char(digits.get_char(0).unwrap())
  if digits.length() > 1 {
    out.write_char('.')
    out.write_string(digits[1:].to_owned())
  }
  out.write_char('e')
  if exp10 >= 0 {
    out.write_char('+')
  } else {
    out.write_char('-')
  }
  let mag = if exp10 < 0 { -exp10 } else { exp10 }
  let text = mag.to_string()
  if text.length() < 2 {
    out.write_char('0')
  }
  out.write_string(text)
}