///|
pub fn render_value_as_plist(value : Value) -> String {
  let buf = StringBuilder::new()
  buf.write_string("\n")
  buf.write_string(
    "\n",
  )
  buf.write_string("\n")
  render_plist_value(value, 0, buf)
  buf.write_char('\n')
  buf.write_string("\n")
  buf.to_string()
}

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

///|
fn render_plist_value(value : Value, indent : Int, buf : StringBuilder) -> Unit {
  // PKL-153f: directive short-circuit emits the `text` verbatim into
  // the plist body — Apple Pkl bypasses the usual `` /
  // `` wrapping for RenderDirective.
  match render_directive_text(value) {
    Some(text) => {
      write_plist_indent(buf, indent)
      buf.write_string(text)
      return
    }
    None => ()
  }
  match value {
    ThunkValue(_) => render_plist_value(force_eval_thunk(value), indent, buf)
    IntValue(n) => {
      write_plist_indent(buf, indent)
      buf.write_string("\{n}")
    }
    FloatValue(d) => {
      write_plist_indent(buf, indent)
      buf.write_string("\{render_float_text(d)}")
    }
    BoolValue(true) => {
      write_plist_indent(buf, indent)
      buf.write_string("")
    }
    BoolValue(false) => {
      write_plist_indent(buf, indent)
      buf.write_string("")
    }
    NullValue => {
      // Top-level Null lands here too — emit an empty ``
      // shape so the output stays valid plist XML. Object / array
      // contexts intercept Null earlier and elide / error per the
      // upstream rule.
      write_plist_indent(buf, indent)
      buf.write_string("")
    }
    DeferredImportValue(_) => {
      write_plist_indent(buf, indent)
      buf.write_string("")
    }
    StringValue(s) => {
      write_plist_indent(buf, indent)
      buf.write_string("")
      write_plist_escaped(s, buf)
      buf.write_string("")
    }
    ObjectValue(members) => render_plist_dict_from_members(members, indent, buf)
    MappingValue(entries) | DefaultedMappingValue(_, entries, _) =>
      render_plist_dict_from_entries(entries, indent, buf)
    ListingValue(elements)
    | DefaultedListingValue(_, elements, _)
    | ListValue(elements) => render_plist_array(elements, indent, buf)
    FunctionValue(_, _, _, _, _) => {
      write_plist_indent(buf, indent)
      buf.write_string("")
    }
    DurationValue(n, unit) => {
      write_plist_indent(buf, indent)
      buf.write_string("\{n} \{unit}")
    }
    DataSizeValue(n, unit) => {
      write_plist_indent(buf, indent)
      buf.write_string("\{n} \{unit}")
    }
    RegexValue(pattern) => {
      write_plist_indent(buf, indent)
      buf.write_string("")
      write_plist_escaped(pattern, buf)
      buf.write_string("")
    }
    BytesValue(bytes) => {
      write_plist_indent(buf, indent)
      buf.write_string("\{@base64.encode(bytes[:])}")
    }
    // PKL-119a: Pair renders as a 2-element ``; reuse the
    // listing path so element indent / null handling stay consistent
    // with sibling collections.
    PairValue(first, second) => render_plist_array([first, second], indent, buf)
    // PKL-119b: IntSeq materializes and reuses the plist ``
    // path so the rendered shape matches a hand-written Listing of
    // the same elements.
    IntSeqValue(start, end_v, step) =>
      render_plist_array(intseq_materialize(start, end_v, step), indent, buf)
    // PKL-119c: Set reuses the same `` path.
    SetValue(elements) => render_plist_array(elements, indent, buf)
    // PKL-119d: Map reuses the mapping-entries `` path.
    MapValue(entries) => render_plist_dict_from_entries(entries, indent, buf)
  }
}

///|
fn render_plist_dict_from_members(
  members : Array[ValueMember],
  indent : Int,
  buf : StringBuilder,
) -> Unit {
  let visible = visible_members(members)
  // Apple's `omitNullProperties` default is `true`; skip Null entries
  // before the dict envelope so an all-Null object renders as an
  // empty ``.
  let kept : Array[ValueMember] = []
  for field in visible {
    if !(field.value is NullValue) {
      kept.push(field)
    }
  }
  write_plist_indent(buf, indent)
  if kept.length() == 0 {
    buf.write_string("")
    return
  }
  buf.write_string("")
  for field in kept {
    buf.write_char('\n')
    write_plist_indent(buf, indent + 2)
    buf.write_string("")
    write_plist_escaped(field.name, buf)
    buf.write_string("")
    buf.write_char('\n')
    render_plist_value(field.value, indent + 2, buf)
  }
  buf.write_char('\n')
  write_plist_indent(buf, indent)
  buf.write_string("")
}

///|
fn render_plist_dict_from_entries(
  entries : Array[ValueEntry],
  indent : Int,
  buf : StringBuilder,
) -> Unit {
  let kept : Array[ValueEntry] = []
  for entry in entries {
    if !(entry.value is NullValue) {
      kept.push(entry)
    }
  }
  write_plist_indent(buf, indent)
  if kept.length() == 0 {
    buf.write_string("")
    return
  }
  buf.write_string("")
  for entry in kept {
    buf.write_char('\n')
    write_plist_indent(buf, indent + 2)
    buf.write_string("")
    write_plist_escaped(plist_coerce_key(entry.key), buf)
    buf.write_string("")
    buf.write_char('\n')
    render_plist_value(entry.value, indent + 2, buf)
  }
  buf.write_char('\n')
  write_plist_indent(buf, indent)
  buf.write_string("")
}

///|
fn render_plist_array(
  elements : Array[Value],
  indent : Int,
  buf : StringBuilder,
) -> Unit {
  write_plist_indent(buf, indent)
  if elements.length() == 0 {
    buf.write_string("")
    return
  }
  buf.write_string("")
  for element in elements {
    if element is NullValue {
      continue
    }
    buf.write_char('\n')
    render_plist_value(element, indent + 2, buf)
  }
  buf.write_char('\n')
  write_plist_indent(buf, indent)
  buf.write_string("")
}

///|
fn write_plist_indent(buf : StringBuilder, indent : Int) -> Unit {
  for i = 0; i < indent; i = i + 1 {
    buf.write_char(' ')
  }
}

///|
fn write_plist_escaped(value : String, buf : StringBuilder) -> Unit {
  for c in value {
    match c {
      '&' => buf.write_string("&")
      '<' => buf.write_string("<")
      '>' => buf.write_string(">")
      _ => buf.write_char(c)
    }
  }
}

///|
fn plist_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()
    }
  }
}

///|
/// Render a Value as a Java Properties document, mirroring the shape of
/// `pkl eval -f properties`.
///
/// Top-level Object / Mapping members flatten into dotted keys. Scalars emit
/// as `key = value` lines (unquoted, with property-style escaping for `\`,
/// `\n`, `\t`, `\r`, `\f`, leading space, `:`, `=`, `!`, `#`). Null leaves
/// are omitted (Apple Pkl's `omitNullProperties` default is `true`). Listings
/// render as a compact JSON-style single-line value (`[1,2,3]`), and the
/// JSON form's own `:` separators are then property-escaped to `\:`. Empty
/// Object / Mapping leaves are dropped entirely; empty Listings still emit
/// `key = []`. Top-level non-mapping values produce an empty document.
///
/// Apple Pkl's `restrictCharset = true` mode (emit `\uXXXX` for non-ASCII)
/// remains a follow-up — the default is `false` here, so unicode passes
/// through verbatim.