// zipc - ZIP archive and deflate codec for MoonBit
// Ported from OCaml zipc library
// Original: Copyright (c) 2023 The zipc programmers
// SPDX-License-Identifier: ISC

///|
/// MoonBit ZIP Library - Complete ZIP archive processing.
/// 
/// This is the main entry point for the ZIP archive library, providing:
/// 
/// ## Core Features
/// - **ZIP Creation**: Build archives from files/directories
/// - **ZIP Extraction**: Parse and decompress existing archives  
/// - **DEFLATE Compression**: RFC 1951 compliant encoder/decoder
/// - **Multiple Formats**: Stored, DEFLATE, with extensible compression
/// - **Cross-Platform**: Handles path normalization and DOS timestamps
/// 
/// ## Architecture
/// - `Archive`: Top-level container for multiple files/directories
/// - `Member`: Individual file or directory entry with metadata
/// - `File`: File data with compression and integrity checking
/// - `Compression`: Type-safe compression method enumeration
/// 
/// ## Usage Patterns
/// ```
/// // Create new archive
/// let archive = Archive::empty()
/// archive.add(Member::make_file("readme.txt", content))
/// let zip_bytes = archive.to_bytes()
/// 
/// // Parse existing archive
/// let loaded = Archive::of_bytes(zip_data)
/// let file = loaded.find("readme.txt").unwrap()
/// let content = file.to_bytes()
/// ```
/// 
/// ## Compatibility
/// - Standard ZIP format (PKWARE specification)
/// - UTF-8 filenames with proper encoding flags
/// - DEFLATE compression (RFC 1951)
/// - zlib/gzip parity for CRC32 and Adler32 integrity
///
/// ## Layered Architecture (Condensed)
/// Level 0: checksum/{crc32, adler32} (no deps)
/// Level 1: deflate/internal/{bytebuf, bitstream, huffman, lz77}
/// Level 2: deflate (encoder + decoder unified) + zlib wrapper
/// Level 3: types/{ptime,fpath} utilities
/// Level 4: file (ZIP local file structure) & member (central directory abstraction)
/// Level 5: archive (multi-entry orchestration)
/// Level 6: top-level re‑exports (this module)
///
/// ## Feature Completion Snapshot
/// - Compression: stored, fixed Huffman, dynamic Huffman (two‑pass LZ77 + tree build)
/// - Decompression: all block types (stored/fixed/dynamic) + integrity checks
/// - Checksums: CRC-32 (gzip/zip), Adler-32 (zlib)
/// - ZIP: local headers, central directory, EOCD, UTF-8 flag, DOS time conversion
/// - High-level API: `deflate()`, `inflate()`, `zlib_compress()`, `zlib_decompress()`, archive build & parse
///
/// ## Determinism
/// Gzip wrapper emits deterministic headers (MTIME=0, XFL=0, OS=0xFF) to enable reproducible builds; tests assert
/// Python parity via `gzip.compress(..., mtime=0)` for canonical fixtures.
///
/// ## Future Hooks
/// - Optional gzip header parameters (mtime/xfl)
/// - Streaming multi-block emission (current encoder emits a single final block)
/// - Differential fuzzing harness for corpus evolution
///
/// ## Bonus Features vs Original Port
/// - `Archive::to_array()` helper for indexed iteration
/// - `Member::is_dir()` / `Member::is_file()` convenience predicates
/// - Deterministic gzip output policy (reproducible builds)
///
/// ## Intentional Non-Features / Limitations
/// - ZIP64 (size/count > 4GB / 65,535) not supported
/// - Encryption (legacy PKWARE or AES) not implemented
/// - Multi-part / spanned archives not supported
/// - Only Stored + Deflate compression (no BZIP2/LZMA)
/// These align with scope control and can be revisited if requirements evolve.
///
/// This embedded summary replaces several historical planning/status markdown files to keep living documentation
/// close to the code. For deep historical progression see `COMPRESSION_PROGRESS.md`.
/// - CRC-32 integrity checking
/// - DOS timestamp conversion
/// 
/// Based on the OCaml zipc library with MoonBit-specific optimizations.

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

///|
/// Deflate format constants and symbols (RFC 1951)

// Literal/length symbols

///|
// let litlen_sym_max : Int = 285

///|
// let max_litlen_sym_count : Int = 286

///|
// let litlen_end_of_block_sym : Int = 256

///|
// let litlen_first_len_sym : Int = 257

///|
/// Extract base length value (upper bits)
// fn length_value_base(v : Int) -> Int {
//  v >> 4
//}

///|
/// Extract extra bits count (lower bits)
// fn length_value_extra_bits(v : Int) -> Int {
//  v & 0xF
//}

///|
/// Length value table from RFC 1951 3.2.5
/// Each entry packs (base_length << 4) | extra_bits
let length_value_of_sym_table : Array[Int] = [
  // Symbol 257-264: lengths 3-10, 0 extra bits
  3 << 4,
  4 << 4,
  5 << 4,
  6 << 4,
  7 << 4,
  8 << 4,
  9 << 4,
  10 << 4,
  // Symbol 265-268: lengths 11-17, 1 extra bit
  (11 << 4) | 1,
  (13 << 4) | 1,
  (15 << 4) | 1,
  (17 << 4) | 1,
  // Symbol 269-272: lengths 19-31, 2 extra bits
  (19 << 4) | 2,
  (23 << 4) | 2,
  (27 << 4) | 2,
  (31 << 4) | 2,
  // Symbol 273-276: lengths 35-59, 3 extra bits
  (35 << 4) | 3,
  (43 << 4) | 3,
  (51 << 4) | 3,
  (59 << 4) | 3,
  // Symbol 277-280: lengths 67-115, 4 extra bits
  (67 << 4) | 4,
  (83 << 4) | 4,
  (99 << 4) | 4,
  (115 << 4) | 4,
  // Symbol 281-284: lengths 131-227, 5 extra bits
  (131 << 4) | 5,
  (163 << 4) | 5,
  (195 << 4) | 5,
  (227 << 4) | 5,
  // Symbol 285: length 258, 0 extra bits
  258 << 4,
]

///|
test "length_value_table" {
  // Test a few entries from the length value table
  // Symbol 257 -> length 3, 0 extra bits
  json_inspect(length_value_of_sym_table[0], content=48) // 3 << 4
  // Symbol 265 -> length 11, 1 extra bit
  json_inspect(length_value_of_sym_table[8], content=177) // (11 << 4) | 1
  // Symbol 285 -> length 258, 0 extra bits
  json_inspect(length_value_of_sym_table[28], content=4128) // 258 << 4
}

///|
// fn length_value_of_length_sym(sym : Int) -> Int {
//  length_value_of_sym_table[sym - litlen_first_len_sym]
//}

// Distance symbols

///|
// let dist_sym_max : Int = 29

///|
// let max_dist_sym_count : Int = 30

///|
// fn dist_value_base(v : Int) -> Int {
//  v >> 4
//}

///|
// fn dist_value_extra_bits(v : Int) -> Int {
//  v & 0xF
//}

///|
/// Distance value table from RFC 1951 3.2.5
let dist_value_of_sym : Array[Int] = [
  // Symbols 0-3: distances 1-4, 0 extra bits
  1 << 4,
  2 << 4,
  3 << 4,
  4 << 4,
  // Symbols 4-5: distances 5-7, 1 extra bit
  (5 << 4) | 1,
  (7 << 4) | 1,
  // Symbols 6-7: distances 9-13, 2 extra bits
  (9 << 4) | 2,
  (13 << 4) | 2,
  // Symbols 8-9: distances 17-25, 3 extra bits
  (17 << 4) | 3,
  (25 << 4) | 3,
  // Symbols 10-11: distances 33-49, 4 extra bits
  (33 << 4) | 4,
  (49 << 4) | 4,
  // Symbols 12-13: distances 65-97, 5 extra bits
  (65 << 4) | 5,
  (97 << 4) | 5,
  // Symbols 14-15: distances 129-193, 6 extra bits
  (129 << 4) | 6,
  (193 << 4) | 6,
  // Symbols 16-17: distances 257-385, 7 extra bits
  (257 << 4) | 7,
  (385 << 4) | 7,
  // Symbols 18-19: distances 513-769, 8 extra bits
  (513 << 4) | 8,
  (769 << 4) | 8,
  // Symbols 20-21: distances 1025-1537, 9 extra bits
  (1025 << 4) | 9,
  (1537 << 4) | 9,
  // Symbols 22-23: distances 2049-3073, 10 extra bits
  (2049 << 4) | 10,
  (3073 << 4) | 10,
  // Symbols 24-25: distances 4097-6145, 11 extra bits
  (4097 << 4) | 11,
  (6145 << 4) | 11,
  // Symbols 26-27: distances 8193-12289, 12 extra bits
  (8193 << 4) | 12,
  (12289 << 4) | 12,
  // Symbols 28-29: distances 16385-24577, 13 extra bits
  (16385 << 4) | 13,
  (24577 << 4) | 13,
]

///|
test "distance_value_table" {
  // Test a few entries from the distance value table
  // Symbol 0 -> distance 1, 0 extra bits
  json_inspect(dist_value_of_sym[0], content=16) // 1 << 4
  // Symbol 4 -> distance 5, 1 extra bit
  json_inspect(dist_value_of_sym[4], content=81) // (5 << 4) | 1
  // Symbol 29 -> distance 24577, 13 extra bits
  json_inspect(dist_value_of_sym[29], content=393245) // (24577 << 4) | 13
}

// Code length symbols

///|
// let max_codelen_sym_count : Int = 19

///|
/// Order in which code length symbols are transmitted (RFC 1951 3.2.7)
// let codelen_order_of_sym_lengths : Array[Int] = [
//  16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15,
// ]

///|
/// Huffman coding - Re-exported from huffman package
// typealias @huffman.(HuffmanDecoder, HuffmanEncoder, SymInfo)

///|
// let fixed_litlen_decoder : HuffmanDecoder = @huffman.fixed_litlen_decoder

///|
// let fixed_dist_decoder : HuffmanDecoder = @huffman.fixed_dist_decoder

///|
/// Compression - Re-exported from types package
// typealias @types.Compression

///|
/// Ptime - POSIX time utilities re-exported from types package

///|
// pub let dos_epoch : @types.Ptime = @types.dos_epoch

// ============================================================================
// DEFLATE and zlib - Re-exported from deflate package
// ============================================================================

///|
/// File path utilities for ZIP archives

///|
/// File path type and utilities
/// Re-exports from types/fpath package
using @types {type Fpath}

///|
/// Convert string to UTF-8 bytes for ZIP encoding
fn String::utf8(s : String) -> Bytes {
  @encoding/utf8.encode(s)
}

///|
// ============================================================================
// File - Re-exported from file package
// ============================================================================

///|
using @file {type File}

///|
/// FIXME(upstream): find all references does not work
using @member {type Member}

///|
/// ZIP local file header (little endian) ("PK\x03\x04")
const ZIP_LOCAL_FILE_SIG : UInt = 0x04034b50

///|
/// ZIP end of central directory signature (little endian) ("PK\x05\x06")
const ZIP_EOCD_SIG : UInt = 0x06054b50

///|
/// Check if bytes start with ZIP magic signature
fn bytes_has_zip_magic(data : Bytes) -> Bool {
  // FIXME(upstream): u4 should give a warning/error
  // when u4 has a literal pattern, what does it mean??
  // [u32(0x04034b50|0x06054b50),..] => true 
  data[:] is [u32le(ZIP_LOCAL_FILE_SIG | ZIP_EOCD_SIG), ..]
}

///|
/// Central directory file header signature (little endian)
const ZIP_CENTRAL_DIR_SIG : UInt = 0x02014b50

///|
/// ZIP file decoding utilities

///|
/// Find and parse the end of central directory record (EOCD)
/// Returns (cd_offset, cd_size, entry_count)
/// EOCD structure (22 bytes): signature(4) + disk(2) + cd_disk(2) + 
/// entries_this_disk(2) + entries_total(2) + cd_size(4) + cd_offset(4) + comment_len(2)
fn find_and_parse_eocd(data : Bytes) -> (Int, Int, Int) raise {
  let len = data.length()
  guard len >= 22 else {
    fail("File too small to contain EOCD (minimum 22 bytes)")
  }

  // EOCD is at the end, search backwards
  // Maximum comment size is 65535, so search last 65557 bytes (22 + 65535)
  let search_start = if len > 65557 { len - 65557 } else { 0 }
  for i = len - 22; i >= search_start; i = i - 1 {
    if data[i:] is [u32le(ZIP_EOCD_SIG), .. next] {
      // Found EOCD signature, now parse it
      match next {
        [
          u16le(disk), // Disk number (multi-disk not supported)
          u16le(cd_disk), // Disk where central directory starts
          u16le(entries_this_disk), // Number of entries on this disk
          u16le(entries_total), // Total number of entries
          u32le(cd_size), // Size of central directory
          // Offset of start of central directory
          u32le(cd_offset),
          ..,
        ] => {
          guard disk == 0 && cd_disk == 0 else {
            fail("Multi-disk archives not supported")
          }
          guard entries_this_disk == entries_total else {
            fail("Inconsistent entry count in EOCD")
          }
          return (
            cd_offset.reinterpret_as_int(),
            cd_size.reinterpret_as_int(),
            entries_total.reinterpret_as_int(),
          )
        }
        _ => fail("Truncated EOCD record")
      }
    }
  }
  fail("Could not find end of central directory")
}

///|
/// Parse a central directory entry
/// Central directory structure (46 bytes fixed + variable): 
/// sig(4) + version_made_by(2) + version_needed(2) + gp_flags(2) + compression(2) +
/// dos_time(2) + dos_date(2) + crc32(4) + compressed_size(4) + uncompressed_size(4) +
/// filename_len(2) + extra_len(2) + comment_len(2) + disk_num(2) + internal_attrs(2) +
/// external_attrs(4) + local_header_offset(4) + filename + extra + comment
fn parse_central_dir_entry(data : Bytes, pos : Int) -> (Member, Int) raise {
  // Returns (member, next_position)
  match data[pos:] {
    [
      u32le(sig), // Signature (0x02014b50)
      u16le(version_made_by), // Version made by
      u16le(version_needed), // Version needed to extract
      u16le(gp_flags), // General purpose bit flags
      u16le(compression_method), // Compression method
      u16le(dos_time), // Last mod file time (DOS format)
      u16le(dos_date), // Last mod file date (DOS format)
      u32le(crc32), // CRC-32 checksum
      u32le(compressed_size), // Compressed size
      u32le(uncompressed_size), // Uncompressed size
      u16le(filename_len), // Filename length
      u16le(extra_len), // Extra field length
      u16le(comment_len), // File comment length
      u16le(_disk_number_start), // Disk number where file starts
      u16le(_internal_attrs), // Internal file attributes
      u32le(external_attrs), // External file attributes (Unix permissions)
      // Offset of local file header
      u32le(local_header_offset),
      .. next,
    ] => {
      if sig != ZIP_CENTRAL_DIR_SIG {
        fail("Invalid central directory signature")
      }
      // Convert UInt to appropriate types
      let version_made_by : UInt16 = version_made_by.to_uint16()
      let version_needed : UInt16 = version_needed.to_uint16()
      let gp_flags : UInt16 = gp_flags.to_uint16()
      let compression_method = compression_method.reinterpret_as_int()
      let dos_time : UInt16 = dos_time.to_uint16()
      let dos_date : UInt16 = dos_date.to_uint16()
      let compressed_size = compressed_size.reinterpret_as_int()
      let uncompressed_size = uncompressed_size.reinterpret_as_int()
      let filename_len = filename_len.reinterpret_as_int()
      let extra_len = extra_len.reinterpret_as_int()
      let comment_len = comment_len.reinterpret_as_int()
      let local_header_offset = local_header_offset.reinterpret_as_int()
      guard next.length() >= filename_len else {
        fail("Truncated central directory entry")
      }
      let filename_bytes = next[:filename_len]
      // Convert UTF-8 bytes to string - decode returns String, throws on error
      let path_str = @encoding/utf8.decode(filename_bytes) catch {
        _ => fail("Failed to decode UTF-8 filename")
      }
      let path = @fpath.Fpath(path_str)

      // Calculate next position

      // Determine if directory (path ends with /)
      // TODO(upstream): add String::last
      let is_dir = path.length() > 0 && path[path.length() - 1] == '/'

      // Extract file mode from external attributes (Unix: high 16 bits)
      let mode = (external_attrs >> 16).reinterpret_as_int() & 0xFFFF
      let file_mode = if mode == 0 {
        if is_dir {
          @types.FileMode(0o755)
        } else {
          @types.FileMode(0o644)
        }
      } else {
        @types.FileMode(mode)
      }
      // Parse local file header to get actual data offset
      // Local file header structure (30 bytes fixed + variable):
      // sig(4) + version_needed(2) + gp_flags(2) + compression(2) + dos_time(2) +
      // dos_date(2) + crc32(4) + compressed_size(4) + uncompressed_size(4) +
      // filename_len(2) + extra_len(2) + filename + extra + file_data

      // Create member
      let kind = if is_dir {
        @member.Dir
      } else {
        let compression = @types.compression(compression_method)
        let data_offset = match data[local_header_offset:] {
          [
            u32le(local_sig), // Signature (0x04034b50)
            u16le(_), // Version needed to extract
            u16le(_), // General purpose bit flags
            u16le(_), // Compression method
            u16le(_), // Last mod file time
            u16le(_), // Last mod file date
            u32le(_), // CRC-32 checksum
            u32le(_), // Compressed size
            u32le(_), // Uncompressed size
            u16le(local_filename_len), // Filename length
            // Extra field length
            u16le(local_extra_len),
            ..,
          ] => {
            if local_sig != ZIP_LOCAL_FILE_SIG {
              fail("Invalid local file header signature")
            }
            let local_filename_len = local_filename_len.reinterpret_as_int()
            let local_extra_len = local_extra_len.reinterpret_as_int()
            local_header_offset + 30 + local_filename_len + local_extra_len
          }
          _ => fail("Invalid local header offset")
        }
        File::make(
          data,
          data_offset,
          compressed_size,
          compression,
          uncompressed_size,
          crc32,
          version_made_by~,
          version_needed~,
          gp_flags~,
        )
        |> File
      }
      // Convert DOS time to POSIX time
      let mtime = @types.ptime_of_dos_date_time(dos_date, dos_time)
      let m = @member.make(path, kind, mode=file_mode, mtime~)
      let next_pos = pos + 46 + filename_len + extra_len + comment_len
      (m, next_pos)
    }
    _ => fail("Truncated central directory entry")
  }
}

///|
/// Decode ZIP archive from bytes
pub fn Archive::of_bytes(data : Bytes) -> Archive raise {
  // Check magic
  if !bytes_has_zip_magic(data) {
    fail("Not a ZIP file: missing magic signature")
  }

  // Find and parse EOCD
  let (cd_offset, _cd_size, entry_count) = find_and_parse_eocd(data)

  // Parse central directory entries
  let archive = Archive::empty()
  // let mut pos = cd_offset
  for i = 0, pos = cd_offset; i < entry_count; {
    let (m, next_pos) = parse_central_dir_entry(data, pos)
    // Add member (last one wins if duplicate paths)
    archive.add(m)
    continue i + 1, next_pos
  }
  archive
}