///|
/// A bounded rendering tree for the private worker, not an invocation codec.
/// Never recover values by parsing Scheme's printed representation.
fn result_tree(value : @scheme.Value) -> Json {
  result_node(value, 0, Ref(500))
}

///|
fn result_node(
  value : @scheme.Value,
  depth : Int32,
  budget : Ref[Int32],
) -> Json {
  fn node(kind : String, fields : Array[(String, Json)]) -> Json {
    let entries = [("kind", @wire.text(kind))]
    entries.append(fields)
    @wire.obj(entries)
  }
  fn omitted(message : String) -> Json {
    node("truncated", [("text", @wire.text(message))])
  }
  fn scalar(
    kind : String,
    text : String,
    extra : Array[(String, Json)],
  ) -> Json {
    if text.length() > 16384 {
      return omitted("Text exceeds the 16,384-character display limit")
    }
    let fields = [("text", @wire.text(text))]
    fields.append(extra)
    node(kind, fields)
  }
  if budget.val <= 0 {
    return omitted("Result display limit reached")
  }
  budget.val -= 1
  // Depth-truncated children consume budget too, including wide values at depth 32.
  if depth > 32 {
    return omitted("Result display limit reached")
  }
  fn children(values : Array[@scheme.Value]) -> Array[Json] {
    let result = []
    for value in values {
      result.push(result_node(value, depth + 1, budget))
      if budget.val <= 0 {
        if result.length() < values.length() {
          result.push(omitted("Remaining items omitted"))
        }
        break
      }
    }
    result
  }
  match value {
    Number(Integer(n)) => scalar("int", n.to_string(), [])
    Number(Rational(n, d)) =>
      scalar("rational", n.to_string() + "/" + d.to_string(), [])
    Number(Real(n)) =>
      scalar("float", @scheme.Number::Real(n).to_text(), [
        ("bits", @wire.text(n.reinterpret_as_uint64().to_string())),
      ])
    Boolean(b) => scalar("bool", if b { "#t" } else { "#f" }, [])
    Character(c) => scalar("char", c.to_string(), [])
    Text(s) => scalar("string", Json::string(s).stringify(), [])
    Symbol(s) => scalar("symbol", s, [])
    Unit => scalar("unit", "#", [])
    Nil => node("list", [("items", Json::array([]))])
    Pair(_, _) => {
      let items = []
      let mut rest = value
      while rest is Pair(head, tail) {
        if budget.val <= 0 {
          items.push(omitted("Remaining list items omitted"))
          return node("list", [("items", Json::array(items))])
        }
        items.push(result_node(head, depth + 1, budget))
        rest = tail
      }
      let fields = [("items", Json::array(items))]
      if !(rest is Nil) {
        fields.push(("tail", result_node(rest, depth + 1, budget)))
      }
      node("list", fields)
    }
    Vector(values) => node("tuple", [("items", Json::array(children(values)))])
    Tagged(tag, values) =>
      node("constructor", [
        ("tag", @wire.text(tag)),
        ("items", Json::array(children(values))),
      ])
    Record(fields) => {
      let entries = fields.iter().collect()
      entries.sort_by((a, b) => a.0.lexical_compare(b.0))
      let values = []
      for (name, child) in entries {
        values.push(
          @wire.obj([
            ("name", @wire.text(name)),
            ("value", result_node(child, depth + 1, budget)),
          ]),
        )
        if budget.val <= 0 {
          if values.length() < entries.length() {
            values.push(
              @wire.obj([
                ("name", @wire.text("…")),
                ("value", omitted("Remaining fields omitted")),
              ]),
            )
          }
          break
        }
      }
      node("record", [("fields", Json::array(values))])
    }
    Closure(_) | Native(_) | NativeFunction(_) =>
      scalar("opaque", "Procedure · inspectable text only", [])
    IR(_) => scalar("opaque", "Morphir value · inspectable text only", [])
    IRType(_) => scalar("opaque", "Morphir type · inspectable text only", [])
    IRFile(_) => scalar("opaque", "Morphir model · inspectable text only", [])
  }
}