///|
pub enum HistogramSort {
  Count
  Payload
  Elements
  Name
}

///|
/// Filters by internal class name prefix and object kind before pagination.
pub fn Summary::select_histogram(
  self : Summary,
  prefix? : String = "",
  kind? : String = "",
  sort? : HistogramSort = Count,
  offset? : Int = 0,
  limit? : Int = 100,
) -> Array[HistogramRow] raise ParseError {
  if kind != "" &&
    kind != "instance" &&
    kind != "object_array" &&
    kind != "primitive_array" {
    raise Invalid(0, "unknown histogram kind")
  }
  let rows = self
    .histogram_rows()
    .filter(row => {
      row.name.has_prefix(prefix) && (kind == "" || kind == row.kind)
    })
  rows.sort_by((a, b) => {
    let primary = match sort {
      Count => b.count.compare(a.count)
      Payload => b.payload_bytes.compare(a.payload_bytes)
      Elements => b.elements.compare(a.elements)
      Name => a.name.compare(b.name)
    }
    if primary != 0 {
      primary
    } else {
      let name = a.name.compare(b.name)
      if name != 0 {
        name
      } else {
        a.class_id.compare(b.class_id)
      }
    }
  })
  let end = page_end(offset, limit, rows.length())
  rows[offset:end].to_owned()
}

///|
pub struct HeapTotals {
  class_objects : Int
  instance_objects : Int
  array_objects : Int
  payload_bytes : Int64
  array_elements : Int64
  largest_payload_id : UInt64
  largest_payload_bytes : Int
}

///|
pub fn Summary::totals(self : Summary) -> HeapTotals {
  let mut payload = 0L
  let mut elements = 0L
  let mut largest_id = 0UL
  let mut largest_size = -1
  for id, object in self.objects {
    payload += object.payload_bytes.to_int64()
    elements += object.length.to_int64()
    if object.payload_bytes > largest_size ||
      (object.payload_bytes == largest_size && id < largest_id) {
      largest_id = id
      largest_size = object.payload_bytes
    }
  }
  {
    class_objects: self.classes,
    instance_objects: self.instances,
    array_objects: self.object_arrays + self.primitive_arrays,
    payload_bytes: payload,
    array_elements: elements,
    largest_payload_id: largest_id,
    largest_payload_bytes: if largest_size < 0 {
      0
    } else {
      largest_size
    },
  }
}

///|
pub struct HistogramDelta {
  name : String
  kind : String
  before_count : Int
  after_count : Int
  count_delta : Int64
  payload_delta : Int64
}

///|
/// Compares class-name aggregates. Object/loader IDs are not stable across dumps.
/// This is a count comparison, not a claim that the same objects survived GC.
pub fn compare_heaps(
  before : Summary,
  after : Summary,
) -> Array[HistogramDelta] raise ParseError {
  let left : Map[(String, String), (Int, Int64)] = Map([])
  let right : Map[(String, String), (Int, Int64)] = Map([])
  let keys : Map[(String, String), Bool] = Map([])
  for pair in [(before, left), (after, right)] {
    for row in pair.0.histogram() {
      let key = (row.name, row.kind)
      let previous = pair.1.get(key).unwrap_or((0, 0L))
      pair.1[key] = (previous.0 + row.count, previous.1 + row.payload_bytes)
      keys[key] = true
    }
  }
  let rows = []
  for key, _ in keys {
    let a = left.get(key).unwrap_or((0, 0L))
    let b = right.get(key).unwrap_or((0, 0L))
    rows.push({
      name: key.0,
      kind: key.1,
      before_count: a.0,
      after_count: b.0,
      count_delta: b.0.to_int64() - a.0.to_int64(),
      payload_delta: b.1 - a.1,
    })
  }
  rows.sort_by((a, b) => {
    let name = a.name.compare(b.name)
    if name != 0 {
      name
    } else {
      a.kind.compare(b.kind)
    }
  })
  rows
}

///|
pub fn HistogramDelta::to_json(self : HistogramDelta) -> Json {
  Json::object({
    "name": Json::string(self.name),
    "kind": Json::string(self.kind),
    "before_count": json_int(self.before_count),
    "after_count": json_int(self.after_count),
    "count_delta": Json::string(self.count_delta.to_string()),
    "payload_delta": Json::string(self.payload_delta.to_string()),
  })
}

///|
pub fn Summary::largest_objects(
  self : Summary,
  limit? : Int = 20,
) -> Array[ObjectInfo] raise ParseError {
  let end = page_end(0, limit, self.objects.length())
  if end == 0 {
    return []
  }
  let rows = self.objects.values().to_array()
  rows.sort_by((a, b) => {
    let size = b.payload_bytes.compare(a.payload_bytes)
    if size != 0 {
      size
    } else {
      a.id.compare(b.id)
    }
  })
  rows[0:end].to_owned()
}