///|
/// JSON value tree used by the warm-up pretty-printer.
/// Object members keep insertion order, so `Obj` stores an array of pairs
/// instead of a hash map.
pub(all) enum JsonValue {
  Null
  Bool(Bool)
  Number(Double)
  Str(String)
  Arr(Array[JsonValue])
  Obj(Array[(String, JsonValue)])
} derive(Eq)

///|
pub extend JsonValue with Eq::{not_equal, equal}

///|
/// Render a JSON value as indented, human-readable text.
///
/// Behavior mirrors `JSON.stringify(value, null, indent)`:
/// - empty objects/arrays render as `{}` / `[]`
/// - every other container spans multiple lines, one member per line
/// - `indent` is the number of spaces per nesting level (default 2)
pub fn JsonValue::pretty(self : JsonValue, indent~ : Int32 = 2) -> String {
  let buf = StringBuilder()
  write_value(buf, self, indent, 0)
  buf.to_string()
}

///|
fn write_indent(buf : StringBuilder, width : Int32, level : Int32) -> Unit {
  let total = width * level
  for _ in 0.. Unit {
  match value {
    Null => buf.write_string("null")
    Bool(b) => buf.write_string(if b { "true" } else { "false" })
    Number(n) => buf.write_object(n)
    Str(s) => write_escaped(buf, s)
    Arr(items) =>
      if items.is_empty() {
        buf.write_string("[]")
      } else {
        buf.write_char('[')
        for i, item in items {
          if i > 0 {
            buf.write_char(',')
          }
          buf.write_char('\n')
          write_indent(buf, indent, level + 1)
          write_value(buf, item, indent, level + 1)
        }
        buf.write_char('\n')
        write_indent(buf, indent, level)
        buf.write_char(']')
      }
    Obj(members) =>
      if members.is_empty() {
        buf.write_string("{}")
      } else {
        buf.write_char('{')
        for i, (key, elem) in members {
          if i > 0 {
            buf.write_char(',')
          }
          buf.write_char('\n')
          write_indent(buf, indent, level + 1)
          write_escaped(buf, key)
          buf.write_string(": ")
          write_value(buf, elem, indent, level + 1)
        }
        buf.write_char('\n')
        write_indent(buf, indent, level)
        buf.write_char('}')
      }
  }
}

///|
fn write_escaped(buf : StringBuilder, s : String) -> Unit {
  buf.write_char('"')
  for ch in s {
    match ch {
      '"' => buf.write_string("\\\"")
      '\\' => buf.write_string("\\\\")
      '\n' => buf.write_string("\\n")
      '\r' => buf.write_string("\\r")
      '\t' => buf.write_string("\\t")
      '\b' => buf.write_string("\\b")
      _ => {
        let code = ch.to_int()
        // Escape C0 control chars and DEL the way JSON.stringify does.
        if code < 0x20 || code == 0x7F {
          buf.write_string("\\u00")
          buf.write_char(hex_digit(code / 16))
          buf.write_char(hex_digit(code % 16))
        } else {
          buf.write_char(ch)
        }
      }
    }
  }
  buf.write_char('"')
}

///|
fn hex_digit(n : Int32) -> Char {
  let code = if n < 10 { '0'.to_int() + n } else { 'a'.to_int() + n - 10 }
  code.to_char().unwrap()
}