///|
/// Racket's `write` for a flonum.
///
/// MoonBit's own `to_string` already produces the shortest round-tripping
/// digits, and agrees with Racket on every value in the corpus. What it does
/// NOT agree on is presentation: Racket forces a `.0` on an integral value,
/// switches to exponent notation outside `[1e-4, 1e14)`, and writes the
/// exponent's sign. So the digits are taken from MoonBit and laid out again
/// here.
pub fn double_to_string(d : Double) -> String {
  if d != d {
    return "+nan.0"
  }
  if d == @double.infinity {
    return "+inf.0"
  }
  if d == @double.neg_infinity {
    return "-inf.0"
  }
  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 Racket writes it as `-0.0`. So the sign
  // is decided separately from whether there is one to strip.
  let negative = signed_text || (d == 0.0 && d.reinterpret_as_uint64() != 0UL)
  let body = if signed_text {
    text.clamped_view(start=1).to_owned()
  } else {
    text
  }
  let (digits, exp10) = decompose(body)
  let out = StringBuilder()
  if negative {
    out.write_char('-')
  }
  if digits == "0" {
    // Racket writes negative zero as `-0.0`; the sign is part of the value.
    out.write_string("0.0")
    return out.to_string()
  }
  if exp10 >= -4 && exp10 <= 13 {
    write_positional(out, digits, exp10)
  } else {
    write_exponential(out, digits, exp10)
  }
  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.clamped_view(end=i).to_owned()
      let tail = body.clamped_view(start=i + 1).to_owned()
      exp = parse_signed_int(tail)
    }
    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.clamped_view(start=lead).to_owned()
  // Trailing zeros are not significant either.
  let mut end = digits.length()
  while end > 1 && digits.get_char(end - 1) is Some('0') {
    end = end - 1
  }
  digits = digits.clamped_view(end~).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 at least one digit after.
fn write_positional(out : StringBuilder, digits : String, exp10 : Int) -> Unit {
  if exp10 >= 0 {
    let int_len = exp10 + 1
    for i in 0.. int_len {
      out.write_string(digits.clamped_view(start=int_len).to_owned())
    } else {
      out.write_char('0')
    }
  } else {
    out.write_string("0.")
    for _ in 0..<(-exp10 - 1) {
      out.write_char('0')
    }
    out.write_string(digits)
  }
}

///|
/// `1e+14`, `1.5e-7` — a point only when there is more than one digit.
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.clamped_view(start=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 }
  out.write_string(mag.to_string())
}

///|
/// Racket's `write` for a string.
///
/// Named escapes where there is one, `\uXXXX` for the other non-printing
/// characters, and everything else as itself -- including non-ASCII, which
/// Racket does not escape.
pub fn string_to_string(s : String) -> String {
  let out = StringBuilder()
  out.write_char('"')
  for c in s {
    let u = c.to_int()
    match c {
      '"' => out.write_string("\\\"")
      '\\' => out.write_string("\\\\")
      '\u{7}' => out.write_string("\\a")
      '\u{8}' => out.write_string("\\b")
      '\t' => out.write_string("\\t")
      '\n' => out.write_string("\\n")
      '\u{B}' => out.write_string("\\v")
      '\u{C}' => out.write_string("\\f")
      '\r' => out.write_string("\\r")
      '\u{1B}' => out.write_string("\\e")
      _ =>
        if @unicode.is_string_literal_plain(c) {
          out.write_char(c)
        } else if u > 0xFFFF {
          out.write_string("\\U")
          out.write_string(pad_upper_hex(u, 8))
        } else {
          out.write_string("\\u")
          out.write_string(pad_upper_hex(u, 4))
        }
    }
  }
  out.write_char('"')
  out.to_string()
}

///|
fn pad_upper_hex(v : Int, width : Int) -> String {
  let hex = v.to_string(radix=16).to_upper()
  let out = StringBuilder()
  for _ in 0..<(width - hex.length()) {
    out.write_char('0')
  }
  out.write_string(hex)
  out.to_string()
}

///|
/// Racket's `write` for a byte string.
///
/// Octal for anything not printable, and padded to three digits only when the
/// next character is an octal digit -- `#"\0001"` is byte zero then `1`, which
/// `#"\01"` would not be.
pub fn bytes_to_string(b : Bytes) -> String {
  let out = StringBuilder()
  out.write_string("#\"")
  for i in 0..= 48 &&
      b[i + 1].to_int() <= 55
    match v {
      0x22 => out.write_string("\\\"")
      0x5C => out.write_string("\\\\")
      0x07 => out.write_string("\\a")
      0x08 => out.write_string("\\b")
      0x09 => out.write_string("\\t")
      0x0A => out.write_string("\\n")
      0x0B => out.write_string("\\v")
      0x0C => out.write_string("\\f")
      0x0D => out.write_string("\\r")
      0x1B => out.write_string("\\e")
      _ =>
        if v >= 0x20 && v < 0x7F {
          out.write_char(v.unsafe_to_char())
        } else {
          out.write_char('\\')
          let oct = v.to_string(radix=8)
          if next_is_octal {
            for _ in 0..<(3 - oct.length()) {
              out.write_char('0')
            }
          }
          out.write_string(oct)
        }
    }
  }
  out.write_char('"')
  out.to_string()
}