///|
pub fn validate_log(
  log : Log,
  limits? : Limits = default_limits(),
) -> Unit raise McapError {
  if limits.max_record_bytes < 22 ||
    limits.max_string_bytes < 0 ||
    limits.max_records < 0 ||
    limits.max_decompressed_bytes < 0 {
    raise Invalid("invalid resource limits")
  }
  let schema_ids : Map[Int, Bool] = {}
  for schema in log.schemas {
    if schema_ids.contains(schema.id) {
      raise Invalid("duplicate schema id")
    }
    schema_ids[schema.id] = true
    let body = encode_schema(schema)
    if body.length() > limits.max_record_bytes {
      raise Invalid("schema record exceeds limit")
    }
    ignore(decode_schema(body, limits~))
  }
  let channel_ids : Map[Int, Bool] = {}
  for channel in log.channels {
    if channel_ids.contains(channel.id) {
      raise Invalid("duplicate channel id")
    }
    channel_ids[channel.id] = true
    if channel.schema_id != 0 && !schema_ids.contains(channel.schema_id) {
      raise Invalid("channel references missing schema")
    }
    let body = encode_channel(channel)
    if body.length() > limits.max_record_bytes {
      raise Invalid("channel record exceeds limit")
    }
    ignore(decode_channel(body, limits~))
  }
  if log.messages.length() > limits.max_records {
    raise Invalid("message budget exceeded")
  }
  for message in log.messages {
    if !channel_ids.contains(message.channel_id) {
      raise Invalid("message references missing channel")
    }
    if message.data.length() > limits.max_record_bytes - 22 {
      raise Invalid("message record exceeds limit")
    }
  }
  let header = encode_header(log.header)
  if header.length() > limits.max_record_bytes {
    raise Invalid("Header record exceeds limit")
  }
  ignore(decode_header(header, limits~))
  for metadata in log.metadata {
    let body = encode_metadata(metadata)
    if body.length() > limits.max_record_bytes {
      raise Invalid("Metadata record exceeds limit")
    }
    ignore(decode_metadata(body, limits~))
  }
  for attachment in log.attachments {
    let body = encode_attachment(attachment)
    if body.length() > limits.max_record_bytes {
      raise Invalid("Attachment exceeds record budget")
    }
    ignore(decode_attachment(body, limits~))
  }
  let total = log.attachments.length() +
    log.schemas.length() +
    log.channels.length() +
    log.messages.length() +
    log.metadata.length() +
    3
  if total > limits.max_records {
    raise Invalid("total record budget exceeded")
  }
}

///|
/// Minimal interoperable layout; zero summary pointers mean a sequential file.
pub fn write_log(
  log : Log,
  checksums? : Bool = true,
  limits? : Limits = default_limits(),
) -> Bytes raise McapError {
  validate_log(log, limits~)
  let out = @buffer.Buffer::Buffer()
  out.write_bytes(magic()[:])
  out.write_bytes(frame(0x01, encode_header(log.header))[:])
  for schema in log.schemas {
    out.write_bytes(frame(0x03, encode_schema(schema))[:])
  }
  for channel in log.channels {
    out.write_bytes(frame(0x04, encode_channel(channel))[:])
  }
  for message in log.messages {
    out.write_bytes(frame(0x05, encode_message(message))[:])
  }
  for metadata in log.metadata {
    out.write_bytes(frame(0x0c, encode_metadata(metadata))[:])
  }
  for attachment in log.attachments {
    out.write_bytes(frame(0x09, encode_attachment(attachment, checksums~))[:])
  }
  let checksum = if checksums { crc32(out.to_bytes()) } else { 0U }
  out.write_bytes(frame(0x0f, checksum.mcap_le_bytes())[:])
  out.write_bytes(
    frame(
      0x02,
      encode_footer({
        summary_start: 0UL,
        summary_offset_start: 0UL,
        summary_crc: 0U,
      }),
    )[:],
  )
  out.write_bytes(magic()[:])
  out.to_bytes()
}