// File data in a ZIP archive
// Represents a single file entry with its compressed data and metadata

///|
/// Compression type - Re-exported from types package
using @types {type Compression}

///|
/// Helper: UInt to hex string
fn UInt::to_hex_string(self : UInt) -> String {
  let hex_digits = [
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
  ]
  let mut result = ""
  let v = self
  for i = 0; i < 8; i = i + 1 {
    let digit = ((v >> (28 - i * 4)) & (0xF : UInt)).reinterpret_as_int()
    result = result + Char::to_string(hex_digits[digit])
  }
  result
}

///|
/// File data in a ZIP archive
///
/// Represents a single file entry with its compressed data and metadata.
/// This struct stores the essential information from both the Local File Header
/// and Central Directory File Header as defined in the ZIP specification.
///
/// # ZIP Format Background
/// The ZIP file format stores files in a specific structure:
/// 1. Local file header + compressed data (for each file)
/// 2. Central directory (index of all files)
/// 3. End of central directory record (EOCD)
///
/// This struct combines the metadata from these structures.
pub struct File {
  /// Version made by (2 bytes as per ZIP spec)
  ///
  /// Upper byte: host system (0=DOS, 3=UNIX, etc.)
  /// Lower byte: PKZIP version (e.g., 20 = v2.0, 45 = v4.5)
  ///
  /// Default: 0x0314 = (3 << 8) | 20 = UNIX + PKZIP 2.0
  ///
  /// This field indicates the system and software version that created
  /// the ZIP entry, which can affect how file attributes are interpreted.
  version_made_by : UInt16
  /// Version needed to extract (2 bytes as per ZIP spec)
  ///
  /// Minimum PKZIP version required to extract this file.
  /// - 10 = PKZIP 1.0 (stored/shrunk)
  /// - 20 = PKZIP 2.0 (deflate compression)
  /// - 45 = PKZIP 4.5 (ZIP64 extensions)
  ///
  /// Default: 20 (PKZIP 2.0 for deflate support)
  ///
  /// This ensures compatibility by declaring the minimum version needed
  /// to properly extract and decompress the file data.
  version_needed_to_extract : UInt16
  /// General purpose bit flags (2 bytes as per ZIP spec)
  ///
  /// Bit 0: if set, file is encrypted
  /// Bit 1-2: compression-specific flags
  /// Bit 3: if set, CRC-32 and sizes are in data descriptor after file data
  /// Bit 11 (0x800): if set, filename and comment are UTF-8 encoded
  ///
  /// Default: 0x0800 (UTF-8 encoding enabled)
  ///
  /// These flags control various aspects of file handling, encryption,
  /// and character encoding in the ZIP archive.
  gp_flags : UInt16
  /// Compression method (enum mapped to ZIP spec values)
  ///
  /// ZIP specification compression methods:
  /// - 0 = Stored (no compression)
  /// - 8 = Deflate (RFC 1951)
  /// - 12 = Bzip2
  /// - 14 = LZMA
  /// - 93 = Zstandard
  /// - 95 = XZ
  ///
  /// This implementation primarily supports Stored (0) and Deflate (8).
  /// The Compression enum provides type-safe access to these values.
  compression : Compression
  /// Start offset within compressed_bytes buffer
  ///
  /// When compressed_bytes contains multiple files or has a header,
  /// this indicates where this file's compressed data actually begins.
  /// Allows sharing a single Bytes buffer among multiple Files.
  ///
  /// For standalone file data, this is typically 0.
  start : Int
  /// Compressed size in bytes
  ///
  /// The actual number of bytes occupied by the compressed data
  /// in the compressed_bytes buffer, starting from the 'start' offset.
  ///
  /// Must not exceed max_file_size (2^32 - 1 = 4GB) for non-ZIP64 archives.
  /// For ZIP64 archives, larger sizes are stored in extra fields.
  compressed_size : Int
  /// The compressed data buffer
  ///
  /// Contains the actual compressed file data. This may be:
  /// - The complete compressed stream for this file
  /// - A shared buffer containing multiple files' data
  /// - The original ZIP archive's raw bytes
  ///
  /// Use 'start' and 'compressed_size' to extract this file's data.
  /// Call to_bytes() to decompress and verify the data.
  compressed_bytes : Bytes
  /// Uncompressed size in bytes
  ///
  /// The expected size of the file after decompression.
  /// Used to:
  /// - Pre-allocate output buffers during decompression
  /// - Validate decompression completed successfully
  /// - Display file sizes to users
  ///
  /// Must match the actual decompressed data size for integrity.
  /// Must not exceed max_file_size (4GB) for non-ZIP64 archives.
  decompressed_size : Int
  /// CRC-32 checksum of uncompressed data
  ///
  /// IEEE 802.3 CRC-32 polynomial (0xEDB88320) as defined in RFC 1952.
  /// Computed on the uncompressed file data for integrity verification.
  ///
  /// During extraction:
  /// 1. File is decompressed
  /// 2. CRC-32 is calculated on decompressed data
  /// 3. Result is compared with this stored value
  /// 4. Mismatch indicates data corruption
  ///
  /// This is the primary integrity check in the ZIP format.
  decompressed_crc32 : UInt
}

///|
/// Default values for ZIP file metadata
pub let gp_flag_encrypted : UInt16 = 0x1

///|
pub let gp_flag_utf8 : UInt16 = 0x800

///|
pub let gp_flag_default : UInt16 = gp_flag_utf8

///|
pub let version_made_by_default : UInt16 = (3 << 8) | 20 // UNIX + PKZIP 2.0

///|
pub let version_needed_default : UInt16 = 20 // PKZIP 2.0

///|
/// Maximum file size in non-ZIP64 archives
pub let max_file_size : Int64 = 4294967295L // 2^32 - 1 (4GB)

///|
/// Create file data from compressed bytes
/// Build a File metadata + data wrapper from already-compressed content.
/// Performs size limit checks and applies default versions/flags if optionals are None.
pub fn File::make(
  compressed_bytes : Bytes,
  start : Int,
  compressed_size : Int,
  compression : Compression,
  decompressed_size : Int,
  decompressed_crc32 : UInt,
  version_made_by? : UInt16 = version_made_by_default,
  version_needed? : UInt16 = version_needed_default,
  gp_flags? : UInt16 = gp_flag_default,
) -> File raise {
  if compressed_size.to_int64() > max_file_size ||
    decompressed_size.to_int64() > max_file_size {
    fail(
      "Maximum ZIP file size \{max_file_size} exceeded: compressed=\{compressed_size}, decompressed=\{decompressed_size}",
    )
  }
  {
    version_made_by,
    version_needed_to_extract: version_needed,
    gp_flags,
    compression,
    start,
    compressed_size,
    compressed_bytes,
    decompressed_size,
    decompressed_crc32,
  }
}

///|
/// Create stored (uncompressed) file data from bytes
/// Construct a Stored (uncompressed) File directly from raw bytes.
/// Computes CRC-32 and sets compression=Stored.
pub fn File::stored_of_bytes(
  bytes : Bytes,
  start : Int,
  len : Int,
) -> File raise {
  let crc = @crc32.bytes_crc32(bytes[start:start + len])
  File::make(bytes, start, len, Compression::Stored, len, crc)
}

///|
/// Create deflate-compressed file data from bytes
/// Uses LZ77 + Huffman compression with optimal block type selection
/// Compress raw bytes using DEFLATE at optional level and wrap as File.
/// Chooses dynamic vs fixed Huffman based on size & level.
pub fn File::deflate_of_bytes(
  bytes : Bytes,
  start : Int,
  len : Int,
  level? : @deflate.DeflateLevel,
) -> File raise {
  // Determine compression parameters based on level
  let (good_match, max_chain, use_dynamic) = match level {
    Some(@deflate.DeflateLevel::None) => {
      // No compression, use stored block
      let compressed = @deflate.deflate_stored(bytes[start:start + len])
      let crc = @crc32.bytes_crc32(bytes[start:start + len])
      return File::make(
        compressed,
        0,
        compressed.length(),
        Compression::Deflate,
        len,
        crc,
      )
    }
    Some(@deflate.DeflateLevel::Fast) => (4, 128, false) // Fast: fixed Huffman only
    Some(@deflate.DeflateLevel::Default) | None => (8, 1024, true) // Default: dynamic Huffman
    Some(@deflate.DeflateLevel::Best) => (32, 4096, true) // Best: dynamic Huffman with max effort
  }

  // Choose between Fixed and Dynamic Huffman
  // Dynamic Huffman provides better compression but has header overhead
  // For small data (<256 bytes), fixed Huffman is often better
  let compressed = if use_dynamic && len >= 256 {
    // Use Dynamic Huffman for better compression on larger data
    @deflate.deflate_dynamic(
      bytes[start:start + len],
      true,
      good_match,
      max_chain,
    )
  } else {
    // Use Fixed Huffman for small data or Fast level
    @deflate.deflate_fixed(
      bytes[start:start + len],
      true,
      good_match,
      max_chain,
    )
  }
  let crc = @crc32.bytes_crc32(bytes[start:start + len])
  File::make(compressed, 0, compressed.length(), Compression::Deflate, len, crc)
}

///|
/// Get compression format
pub fn File::compression(self : File) -> Compression {
  self.compression
}

///|
/// Get start offset in compressed_bytes
pub fn File::start(self : File) -> Int {
  self.start
}

///|
/// Get compressed size
pub fn File::compressed_size(self : File) -> Int {
  self.compressed_size
}

///|
/// Get compressed bytes (the full buffer)
pub fn File::compressed_bytes(self : File) -> Bytes {
  self.compressed_bytes
}

///|
/// Extract just the compressed data as a standalone Bytes object
/// Copy out just this file's compressed payload into a fresh Bytes buffer.
pub fn File::compressed_bytes_to_bytes(self : File) -> Bytes {
  let result = Array::make(self.compressed_size, b'\x00')
  for i = 0; i < self.compressed_size; i = i + 1 {
    result[i] = self.compressed_bytes[self.start + i]
  }
  Bytes::from_array(FixedArray::from_iter(result[0:].iter()))
}

///|
/// Get decompressed size
pub fn File::decompressed_size(self : File) -> Int {
  self.decompressed_size
}

///|
/// Get decompressed CRC-32
pub fn File::decompressed_crc32(self : File) -> UInt {
  self.decompressed_crc32
}

///|
/// Get version made by
pub fn File::version_made_by(self : File) -> UInt16 {
  self.version_made_by
}

///|
/// Get version needed to extract
pub fn File::version_needed_to_extract(self : File) -> UInt16 {
  self.version_needed_to_extract
}

///|
/// Get general purpose flags
pub fn File::gp_flags(self : File) -> UInt16 {
  self.gp_flags
}

///|
/// Check if file is encrypted
/// Whether general-purpose bit flag indicates encryption (unsupported here).
pub fn File::is_encrypted(self : File) -> Bool {
  (self.gp_flags & gp_flag_encrypted) != 0
}

///|
/// Check if we can extract (decompress) this file
/// True if decompression supported and file not encrypted.
pub fn File::can_extract(self : File) -> Bool {
  if self.is_encrypted() {
    false
  } else {
    match self.compression {
      Stored | Deflate => true
      _ => false
    }
  }
}

///|
/// Decompress file data without CRC check
/// Decompress to bytes (or copy if stored) and return (data, computed_crc32) without verifying.
pub fn File::to_bytes_no_crc_check(self : File) -> (Bytes, UInt) raise {
  if self.is_encrypted() {
    fail("Encrypted files are not supported")
  }
  match self.compression {
    Stored => {
      // Just extract the bytes, no decompression needed
      let result_arr = Array::make(self.compressed_size, b'\x00')
      for i = 0; i < self.compressed_size; i = i + 1 {
        result_arr[i] = self.compressed_bytes[self.start + i]
      }
      let result = Bytes::from_array(
        FixedArray::from_iter(result_arr[0:].iter()),
      )
      let crc = @crc32.bytes_crc32(result[:])
      (result, crc)
    }
    Deflate => {
      let decompressed = @deflate.inflate(
        self.compressed_bytes[self.start:self.start + self.compressed_size],
        decompressed_size=self.decompressed_size,
      )
      let crc = @crc32.bytes_crc32(decompressed[:])
      (decompressed, crc)
    }
    _ => fail("Compression format \{Repr(self.compression)} not supported")
  }
}

///|
/// Decompress file data with CRC check
/// Decompress and validate CRC-32, raising on mismatch.
pub fn File::to_bytes(self : File) -> Bytes raise {
  let (result, found_crc) = self.to_bytes_no_crc_check()
  let expected_crc = self.decompressed_crc32
  if found_crc != expected_crc {
    fail(
      "CRC-32 mismatch: expected \{expected_crc.to_hex_string()}, found \{found_crc.to_hex_string()}",
    )
  }
  result
}