// Gzip format support - deflate with gzip headers and CRC-32
// RFC 1952: GZIP file format specification

// Gzip compressed data structure

///|
pub struct GzipData {
  compressed_bytes : Bytes
  original_size : Int
  crc32_checksum : @crc32.Crc32
  filename : String?
  comment : String?
  modification_time : Int // Unix timestamp
}

// Gzip header flags

///|
let _ftext_flag : Int = 0x01 // File is probably ASCII text

///|
let fhcrc_flag : Int = 0x02 // Header CRC present

///|
let fextra_flag : Int = 0x04 // Extra fields present

///|
let fname_flag : Int = 0x08 // Original filename present

///|
let fcomment_flag : Int = 0x10 // Comment present

// Compress data using gzip format (deflate + gzip headers + CRC-32)

///|
pub fn gzip_compress_bytes(
  data : Bytes,
  level : Level,
  filename : String?,
  comment : String?,
  modification_time : Int,
) -> GzipData {
  // Calculate CRC-32 of original data
  let crc = @crc32.bytes(data)

  // Compress using deflate
  match deflate_compress(data, level) {
    deflate_data => {
      // Build gzip header
      let mut header = Bytes::from_array([])

      // Magic number (2 bytes): 0x1f, 0x8b
      header = header + Bytes::from_array([0x1f, 0x8b])

      // Compression method (1 byte): 8 for deflate
      header = header + Bytes::from_array([0x08])

      // Flags (1 byte)
      let mut flags = 0
      match filename {
        Some(_) => flags = flags | fname_flag
        None => ()
      }
      match comment {
        Some(_) => flags = flags | fcomment_flag
        None => ()
      }
      header = header + Bytes::from_array([flags.to_byte()])

      // Modification time (4 bytes, little-endian)
      header = header + write_u32_le_bytes(modification_time)

      // Extra flags (1 byte)
      let extra_flags = match level {
        Level::Best => 2 // Maximum compression
        Level::Fast => 4 // Fastest compression
        _ => 0 // Default
      }
      header = header + Bytes::from_array([extra_flags.to_byte()])

      // Operating system (1 byte): 255 for unknown
      header = header + Bytes::from_array([0xff])

      // Optional filename (null-terminated)
      match filename {
        Some(name) =>
          header = header + name.to_bytes() + Bytes::from_array([0x00])
        None => ()
      }

      // Optional comment (null-terminated)
      match comment {
        Some(comm) =>
          header = header + comm.to_bytes() + Bytes::from_array([0x00])
        None => ()
      }

      // Build trailer: CRC-32 (4 bytes) + original size (4 bytes), both little-endian
      let trailer = write_u32_le_bytes(crc.0.to_int()) +
        write_u32_le_bytes(data.length())

      // Combine header + deflate data + trailer
      let gzip_bytes = header + deflate_data.compressed_bytes + trailer
      {
        compressed_bytes: gzip_bytes,
        original_size: data.length(),
        crc32_checksum: crc,
        filename,
        comment,
        modification_time,
      }
    }
  }
}

// Decompress gzip data

///|
pub fn gzip_decompress_bytes(gzip_data : GzipData) -> Bytes raise {
  let data = gzip_data.compressed_bytes
  if data.length() < 18 { // Minimum: 10-byte header + 8-byte trailer
    fail("Gzip data too short (minimum 18 bytes)")
  }

  // Parse gzip header
  let mut offset = 0

  // Check magic number
  if data[offset].to_int() != 0x1f || data[offset + 1].to_int() != 0x8b {
    fail("Invalid gzip magic number")
  }
  offset = offset + 2

  // Check compression method
  if data[offset].to_int() != 8 {
    fail("Unsupported compression method: " + data[offset].to_int().to_string())
  }
  offset = offset + 1

  // Read flags
  let flags = data[offset].to_int()
  offset = offset + 1

  // Skip modification time (4 bytes)
  offset = offset + 4

  // Skip extra flags (1 byte)
  offset = offset + 1

  // Skip operating system (1 byte)
  offset = offset + 1

  // Skip extra fields if present
  if (flags & fextra_flag) != 0 {
    if offset + 2 > data.length() {
      fail("Truncated gzip extra fields")
    }
    let extra_len = gzip_read_u16_le_bytes(data, offset)
    offset = offset + 2 + extra_len
    if offset > data.length() {
      fail("Truncated gzip extra fields data")
    }
  }

  // Skip filename if present
  if (flags & fname_flag) != 0 {
    while offset < data.length() && data[offset].to_int() != 0 {
      offset = offset + 1
    }
    if offset >= data.length() {
      fail("Truncated gzip filename")
    }
    offset = offset + 1 // Skip null terminator
  }

  // Skip comment if present
  if (flags & fcomment_flag) != 0 {
    while offset < data.length() && data[offset].to_int() != 0 {
      offset = offset + 1
    }
    if offset >= data.length() {
      fail("Truncated gzip comment")
    }
    offset = offset + 1 // Skip null terminator
  }

  // Skip header CRC if present
  if (flags & fhcrc_flag) != 0 {
    offset = offset + 2
    if offset > data.length() {
      fail("Truncated gzip header CRC")
    }
  }

  // Extract deflate data (everything except trailer)
  if offset + 8 > data.length() {
    fail("Truncated gzip trailer")
  }
  let deflate_end = data.length() - 8
  let deflate_bytes = data[offset:deflate_end].to_bytes()

  // Extract trailer: CRC-32 and original size
  let expected_crc = read_u32_le_bytes(data, deflate_end).to_int64()
  let expected_size = read_u32_le_bytes(data, deflate_end + 4)

  // Decompress deflate data
  let _deflate_data : DeflateData = {
    compressed_bytes: deflate_bytes,
    original_size: expected_size,
    adler32_checksum: 0L, // Gzip uses CRC-32, not Adler-32
  }

  // Use bytes-based deflate decompression to avoid String conversion corruption
  let decompressed_bytes = decompress_deflate_blocks_bytes(deflate_bytes)
  // Verify size
  if decompressed_bytes.length() != expected_size {
    fail(
      "Gzip size mismatch: expected " +
      expected_size.to_string() +
      ", got " +
      decompressed_bytes.length().to_string(),
    )
  }

  // Verify CRC-32 checksum
  let computed_crc = @crc32.bytes(decompressed_bytes)
  if computed_crc != expected_crc {
    fail("Gzip CRC-32 checksum mismatch")
  }
  decompressed_bytes
}

// Helper function to write 32-bit little-endian integer as bytes

///|
fn write_u32_le_bytes(value : Int) -> Bytes {
  let b0 = value & 0xff
  let b1 = (value >> 8) & 0xff
  let b2 = (value >> 16) & 0xff
  let b3 = (value >> 24) & 0xff
  Bytes::from_array([b0.to_byte(), b1.to_byte(), b2.to_byte(), b3.to_byte()])
}

// Helper function to read 16-bit little-endian integer from bytes

///|
fn gzip_read_u16_le_bytes(data : Bytes, offset : Int) -> Int {
  if offset + 1 >= data.length() {
    return 0
  }
  let b0 = data[offset].to_int()
  let b1 = data[offset + 1].to_int()
  b0 + (b1 << 8)
}

// Helper function to read 32-bit little-endian integer from bytes

///|
fn read_u32_le_bytes(data : Bytes, offset : Int) -> Int {
  if offset + 3 >= data.length() {
    return 0
  }
  let b0 = data[offset].to_int()
  let b1 = data[offset + 1].to_int()
  let b2 = data[offset + 2].to_int()
  let b3 = data[offset + 3].to_int()
  b0 + (b1 << 8) + (b2 << 16) + (b3 << 24)
}

// Bytes-based gzip functions (more efficient for binary data)

///|
pub fn gzip_of_bytes(
  data : Bytes,
  level : Level,
  filename : String?,
  comment : String?,
) -> Bytes {
  // Use the modern bytes-based implementation directly
  match gzip_compress_bytes(data, level, filename, comment, 0) {
    gzip_data => gzip_data.compressed_bytes
  }
}

///|
pub fn gzip_to_bytes(compressed_data : Bytes) -> Bytes raise {
  // Use the modern bytes-based implementation directly
  let gzip_data : GzipData = {
    compressed_bytes: compressed_data,
    original_size: 0, // Will be read from trailer
    crc32_checksum: @crc32.Crc32(0L), // Will be read from trailer
    filename: None,
    comment: None,
    modification_time: 0,
  }
  gzip_decompress_bytes(gzip_data)
}

// Extract metadata from gzip file

///|
pub fn gzip_extract_metadata_bytes(
  compressed_data : Bytes,
) -> Result[GzipData, String] {
  if compressed_data.length() < 18 {
    return Err("Gzip data too short")
  }
  let mut offset = 0

  // Check magic number
  if compressed_data[offset].to_int() != 0x1f ||
    compressed_data[offset + 1].to_int() != 0x8b {
    return Err("Invalid gzip magic number")
  }
  offset = offset + 2

  // Skip compression method
  offset = offset + 1

  // Read flags
  let flags = compressed_data[offset].to_int()
  offset = offset + 1

  // Read modification time
  let mtime = read_u32_le_bytes(compressed_data, offset)
  offset = offset + 4

  // Skip extra flags and OS
  offset = offset + 2

  // Skip extra fields if present
  if (flags & fextra_flag) != 0 {
    let extra_len = gzip_read_u16_le_bytes(compressed_data, offset)
    offset = offset + 2 + extra_len
  }

  // Read filename if present
  let mut filename : String? = None
  if (flags & fname_flag) != 0 {
    let start = offset
    while offset < compressed_data.length() &&
          compressed_data[offset].to_int() != 0 {
      offset = offset + 1
    }
    if offset < compressed_data.length() {
      filename = Some(compressed_data[start:offset].to_bytes().to_string())
      offset = offset + 1
    }
  }

  // Read comment if present
  let mut comment : String? = None
  if (flags & fcomment_flag) != 0 {
    let start = offset
    while offset < compressed_data.length() &&
          compressed_data[offset].to_int() != 0 {
      offset = offset + 1
    }
    if offset < compressed_data.length() {
      comment = Some(compressed_data[start:offset].to_bytes().to_string())
      offset = offset + 1
    }
  }

  // Read trailer
  let trailer_offset = compressed_data.length() - 8
  let crc = read_u32_le_bytes(compressed_data, trailer_offset).to_int64()
  let original_size = read_u32_le_bytes(compressed_data, trailer_offset + 4)
  Ok({
    compressed_bytes: compressed_data,
    original_size,
    crc32_checksum: crc,
    filename,
    comment,
    modification_time: mtime,
  })
}