///|
/// Python's `repr` of a string.
///
/// It lives here, below everything, because three consumers need the same
/// escaping: the token dump, the AST dump and the runtime's `repr` of a value.
/// One implementation, one set of rules:
///
///   * The quote is `'`, unless the string contains a `'` and no `"`.
///   * `\\`, `\n`, `\r`, `\t` and the chosen quote are escaped by name; the
///     OTHER quote never is.
///   * A code point Python calls unprintable becomes `\xNN`, `\uNNNN` or
///     `\UNNNNNNNN`, by size.
///
/// Printability is exact below U+0100 -- the control ranges, U+007F, the C1
/// block, U+00A0 and U+00AD -- and everything above is treated as printable.
/// That is decision 8 of the plan: the exact answer is the Unicode general
/// category tables, the conformance suite is ASCII, and the difference shows
/// only on a format character or an unassigned code point in a string that a
/// program prints back out.
pub fn py_repr(s : String) -> String {
  let has_single = s.contains_char('\'')
  let has_double = s.contains_char('"')
  let quote = if has_single && !has_double { '"' } else { '\'' }
  let out = StringBuilder()
  out.write_char(quote)
  for c in s {
    match c {
      '\\' => out.write_string("\\\\")
      '\n' => out.write_string("\\n")
      '\r' => out.write_string("\\r")
      '\t' => out.write_string("\\t")
      _ =>
        if c == quote {
          out.write_char('\\')
          out.write_char(c)
        } else if is_printable(c) {
          out.write_char(c)
        } else {
          out.write_string(escape_code_point(c.to_int()))
        }
    }
  }
  out.write_char(quote)
  out.to_string()
}

///|
/// Python's `str.isprintable`, exactly below U+0100 and optimistically above.
pub fn is_printable(c : Char) -> Bool {
  let n = c.to_int()
  if n < 0x20 {
    false
  } else if n < 0x7F {
    true
  } else if n <= 0xA0 {
    // U+007F and the C1 controls, and U+00A0 (a separator, which `isprintable`
    // excludes even though it is not a control).
    false
  } else if n == 0xAD {
    false
  } else {
    true
  }
}

///|
/// `\xNN`, `\uNNNN` or `\UNNNNNNNN`, by size, lowercase as Python writes them.
fn escape_code_point(n : Int) -> String {
  if n < 0x100 {
    "\\x" + hex_digits(n, 2)
  } else if n < 0x10000 {
    "\\u" + hex_digits(n, 4)
  } else {
    "\\U" + hex_digits(n, 8)
  }
}

///|
/// `n` in lowercase hexadecimal, zero-padded to `width`.
fn hex_digits(n : Int, width : Int) -> String {
  let digits = "0123456789abcdef".to_array()
  let out = StringBuilder()
  for k = width - 1; k >= 0; k = k - 1 {
    out.write_char(digits[(n >> (k * 4)) & 0xF])
  }
  out.to_string()
}

///|
/// Python's `repr` of a bytes object.
///
/// The same shape as a string's, with two differences: the `b` prefix, and
/// "printable" meaning printable ASCII -- every byte outside `0x20..0x7E` is
/// `\xNN`, whatever a code point of that value would be.
pub fn py_bytes_repr(b : Bytes) -> String {
  let mut has_single = false
  let mut has_double = false
  for byte in b {
    if byte == b'\'' {
      has_single = true
    }
    if byte == b'"' {
      has_double = true
    }
  }
  let quote = if has_single && !has_double { '"' } else { '\'' }
  let out = StringBuilder()
  out.write_string("b")
  out.write_char(quote)
  for byte in b {
    let n = byte.to_int()
    if n == 0x5C {
      out.write_string("\\\\")
    } else if n == 0x0A {
      out.write_string("\\n")
    } else if n == 0x0D {
      out.write_string("\\r")
    } else if n == 0x09 {
      out.write_string("\\t")
    } else if n == quote.to_int() {
      out.write_char('\\')
      out.write_char(quote)
    } else if n >= 0x20 && n < 0x7F {
      out.write_char(n.unsafe_to_char())
    } else {
      out.write_string("\\x" + hex_digits(n, 2))
    }
  }
  out.write_char(quote)
  out.to_string()
}

///|
/// Four lowercase hexadecimal digits, for a JSON escape or a code point.
pub fn hex4(n : Int) -> String {
  hex_digits(n, 4)
}