///|
/// Write Zlib header
fn zlh(c : FixedArray[Byte], o : ZlibOptions) -> Unit {
  let lv = o.level
  let fl_val = if lv == 0 {
    0
  } else if lv < 6 {
    1
  } else if lv == 9 {
    3
  } else {
    2
  }
  c[0] = b'\x78' // CMF: CM=8, CINFO=7
  let has_dict = match o.dictionary {
    Some(_) => 32
    None => 0
  }
  c[1] = ((fl_val << 6) | has_dict).to_byte()
  // fix check bits
  let check = ((c[0].to_int() << 8) | c[1].to_int()) % 31
  c[1] = (c[1].to_int() | (31 - check)).to_byte()
  match o.dictionary {
    Some(dict) => {
      let h = AdlerState::new()
      h.push(dict)
      let dv = h.digest().reinterpret_as_int()
      wbytes(c, 2, dv)
    }
    None => ()
  }
}

///|
/// Parse Zlib header and return data start offset
fn zls(d : FixedArray[Byte], has_dict : Bool) -> Int raise FzipError {
  if d.length() < 2 {
    raise fzip_err(InvalidHeader, msg="invalid zlib data")
  }
  if (d[0].to_int() & 15) != 8 ||
    d[0].to_int() >> 4 > 7 ||
    ((d[0].to_int() << 8) | d[1].to_int()) % 31 != 0 {
    raise fzip_err(InvalidHeader, msg="invalid zlib data")
  }
  let dict_flag = (d[1].to_int() >> 5) & 1
  if dict_flag == 1 && !has_dict {
    raise fzip_err(InvalidHeader, msg="invalid zlib data: need dictionary")
  }
  if dict_flag == 0 && has_dict {
    raise fzip_err(
      InvalidHeader,
      msg="invalid zlib data: unexpected dictionary",
    )
  }
  // header size: 2 bytes + optional 4 bytes for dict checksum
  let extra = if ((d[1].to_int() >> 3) & 4) != 0 { 4 } else { 0 }
  extra + 2
}

///|
/// Compress data into a Zlib stream.
///
/// The output contains a Zlib header, a raw DEFLATE payload, and an Adler-32
/// checksum footer. If `ZlibOptions.dictionary` is set, the dictionary checksum
/// is written into the header and callers must provide the same dictionary when
/// decompressing.
pub fn zlib_sync(
  data : FixedArray[Byte],
  opts? : ZlibOptions = ZlibOptions::default(),
) -> FixedArray[Byte] {
  let a = AdlerState::new()
  let pre = match opts.dictionary {
    Some(_) => 6
    None => 2
  }
  let (d, len) = dopt(
    data,
    { level: opts.level, mem: opts.mem, dictionary: opts.dictionary },
    pre,
    4,
    None,
    adler_state=Some(a),
  )
  zlh(d, opts)
  wbytes(d, len - 4, a.digest().reinterpret_as_int())
  trim_buf(d, len)
}

///|
/// Decompress a Zlib stream.
///
/// The Zlib header is validated before inflating the inner DEFLATE payload. The
/// Adler-32 footer is verified by default and can be disabled with
/// `UnzlibOptions.verify_checksum` for trusted inputs. For dictionary streams,
/// fzip checks that a dictionary is expected but does not compare the header's
/// dictionary checksum with the provided dictionary.
pub fn unzlib_sync(
  data : FixedArray[Byte],
  opts? : UnzlibOptions = UnzlibOptions::default(),
) -> FixedArray[Byte] raise FzipError {
  let has_dict = match opts.dictionary {
    Some(_) => true
    None => false
  }
  let st = zls(data, has_dict)
  let end = data.length() - 4
  if end < st {
    raise fzip_err(InvalidHeader, msg="invalid zlib data")
  }
  let adler_st : AdlerState? = if opts.verify_checksum {
    Some(AdlerState::new())
  } else {
    None
  }
  let (buf, len) = inflt(
    data,
    InflateState::new(2),
    opts.out,
    opts.dictionary,
    opts.max_input_size,
    opts.max_output_size,
    dat_off=st,
    dat_end=end,
    adler_state=adler_st,
  )
  match adler_st {
    Some(a_s) => {
      let expected_adler = b4(data, end)
      if a_s.digest() != expected_adler {
        raise fzip_err(InvalidChecksum, msg="zlib Adler-32 mismatch")
      }
    }
    None => ()
  }
  trim_buf(buf, len)
}