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

///|
pub extend Compression with Eq::{not_equal, equal}

///|
pub extend Compression with Debug::{to_repr}

///|
/// Resource bounds applied to a bounded read. Every field is a ceiling in
/// bytes (or, for `max_entries`, a count); `ReadLimits::default()` imposes no
/// limits. `max_preserved_source_bytes` bounds the raw local/central records
/// retained in memory so a byte-preserving rewrite cannot be weaponized as an
/// unbounded retention bomb.
pub struct ReadLimits {
  max_package_bytes : Int
  max_entries : Int
  max_entry_uncompressed_bytes : Int
  max_total_uncompressed_bytes : Int
  max_preserved_source_bytes : Int
}

///|
/// The default, unlimited resource bounds.
pub fn ReadLimits::default() -> ReadLimits {
  {
    max_package_bytes: 0x7fff_ffff,
    max_entries: 0x7fff_ffff,
    max_entry_uncompressed_bytes: 0x7fff_ffff,
    max_total_uncompressed_bytes: 0x7fff_ffff,
    max_preserved_source_bytes: 0x7fff_ffff,
  }
}

///|
/// A copy of `self` with a new package-size ceiling.
pub fn ReadLimits::with_package_limit(
  self : ReadLimits,
  limit : Int,
) -> ReadLimits {
  {
    max_package_bytes: limit,
    max_entries: self.max_entries,
    max_entry_uncompressed_bytes: self.max_entry_uncompressed_bytes,
    max_total_uncompressed_bytes: self.max_total_uncompressed_bytes,
    max_preserved_source_bytes: self.max_preserved_source_bytes,
  }
}

///|
/// A copy of `self` with a new entry-count ceiling.
pub fn ReadLimits::with_entries_limit(
  self : ReadLimits,
  limit : Int,
) -> ReadLimits {
  {
    max_package_bytes: self.max_package_bytes,
    max_entries: limit,
    max_entry_uncompressed_bytes: self.max_entry_uncompressed_bytes,
    max_total_uncompressed_bytes: self.max_total_uncompressed_bytes,
    max_preserved_source_bytes: self.max_preserved_source_bytes,
  }
}

///|
/// A copy of `self` with a new per-entry uncompressed ceiling.
pub fn ReadLimits::with_entry_limit(
  self : ReadLimits,
  limit : Int,
) -> ReadLimits {
  {
    max_package_bytes: self.max_package_bytes,
    max_entries: self.max_entries,
    max_entry_uncompressed_bytes: limit,
    max_total_uncompressed_bytes: self.max_total_uncompressed_bytes,
    max_preserved_source_bytes: self.max_preserved_source_bytes,
  }
}

///|
/// A copy of `self` with a new total uncompressed ceiling.
pub fn ReadLimits::with_total_limit(
  self : ReadLimits,
  limit : Int,
) -> ReadLimits {
  {
    max_package_bytes: self.max_package_bytes,
    max_entries: self.max_entries,
    max_entry_uncompressed_bytes: self.max_entry_uncompressed_bytes,
    max_total_uncompressed_bytes: limit,
    max_preserved_source_bytes: self.max_preserved_source_bytes,
  }
}

///|
/// A copy of `self` with a new preserved-source-record ceiling.
pub fn ReadLimits::with_preserved_source_limit(
  self : ReadLimits,
  limit : Int,
) -> ReadLimits {
  {
    max_package_bytes: self.max_package_bytes,
    max_entries: self.max_entries,
    max_entry_uncompressed_bytes: self.max_entry_uncompressed_bytes,
    max_total_uncompressed_bytes: self.max_total_uncompressed_bytes,
    max_preserved_source_bytes: limit,
  }
}

///|
/// Raw local and central directory records captured from a read, kept so an
/// unchanged entry can be re-emitted byte-for-byte without re-encoding.
priv struct SourceRecord {
  local_record : Bytes
  central_record : Bytes
  central_zip64_offset_position : Int?
}

///|
/// A single archive entry: its decoded payload plus the metadata the central
/// directory carried for it. Entries produced by `read` also retain their raw
/// source records (`raw_local_record` / `raw_central_record`) 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 : UInt
  priv data_descriptor : Bool
  priv compressed_size : Int
  priv source : SourceRecord?
  priv origin_local_offset : Int?
}

///|
pub fn Entry::name(self : Entry) -> String {
  self.name
}

///|
pub fn Entry::data(self : Entry) -> BytesView {
  self.data
}

///|
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) -> UInt {
  self.crc32
}

///|
pub fn Entry::data_descriptor(self : Entry) -> Bool {
  self.data_descriptor
}

///|
/// The original local header + payload (+ data descriptor) bytes of a pristine
/// entry read from an archive, or `None` for entries created in memory or
/// replaced after a read.
pub fn Entry::raw_local_record(self : Entry) -> BytesView? {
  self.source.map(source => source.local_record)
}

///|
/// The original central directory record bytes of a pristine entry read from an
/// archive, or `None` for entries created in memory or replaced after a read.
pub fn Entry::raw_central_record(self : Entry) -> BytesView? {
  self.source.map(source => source.central_record)
}

///|
/// 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 : Bytes
  zip64_end_record : Bytes?
  zip64_locator : Bytes?
}

///|
/// 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?
}

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

///|
/// Append a new entry whose payload is `data`. `data` is owned by the archive,
/// so later `Bytes` mutation cannot corrupt it. The default method is
/// `Deflate`; pass `Store` to keep the payload verbatim. Setting
/// `data_descriptor=true` emits the entry in streaming form (zeroed local
/// sizes + a trailing data descriptor).
pub fn Archive::add(
  self : Archive,
  name : String,
  data : BytesView,
  compression? : Compression = Deflate,
  data_descriptor? : Bool = false,
) -> Unit {
  let data = data.to_owned()
  self.entries.push({
    name,
    data,
    compression,
    data_descriptor,
    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 {
  for index in 0.. Bool {
  for index in 0.. BytesView? {
  for entry in self.entries {
    if entry.name()[:] == name {
      return Some(entry.data())
    }
  }
  None
}

///|
/// 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.comment = comment.to_owned()
  self.trailer = None
}

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

///|
/// Deprecated alias of `Archive::clone`.
#deprecated("use `Archive::clone` instead")
pub fn Archive::fork(self : Archive) -> Archive {
  self.clone()
}

///|
/// Conservatively estimates bytes retained by this materialized archive: the
/// archive comment, captured trailer records, and per-entry payloads, names,
/// and preserved source records (each with a bookkeeping reserve). Payloads
/// and records are counted exactly; only allocator/container overhead uses a
/// fixed per-entry reserve. The source package buffer an archive was read from
/// is not included.
pub fn Archive::retained_size_estimate(self : Archive) -> Int64 {
  let mut total = self.comment.length().to_int64()
  match self.trailer {
    Some(trailer) => {
      total = total + trailer.classic_end_record.length().to_int64()
      match trailer.zip64_end_record {
        Some(record) => total = total + record.length().to_int64()
        None => ()
      }
      match trailer.zip64_locator {
        Some(locator) => total = total + locator.length().to_int64()
        None => ()
      }
    }
    None => ()
  }
  for entry in self.entries {
    total = total + entry.data.length().to_int64()
    total = total + entry.name.length().to_int64() * 2L
    // Entry/array slots, references, option wrappers, and allocator metadata.
    total = total + 512L
    match entry.source {
      Some(source) => {
        total = total + source.local_record.length().to_int64()
        total = total + source.central_record.length().to_int64()
        total = total + 128L
      }
      None => ()
    }
  }
  total
}