///|
fn json_id(id : UInt64) -> Json {
  // Decimal strings preserve object IDs beyond JavaScript's 53-bit range.
  Json::string(id.to_string())
}

///|
fn json_int(value : Int) -> Json {
  Json::number(value.to_double())
}

///|
pub fn Value::to_json(self : Value) -> Json {
  let value = match self {
    Reference(id) => json_id(id)
    Boolean(value) => Json::boolean(value)
    Character(value) | ByteValue(value) | ShortValue(value) | IntValue(value) =>
      json_int(value)
    LongValue(value) => Json::string(value.to_string())
    FloatValue(value) | DoubleValue(value) =>
      // NaN/Infinity have no JSON numeric representation.
      if value.is_nan() {
        Json::string("NaN")
      } else if value.is_pos_inf() {
        Json::string("Infinity")
      } else if value.is_neg_inf() {
        Json::string("-Infinity")
      } else {
        Json::number(value)
      }
  }
  Json::object({
    "type": Json::string(value_type_name(self.kind()) catch { _ => "unknown" }),
    "value": value,
  })
}

///|
pub fn Summary::summary_json(self : Summary) -> Json {
  Json::object({
    "format": Json::string("JAVA PROFILE 1.0.2"),
    "id_size": json_int(self.id_size),
    "timestamp_ms": json_id(self.timestamp_ms),
    "file_bytes": json_int(self.file_size()),
    "classes": json_int(self.classes),
    "instances": json_int(self.instances),
    "object_arrays": json_int(self.object_arrays),
    "primitive_arrays": json_int(self.primitive_arrays),
    "roots": json_int(self.root_count()),
    "stack_frames": json_int(self.frames.length()),
    "stack_traces": json_int(self.traces.length()),
  })
}

///|
pub fn HistogramRow::to_json(self : HistogramRow) -> Json {
  Json::object({
    "name": Json::string(self.name),
    "display_name": Json::string(display_class_name(self.name)),
    "class_id": json_id(self.class_id),
    "loader_id": json_id(self.loader_id),
    "kind": Json::string(self.kind),
    "count": json_int(self.count),
    "payload_bytes": Json::string(self.payload_bytes.to_string()),
    "elements": Json::string(self.elements.to_string()),
  })
}

///|
pub fn Summary::histogram_json(self : Summary) -> Json raise ParseError {
  Json::array(self.histogram().map(row => row.to_json()))
}

///|
pub fn Summary::histogram_csv(self : Summary) -> String raise ParseError {
  let out = StringBuilder()
  out.write_string(
    "name,class_id,loader_id,kind,count,payload_bytes,elements\r\n",
  )
  for row in self.histogram() {
    let cells = [
      row.name,
      row.class_id.to_string(),
      row.loader_id.to_string(),
      row.kind,
      row.count.to_string(),
      row.payload_bytes.to_string(),
      row.elements.to_string(),
    ]
    out.write_string(cells.map(csv_cell).join(","))
    out.write_string("\r\n")
  }
  out.to_string()
}

///|
fn field_json(field : FieldValue) -> Json {
  Json::object({
    "declaring_class_id": json_id(field.declaring_class_id),
    "name": Json::string(field.name),
    "value": field.value.to_json(),
  })
}

///|
pub fn Summary::class_json(
  self : Summary,
  id : UInt64,
) -> Json raise ParseError {
  let cls = match self.class_info(id) {
    Some(cls) => cls
    None => raise Invalid(0, "class not found")
  }
  let fields = cls.fields.map(field => {
    Json::object({
      "name": Json::string(self.string(field.name_id).unwrap_or("")),
      "type": Json::string(value_type_name(field.kind) catch { _ => "unknown" }),
    })
  })
  let constants = cls.constants.map(entry => {
    Json::object({
      "index": json_int(entry.index),
      "value": entry.value.to_json(),
    })
  })
  Json::object({
    "id": json_id(id),
    "name": Json::string(self.class_name(id).unwrap_or("")),
    "superclass_id": json_id(cls.superclass_id),
    "loader_id": json_id(cls.loader_id),
    "signers_id": json_id(cls.signers_id),
    "protection_domain_id": json_id(cls.protection_domain_id),
    "reported_instance_size": json_int(cls.instance_size),
    "fields": Json::array(fields),
    "statics": Json::array(self.static_fields(id).map(field_json)),
    "constants": Json::array(constants),
  })
}

///|
/// Exports one object; array pages include total length and requested offset.
pub fn Summary::object_json(
  self : Summary,
  id : UInt64,
  offset? : Int = 0,
  limit? : Int = 100,
) -> Json raise ParseError {
  if self.definitions.contains(id) {
    return self.class_json(id)
  }
  let object = self.require_object(id)
  let fields : Map[String, Json] = {
    "id": json_id(id),
    "kind": Json::string(object.kind.label()),
    "class_id": json_id(object.class_id),
    "stack_serial": json_id(object.stack_serial),
    "payload_bytes": json_int(object.payload_bytes),
    "payload_offset": json_int(object.payload_offset),
  }
  match object.kind {
    Instance => {
      fields["class_name"] = Json::string(
        self.class_name(object.class_id).unwrap_or(""),
      )
      fields["fields"] = Json::array(self.instance_fields(id).map(field_json))
    }
    ObjectArray | PrimitiveArray => {
      fields["length"] = json_int(object.length)
      fields["offset"] = json_int(offset)
      fields["values"] = Json::array(
        self.array_values(id, offset~, limit~).map(value => value.to_json()),
      )
      if object.kind == PrimitiveArray {
        fields["element_type"] = Json::string(
          value_type_name(object.primitive_kind),
        )
      }
    }
  }
  Json::object(fields)
}

///|
pub fn RootInfo::to_json(self : RootInfo) -> Json {
  Json::object({
    "kind": Json::string(self.label()),
    "object_id": json_id(self.object_id),
    "related_id": json_id(self.related_id),
    "thread_serial": json_id(self.thread_serial),
    "frame_number": json_id(self.frame_number),
  })
}

///|
pub fn ReferenceEdge::to_json(self : ReferenceEdge) -> Json {
  Json::object({
    "source_id": json_id(self.source_id),
    "target_id": json_id(self.target_id),
    "label": Json::string(self.label),
  })
}

///|
pub fn ResolvedFrame::to_json(self : ResolvedFrame) -> Json {
  Json::object({
    "id": json_id(self.id),
    "class_name": Json::string(self.class_name),
    "method_name": Json::string(self.method_name),
    "signature": Json::string(self.signature),
    "source_name": Json::string(self.source_name),
    "line_number": json_int(self.line_number),
  })
}