///|
/// 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 d.length() < 10 {
    raise fzip_err(InvalidHeader, msg="invalid gzip data")
  }
  if d[0] != b'\x1F' || d[1] != b'\x8B' || d[2] != b'\x08' {
    raise fzip_err(InvalidHeader, msg="invalid gzip data")
  }
  let flg = d[3].to_int()
  if (flg & 0xE0) != 0 {
    raise fzip_err(InvalidHeader, msg="invalid gzip data")
  }
  if flg == 0 {
    return 10
  }
  let mut st = 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 += extra_len + 2
    if st > d.length() {
      raise fzip_err(InvalidHeader, msg="invalid gzip data")
    }
  }
  // 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
  }
  // skip FHCRC
  st + (flg & 2)
}

///|
/// Read a sync-API-safe GZIP ISIZE footer value.
fn gzl(d : FixedArray[Byte]) -> Int raise FzipError {
  let l = d.length()
  let raw = d[l - 4].to_uint() |
    (d[l - 3].to_uint() << 8) |
    (d[l - 2].to_uint() << 16) |
    (d[l - 1].to_uint() << 24)
  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 })
}

///|
/// 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.
///
/// The GZIP header is parsed, the inner DEFLATE payload is inflated, and the
/// CRC-32 footer is verified by default. Set `verify_checksum` to `false` in
/// `GunzipOptions` only when checksum validation is handled elsewhere. If no
/// output buffer is provided, fzip allocates one using the GZIP ISIZE footer.
pub fn gunzip_sync(
  data : FixedArray[Byte],
  opts? : GunzipOptions = GunzipOptions::default(),
) -> FixedArray[Byte] raise FzipError {
  let st = gzs(data)
  if st + 8 > data.length() {
    raise fzip_err(InvalidHeader, msg="invalid gzip data")
  }
  let isize = gzl(data)
  if isize > opts.max_output_size {
    raise fzip_err(InvalidZipData, msg="gzip ISIZE exceeds max_output_size")
  }
  let out = match opts.out {
    Some(o) => o
    None => FixedArray::make(isize, b'\x00')
  }
  let crc_st : CRC32State? = if opts.verify_checksum {
    Some(CRC32State::new())
  } else {
    None
  }
  let (buf, len) = inflt(
    data,
    InflateState::new(2),
    Some(out),
    opts.dictionary,
    opts.max_input_size,
    opts.max_output_size,
    dat_off=st,
    dat_end=data.length() - 8,
    crc_state=crc_st,
  )
  match crc_st {
    Some(cs) => {
      let expected_crc = b4(data, data.length() - 8)
      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")
  }
  trim_buf(buf, len)
}