///|
pub fn render_value_as_properties(value : Value) -> String {
  // PKL-153f: a `RenderDirective` at the document root short-circuits
  // the rest of the renderer and emits its `text` verbatim, mirroring
  // Apple Pkl's behaviour across PCF / JSON / YAML / Properties.
  match render_directive_text(value) {
    Some(text) => return text
    None => ()
  }
  let buf = StringBuilder::new()
  match value {
    ObjectValue(members) => render_properties_members(members, "", buf)
    MappingValue(entries) | DefaultedMappingValue(_, entries, _) =>
      render_properties_entries(entries, "", buf)
    MapValue(entries) => render_properties_entries(entries, "", buf)
    _ => ()
  }
  buf.to_string()
}

///|
pub fn render_value_as_properties_fragment(value : Value) -> String {
  // `renderValue(directive)` returns the directive's `text` directly;
  // any other value passes through the scalar pipe.
  match render_directive_text(value) {
    Some(text) => return text
    None => ()
  }
  let buf = StringBuilder::new()
  write_properties_scalar(value, buf)
  buf.to_string()
}

///|
fn render_properties_members(
  members : Array[ValueMember],
  prefix : String,
  buf : StringBuilder,
) -> Unit {
  for field in visible_members(members) {
    render_properties_leaf(prefix, field.name, field.value, buf)
  }
}

///|
fn render_properties_entries(
  entries : Array[ValueEntry],
  prefix : String,
  buf : StringBuilder,
) -> Unit {
  for entry in entries {
    let key = properties_coerce_key(entry.key)
    render_properties_leaf(prefix, key, entry.value, buf)
  }
}

///|
fn render_properties_leaf(
  prefix : String,
  name : String,
  value : Value,
  buf : StringBuilder,
) -> Unit {
  let full_key = if prefix == "" { name } else { prefix + "." + name }
  // PKL-153f: a RenderDirective in property position emits its `text`
  // raw on the value side (no value escaping / quoting), but Apple
  // Pkl keeps the `=` separator between key and the directive payload.
  match render_directive_text(value) {
    Some(text) => {
      write_properties_key(full_key, buf)
      buf.write_string(" = ")
      buf.write_string(text)
      buf.write_char('\n')
      return
    }
    None => ()
  }
  match value {
    NullValue => ()
    ObjectValue(members) =>
      if members.length() == 0 {
        // Empty Object has no leaves, so drop the key entirely.
      } else {
        render_properties_members(members, full_key, buf)
      }
    MappingValue(entries) | DefaultedMappingValue(_, entries, _) =>
      if entries.length() == 0 {
        // Empty Mapping likewise has no leaves.
      } else {
        render_properties_entries(entries, full_key, buf)
      }
    ListingValue(_)
    | DefaultedListingValue(_, _, _)
    | FunctionValue(_, _, _, _, _) => {
      let compact = render_properties_compact_json(value)
      write_properties_key(full_key, buf)
      buf.write_string(" = ")
      write_properties_value(compact, buf)
      buf.write_char('\n')
    }
    _ => {
      write_properties_key(full_key, buf)
      buf.write_string(" = ")
      write_properties_scalar(value, buf)
      buf.write_char('\n')
    }
  }
}

///|
fn render_properties_compact_json(value : Value) -> String {
  let buf = StringBuilder::new()
  write_compact_json_value(value, buf)
  buf.to_string()
}

///|
fn write_compact_json_value(value : Value, buf : StringBuilder) -> Unit {
  match value {
    ThunkValue(_) => write_compact_json_value(force_eval_thunk(value), 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) => {
      let visible = visible_members(members)
      buf.write_char('{')
      for i = 0; i < visible.length(); i = i + 1 {
        if i > 0 {
          buf.write_char(',')
        }
        render_json_string(visible[i].name, buf)
        buf.write_char(':')
        write_compact_json_value(visible[i].value, buf)
      }
      buf.write_char('}')
    }
    MappingValue(entries) | DefaultedMappingValue(_, entries, _) => {
      buf.write_char('{')
      for i = 0; i < entries.length(); i = i + 1 {
        if i > 0 {
          buf.write_char(',')
        }
        render_json_string(json_coerce_key(entries[i].key), buf)
        buf.write_char(':')
        write_compact_json_value(entries[i].value, buf)
      }
      buf.write_char('}')
    }
    ListingValue(elements)
    | DefaultedListingValue(_, elements, _)
    | ListValue(elements) => {
      buf.write_char('[')
      for i = 0; i < elements.length(); i = i + 1 {
        if i > 0 {
          buf.write_char(',')
        }
        write_compact_json_value(elements[i], buf)
      }
      buf.write_char(']')
    }
    FunctionValue(_, _, _, _, _) => buf.write_string("null")
    DurationValue(n, unit) => render_json_string("\{n}.\{unit}", buf)
    DataSizeValue(n, unit) => render_json_string("\{n}.\{unit}", buf)
    RegexValue(pattern) => render_json_string(pattern, buf)
    BytesValue(bytes) => render_json_string(@base64.encode(bytes[:]), buf)
    // PKL-119a: properties uses the compact-JSON path for composite
    // values; project Pair the same way so a `Pair(1, "a")` shows up
    // as `[1,"a"]` inside the property line.
    PairValue(first, second) => {
      buf.write_char('[')
      write_compact_json_value(first, buf)
      buf.write_char(',')
      write_compact_json_value(second, buf)
      buf.write_char(']')
    }
    // PKL-119b: IntSeq materializes and reuses the listing path so
    // properties lines see `[1,2,3]` for `IntSeq(1, 3)`.
    IntSeqValue(start, end_v, step) => {
      let elements = intseq_materialize(start, end_v, step)
      buf.write_char('[')
      for i = 0; i < elements.length(); i = i + 1 {
        if i > 0 {
          buf.write_char(',')
        }
        write_compact_json_value(elements[i], buf)
      }
      buf.write_char(']')
    }
    // PKL-119c: Set reuses the same compact array form.
    SetValue(elements) => {
      buf.write_char('[')
      for i = 0; i < elements.length(); i = i + 1 {
        if i > 0 {
          buf.write_char(',')
        }
        write_compact_json_value(elements[i], buf)
      }
      buf.write_char(']')
    }
    // PKL-119d: Map projects as a compact JSON object, mirroring the
    // MappingValue projection so properties output sees `{"a":1,"b":2}`
    // for `Map("a", 1, "b", 2)`.
    MapValue(entries) => {
      buf.write_char('{')
      for i = 0; i < entries.length(); i = i + 1 {
        if i > 0 {
          buf.write_char(',')
        }
        render_json_string(json_coerce_key(entries[i].key), buf)
        buf.write_char(':')
        write_compact_json_value(entries[i].value, buf)
      }
      buf.write_char('}')
    }
  }
}

///|
fn write_properties_scalar(value : Value, buf : StringBuilder) -> Unit {
  match value {
    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("")
    StringValue(s) => write_properties_value(s, buf)
    DurationValue(n, unit) => write_properties_value("\{n}.\{unit}", buf)
    DataSizeValue(n, unit) => write_properties_value("\{n}.\{unit}", buf)
    RegexValue(pattern) => write_properties_value(pattern, buf)
    BytesValue(bytes) => write_properties_value(@base64.encode(bytes[:]), buf)
    _ =>
      // Unreachable in practice: render_properties_leaf already dispatches
      // composites through the compact-JSON path.
      write_properties_value(render_properties_compact_json(value), buf)
  }
}

///|
fn write_properties_key(key : String, buf : StringBuilder) -> Unit {
  for c in key {
    match c {
      '\\' => buf.write_string("\\\\")
      '\n' => buf.write_string("\\n")
      '\r' => buf.write_string("\\r")
      '\t' => buf.write_string("\\t")
      '\u{0C}' => buf.write_string("\\f")
      ' ' => buf.write_string("\\ ")
      ':' => buf.write_string("\\:")
      '=' => buf.write_string("\\=")
      '!' => buf.write_string("\\!")
      '#' => buf.write_string("\\#")
      _ => buf.write_char(c)
    }
  }
}

///|
fn write_properties_value(s : String, buf : StringBuilder) -> Unit {
  let mut first = true
  for c in s {
    if first && c == ' ' {
      buf.write_string("\\ ")
    } else {
      match c {
        '\\' => buf.write_string("\\\\")
        '\n' => buf.write_string("\\n")
        '\r' => buf.write_string("\\r")
        '\t' => buf.write_string("\\t")
        '\u{0C}' => buf.write_string("\\f")
        ':' => buf.write_string("\\:")
        '=' => buf.write_string("\\=")
        '!' => buf.write_string("\\!")
        '#' => buf.write_string("\\#")
        _ => buf.write_char(c)
      }
    }
    first = false
  }
}

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