///|
/// Write GZIP header
fn gzh(c : FixedArray[Byte], o : GzipOptions) -> Unit {
  c[0] = b'\x1F' // magic 1
  c[1] = b'\x8B' // magic 2
  c[2] = b'\x08' // CM = deflate
  let xfl : Byte = if o.level < 2 {
    b'\x04'
  } else if o.level == 9 {
    b'\x02'
  } else {
    b'\x00'
  }
  c[8] = xfl
  c[9] = b'\x03' // OS = Unix
  if o.mtime != 0 {
    wbytes(c, 4, o.mtime)
  }
  if o.filename.length() > 0 {
    c[3] = b'\x08' // FNAME flag
    let fn_bytes = o.filename
    for i in 0.. Int raise FzipError {
  if offset < 0 || offset > d.length() - 10 {
    raise fzip_err(InvalidHeader, msg="invalid gzip data")
  }
  if d[offset] != b'\x1F' ||
    d[offset + 1] != b'\x8B' ||
    d[offset + 2] != b'\x08' {
    raise fzip_err(InvalidHeader, msg="invalid gzip data")
  }
  let flg = d[offset + 3].to_int()
  if (flg & 0xE0) != 0 {
    raise fzip_err(InvalidHeader, msg="invalid gzip data")
  }
  if flg == 0 {
    return offset + 10
  }
  let mut st = offset + 10
  if (flg & 4) != 0 {
    if st > d.length() - 2 {
      raise fzip_err(InvalidHeader, msg="invalid gzip data")
    }
    let extra_len = d[st].to_int() | (d[st + 1].to_int() << 8)
    st += 2
    if extra_len > d.length() - st {
      raise fzip_err(InvalidHeader, msg="invalid gzip data")
    }
    st += extra_len
  }
  // skip FNAME and FCOMMENT
  let mut zs = ((flg >> 3) & 1) + ((flg >> 4) & 1)
  while zs > 0 {
    if st >= d.length() {
      raise fzip_err(InvalidHeader, msg="invalid gzip data")
    }
    if d[st] == b'\x00' {
      zs -= 1
    }
    st += 1
  }
  // validate and skip FHCRC
  if (flg & 2) != 0 {
    if st > d.length() - 2 {
      raise fzip_err(InvalidHeader, msg="invalid gzip data")
    }
    let expected_crc = d[st].to_uint() | (d[st + 1].to_uint() << 8)
    let crc = CRC32State::new()
    crc.push_range(d, offset, st - offset)
    if (crc.digest() & 0xFFFFU) != expected_crc {
      raise fzip_err(InvalidChecksum, msg="gzip header CRC-16 mismatch")
    }
    st += 2
  }
  st
}

///|
/// Read a sync-API-safe GZIP ISIZE value from a member footer.
fn gzl(d : FixedArray[Byte], footer : Int) -> Int raise FzipError {
  if footer < 0 || footer > d.length() - 8 {
    raise fzip_err(InvalidHeader, msg="invalid gzip data")
  }
  let raw = b4(d, footer + 4)
  if raw > max_int_val().reinterpret_as_uint() {
    raise fzip_err(InvalidZipData, msg="gzip ISIZE exceeds Int range")
  }
  raw.reinterpret_as_int()
}

///|
/// Calculate GZIP header length
fn gzhl(o : GzipOptions) -> Int {
  10 + (if o.filename.length() > 0 { o.filename.length() + 1 } else { 0 })
}

///|
/// Select the fixed output buffer for the single-member fast path.
fn single_member_gunzip_buffer(
  isize : Int,
  out : FixedArray[Byte]?,
) -> FixedArray[Byte] raise FzipError {
  match out {
    Some(buf) => {
      if isize > buf.length() {
        raise fzip_err(InvalidZipData, msg="output buffer too small")
      }
      buf
    }
    None => FixedArray::make(isize, b'\x00')
  }
}

///|
/// Try the fixed-buffer one-shot path used by ordinary single-member streams.
/// Return `None` when the first DEFLATE stream ends before the final footer or
/// the final ISIZE-sized candidate buffer is insufficient. The caller then
/// falls back to concatenated-member decoding and produces the definitive error.
fn try_gunzip_single_member(
  data : FixedArray[Byte],
  data_start : Int,
  footer : Int,
  isize : Int,
  opts : GunzipOptions,
) -> FixedArray[Byte]? raise FzipError {
  let crc_st : CRC32State? = if opts.verify_checksum {
    Some(CRC32State::new())
  } else {
    None
  }
  let inflate_st = InflateState::new(2)
  let output = single_member_gunzip_buffer(isize, opts.out)
  let (buf, len) = try
    inflt(
      data,
      inflate_st,
      Some(output),
      opts.dictionary,
      opts.max_input_size,
      opts.max_output_size,
      dat_off=data_start,
      dat_end=footer,
      crc_state=crc_st,
    )
  catch {
    FzipError(code=InvalidZipData, ..) => return None
    err => raise err
  } noraise {
    result => result
  }
  if inflate_st.final_ == 0 || !(inflate_st.lm is None) {
    raise fzip_err(UnexpectedEOF, msg="truncated gzip DEFLATE data")
  }
  let actual_footer = shft(inflate_st.pos)
  if actual_footer < data_start || actual_footer > footer {
    raise fzip_err(InvalidHeader, msg="invalid gzip data")
  }
  if actual_footer != footer {
    return None
  }
  match crc_st {
    Some(cs) => {
      let expected_crc = b4(data, footer)
      if cs.digest() != expected_crc {
        raise fzip_err(InvalidChecksum, msg="gzip CRC-32 mismatch")
      }
    }
    None => ()
  }
  if len != isize {
    raise fzip_err(InvalidZipData, msg="gzip ISIZE mismatch")
  }
  Some(trim_buf(buf, len))
}

///|
/// Compress data into a GZIP stream.
///
/// The output contains a GZIP header, a DEFLATE payload, and a footer with the
/// CRC-32 checksum and original input size. `GzipOptions` controls compression
/// level, optional dictionary use, and header metadata such as timestamp and
/// original filename. When a dictionary is used, callers must pass the same
/// dictionary to `gunzip_sync`; the GZIP format does not carry a dictionary ID.
pub fn gzip_sync(
  data : FixedArray[Byte],
  opts? : GzipOptions = GzipOptions::default(),
) -> FixedArray[Byte] {
  let c = CRC32State::new()
  let l = data.length()
  let (d, len) = dopt(
    data,
    { level: opts.level, mem: opts.mem, dictionary: opts.dictionary },
    gzhl(opts),
    8,
    None,
    crc_state=Some(c),
  )
  gzh(d, opts)
  wbytes(d, len - 8, c.digest().reinterpret_as_int())
  wbytes(d, len - 4, l)
  trim_buf(d, len)
}

///|
/// Decompress a GZIP stream.
///
/// Every concatenated GZIP member is parsed and inflated in order, and their
/// outputs are joined. Each member's ISIZE is always validated and its CRC-32
/// footer is verified by default. Optional FHCRC header checksums are always
/// validated when present. `max_input_size` applies to the complete compressed
/// stream. Set `verify_checksum` to `false` in `GunzipOptions` only when footer
/// checksum validation is handled elsewhere.
pub fn gunzip_sync(
  data : FixedArray[Byte],
  opts? : GunzipOptions = GunzipOptions::default(),
) -> FixedArray[Byte] raise FzipError {
  if data.length() == 0 {
    raise fzip_err(InvalidHeader, msg="invalid gzip data")
  }
  if opts.max_input_size < 0 {
    raise fzip_err(InvalidZipData, msg="input exceeds max_input_size")
  }
  if data.length() > opts.max_input_size {
    raise fzip_err(InvalidZipData, msg="input exceeds max_input_size")
  }
  if opts.max_output_size < 0 {
    raise fzip_err(
      InvalidZipData,
      msg="uncompressed size exceeds max_output_size",
    )
  }
  let data_start = gzs(data)
  let final_footer = data.length() - 8
  if data_start > final_footer {
    raise fzip_err(InvalidHeader, msg="invalid gzip data")
  }
  let final_isize = gzl(data, final_footer)
  if final_isize > opts.max_output_size {
    raise fzip_err(InvalidZipData, msg="gzip ISIZE exceeds max_output_size")
  }
  match
    try_gunzip_single_member(data, data_start, final_footer, final_isize, opts) {
    Some(result) => return result
    None => ()
  }
  let chunks : Array[FixedArray[Byte]] = []
  let mut member_offset = 0
  let mut total_output = 0
  while member_offset < data.length() {
    let data_start = gzs(data, offset=member_offset)
    if data_start > data.length() - 8 {
      raise fzip_err(InvalidHeader, msg="invalid gzip data")
    }
    let output_remaining = opts.max_output_size - total_output
    let input_remaining = data.length() - data_start
    let initial_size = if input_remaining < output_remaining {
      if input_remaining < 32768 {
        input_remaining
      } else {
        32768
      }
    } else if output_remaining < 32768 {
      output_remaining
    } else {
      32768
    }
    let crc_st : CRC32State? = if opts.verify_checksum {
      Some(CRC32State::new())
    } else {
      None
    }
    let inflate_st = InflateState::new(0)
    let (member_buf, member_len) = inflt(
      data,
      inflate_st,
      Some(FixedArray::make(initial_size, b'\x00')),
      opts.dictionary,
      opts.max_input_size,
      output_remaining,
      dat_off=data_start,
      dat_end=data.length(),
      crc_state=crc_st,
    )
    if inflate_st.final_ == 0 || !(inflate_st.lm is None) {
      raise fzip_err(UnexpectedEOF, msg="truncated gzip DEFLATE data")
    }
    let footer = shft(inflate_st.pos)
    if footer < data_start || footer > data.length() - 8 {
      raise fzip_err(InvalidHeader, msg="invalid gzip data")
    }
    match crc_st {
      Some(cs) => {
        let expected_crc = b4(data, footer)
        if cs.digest() != expected_crc {
          raise fzip_err(InvalidChecksum, msg="gzip CRC-32 mismatch")
        }
      }
      None => ()
    }
    let isize = gzl(data, footer)
    if isize > output_remaining {
      raise fzip_err(InvalidZipData, msg="gzip ISIZE exceeds max_output_size")
    }
    if member_len != isize {
      raise fzip_err(InvalidZipData, msg="gzip ISIZE mismatch")
    }
    chunks.push(trim_buf(member_buf, member_len))
    total_output += member_len
    member_offset = footer + 8
  }
  match opts.out {
    Some(out) => {
      if total_output > out.length() {
        raise fzip_err(InvalidZipData, msg="output buffer too small")
      }
      let mut offset = 0
      for i in 0.. if chunks.length() == 1 { chunks[0] } else { concat_chunks(chunks) }
  }
}