///|
/// Parse failures for the versioned operation-journal word format.
pub(all) enum JournalCodecError {
  MissingHeader
  UnsupportedVersion(Int)
  InvalidOperationCount(Int)
  InvalidTag(Int)
  TruncatedPayload(Int)
  InvalidBatchCount(Int)
}

///|
/// Encode a replayable log as `[1, operation_count, tag, payload...]` words.
/// Tags 1-6 represent Add, Remove, AddRange, RemoveRange, AddAll, RemoveAll.
pub fn RoaringOperationLog::encode_words(
  self : RoaringOperationLog,
) -> Array[Int] {
  let output : Array[Int] = [1, self.operations.length()]
  for operation in self.operations {
    match operation {
      Add(value) => {
        output.push(1)
        output.push(value)
      }
      Remove(value) => {
        output.push(2)
        output.push(value)
      }
      AddRange(start, end) => {
        output.push(3)
        output.push(start)
        output.push(end)
      }
      RemoveRange(start, end) => {
        output.push(4)
        output.push(start)
        output.push(end)
      }
      AddAll(values) => {
        output.push(5)
        output.push(values.length())
        for value in values {
          output.push(value)
        }
      }
      RemoveAll(values) => {
        output.push(6)
        output.push(values.length())
        for value in values {
          output.push(value)
        }
      }
    }
  }
  output
}

///|
/// Decode a stable journal payload with a caller-provided checkpoint.
pub fn decode_operation_log(
  checkpoint : RoaringBitmap,
  words : Array[Int],
) -> Result[RoaringOperationLog, JournalCodecError] {
  if words.length() == 0 {
    return Err(MissingHeader)
  }
  if words[0] != 1 {
    return Err(UnsupportedVersion(words[0]))
  }
  if words.length() < 2 || words[1] < 0 {
    return Err(
      InvalidOperationCount(if words.length() < 2 { -1 } else { words[1] }),
    )
  }
  let expected = words[1]
  let mut index = 2
  let operations : Array[BitmapOperation] = []
  while operations.length() < expected {
    if index >= words.length() {
      return Err(TruncatedPayload(index))
    }
    let tag = words[index]
    index += 1
    if tag == 1 || tag == 2 {
      if index >= words.length() {
        return Err(TruncatedPayload(index))
      }
      let value = words[index]
      index += 1
      if tag == 1 {
        operations.push(Add(value))
      } else {
        operations.push(Remove(value))
      }
    } else if tag == 3 || tag == 4 {
      if index + 1 >= words.length() {
        return Err(TruncatedPayload(index))
      }
      let start = words[index]
      let end = words[index + 1]
      index += 2
      if tag == 3 {
        operations.push(AddRange(start, end))
      } else {
        operations.push(RemoveRange(start, end))
      }
    } else if tag == 5 || tag == 6 {
      if index >= words.length() {
        return Err(TruncatedPayload(index))
      }
      let count = words[index]
      index += 1
      if count < 0 || count > words.length() - index {
        return Err(InvalidBatchCount(count))
      }
      let values : Array[Int] = []
      for _ in 0.. Result[BitmapFrame, RoaringError] {
  match self.replay() {
    Err(error) => Err(error)
    Ok(bitmap) =>
      match bitmap.frame(format) {
        Ok(frame) => Ok(frame)
        Err(_) => Err(RangeTooLarge(bitmap.cardinality()))
      }
  }
}