///|
/// Print a datum the way Racket's `write` would.
///
/// Needed because a `#{...}` escape's raw text is the datum re-printed, not the
/// source read: `#{ foo }` has raw `#{foo}`. Only the shapes an escape actually
/// carries are handled precisely; a flonum falls back to MoonBit's own
/// formatting, which is a known and narrow risk — reproducing Racket's
/// shortest-round-trip printer is a project of its own, and no corpus file puts
/// a flonum in an escape.
fn write_racket(d : @sexp.Datum) -> String {
  let buf = StringBuilder()
  write_racket_into(buf, d)
  buf.to_string()
}

///|
fn write_racket_into(buf : StringBuilder, d : @sexp.Datum) -> Unit {
  match d {
    Sym(name) => write_racket_symbol(buf, name)
    Kw(name) => {
      buf.write_string("#:")
      buf.write_string(name)
    }
    Str(s) => {
      buf.write_char('"')
      for c in s {
        match c {
          '"' => buf.write_string("\\\"")
          '\\' => buf.write_string("\\\\")
          '\n' => buf.write_string("\\n")
          '\r' => buf.write_string("\\r")
          '\t' => buf.write_string("\\t")
          _ => buf.write_char(c)
        }
      }
      buf.write_char('"')
    }
    Bs(b) => {
      buf.write_string("#\"")
      for i in 0..= 0x20 && v < 0x7F && v != 0x22 && v != 0x5C {
          buf.write_char(v.unsafe_to_char())
        } else {
          buf.write_string("\\")
          buf.write_string(v.to_string(radix=8))
        }
      }
      buf.write_char('"')
    }
    Ch(c) => write_racket_char(buf, c)
    Bool_(v) => buf.write_string(if v { "#t" } else { "#f" })
    Void => buf.write_string("#")
    Int_(n) => buf.write_string(n.to_string())
    Rat(n, den) => {
      buf.write_string(n.to_string())
      buf.write_char('/')
      buf.write_string(den.to_string())
    }
    Flo(v) => buf.write_string(v.to_string())
    Nil => buf.write_string("()")
    Pair(_) => {
      buf.write_char('(')
      let mut cur = d
      let mut first = true
      while cur is Pair(head, tail) {
        if !first {
          buf.write_char(' ')
        }
        first = false
        write_racket_into(buf, head)
        cur = tail
      }
      if !(cur is Nil) {
        buf.write_string(" . ")
        write_racket_into(buf, cur)
      }
      buf.write_char(')')
    }
    Vec(xs) => {
      buf.write_string("#(")
      for i in 0.. 0 {
          buf.write_char(' ')
        }
        write_racket_into(buf, xs[i])
      }
      buf.write_char(')')
    }
    Rx(px, pattern) => {
      buf.write_string(if px { "#px" } else { "#rx" })
      write_racket_into(buf, Str(pattern))
    }
    Other(text) => buf.write_string(text)
  }
}

///|
/// Racket's two ways of quoting a symbol, and when it uses which.
///
/// Bars wrap the whole name when it would otherwise read as something else --
/// a number, `.`, the empty symbol -- or when it contains whitespace, a
/// delimiter, or a backslash. Otherwise a bare `|` inside the name is escaped
/// with a backslash and the rest is printed as-is, so `|>` prints as `\|>`
/// rather than `|\||>|`.
fn write_racket_symbol(buf : StringBuilder, name : String) -> Unit {
  if symbol_needs_bars(name) {
    buf.write_char('|')
    for c in name {
      if c == '|' || c == '\\' {
        buf.write_char('\\')
      }
      buf.write_char(c)
    }
    buf.write_char('|')
    return
  }
  for c in name {
    if c == '|' {
      buf.write_char('\\')
    }
    buf.write_char(c)
  }
}

///|
/// Racket's character syntax: a name for the ones that have one, the character
/// itself when it is printable, and a hex escape otherwise.
fn write_racket_char(buf : StringBuilder, c : Char) -> Unit {
  buf.write_string("#\\")
  let u = c.to_int()
  match c {
    '\u{0}' => buf.write_string("nul")
    '\u{7}' => buf.write_string("alarm")
    '\u{8}' => buf.write_string("backspace")
    '\t' => buf.write_string("tab")
    '\n' => buf.write_string("newline")
    '\u{B}' => buf.write_string("vtab")
    '\u{C}' => buf.write_string("page")
    '\r' => buf.write_string("return")
    ' ' => buf.write_string("space")
    '\u{7F}' => buf.write_string("rubout")
    _ =>
      if u < 0x20 {
        buf.write_string("u")
        write_hex_padded(buf, u, 4)
      } else if u > 0xFFFF {
        buf.write_string("U")
        write_hex_padded(buf, u, 8)
      } else {
        buf.write_char(c)
      }
  }
}

///|
/// Uppercase, which is what Racket prints in a character escape: `#\\u001F`,
/// `#\\U0001FFFF`.
fn write_hex_padded(buf : StringBuilder, v : Int, width : Int) -> Unit {
  let hex = v.to_string(radix=16).to_upper()
  for _ in 0..<(width - hex.length()) {
    buf.write_char('0')
  }
  buf.write_string(hex)
}

///|
/// Whether Racket would print this symbol inside `|...|`.
///
/// It does so whenever reading the bare spelling back would not give the same
/// symbol: an empty name, one that looks like a number, or one containing a
/// character the reader treats specially.
fn symbol_needs_bars(name : String) -> Bool {
  if name.length() == 0 {
    return true
  }
  if racket_number(name) is Some(_) {
    return true
  }
  if name == "." {
    return true
  }
  for c in name {
    // `|` is NOT here: Racket escapes it with a backslash instead. `#` is not
    // here either -- `#%module-begin` prints bare.
    if is_racket_delim(c) || c == '\\' {
      return true
    }
  }
  false
}