/// # zipx - Pure MoonBit ZIP Extraction Library
///
/// A pure MoonBit implementation of ZIP archive extraction with no FFI
/// and no system command dependencies.
///
/// ## Basic Usage
///
/// Extract all files from a ZIP archive:
///
/// ```moonbit nocheck
/// let zip_data = @fs.read_file("archive.zip").binary()
/// let files = unzip_file(zip_data)
/// for (name, data) in files.iter() {
///   println("Extracted: " + name)
/// }
/// ```
///
/// List files without extracting:
///
/// ```moonbit nocheck
/// let zip = zip_file_open(zip_data)
/// let names = zip_file_list(zip)
/// for name in names.iter() {
///   println(name)
/// }
/// ```

///|
/// ## Compression Method
///
/// The compression method used in a ZIP file entry.
///
/// - `Stored`: No compression (stored as-is)
/// - `Deflated`: DEFLATE compression
/// - `Unsupported(UInt16)`: Unsupported compression method
pub enum CompressionMethod {
  Stored
  Deflated
  Unsupported(UInt16)
}

///|
/// ## Central Directory File Header
///
/// Represents a file entry in the ZIP archive's central directory.
/// Contains metadata about each file in the archive.
pub struct CentralDirectoryFileHeader {
  signature : UInt
  version_made_by : UInt16
  version_needed : UInt16
  flags : UInt16
  compression_method : CompressionMethod
  last_mod_time : UInt16
  last_mod_date : UInt16
  crc32 : UInt
  compressed_size : UInt
  uncompressed_size : UInt
  filename_length : UInt16
  extra_field_length : UInt16
  comment_length : UInt16
  disk_number_start : UInt16
  internal_file_attrs : UInt16
  external_file_attrs : UInt
  local_header_offset : UInt
  filename : String
  extra_field : Bytes
  comment : String
}

///|
/// ## ZipFile
///
/// Represents an open ZIP archive file.
/// Use `zip_file_open` to create an instance.
pub struct ZipFile {
  data : Bytes
  eocd : EndOfCentralDirectoryRecord
  cd_entries : Array[CentralDirectoryFileHeader]
}

///|
/// ## Open a ZIP File
///
/// Opens a ZIP archive from byte data and returns a `ZipFile` handle.
///
/// # Parameters
/// - `data`: The raw bytes of the ZIP file
///
/// # Returns
/// A `ZipFile` handle for reading the archive
///
/// # Throws
/// - If the end of central directory record cannot be found
/// - If the central directory is corrupted
///
/// # Example
/// ```moonbit nocheck
/// let zip = zip_file_open(zip_data)
///
/// let files = zip_file_list(zip)
/// ```
pub fn zip_file_open(data : Bytes) -> ZipFile raise {
  match parse_end_of_central_directory_record(data) {
    None => fail("Cannot find end of central directory record")
    Some(e) => {
      let len : Int = e.cd_entries_total.to_int()
      let cd_entries : Array[CentralDirectoryFileHeader] = []

      let mut offset : Int = e.cd_offset.reinterpret_as_int()
      let end_offset : Int = offset + e.cd_size.reinterpret_as_int()

      while offset < end_offset {
        match parse_central_directory_file_header(data, offset) {
          None => break
          Some(entry) => {
            cd_entries.push(entry)
            offset = offset +
              46 +
              entry.filename_length.to_int() +
              entry.extra_field_length.to_int() +
              entry.comment_length.to_int()
          }
        }
      }

      if cd_entries.length() != len {
        fail("Central directory entry count mismatch")
      }

      ZipFile::{ data, eocd: e, cd_entries }
    }
  }
}

///|
/// ## Extract a Single File
///
/// Extracts a single file from the ZIP archive by index.
///
/// # Parameters
/// - `zip`: The open ZIP file handle
/// - `index`: The zero-based index of the file to extract
///
/// # Returns
/// A tuple of `(filename, data)` containing the extracted file
///
/// # Throws
/// - If the index is out of bounds
/// - If the local file header cannot be found
/// - If decompression fails
///
/// # Example
/// ```moonbit nocheck
/// let (filename, data) = zip_file_extract(zip, 0)
/// ```
pub fn zip_file_extract(zip : ZipFile, index : Int) -> (String, Bytes) raise {
  if index < 0 || index >= zip.cd_entries.length() {
    fail("Invalid file index")
  }

  let entry : CentralDirectoryFileHeader = zip.cd_entries[index]
  let (actual_compressed_size, actual_uncompressed_size) =
    if entry.compressed_size == 0xFFFFFFFF || entry.uncompressed_size == 0xFFFFFFFF {
      match parse_zip64_extra_info(entry.extra_field) {
        Some((uncomp, comp, _)) => (comp, uncomp)
        None => fail("ZIP64 sizes not found in extra field")
      }
    } else {
      (entry.compressed_size, entry.uncompressed_size)
    }

  let local_offset : Int = if entry.local_header_offset == 0xFFFFFFFF {
    match parse_zip64_extra_info(entry.extra_field) {
      Some((_, _, offset)) => offset.reinterpret_as_int()
      None => fail("ZIP64 local header offset not found in extra field")
    }
  } else {
    entry.local_header_offset.reinterpret_as_int()
  }

  match parse_local_file_header(zip.data, local_offset) {
    None => fail("Cannot find local file header")
    Some(h) => {
      let data_start : Int = local_offset +
        30 +
        h.filename_length.to_int() +
        h.extra_field_length.to_int()

      let local_compressed_size : UInt = if h.compressed_size == 0 ||
        h.compressed_size == 0xFFFFFFFF {
        match parse_zip64_extra_info(h.extra_field) {
          Some((_, comp, _)) => comp
          None => actual_compressed_size
        }
      } else {
        h.compressed_size
      }

      let compressed_data_view = zip.data.get_view(
        start=data_start,
        end=data_start + local_compressed_size.reinterpret_as_int(),
      )

      match compressed_data_view {
        None => fail("Cannot read compressed data")
        Some(cdv) => {
          let compressed_data : Bytes = cdv.to_owned()

          match h.compression_method {
            CompressionMethod::Stored => (entry.filename, compressed_data)
            CompressionMethod::Deflated =>
              (entry.filename, inflate(compressed_data, actual_uncompressed_size.reinterpret_as_int()))
            CompressionMethod::Unsupported(m) =>
              fail("Unsupported compression method: " + m.to_int().to_string())
          }
        }
      }
    }
  }
}

///|
/// ## List Files in ZIP
///
/// Returns a list of all filenames in the ZIP archive.
///
/// # Parameters
/// - `zip`: The open ZIP file handle
///
/// # Returns
/// An array of filenames in the order they appear in the archive
///
/// # Example
/// ```moonbit nocheck
/// let files = zip_file_list(zip)
/// for filename in files.iter() {
///   println(filename)
/// }
/// ```
pub fn zip_file_list(zip : ZipFile) -> Array[String] {
  let len : Int = zip.cd_entries.length()
  let names : Array[String] = Array::make(len, "")

  for i in 0.. Bytes raise {
  let br : BitReader = BitReader::new(data)
  let result : Array[Byte] = Array::make(uncompressed_size, 0b0)
  let mut pos : Int = 0

  while br.bits > 0 || br.pos < br.data.length() {
    match inflate_block(br, result, pos) {
      (Continue, new_pos) => pos = new_pos
      (Done, new_pos) => {
        pos = new_pos
        break
      }
      (Error(msg), _) => fail("inflate error: " + msg)
    }
  }

  Bytes::from_array(result[0:pos])
}

///|
/// ## Unzip All Files
///
/// Extracts all files from a ZIP archive at once.
///
/// # Parameters
/// - `data`: The raw bytes of the ZIP file
///
/// # Returns
/// An array of tuples `(filename, data)` for all extracted files
///
/// # Throws
/// - If the ZIP file is invalid or corrupted
/// - If decompression fails for any file
///
/// # Example
/// ```moonbit nocheck
/// let extracted = unzip_file(zip_data)
/// for entry in extracted.iter() {
///   let (filename, data) = entry
///   println("Extracted: " + filename)
/// }
/// ```
pub fn unzip_file(data : Bytes) -> Array[(String, Bytes)] raise {
  let start = @env.now()
  let zip : ZipFile = zip_file_open(data)
  let open_end = @env.now()
  let len : Int = zip.cd_entries.length()
  let result : Array[(String, Bytes)] = Array::make(len, ("", Bytes::new(0)))

  for i in 0.. Array[(String, Bytes)] {
  let start_total = @env.now()
  let data = @fs.read_file(path).binary()
  let read_end = @env.now()
  let files = unzip_file(data)
  let unzip_end = @env.now()
  @log.timing("[TIMING] read_file: \{(read_end - start_total).to_int()}ns")
  @log.timing("[TIMING] unzip_file: \{(unzip_end - read_end).to_int()}ns")
  match output_dir {
    Some(dir) => {
      let exists = @fs.exists(dir)
      if exists {
        let kind = @fs.kind(dir)
        match kind {
          @fs.FileKind::Directory => ()
          _ => fail("Output path is not a directory: " + dir)
        }
      } else {
        @fs.mkdir(dir, permission=0o755, recursive=true)
      }

      for entry in files.iter() {
        let (filename, file_data) = entry
        let output_path = dir + "/" + filename
        if filename.has_suffix("/") {
          @fs.mkdir(output_path, permission=0o755, recursive=true) catch {
            _ => ()
          }
        } else {
          let last_slash = find_last_slash(filename)
          if last_slash >= 0 {
            let sub_dir = dir + "/" + filename[0:last_slash].to_owned()
            @fs.mkdir(sub_dir, permission=0o755, recursive=true) catch {
              _ => ()
            }
          }
          @fs.write_file(output_path, file_data, create_mode=CreateOrTruncate) catch {
            _ => ()
          }
        }
      }
      files
    }
    None => files
  }
}

///|
fn find_last_slash(s : String) -> Int {
  let bytes = @utf8.encode(s)
  let mut last = -1
  let mut i = 0
  while i < bytes.length() {
    if bytes[i].to_uint() == 0x2F {
      last = i
    }
    i = i + 1
  }
  last
}