///|
/// Indentation width used when the caller does not ask for a specific one.
pub let default_indent : Int = 2

///|
/// Largest indentation width the formatter accepts.
pub let max_indent : Int = 16

///|
/// Clamp a caller-supplied indent width into the supported `0..=16` range.
pub fn clamp_indent(indent : Int) -> Int {
  if indent < 0 {
    0
  } else if indent > max_indent {
    max_indent
  } else {
    indent
  }
}

///|
/// Render one code point as a four-digit `\uXXXX` escape body.
fn hex4(code : Int) -> String {
  let digits = "0123456789abcdef"
  let table = digits.to_array()
  let buf = StringBuilder()
  for shift in [12, 8, 4, 0] {
    buf.write_char(table[(code >> shift) & 0xF])
  }
  buf.to_string()
}

///|
/// Append `value` to `buf` as the *contents* of a JSON string.
///
/// Quotes and backslashes are escaped, the short escapes are used where one
/// exists, and any remaining control character becomes a `\uXXXX` escape.
pub fn escape_json_string(value : String) -> String {
  let buf = StringBuilder()
  for ch in value {
    match ch {
      '"' => buf.write_string("\\\"")
      '\\' => buf.write_string("\\\\")
      '\n' => buf.write_string("\\n")
      '\r' => buf.write_string("\\r")
      '\t' => buf.write_string("\\t")
      '\u{08}' => buf.write_string("\\b")
      '\u{0C}' => buf.write_string("\\f")
      _ =>
        if ch.to_int() < 0x20 {
          buf.write_string("\\u")
          buf.write_string(hex4(ch.to_int()))
        } else {
          buf.write_char(ch)
        }
    }
  }
  buf.to_string()
}

///|
/// Emit the line break and leading spaces that open a nesting level.
fn break_line(buf : StringBuilder, level : Int, width : Int) -> Unit {
  buf.write_char('\n')
  let spaces = level * width
  for _ in 0.. Unit {
  match json {
    Null => buf.write_string("null")
    Bool(value~) => buf.write_string(if value { "true" } else { "false" })
    // The parser keeps the original literal, so `1e2` and `1.0` round-trip
    // exactly instead of being reformatted through a Double.
    Number(raw~) => buf.write_string(raw)
    Text(value~) => {
      buf.write_char('"')
      buf.write_string(escape_json_string(value))
      buf.write_char('"')
    }
    Array(items~) =>
      if items.is_empty() {
        buf.write_string("[]")
      } else {
        buf.write_char('[')
        let mut first = true
        for item in items {
          if !first {
            buf.write_char(',')
          }
          first = false
          break_line(buf, level + 1, width)
          write_json(buf, item, level + 1, width)
        }
        break_line(buf, level, width)
        buf.write_char(']')
      }
    Object(members~) =>
      if members.is_empty() {
        buf.write_string("{}")
      } else {
        buf.write_char('{')
        let mut first = true
        for entry in members {
          let (key, value) = entry
          if !first {
            buf.write_char(',')
          }
          first = false
          break_line(buf, level + 1, width)
          buf.write_char('"')
          buf.write_string(escape_json_string(key))
          buf.write_string("\": ")
          write_json(buf, value, level + 1, width)
        }
        break_line(buf, level, width)
        buf.write_char('}')
      }
  }
}

///|
/// Render `json` as indented, human-readable JSON text.
///
/// `indent` is the number of spaces per nesting level and is clamped to
/// `0..=16`. An indent of `0` keeps the line breaks but drops the leading
/// spaces. Empty arrays and objects are rendered inline as `[]` and `{}`.
pub fn format(json : @pjson.Json, indent : Int) -> String {
  let width = clamp_indent(indent)
  let buf = StringBuilder()
  write_json(buf, json, 0, width)
  buf.to_string()
}

///|
/// Write `json` to `buf` as one unbroken line.
///
/// No line break and no padding space is emitted anywhere, including after the
/// colon that separates an object key from its value. The only spaces that
/// survive are the ones inside string contents, which are data and are escaped
/// exactly as the indented form escapes them.
fn write_compact(buf : StringBuilder, json : @pjson.Json) -> Unit {
  match json {
    Null => buf.write_string("null")
    Bool(value~) => buf.write_string(if value { "true" } else { "false" })
    Number(raw~) => buf.write_string(raw)
    Text(value~) => {
      buf.write_char('"')
      buf.write_string(escape_json_string(value))
      buf.write_char('"')
    }
    // An empty array or object writes its two brackets and nothing between
    // them, so `[]` and `{}` need no special case here.
    Array(items~) => {
      buf.write_char('[')
      let mut first = true
      for item in items {
        if !first {
          buf.write_char(',')
        }
        first = false
        write_compact(buf, item)
      }
      buf.write_char(']')
    }
    Object(members~) => {
      buf.write_char('{')
      let mut first = true
      for entry in members {
        let (key, value) = entry
        if !first {
          buf.write_char(',')
        }
        first = false
        buf.write_char('"')
        buf.write_string(escape_json_string(key))
        buf.write_string("\":")
        write_compact(buf, value)
      }
      buf.write_char('}')
    }
  }
}

///|
/// Render `json` on a single line, with no padding spaces.
///
/// The result parses back to the same document as `format(json, indent)`; only
/// the layout differs. Callers that ask for this are usually piping the output
/// somewhere a line break would be noise, so the indent width has no effect.
pub fn format_compact(json : @pjson.Json) -> String {
  let buf = StringBuilder()
  write_compact(buf, json)
  buf.to_string()
}

///|
/// Return `json` with the members of every object ordered by key.
///
/// Keys are compared by code point — `compare_text`, the same comparison
/// `--sort-by` orders values with — so ASCII keys come out the way a dictionary
/// would list them, and upper case sorts before lower case. A key that is the
/// start of another one sorts first, and nothing is decided by how long a key
/// is: `"apple"` comes before `"pear"` because `a` is before `p`, where a
/// comparison that looked at the lengths first would have put `"pear"` first.
/// The ordering reaches every object in the document, including the ones nested
/// inside arrays.
///
/// The argument is not modified. Each object is rebuilt around a fresh array
/// rather than sorted in place, so a caller that still holds the original tree
/// keeps seeing the document in the order it was written.
pub fn sort_keys(json : @pjson.Json) -> @pjson.Json {
  match json {
    Object(members~) => {
      let rebuilt = members.map(fn(entry) {
        let (key, value) = entry
        (key, sort_keys(value))
      })
      rebuilt.sort_by(fn(left, right) {
        let (left_key, _) = left
        let (right_key, _) = right
        compare_text(left_key, right_key)
      })
      Object(members=rebuilt)
    }
    Array(items~) => Array(items=items.map(fn(item) { sort_keys(item) }))
    // Everything else is a leaf: a scalar holds no keys to order.
    leaf => leaf
  }
}

///|
/// Return `json` with the leading and trailing whitespace removed from every
/// string in it.
///
/// The whitespace removed is the set `String::trim` defines — space, tab,
/// newline and carriage return — which is exactly the set JSON itself treats as
/// whitespace between tokens. Only the ends of a string are touched, so spaces
/// inside a value are part of it and stay: `"  a  b  "` becomes `"a  b"`.
///
/// Keys are left alone. A key is a name rather than a value, and trimming one
/// would rename it: two keys differing only in the spaces around them would
/// collapse into the repeated key the parser refuses, turning a document that
/// parsed into one that would not.
///
/// The argument is not modified, for the same reason `sort_keys` is not: the
/// containers are rebuilt, so a caller still holding the original tree keeps
/// seeing the document as it was written.
pub fn trim_strings(json : @pjson.Json) -> @pjson.Json {
  match json {
    Text(value~) => Text(value=value.trim().to_owned())
    Object(members~) => {
      let rebuilt = members.map(fn(entry) {
        let (key, value) = entry
        (key, trim_strings(value))
      })
      Object(members=rebuilt)
    }
    Array(items~) => Array(items=items.map(fn(item) { trim_strings(item) }))
    // Everything else is a leaf: a number keeps the literal it was written
    // with, and there is nothing in a null or a boolean to trim.
    leaf => leaf
  }
}