///|
/// Parses TOML and emits the tagged JSON representation required by
/// toml-test decoders. Object keys are sorted recursively for stable output.
pub fn decode_tagged_json(source : String) -> Result[String, TomlError] {
  match parse(source) {
    Ok(table) => Ok(decoder_table_to_string(table))
    Err(error) => Err(error)
  }
}

///|
/// Encodes one already-parsed TOML value with the same deterministic tagged
/// JSON contract as `decode_tagged_json`.
pub fn encode_tagged_value(value : Value) -> String {
  decoder_value_to_string(value)
}

///|
fn decoder_table_to_string(table : Table) -> String {
  let keys = table.keys().collect()
  keys.sort_by(decoder_compare_keys)
  let fields : Array[String] = []
  for key in keys {
    match table.get(key) {
      Some(value) =>
        fields.push(json_string(key) + ":" + decoder_value_to_string(value))
      None => ()
    }
  }
  "{" + join(fields, ",") + "}"
}

///|
/// MoonBit's built-in String comparison uses shortlex order. JSON object keys
/// use ordinary code-point lexicographic order here so output is predictable.
fn decoder_compare_keys(left : String, right : String) -> Int {
  let left_chars : Array[Char] = left.iter().collect()
  let right_chars : Array[Char] = right.iter().collect()
  let common = if left_chars.length() < right_chars.length() {
    left_chars.length()
  } else {
    right_chars.length()
  }
  for i in 0.. String {
  match value {
    String(s) => decoder_tagged("string", s)
    Integer(i) => decoder_tagged("integer", i.to_string())
    Float(f) => decoder_tagged("float", format_float(f))
    Boolean(b) => decoder_tagged("bool", if b { "true" } else { "false" })
    OffsetDateTime(dt) =>
      decoder_tagged(
        "datetime",
        format_date(dt.date) +
        "T" +
        format_time(dt.time) +
        format_offset(dt.offset_minutes),
      )
    LocalDateTime(dt) =>
      decoder_tagged(
        "datetime-local",
        format_date(dt.date) + "T" + format_time(dt.time),
      )
    LocalDate(date) => decoder_tagged("date-local", format_date(date))
    LocalTime(time) => decoder_tagged("time-local", format_time(time))
    Array(values) => {
      let items : Array[String] = []
      for item in values {
        items.push(decoder_value_to_string(item))
      }
      "[" + join(items, ",") + "]"
    }
    Table(nested) => decoder_table_to_string(nested)
  }
}

///|
fn decoder_tagged(kind : String, value : String) -> String {
  "{\"type\":" + json_string(kind) + ",\"value\":" + json_string(value) + "}"
}

///|
/// Delegates quoting and escaping to the repository's JSON implementation.
fn json_string(value : String) -> String {
  Json::string(value).stringify()
}