///|
pub(all) struct MetadataIndex {
  offset : UInt64
  length : UInt64
  name : String
}

///|
pub fn encode_metadata_index(value : MetadataIndex) -> Bytes {
  let out = @buffer.Buffer::Buffer()
  out.write_uint64_le(value.offset)
  out.write_uint64_le(value.length)
  put_text(out, value.name)
  out.to_bytes()
}

///|
pub fn decode_metadata_index(
  bytes : Bytes,
  limits? : Limits = default_limits(),
) -> MetadataIndex raise McapError {
  let input = cursor(bytes, limits)
  let offset = input.u64()
  let length = input.u64()
  let name = input.text()
  { offset, length, name, }
}

///|
/// Check grouped summary records and every declared SummaryOffset span.
pub fn validate_summary(
  bytes : Bytes,
  limits? : Limits = default_limits(),
) -> Int raise McapError {
  let records = scan_file(bytes, limits~)
  let footer = decode_footer(records[records.length() - 1].body)
  let footer_offset = records[records.length() - 1].offset.to_uint64()
  let data_end = records.iter().find_first(fn(r) { r.opcode == 0x0f })
  let data_end = match data_end {
    Some(value) => value
    None => raise Invalid("missing DataEnd")
  }
  let summary : Array[Record] = []
  let offsets : Array[Record] = []
  for record in records {
    let position = record.offset.to_uint64()
    if position <= data_end.offset.to_uint64() || record.opcode == 0x02 {
      continue
    }
    if footer.summary_start == 0UL || position < footer.summary_start {
      raise Invalid("records between DataEnd and summary")
    }
    if footer.summary_offset_start != 0UL &&
      position >= footer.summary_offset_start {
      if record.opcode != 0x0e {
        raise Invalid("non-SummaryOffset in offset section")
      }
      offsets.push(record)
    } else {
      if record.opcode != 0x03 &&
        record.opcode != 0x04 &&
        record.opcode != 0x08 &&
        record.opcode != 0x0a &&
        record.opcode != 0x0b &&
        record.opcode != 0x0d {
        raise Invalid("record opcode not allowed in summary")
      }
      summary.push(record)
    }
  }
  if footer.summary_offset_start != 0UL &&
    !offsets.any(fn(r) { r.offset.to_uint64() == footer.summary_offset_start }) {
    raise Invalid("SummaryOffset pointer is not an offset record boundary")
  }
  let seen : Map[Byte, Bool] = {}
  let groups : Map[Byte, (UInt64, UInt64)] = {}
  let mut previous : Byte? = None
  for record in summary {
    if previous != Some(record.opcode) {
      if seen.contains(record.opcode) {
        raise Invalid("summary records not grouped by opcode")
      }
      seen[record.opcode] = true
      previous = Some(record.opcode)
      groups[record.opcode] = (record.offset.to_uint64(), 0UL)
    }
    let group = groups[record.opcode]
    groups[record.opcode] = (
      group.0,
      group.1 + (record.body.length() + 9).to_uint64(),
    )
  }
  let indexed : Map[Byte, Bool] = {}
  for record in offsets {
    let value = decode_summary_offset(record.body)
    if indexed.contains(value.opcode) {
      raise Invalid("duplicate SummaryOffset opcode")
    }
    indexed[value.opcode] = true
    if value.start > footer_offset || value.length > footer_offset - value.start {
      raise Invalid("SummaryOffset span outside summary")
    }
    match groups.get(value.opcode) {
      Some((start, length)) =>
        if value.start != start || value.length != length {
          raise Invalid("SummaryOffset span mismatch")
        }
      None =>
        if value.length != 0UL ||
          value.start < footer.summary_start ||
          value.start >
          (if footer.summary_offset_start == 0UL {
            footer_offset
          } else {
            footer.summary_offset_start
          }) {
          raise Invalid("SummaryOffset references absent nonempty group")
        }
    }
  }
  groups.length()
}

///|
pub fn validate_metadata_indexes(
  bytes : Bytes,
  limits? : Limits = default_limits(),
) -> Int raise McapError {
  let records = scan_file(bytes, limits~)
  let physical : Map[UInt64, Record] = {}
  for record in records {
    physical[record.offset.to_uint64()] = record
  }
  let seen : Map[UInt64, Bool] = {}
  let mut count = 0
  for record in records {
    if record.opcode != 0x0d {
      continue
    }
    let index = decode_metadata_index(record.body, limits~)
    if seen.contains(index.offset) {
      raise Invalid("duplicate MetadataIndex")
    }
    seen[index.offset] = true
    let target = match physical.get(index.offset) {
      Some(value) => value
      None => raise Invalid("MetadataIndex target outside record boundary")
    }
    if target.opcode != 0x0c ||
      index.length != (target.body.length() + 9).to_uint64() {
      raise Invalid("MetadataIndex type or length mismatch")
    }
    if decode_metadata(target.body, limits~).name != index.name {
      raise Invalid("MetadataIndex name mismatch")
    }
    count += 1
  }
  count
}