///|
/// The ZIP compression methods this package can read and write.
pub(all) enum Compression {
  Store
  Deflate
} derive(Eq, @debug.Debug)

///|
fn Compression::code(compression : Compression) -> Int32 {
  match compression {
    Store => 0
    Deflate => 8
  }
}

///|
/// Resource bounds applied to a bounded read. Every field is a ceiling in
/// bytes (or, for `max_entries`, a count); `ReadLimits()` imposes no
/// limits beyond the in-memory Int32 capacity. `max_package_bytes` also bounds
/// the complete source retained for byte-preserving rewrites.
#valtype
pub struct ReadLimits {
  max_package_bytes : Int32
  max_entries : Int32
  max_entry_uncompressed_bytes : Int32
  max_total_uncompressed_bytes : Int32
}

///|
/// Construct resource bounds. Omitted ceilings default to the largest `Int`.
/// Each supplied ceiling must be non-negative; `read` raises `InvalidArgument`
/// for a negative one and rejects an exceeded ceiling before accepting the
/// corresponding resource.
pub fn ReadLimits::ReadLimits(
  max_package_bytes~ : Int32 = 0x7fff_ffff,
  max_entries~ : Int32 = 0x7fff_ffff,
  max_entry_uncompressed_bytes~ : Int32 = 0x7fff_ffff,
  max_total_uncompressed_bytes~ : Int32 = 0x7fff_ffff,
) -> ReadLimits {
  {
    max_package_bytes,
    max_entries,
    max_entry_uncompressed_bytes,
    max_total_uncompressed_bytes,
  }
}

///|
/// Views of source records used to reuse an unchanged entry's encoded payload
/// and metadata. Central-directory offsets may be patched when writing.
priv struct SourceRecord {
  local_record : BytesView
  central_record : BytesView
  central_zip64_offset_position : Int32?
}

///|
/// A single archive entry: its decoded payload plus the metadata the central
/// directory carried for it. Entries produced by `read` retain their source
/// records for byte-preserving rewrites; entries built with `Archive::add` do not.
pub struct Entry {
  priv name : String
  priv data : Bytes
  priv compression : Compression
  priv crc32 : UInt32
  priv data_descriptor : Bool
  priv compressed_size : Int32
  priv source : SourceRecord?
  priv origin_local_offset : Int32?
}

///|
/// The entry name decoded as UTF-8.
pub fn Entry::name(self : Entry) -> String {
  self.name
}

///|
/// A read-only view of the decompressed payload.
pub fn Entry::data(self : Entry) -> BytesView {
  self.data
}

///|
/// The compression method used for this entry.
pub fn Entry::compression(self : Entry) -> Compression {
  self.compression
}

///|
/// The entry's stored CRC-32: the central-directory value for entries read from
/// an archive, or computed at `add` time for entries created in memory.
pub fn Entry::crc32(self : Entry) -> UInt32 {
  self.crc32
}

///|
/// Raw end-of-archive records captured from a read so a byte-preserving write
/// can re-emit the trailer (with patched counts/sizes/offsets) instead of
/// regenerating it.
priv struct TrailerTemplate {
  classic_end_record : BytesView
  zip64_end_record : BytesView?
  zip64_locator : BytesView?
}

///|
/// An in-memory ZIP archive: an ordered list of entries plus an archive
/// comment. Reading a ZIP materializes this; writing serializes it back.
pub struct Archive {
  priv entries : Array[Entry]
  priv mut comment : Bytes
  priv mut trailer : TrailerTemplate?
  priv mut original : Bytes?
}

///|
/// Create an empty archive.
pub fn Archive::Archive() -> Archive {
  { entries: [], comment: b"", trailer: None, original: None, }
}

///|
/// Append a new entry, retaining its payload as `Bytes`. The default method is
/// `Deflate`; pass `Store` to keep the payload verbatim. Serialization chooses
/// the local-header and descriptor layout; use `Writer` for chunked input.
pub fn Archive::add(
  self : Archive,
  name : String,
  data : BytesView,
  compression~ : Compression = Deflate,
) -> Unit {
  let data = data.to_owned()
  self.original = None
  self.entries.push({
    name,
    data,
    compression,
    data_descriptor: false,
    crc32: @checksum.crc32(data),
    compressed_size: data.length(),
    source: None,
    origin_local_offset: None,
  })
}

///|
/// Replace the payload of the entry named `name`. The entry keeps its method
/// policy but drops its raw source records (the next write re-encodes it).
/// Its original position in the archive is retained, so a byte-preserving
/// write keeps the entry where it was. Returns `false` when no such entry
/// exists.
pub fn Archive::replace(
  self : Archive,
  name : StringView,
  data : BytesView,
) -> Bool {
  guard self.entries.search_by(entry => entry.name()[:] == name) is Some(index) else {
    return false
  }
  self.original = None
  let entry = self.entries[index]
  let data = data.to_owned()
  self.entries[index] = {
    name: entry.name,
    data,
    compression: entry.compression,
    data_descriptor: entry.data_descriptor,
    crc32: @checksum.crc32(data),
    compressed_size: data.length(),
    source: None,
    origin_local_offset: entry.origin_local_offset,
  }
  true
}

///|
/// Remove the entry named `name`. Returns `false` when no such entry exists.
pub fn Archive::remove(self : Archive, name : StringView) -> Bool {
  guard self.entries.search_by(entry => entry.name()[:] == name) is Some(index) else {
    return false
  }
  self.original = None
  ignore(self.entries.remove(index))
  true
}

///|
/// A read-only view of an entry payload, or `None` when absent.
pub fn Archive::get(self : Archive, name : StringView) -> BytesView? {
  guard self.entries.search_by(entry => entry.name()[:] == name) is Some(index) else {
    return None
  }
  Some(self.entries[index].data())
}

///|
/// The archive's entry list in order.
pub fn Archive::entries(self : Archive) -> ArrayView[Entry] {
  self.entries
}

///|
/// The archive-level comment bytes (empty when none).
pub fn Archive::comment(self : Archive) -> BytesView {
  self.comment
}

///|
/// Replace the archive-level comment. Long comments (over 65535 bytes) are
/// rejected at write time. Changing the comment invalidates a retained
/// end-record template, so a later byte-preserving write regenerates the
/// trailer with the new comment.
pub fn Archive::set_comment(self : Archive, comment : BytesView) -> Unit {
  self.original = None
  self.comment = comment.to_owned()
  self.trailer = None
}

///|
/// Returns an independently mutable snapshot of this archive. The clone shares
/// entry payloads and source storage but owns a
/// fresh entries array, so `add`/`replace`/`remove` on either side never
/// affects the other.
pub fn Archive::clone(self : Archive) -> Archive {
  {
    entries: self.entries.copy(),
    comment: self.comment,
    trailer: self.trailer,
    original: self.original,
  }
}