///|
/// Output storage for the one-shot decoder. Unlike the general-purpose core
/// `Buffer`, this exposes the fixed backing array needed for DEFLATE's
/// overlapping back-reference copies.
priv struct DecodeOutput {
  mut storage : FixedArray[Byte]
  mut length : Int
  fixed_capacity : Bool
}

///|
fn DecodeOutput::DecodeOutput(size_hint : Int) -> DecodeOutput {
  {
    storage: FixedArray::make(size_hint.max(1), b'\x00'),
    length: 0,
    fixed_capacity: false,
  }
}

///|
/// Use caller-owned storage for a one-shot decode. Capacity exhaustion becomes
/// the existing public `OutputLimitExceeded` failure instead of reallocating.
fn DecodeOutput::into(storage : FixedArray[Byte]) -> DecodeOutput {
  { storage, length: 0, fixed_capacity: true, }
}

///|
/// Byte counts use signed 32-bit Int on every backend.
const MAX_DECODE_OUTPUT_SIZE : Int = 0x7FFFFFFF

///|
/// Both counts are non-negative. Check the remaining range before adding so
/// overflow cannot bypass the capacity check or reach an array write.
fn checked_decode_output_length(
  length : Int,
  additional : Int,
) -> Int raise InflateError {
  guard additional <= MAX_DECODE_OUTPUT_SIZE - length else {
    raise InflateError(OutputLimitExceeded, "DEFLATE output size overflow")
  }
  length + additional
}

///|
/// Retain geometric growth when doubling fits; otherwise allocate only the
/// required size. Keeping this arithmetic separate permits allocation-free
/// tests at the integer limit.
fn grown_decode_output_capacity(current : Int, required : Int) -> Int {
  if current <= MAX_DECODE_OUTPUT_SIZE / 2 {
    (current * 2).max(required)
  } else {
    required
  }
}

///|
fn DecodeOutput::ensure_capacity(
  self : DecodeOutput,
  additional : Int,
) -> Unit raise InflateError {
  let required = checked_decode_output_length(self.length, additional)
  guard required > self.storage.length() else { return }
  guard !self.fixed_capacity else {
    raise InflateError(
      OutputLimitExceeded,
      "decoded output exceeds output buffer",
    )
  }
  let capacity = grown_decode_output_capacity(self.storage.length(), required)
  self.storage = FixedArray::make_and_blit(
    self.storage,
    allocate_len=capacity,
    init=b'\x00',
    len=self.length,
  )
}

///|
fn DecodeOutput::append_byte(
  self : DecodeOutput,
  byte : Byte,
) -> Unit raise InflateError {
  self.ensure_capacity(1)
  self.storage[self.length] = byte
  self.length = self.length + 1
}

///|
fn DecodeOutput::append_bytes(
  self : DecodeOutput,
  source : Bytes,
  source_offset : Int,
  length : Int,
) -> Unit raise InflateError {
  guard length > 0 else { return }
  self.ensure_capacity(length)
  self.storage.blit_from_bytes(self.length, source, source_offset, length)
  self.length = self.length + length
}

///|
/// Check capacity, then append a match without writing beyond its exact length.
/// The one-shot driver splits matches at cancellation boundaries.
fn DecodeOutput::append_repeated_suffix(
  self : DecodeOutput,
  distance : Int,
  length : Int,
) -> Unit raise InflateError {
  guard length > 0 else { return }
  self.ensure_capacity(length)
  let start = self.length
  let storage = self.storage
  if length <= 8 {
    // Keep the common short-text match in the caller to avoid an extra
    // helper call. Forward byte copies also handle overlapping history.
    for i in 0..= 16 keeps them disjoint.
/// The final vector may rewrite the preceding output bytes, but ends exactly
/// at the match boundary and only reads history that has already been produced.
#cfg(any(target="native", target="wasm"))
fn copy_match_bytes(
  storage : FixedArray[Byte],
  start : Int,
  distance : Int,
  length : Int,
) -> Unit {
  if distance >= 16 && length >= 16 {
    for offset = 0; offset + 16 <= length; offset = offset + 16 {
      @v128.v128_store(
        storage,
        start + offset,
        @v128.v128_load(storage, start + offset - distance),
      )
    }
    if (length & 15) != 0 {
      let offset = length - 16
      @v128.v128_store(
        storage,
        start + offset,
        @v128.v128_load(storage, start + offset - distance),
      )
    }
  } else {
    copy_match_bytes_scalar(storage, start, distance, length)
  }
}

///|
#cfg(not(any(target="native", target="wasm")))
fn copy_match_bytes(
  storage : FixedArray[Byte],
  start : Int,
  distance : Int,
  length : Int,
) -> Unit {
  copy_match_bytes_scalar(storage, start, distance, length)
}

///|
/// Longer overlapping matches double their generated prefix. This is also
/// the fallback on targets without native SIMD; the caller handles short matches.
fn copy_match_bytes_scalar(
  storage : FixedArray[Byte],
  start : Int,
  distance : Int,
  length : Int,
) -> Unit {
  let first = distance.min(length)
  FixedArray::unsafe_blit(storage, start, storage, start - distance, first)
  let mut copied = first
  while copied < length {
    let chunk = copied.min(length - copied)
    FixedArray::unsafe_blit(storage, start + copied, storage, start, chunk)
    copied = copied + chunk
  }
}

///|
fn DecodeOutput::to_bytes(self : DecodeOutput) -> Bytes {
  Bytes::from_array(self.storage[:self.length])
}

///|
/// In-memory DEFLATE decoder. Reads from a `Bytes` and appends to `out`.
priv struct MemDecoder {
  mut input : Bytes
  mut pos : Int // next input byte
  mut bitbuf : UInt // LSB-first bit accumulator
  mut bit_count : Int // valid bits in `bitbuf`
  mut out : DecodeOutput
  mut cancelled : () -> Bool // polled every `CANCEL_GRANULARITY` produced bytes
  mut next_cancel : Int // poll `cancelled` once `out` reaches this length
  dyn_litlen : HuffmanDecoder // literal/length (also reused for the code-length tree)
  dyn_dist : HuffmanDecoder // distance
  clbits : FixedArray[Int] // decoded literal+distance code lengths
  codebits : FixedArray[Int] // code-length code lengths
}

///|
const CANCEL_GRANULARITY : Int = 4096

///|
/// A DEFLATE Huffman code is at most 15 bits. The one-shot decoder can keep a
/// 24-bit reservoir because it is allowed to consume trailing bytes after the
/// final block, as documented by `inflate_all`'s prefix semantics.
const HUFFMAN_FAST_BITS : Int = 15

///|
const HUFFMAN_FAST_REFILL_BITS : Int = 24

///|
/// Output chunk size for the streaming one-shot drivers: each `step` call
/// writes into this scratch, so cancellation is polled at this granularity.
const INFLATE_SCRATCH_SIZE : Int = 8192

///|
fn MemDecoder::MemDecoder(input : Bytes, cancelled : () -> Bool) -> MemDecoder {
  {
    input,
    pos: 0,
    bitbuf: 0,
    bit_count: 0,
    out: DecodeOutput(input.length()),
    cancelled,
    next_cancel: 0,
    dyn_litlen: HuffmanDecoder(),
    dyn_dist: HuffmanDecoder(),
    clbits: FixedArray::make(MAX_NUM_LIT + MAX_NUM_DIST, 0),
    codebits: FixedArray::make(NUM_CODES, 0),
  }
}

///|
/// Reinitialize an in-memory decoder over a new stream while retaining its
/// Huffman and dynamic-header workspace. `output` becomes the complete
/// back-reference history, so no secondary output allocation is needed.
fn MemDecoder::reset_into(
  self : MemDecoder,
  input : Bytes,
  output : FixedArray[Byte],
  cancelled : () -> Bool,
) -> Unit {
  self.input = input
  self.pos = 0
  self.bitbuf = 0
  self.bit_count = 0
  self.out = DecodeOutput::into(output)
  self.cancelled = cancelled
  self.next_cancel = 0
}

///|
fn MemDecoder::check_cancel(self : MemDecoder) -> Unit raise InflateError {
  guard self.out.length >= self.next_cancel else { return }
  if (self.cancelled)() {
    raise InflateError(Cancelled, "cancelled by caller")
  }
  self.next_cancel = self.out.length + CANCEL_GRANULARITY
}

///|
#inline
fn MemDecoder::push(self : MemDecoder, b : Byte) -> Unit raise InflateError {
  self.out.append_byte(b)
  self.check_cancel()
}

///|
fn MemDecoder::pull_byte(self : MemDecoder) -> Bool {
  if self.pos >= self.input.length() {
    return false
  }
  self.bitbuf = self.bitbuf | (self.input[self.pos].to_uint() << self.bit_count)
  self.bit_count = self.bit_count + 8
  self.pos = self.pos + 1
  true
}

///|
fn MemDecoder::need(self : MemDecoder, n : Int) -> Unit raise InflateError {
  while self.bit_count < n {
    if !self.pull_byte() {
      raise InflateError(Truncated, "unexpected end of input")
    }
  }
}

///|
fn MemDecoder::read_bits(self : MemDecoder, n : Int) -> Int raise InflateError {
  self.need(n)
  let v = (self.bitbuf & ((1U << n) - 1)).reinterpret_as_int()
  self.bitbuf = self.bitbuf >> n
  self.bit_count = self.bit_count - n
  v
}

///|
fn MemDecoder::huff_sym(
  self : MemDecoder,
  h : HuffmanDecoder,
) -> Int raise InflateError {
  let mut n = h.min
  for ;; {
    while self.bit_count < n {
      if !self.pull_byte() {
        raise InflateError(Truncated, "unexpected end of input")
      }
    }
    let mut chunk = h.chunks[(self.bitbuf & 0x1FF).reinterpret_as_int()]
    n = (chunk & 0xF).reinterpret_as_int()
    if n > HUFFMAN_CHUNK_BITS {
      let link = ((self.bitbuf >> HUFFMAN_CHUNK_BITS) & h.link_mask).reinterpret_as_int()
      chunk = h.links[(chunk >> HUFFMAN_VALUE_SHIFT).reinterpret_as_int() + link]
      n = (chunk & 0xF).reinterpret_as_int()
    }
    if n <= self.bit_count {
      if n == 0 {
        raise InflateError(Corrupt, "corrupt: bad Huffman code")
      }
      self.bitbuf = self.bitbuf >> n
      self.bit_count = self.bit_count - n
      return (chunk >> HUFFMAN_VALUE_SHIFT).reinterpret_as_int()
    }
  }
}

///|
/// Refill the one-shot bit reservoir in two-byte groups when possible. This is
/// intentionally private to `MemDecoder`: a streaming inflater must not read
/// past the current symbol because those bytes can be a container trailer.
#inline
fn MemDecoder::refill_huffman_fast(self : MemDecoder) -> Unit {
  while self.bit_count < HUFFMAN_FAST_REFILL_BITS &&
        self.pos < self.input.length() {
    if self.bit_count <= HUFFMAN_FAST_BITS && self.pos + 1 < self.input.length() {
      let pair = self.input[self.pos].to_uint() |
        (self.input[self.pos + 1].to_uint() << 8)
      self.bitbuf = self.bitbuf | (pair << self.bit_count)
      self.bit_count = self.bit_count + 16
      self.pos = self.pos + 2
    } else {
      self.bitbuf = self.bitbuf |
        (self.input[self.pos].to_uint() << self.bit_count)
      self.bit_count = self.bit_count + 8
      self.pos = self.pos + 1
    }
  }
}

///|
/// Decode a data-block symbol using the prefilled fast reservoir when possible.
/// Unlike the dynamic-header path, this loop is the dominant one-shot decode
/// cost, so it keeps a wide reservoir and only falls back near physical EOF.
#inline
fn MemDecoder::huff_sym_data(
  self : MemDecoder,
  h : HuffmanDecoder,
) -> Int raise InflateError {
  self.refill_huffman_fast()
  if self.bit_count < HUFFMAN_FAST_BITS {
    return self.huff_sym(h)
  }
  let mut chunk = h.chunks[(self.bitbuf & 0x1FF).reinterpret_as_int()]
  let mut n = (chunk & 0xF).reinterpret_as_int()
  if n > HUFFMAN_CHUNK_BITS {
    let link = ((self.bitbuf >> HUFFMAN_CHUNK_BITS) & h.link_mask).reinterpret_as_int()
    chunk = h.links[(chunk >> HUFFMAN_VALUE_SHIFT).reinterpret_as_int() + link]
    n = (chunk & 0xF).reinterpret_as_int()
  }
  guard n != 0 else { raise InflateError(Corrupt, "corrupt: bad Huffman code") }
  self.bitbuf = self.bitbuf >> n
  self.bit_count = self.bit_count - n
  (chunk >> HUFFMAN_VALUE_SHIFT).reinterpret_as_int()
}

///|
/// Decode and copy one length/distance pair. Keeping the uncommon match path
/// separate lets `decode_block` keep its literal fast loop branch-predictable.
#inline
fn MemDecoder::decode_match(
  self : MemDecoder,
  code : Int,
  hd : HuffmanDecoder?,
) -> Unit raise InflateError {
  guard code < 286 else {
    raise InflateError(Corrupt, "corrupt: invalid literal/length code")
  }
  let length_info = length_code_info_table[code - 257]
  let length = (length_info >> 3) + self.read_bits(length_info & 0x7)
  let dsym = if hd is Some(h) {
    self.huff_sym_data(h)
  } else {
    fixed_distance_symbol(self.read_bits(5))
  }
  guard dsym.reinterpret_as_uint() < MAX_NUM_DIST.reinterpret_as_uint() else {
    raise InflateError(Corrupt, "corrupt: invalid distance code")
  }
  let distance_info = distance_code_info_table[dsym]
  // `read_bits(0)` reads nothing and returns 0, so the base serves as the
  // distance when the code carries no extra bits.
  let dist = (distance_info >> 4) + self.read_bits(distance_info & 0xF)
  let available_history = self.out.length
  guard available_history >= WINDOW_SIZE ||
    back_reference_distance_is_valid(dist, available_history) else {
    raise InflateError(Corrupt, "corrupt: distance too far back")
  }
  // Copy a complete match here when capacity and the cancellation interval
  // permit it. Subtract before comparing so the output bound cannot overflow.
  // Short text matches use forward byte copies, including overlapping history.
  let out = self.out
  let start = out.length
  if length <= out.storage.length() - start && length < self.next_cancel - start {
    let storage = out.storage
    if length <= 16 {
      for i in 0.. Unit raise InflateError {
  for ;; {
    let v = self.huff_sym_data(hl)
    guard v >= 256 else {
      self.push(v.to_byte())
      // Most DEFLATE streams contain literal runs. Decode one extra symbol
      // before returning to the match/EOB dispatch to amortize that branch.
      let next = self.huff_sym_data(hl)
      guard next >= 256 else {
        self.push(next.to_byte())
        continue
      }
      guard next != 256 else { return }
      self.decode_match(next, hd)
      continue
    }
    guard v != 256 else { return }
    self.decode_match(v, hd)
  }
}

///|
fn MemDecoder::copy_match(
  self : MemDecoder,
  distance : Int,
  length : Int,
) -> Unit raise InflateError {
  let mut remaining = length
  while remaining > 0 {
    // Keep the public cancellation cadence while copying in large chunks.
    let until_cancel = self.next_cancel - self.out.length
    let chunk = remaining.min(until_cancel)
    self.out.append_repeated_suffix(distance, chunk)
    remaining = remaining - chunk
    self.check_cancel()
  }
}

///|
fn MemDecoder::copy_stored(
  self : MemDecoder,
  length : Int,
) -> Unit raise InflateError {
  let mut remaining = length
  while remaining > 0 {
    // Preserve `inflate_all`'s cancellation cadence while copying each chunk
    // through the core bulk-copy primitive.
    let until_cancel = self.next_cancel - self.out.length
    let chunk = remaining.min(until_cancel)
    self.out.append_bytes(self.input, self.pos, chunk)
    self.pos = self.pos + chunk
    remaining = remaining - chunk
    self.check_cancel()
  }
}

///|
fn MemDecoder::stored_block(self : MemDecoder) -> Unit raise InflateError {
  // The fast Huffman reader can have prefetched whole bytes beyond the current
  // block header. Byte alignment discards only the remaining partial byte; put
  // those unread whole bytes back so LEN/NLEN starts at the correct input byte.
  self.pos = self.pos - (self.bit_count >> 3)
  self.bitbuf = 0
  self.bit_count = 0
  if self.pos + 4 > self.input.length() {
    raise InflateError(Truncated, "unexpected end of input")
  }
  let len = self.input[self.pos].to_int() |
    (self.input[self.pos + 1].to_int() << 8)
  let nlen = self.input[self.pos + 2].to_int() |
    (self.input[self.pos + 3].to_int() << 8)
  self.pos = self.pos + 4
  validate_stored_length(len, nlen)
  if self.pos + len > self.input.length() {
    raise InflateError(Truncated, "unexpected end of input")
  }
  self.copy_stored(len)
}

///|
fn MemDecoder::read_dynamic(self : MemDecoder) -> Unit raise InflateError {
  let (nlit, ndist) = decode_dynamic_header(
    n => self.read_bits(n),
    () => self.huff_sym(self.dyn_litlen),
    self.dyn_litlen,
    self.codebits,
    self.clbits,
  )
  guard self.dyn_litlen.initialize(self.clbits[0:nlit]) else {
    raise InflateError(Corrupt, "corrupt: bad literal/length tree")
  }
  guard self.dyn_dist.initialize(self.clbits[nlit:nlit + ndist]) else {
    raise InflateError(Corrupt, "corrupt: bad distance tree")
  }
}

///|
fn MemDecoder::run(self : MemDecoder) -> Unit raise InflateError {
  self.check_cancel()
  for ;; {
    self.need(3)
    let bfinal = (self.bitbuf & 1) == 1
    let btype = ((self.bitbuf >> 1) & 3).reinterpret_as_int()
    self.bitbuf = self.bitbuf >> 3
    self.bit_count = self.bit_count - 3
    match btype {
      0 => self.stored_block()
      1 => self.decode_block(fixed_huffman_decoder, None)
      2 => {
        self.read_dynamic()
        self.decode_block(self.dyn_litlen, Some(self.dyn_dist))
      }
      _ => raise InflateError(Corrupt, "corrupt: reserved block type")
    }
    if bfinal {
      break
    }
  }
}

///|
/// Decompress a raw DEFLATE stream held entirely in memory. Bytes after the
/// final block are ignored; use `inflate_exact` when an exact framing boundary
/// is required. The output grows without bound; for untrusted input use
/// `inflate_all_limited` or drive `Inflater` with caller-sized output buffers.
/// If supplied, `cancelled` is polled at entry and then every 4096 produced
/// bytes; returning `true` raises `InflateError(Cancelled, _)`.
pub fn inflate_all(
  input : Bytes,
  cancelled? : () -> Bool = () => false,
) -> Bytes raise InflateError {
  let d = MemDecoder(input, cancelled)
  d.run()
  d.out.to_bytes()
}

///|
/// A reusable, whole-buffer raw DEFLATE decompressor. It decodes directly into
/// caller-owned storage while retaining Huffman and dynamic-header workspace
/// across streams.
pub struct Decompressor {
  priv decoder : MemDecoder
}

///|
/// Create a reusable whole-buffer decompressor.
pub fn Decompressor::Decompressor() -> Decompressor {
  { decoder: MemDecoder(b"", () => false), }
}

///|
/// Decompress `input` into `output`, returning the number of bytes written.
/// The complete output must fit in `output`; otherwise this raises
/// `InflateError(OutputLimitExceeded, _)`. As with `inflate_all`, bytes after
/// the final DEFLATE block are ignored. Reuse one `Decompressor` and output
/// buffer to avoid per-stream decoder and result-buffer allocation.
pub fn Decompressor::decompress_into(
  self : Decompressor,
  input : Bytes,
  output : FixedArray[Byte],
  cancelled? : () -> Bool = () => false,
) -> Int raise InflateError {
  self.decoder.reset_into(input, output, cancelled)
  self.decoder.run()
  self.decoder.out.length
}

///|
/// One-shot convenience form of `Decompressor::decompress_into`. It creates a
/// decompressor for this call; use `Decompressor` when decoding many streams.
pub fn inflate_into(
  input : Bytes,
  output : FixedArray[Byte],
  cancelled? : () -> Bool = () => false,
) -> Int raise InflateError {
  Decompressor().decompress_into(input, output, cancelled~)
}

///|
/// Drive the streaming inflater over an in-memory input. If `exact` is true,
/// reject bytes after the final DEFLATE block. If `max_output` is present, one
/// fixed scratch buffer may be filled past that boundary internally, but no
/// oversized result is returned. `cancelled` is polled at entry and once per
/// step (every 8192 produced bytes); returning `true` raises
/// `InflateError(Cancelled, _)`.
fn inflate_streaming_all(
  input : Bytes,
  max_output : Int?,
  exact : Bool,
  cancelled : () -> Bool,
) -> Bytes raise InflateError {
  guard !cancelled() else {
    raise InflateError(Cancelled, "cancelled by caller")
  }
  let inflater = Inflater()
  let scratch = FixedArray::make(INFLATE_SCRATCH_SIZE, b'\x00')
  let decoded : Array[Byte] = []
  let mut input_pos = 0
  for ;; {
    let status = inflater.step_into(input[input_pos:], scratch)
    let consumed = inflater.last_consumed()
    let produced = inflater.last_produced()
    input_pos = input_pos + consumed
    guard !cancelled() else {
      raise InflateError(Cancelled, "cancelled by caller")
    }
    guard max_output.map_or(true, limit => produced <= limit - decoded.length()) else {
      raise InflateError(
        OutputLimitExceeded,
        "decoded output exceeds max_output",
      )
    }
    decoded.append(scratch[:produced])
    match status {
      Done => {
        if exact && input_pos != input.length() {
          raise InflateError(TrailingData, "trailing data after DEFLATE stream")
        }
        return Bytes::from_array(decoded)
      }
      NeedMoreOutput => ()
      NeedMoreInput => raise InflateError(Truncated, "unexpected end of input")
    }
  }
}

///|
/// Decompress exactly one raw DEFLATE stream. Unlike `inflate_all`, this rejects
/// any bytes after the final block.
///
/// Optional controls:
///  - `max_output`: decoded output beyond this size raises
///    `InflateError(OutputLimitExceeded, _)`. A negative value is rejected the
///    same way.
///  - `cancelled`: polled at entry and then once per internal step (every 8192
///    produced bytes); returning `true` raises `InflateError(Cancelled, _)`.
///  - `preallocated`: when `true`, decode runs twice — a counting pass that
///    validates the whole stream, then a second pass filling one exactly sized
///    allocation. Peak memory stays at roughly one decoded output, at the cost
///    of roughly double the decode work; the default `false` grows one buffer
///    in a single pass, trading transient memory for one decode pass.
pub fn inflate_exact(
  input : Bytes,
  max_output? : Int? = None,
  cancelled? : () -> Bool = () => false,
  preallocated? : Bool = false,
) -> Bytes raise InflateError {
  let limit = max_output.map_or(max_output, limit => {
    guard limit >= 0 else {
      raise InflateError(OutputLimitExceeded, "max_output must be non-negative")
    }
    max_output
  })
  if preallocated {
    inflate_preallocated(input, limit, cancelled)
  } else {
    inflate_streaming_all(input, limit, true, cancelled)
  }
}

///|
/// Drive one inflater over the whole in-memory input, delivering produced
/// chunks to `emit` (called once per step with the scratch contents). Returns
/// the total number of bytes produced. `max_output` raises once the total
/// exceeds it; `exact` rejects bytes after the final block; `cancelled` is
/// polled once per step and raises `InflateError(Cancelled, _)`.
fn inflate_drive(
  inflater : Inflater,
  input : Bytes,
  max_output : Int?,
  exact : Bool,
  cancelled : () -> Bool,
  emit : (FixedArray[Byte], Int) -> Unit,
) -> Int raise InflateError {
  let scratch = FixedArray::make(INFLATE_SCRATCH_SIZE, b'\x00')
  let mut total = 0
  let mut input_pos = 0
  for ;; {
    let status = inflater.step_into(input[input_pos:], scratch, end=true)
    let consumed = inflater.last_consumed()
    let produced = inflater.last_produced()
    input_pos = input_pos + consumed
    total = total + produced
    guard !cancelled() else {
      raise InflateError(Cancelled, "cancelled by caller")
    }
    guard max_output.map_or(true, limit => total <= limit) else {
      raise InflateError(
        OutputLimitExceeded,
        "decoded output exceeds max_output",
      )
    }
    emit(scratch, produced)
    // `Inflater::step(end=true)` raises `Truncated` on exhausted input, so
    // only `Done` and `NeedMoreOutput` can be returned here.
    guard status is Done else { continue }
    if exact && input_pos != input.length() {
      raise InflateError(TrailingData, "trailing data after DEFLATE stream")
    }
    return total
  }
}

///|
/// The replay pass must produce exactly the size measured by the counting pass.
/// A mismatch indicates an internal decoder invariant failure rather than bad
/// caller input, so this defensive branch is excluded from behavior coverage.
#coverage.skip
fn validate_preallocation_size(
  size : Int,
  filled : Int,
) -> Unit raise InflateError {
  guard filled == size else {
    raise InflateError(Corrupt, "corrupt: preallocation size mismatch")
  }
}

///|
/// `inflate_exact` with `preallocated=true`: validate and count in one pass
/// over an 8 KiB scratch, then replay the deterministic decode into one exact
/// allocation (the same `Inflater` is reused via `reset`).
fn inflate_preallocated(
  input : Bytes,
  max_output : Int?,
  cancelled : () -> Bool,
) -> Bytes raise InflateError {
  guard !cancelled() else {
    raise InflateError(Cancelled, "cancelled by caller")
  }
  let inflater = Inflater()
  let size = inflate_drive(inflater, input, max_output, true, cancelled, (
    _scratch,
    _produced,
  ) => ())
  inflater.reset()
  let buf = FixedArray::make(size, b'\x00')
  let mut filled = 0
  ignore(
    inflate_drive(inflater, input, None, true, cancelled, (scratch, produced) => {
      scratch.blit_to(buf, len=produced, dst_offset=filled)
      filled = filled + produced
    }),
  )
  validate_preallocation_size(size, filled)
  Bytes::from_array(buf)
}

///|
/// Decompress one raw DEFLATE stream without allowing the returned output to
/// exceed `max_output`. Bytes after the final block retain `inflate_all`'s
/// prefix semantics and are ignored. `cancelled` follows `inflate_all`'s
/// polling contract.
pub fn inflate_all_limited(
  input : Bytes,
  max_output~ : Int,
  cancelled? : () -> Bool = () => false,
) -> Bytes raise InflateError {
  guard max_output >= 0 else {
    raise InflateError(OutputLimitExceeded, "max_output must be non-negative")
  }
  inflate_streaming_all(input, Some(max_output), false, cancelled)
}