///|
pub fn render_value_as_json(value : Value) -> String {
  render_value_as_json_with_indent(value, "  ")
}

///|
pub fn render_value_as_json_with_indent(
  value : Value,
  indent_text : String,
) -> String {
  let buf = StringBuilder::new()
  render_json_value(value, 0, indent_text, buf)
  buf.to_string()
}

///|
pub fn render_value_as_json_document(value : Value) -> String {
  render_value_as_json_document_with_indent(value, "  ")
}

///|
pub fn render_value_as_json_document_with_indent(
  value : Value,
  indent_text : String,
) -> String {
  render_value_as_json_with_indent(value, indent_text) + "\n"
}

///|
fn render_json_value(
  value : Value,
  indent : Int,
  indent_text : String,
  buf : StringBuilder,
) -> Unit {
  match render_directive_text(value) {
    Some(text) => {
      buf.write_string(text)
      return
    }
    None => ()
  }
  match value {
    ThunkValue(_) =>
      render_json_value(force_eval_thunk(value), indent, indent_text, buf)
    IntValue(n) => buf.write_string("\{n}")
    FloatValue(d) => buf.write_string(render_float_text(d))
    BoolValue(true) => buf.write_string("true")
    BoolValue(false) => buf.write_string("false")
    NullValue => buf.write_string("null")
    DeferredImportValue(_) => buf.write_string("null")
    StringValue(s) => render_json_string(s, buf)
    ObjectValue(members) =>
      match dynamic_listing_elements_from_members(members) {
        Some(elements) => render_json_array(elements, indent, indent_text, buf)
        None =>
          match dynamic_mapping_entries_from_members(members) {
            Some(entries) =>
              render_json_object_from_entries(entries, indent, indent_text, buf)
            None =>
              render_json_object_from_members(members, indent, indent_text, buf)
          }
      }
    MappingValue(entries) | DefaultedMappingValue(_, entries, _) =>
      render_json_object_from_entries(entries, indent, indent_text, buf)
    ListingValue(elements)
    | DefaultedListingValue(_, elements, _)
    | ListValue(elements) =>
      render_json_array(elements, indent, indent_text, buf)
    FunctionValue(_, _, _, _, _) => buf.write_string("null")
    // Upstream Pkl projects Duration / DataSize to a literal-style string
    // (e.g. `"5.min"`) in JSON / YAML / Properties. Match that shape until
    // the dedicated projection table from `pkl:base` lands.
    DurationValue(n, unit) => render_json_string("\{n}.\{unit}", buf)
    DataSizeValue(n, unit) => render_json_string("\{n}.\{unit}", buf)
    // PKL-081: Regex projects as the pattern string in JSON / YAML /
    // Properties, mirroring upstream Apple Pkl behavior for opaque
    // stdlib values.
    RegexValue(pattern) => render_json_string(pattern, buf)
    // PKL-083: Bytes projects as its base64 encoding in non-PCF
    // formats, since none of these formats have a native bytes type
    // and base64 is the upstream-compatible representation.
    BytesValue(bytes) => render_json_string(@base64.encode(bytes[:]), buf)
    // PKL-119a: Pair renders as a 2-element JSON array. Matches
    // Apple Pkl's `Pair`-to-JSON projection.
    PairValue(first, second) =>
      render_json_array([first, second], indent, indent_text, buf)
    // PKL-119b: IntSeq materializes to an Int array first, then flows
    // through the standard JSON array projection.
    IntSeqValue(start, end_v, step) =>
      render_json_array(
        intseq_materialize(start, end_v, step),
        indent,
        indent_text,
        buf,
      )
    // PKL-119c: Set renders as a JSON array. Uniqueness is already
    // enforced at construction, so the array element order matches
    // the upstream insertion-order semantics.
    SetValue(elements) => render_json_array(elements, indent, indent_text, buf)
    // PKL-119d: Map projects as a JSON object, matching the
    // existing MappingValue projection. The entry-walker coerces
    // non-string keys the same way `json_coerce_key` already does.
    MapValue(entries) =>
      render_json_object_from_entries(entries, indent, indent_text, buf)
  }
}

///|
fn dynamic_listing_elements_from_members(
  members : Array[ValueMember],
) -> Array[Value]? {
  let elements : Array[Value] = []
  for field in members {
    if is_invisible_member_name(field.name) {
      continue
    }
    if !field.name.has_prefix("@element$") {
      return None
    }
    elements.push(force_eval_thunk(field.value))
  }
  if elements.length() > 0 {
    Some(elements)
  } else {
    None
  }
}

///|
fn render_json_string(value : String, buf : StringBuilder) -> Unit {
  buf.write_char('"')
  for c in value {
    match c {
      '"' => 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 c < ' ' {
          let code = c.to_int()
          let hex = json_hex4(code)
          buf.write_string("\\u")
          buf.write_string(hex)
        } else {
          buf.write_char(c)
        }
    }
  }
  buf.write_char('"')
}

///|
fn json_hex4(code : Int) -> String {
  let digits = "0123456789abcdef"
  let buf = StringBuilder::new()
  let mut i = 12
  while i >= 0 {
    let nibble = (code >> i) & 0xF
    buf.write_char(digits.unsafe_get(nibble).to_int().unsafe_to_char())
    i = i - 4
  }
  buf.to_string()
}

///|
fn json_write_key(value : Value, buf : StringBuilder) -> Unit {
  match render_directive_text(value) {
    Some(text) => buf.write_string(text)
    None => render_json_string(json_coerce_key(value), buf)
  }
}

///|
fn json_coerce_key(value : Value) -> String {
  match value {
    StringValue(s) => s
    IntValue(n) => "\{n}"
    BoolValue(true) => "true"
    BoolValue(false) => "false"
    NullValue => "null"
    _ => {
      let buf = StringBuilder::new()
      render_pcf_inline(value, 0, false, buf)
      buf.to_string()
    }
  }
}

///|
fn write_json_indent(
  buf : StringBuilder,
  indent : Int,
  indent_text : String,
) -> Unit {
  for i = 0; i < indent; i = i + 1 {
    buf.write_string(indent_text)
  }
}

///|
fn render_json_object_from_members(
  members : Array[ValueMember],
  indent : Int,
  indent_text : String,
  buf : StringBuilder,
) -> Unit {
  let visible = visible_members(members)
  if visible.length() == 0 {
    buf.write_string("{}")
    return
  }
  buf.write_string("{\n")
  for i = 0; i < visible.length(); i = i + 1 {
    if i > 0 {
      buf.write_string(",\n")
    }
    write_json_indent(buf, indent + 1, indent_text)
    render_json_string(visible[i].name, buf)
    buf.write_string(": ")
    render_json_value(visible[i].value, indent + 1, indent_text, buf)
  }
  buf.write_char('\n')
  write_json_indent(buf, indent, indent_text)
  buf.write_char('}')
}

///|
fn render_json_object_from_entries(
  entries : Array[ValueEntry],
  indent : Int,
  indent_text : String,
  buf : StringBuilder,
) -> Unit {
  if entries.length() == 0 {
    buf.write_string("{}")
    return
  }
  buf.write_string("{\n")
  for i = 0; i < entries.length(); i = i + 1 {
    if i > 0 {
      buf.write_string(",\n")
    }
    write_json_indent(buf, indent + 1, indent_text)
    json_write_key(entries[i].key, buf)
    buf.write_string(": ")
    render_json_value(entries[i].value, indent + 1, indent_text, buf)
  }
  buf.write_char('\n')
  write_json_indent(buf, indent, indent_text)
  buf.write_char('}')
}

///|
fn render_json_array(
  elements : Array[Value],
  indent : Int,
  indent_text : String,
  buf : StringBuilder,
) -> Unit {
  if elements.length() == 0 {
    buf.write_string("[]")
    return
  }
  buf.write_string("[\n")
  for i = 0; i < elements.length(); i = i + 1 {
    if i > 0 {
      buf.write_string(",\n")
    }
    write_json_indent(buf, indent + 1, indent_text)
    render_json_value(elements[i], indent + 1, indent_text, buf)
  }
  buf.write_char('\n')
  write_json_indent(buf, indent, indent_text)
  buf.write_char(']')
}

///|
/// Render a Value as YAML, mirroring the basic shape of `pkl eval -f yaml`.
///
/// Block style is used throughout: mappings emit `key: value` per line,
/// listings emit `- value` per line at the same column as the parent key, and
/// empty composites use flow form (`[]` / `{}`). String scalars are bare when
/// they parse as plain YAML, single-quoted when they collide with reserved
/// shapes (numeric / keyword / leading indicator / inline `: ` / ` #`), and
/// double-quoted with `\`-escapes when they contain control characters or a
/// backslash. Block scalars (`|`, `|-`, `|+`) for multiline strings remain
/// follow-up work.
///
/// Indentation is fixed at two spaces; Apple Pkl's `indentWidth` knob lives
/// inside the `YamlRenderer` stdlib class (PKL-079 expansion), which our
/// pipeline does not consume yet.