// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// Write a tree as strict JSON, whatever dialect it was read from.
///
/// `indent` of zero, the default, writes the compact form with no space
/// anywhere; anything larger writes one line per member with that many spaces a
/// level, and a space after each colon.
///
/// A number that came in as text goes back out as that text, so a document read
/// and written again is unchanged. Only a number with no such text is formatted,
/// and a number that is not finite โ€” which JSON5 can produce and JSON cannot
/// write โ€” becomes `null`, as every JavaScript implementation does.
pub fn write(value : Json, indent? : Int = 0) -> String {
  let out = StringBuilder()
  put(out, value, indent, 0)
  out.to_string()
}

///|
/// The same, as UTF-8 bytes โ€” the form a body goes onto a wire in.
pub fn write_bytes(value : Json, indent? : Int = 0) -> Bytes {
  @utf8.encode(write(value, indent~)[:])
}

///|
fn put(out : StringBuilder, value : Json, indent : Int, level : Int) -> Unit {
  match value {
    Null => out.write_string("null")
    True => out.write_string("true")
    False => out.write_string("false")
    Number(n, repr~) =>
      match repr {
        Some(text) => out.write_string(text)
        None =>
          if n.is_nan() || n.is_inf() {
            out.write_string("null")
          } else {
            out.write_string(n.to_string())
          }
      }
    String(s) => quote(out, s[:])
    Array(items) => {
      if items.length() == 0 {
        out.write_string("[]")
        return
      }
      out.write_char('[')
      for k, item in items {
        if k > 0 {
          out.write_char(',')
        }
        newline(out, indent, level + 1)
        put(out, item, indent, level + 1)
      }
      newline(out, indent, level)
      out.write_char(']')
    }
    Object(members) => {
      if members.length() == 0 {
        out.write_string("{}")
        return
      }
      out.write_char('{')
      let mut first = true
      for name, field in members {
        if !first {
          out.write_char(',')
        }
        first = false
        newline(out, indent, level + 1)
        quote(out, name[:])
        out.write_char(':')
        if indent > 0 {
          out.write_char(' ')
        }
        put(out, field, indent, level + 1)
      }
      newline(out, indent, level)
      out.write_char('}')
    }
  }
}

///|
fn newline(out : StringBuilder, indent : Int, level : Int) -> Unit {
  if indent > 0 {
    out.write_char('\n')
    for _ in 0..<(indent * level) {
      out.write_char(' ')
    }
  }
}

///|
/// A string as RFC 8259 ยง7 writes one.
///
/// Only what must be escaped is: the quote, the backslash, and the control
/// characters. The two-character forms are used where they exist because they
/// are shorter and what a reader expects to see; everything else below U+0020
/// goes out as `\u00XX`.
fn quote(out : StringBuilder, s : StringView) -> Unit {
  out.write_char('"')
  let mut run = 0
  for i in 0.. "\\\""
      0x5C => "\\\\"
      0x08 => "\\b"
      0x0C => "\\f"
      0x0A => "\\n"
      0x0D => "\\r"
      0x09 => "\\t"
      _ => if c < 0x20 { hex_escape(c) } else { "" }
    }
    if escape != "" {
      out.write_view(s[run:i])
      out.write_string(escape)
      run = i + 1
    }
  }
  out.write_view(s[run:])
  out.write_char('"')
}

///|
fn hex_escape(c : Int) -> String {
  let digits = "0123456789abcdef"
  let out = StringBuilder()
  out.write_string("\\u00")
  out.write_char(digits.get_char((c >> 4) & 0xF).unwrap())
  out.write_char(digits.get_char(c & 0xF).unwrap())
  out.to_string()
}