///|
/// PaintNode JSON serialization for native paint renderer.
/// Produces a compact JSON string without external JSON library dependency.
pub fn PaintNode::to_json_string(self : PaintNode) -> String {
  let buf = StringBuilder::new()
  write_paint_node_json(buf, self)
  buf.to_string()
}

///|
fn write_paint_node_json(buf : StringBuilder, node : PaintNode) -> Unit {
  buf.write_string("{\"id\":")
  write_json_string(buf, node.id)
  buf.write_string(",\"tag\":")
  write_json_string(buf, node.tag)
  buf.write_string(",\"x\":")
  write_json_double(buf, node.x)
  buf.write_string(",\"y\":")
  write_json_double(buf, node.y)
  buf.write_string(",\"w\":")
  write_json_double(buf, node.width)
  buf.write_string(",\"h\":")
  write_json_double(buf, node.height)
  buf.write_string(",\"ox\":")
  write_json_string(buf, overflow_to_str(node.overflow_x))
  buf.write_string(",\"oy\":")
  write_json_string(buf, overflow_to_str(node.overflow_y))
  buf.write_string(",\"p\":")
  write_paint_props_json(buf, node.paint)
  match node.text {
    Some(text) => {
      buf.write_string(",\"text\":")
      write_json_string(buf, text)
    }
    None => ()
  }
  buf.write_string(",\"ch\":[")
  for i, child in node.children {
    if i > 0 {
      buf.write_char(',')
    }
    write_paint_node_json(buf, child)
  }
  buf.write_string("]}")
}

///|
fn write_paint_props_json(buf : StringBuilder, p : PaintProperties) -> Unit {
  buf.write_string("{\"op\":")
  write_json_double(buf, p.opacity)
  buf.write_string(",\"c\":")
  write_color_json(buf, p.color)
  buf.write_string(",\"bg\":")
  write_color_json(buf, p.background_color)
  buf.write_string(",\"fs\":")
  write_json_double(buf, p.font_size)
  if p.line_height != p.font_size {
    buf.write_string(",\"lh\":")
    write_json_double(buf, p.line_height)
  }
  if p.letter_spacing != 0.0 {
    buf.write_string(",\"ls\":")
    write_json_double(buf, p.letter_spacing)
  }
  if p.word_spacing != 0.0 {
    buf.write_string(",\"ws\":")
    write_json_double(buf, p.word_spacing)
  }
  if p.font_weight != 400.0 {
    buf.write_string(",\"fw\":")
    write_json_double(buf, p.font_weight)
  }
  if p.is_bold {
    buf.write_string(",\"b\":true")
  }
  if p.font_family != "" {
    buf.write_string(",\"ff\":")
    write_json_string(buf, p.font_family)
  }
  if p.text_decoration_underline {
    buf.write_string(",\"u\":true")
  }
  if p.text_decoration_line_through {
    buf.write_string(",\"lt\":true")
  }
  if p.text_decoration_overline {
    buf.write_string(",\"ov\":true")
  }
  let vis = match p.visibility {
    @style.Visibility::Visible => "visible"
    @style.Visibility::Hidden => "hidden"
    @style.Visibility::Collapse => "collapse"
  }
  if vis != "visible" {
    buf.write_string(",\"vis\":")
    write_json_string(buf, vis)
  }
  if p.border_top_left_radius != 0.0 ||
    p.border_top_right_radius != 0.0 ||
    p.border_bottom_right_radius != 0.0 ||
    p.border_bottom_left_radius != 0.0 {
    buf.write_string(",\"br\":[")
    write_json_double(buf, p.border_top_left_radius)
    buf.write_char(',')
    write_json_double(buf, p.border_top_right_radius)
    buf.write_char(',')
    write_json_double(buf, p.border_bottom_right_radius)
    buf.write_char(',')
    write_json_double(buf, p.border_bottom_left_radius)
    buf.write_char(']')
  }
  if p.border_top_width != 0.0 ||
    p.border_right_width != 0.0 ||
    p.border_bottom_width != 0.0 ||
    p.border_left_width != 0.0 {
    buf.write_string(",\"bw\":[")
    write_json_double(buf, p.border_top_width)
    buf.write_char(',')
    write_json_double(buf, p.border_right_width)
    buf.write_char(',')
    write_json_double(buf, p.border_bottom_width)
    buf.write_char(',')
    write_json_double(buf, p.border_left_width)
    buf.write_char(']')
  }
  if p.border_top_color != @types.Color::transparent() ||
    p.border_right_color != @types.Color::transparent() ||
    p.border_bottom_color != @types.Color::transparent() ||
    p.border_left_color != @types.Color::transparent() {
    buf.write_string(",\"bc\":[")
    write_color_json(buf, p.border_top_color)
    buf.write_char(',')
    write_color_json(buf, p.border_right_color)
    buf.write_char(',')
    write_color_json(buf, p.border_bottom_color)
    buf.write_char(',')
    write_color_json(buf, p.border_left_color)
    buf.write_char(']')
  }
  buf.write_char('}')
}

///|
fn write_color_json(buf : StringBuilder, c : @types.Color) -> Unit {
  buf.write_char('[')
  buf.write_string(c.r.to_string())
  buf.write_char(',')
  buf.write_string(c.g.to_string())
  buf.write_char(',')
  buf.write_string(c.b.to_string())
  buf.write_char(',')
  write_json_double(buf, c.a)
  buf.write_char(']')
}

///|
fn overflow_to_str(o : @types.Overflow) -> String {
  match o {
    @types.Visible => "visible"
    @types.Hidden => "hidden"
    @types.Clip => "clip"
    @types.Scroll => "scroll"
    @types.Auto => "auto"
  }
}

///|
fn write_json_string(buf : StringBuilder, s : String) -> Unit {
  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('"')
}

///|
fn write_json_double(buf : StringBuilder, v : Double) -> Unit {
  if v == 0.0 {
    buf.write_char('0')
    return
  }
  let rounded = (v * 100.0 + 0.5).to_int().to_double() / 100.0
  buf.write_string(rounded.to_string())
}