///|
fn decompression_budget(
  limits : Limits,
) -> Result[@bounded.Budget, BsdiffError] {
  match limits.validate() {
    Err(e) => Err(e)
    Ok(_) =>
      Ok(@bounded.Budget::{
        remaining: limits.max_decompressed_bytes,
        emitted: 0L,
      })
  }
}

///|
// Standard generator encoding is fixed at bzip2 level 9.
#warnings("-unused_value")
fn compress_block(data : Bytes) -> Bytes {
  @bzip2.bzip2(data[:], level=9)
}

///|
priv struct PatchBudget {
  mut used : Int64
  limit : Int64
}

///|
fn append_compressed(
  out : Array[Byte],
  part : Bytes,
  quota : PatchBudget,
  work : MatchBudget,
) -> Result[Unit, BsdiffError] {
  // used <= limit <= Int max; a Writer emission is bounded independently.
  let value = quota.used + part.length().to_int64()
  if value > quota.limit {
    return Err(
      generation_error(
        ResourceLimitExceeded,
        Header,
        "max_patch_bytes",
        value,
        Some(quota.limit),
      ),
    )
  }
  match work.reserve(2L * part.length().to_int64()) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  quota.used = value
  for byte in part {
    out.push(byte)
  }
  Ok(())
}

///|
// One 32768-byte feed can emit at most one level-9 block (RLE <= 5/4
// expansion). The fixed Writer retains <=900004 RLE bytes; finish emits
// at most one block. Each returned emission is <3 MB, independently of input
// size. See docs/generation-resources.md for the bit and workspace bounds.
// The quota is shared by all three streams; no whole-input output is built
// before checking its size. No copied encoder source or engine change.
fn compress_generation_block(
  data : Bytes,
  quota : PatchBudget,
  work : MatchBudget,
) -> Result[Bytes, BsdiffError] {
  let writer = @bzip2.Writer::new(level=9)
  let out : Array[Byte] = []
  let mut at = 0
  while at < data.length() {
    match work.reserve(1L) {
      Err(e) => return Err(e)
      Ok(_) => ()
    }
    let count = if data.length() - at < 32768 {
      data.length() - at
    } else {
      32768
    }
    let part = writer.push(data[at:at + count])
    match append_compressed(out, part, quota, work) {
      Err(e) => return Err(e)
      Ok(_) => ()
    }
    at = at + count
  }
  match work.reserve(1L) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  match append_compressed(out, writer.finish(), quota, work) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  Ok(Bytes::from_array(out))
}

///|
fn decompress_block(
  data : Bytes,
  cap : Int64,
  budget : @bounded.Budget,
  stage : ErrorStage,
  start : Int64,
) -> Result[Bytes, BsdiffError] {
  let block = match stage {
    Control => "control"
    Diff => "diff"
    Extra => "extra"
    _ =>
      return Err(
        format_error(
          InvalidConfiguration,
          stage,
          None,
          None,
          Some("stage"),
          None,
          None,
        ),
      )
  }
  if cap < 0L || cap > 2147483647L || budget.remaining < 0L || start < 0L {
    return Err(
      format_error(
        InvalidConfiguration,
        stage,
        Some(block),
        None,
        Some("compression_parameters"),
        None,
        None,
      ),
    )
  }
  if start > 9223372036854775807L - data.length().to_int64() {
    return Err(
      format_error(
        IntegerOverflow,
        stage,
        Some(block),
        None,
        Some("compressed_offset"),
        Some(start),
        None,
      ),
    )
  }
  Ok(@bounded.decode(data[:], cap.to_int(), budget).output) catch {
    @bounded.Truncated(at~) =>
      Err(
        format_error(
          TruncatedData,
          stage,
          Some(block),
          Some(start + at.to_int64()),
          None,
          None,
          None,
        ),
      )
    @bounded.Header(at~, what~) =>
      Err(
        format_error(
          InvalidField,
          stage,
          Some(block),
          Some(start + at.to_int64()),
          Some(what),
          None,
          None,
        ),
      )
    @bounded.Code(at~) =>
      Err(
        format_error(
          DecompressionFailed,
          stage,
          Some(block),
          Some(start + at.to_int64()),
          Some("code"),
          None,
          None,
        ),
      )
    @bounded.Checksum(_) =>
      Err(
        format_error(
          DecompressionFailed,
          stage,
          Some(block),
          None,
          Some("crc"),
          None,
          None,
        ),
      )
    @bounded.Limit(field~, value~, limit~) =>
      Err(
        format_error(
          ResourceLimitExceeded,
          stage,
          Some(block),
          None,
          Some(field),
          Some(value),
          Some(limit),
        ),
      )
    @bounded.Parameter =>
      Err(
        format_error(
          InvalidConfiguration,
          stage,
          Some(block),
          None,
          Some("budget"),
          None,
          None,
        ),
      )
  }
}