///|
pub(all) struct VPKEntry {
  path : String
  crc32 : UInt32
  preload : Bytes
  archive : Int32
  offset : Int32
  length : Int32
} derive(Eq, Debug)

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

///|
pub extend VPKEntry with @debug.Debug::{to_repr}

///|
struct VPKArchive {
  version : Int32
  entries : Array[VPKEntry]
  lookup : Map[String, Int32]
  directory : Bytes
  data_offset : Int32
  data_size : Int32
}

///|
pub fn VPKArchive::version(self : VPKArchive) -> Int32 {
  self.version
}

///|
pub fn VPKArchive::embedded_size(self : VPKArchive) -> Int32? {
  if self.data_size < 0 {
    None
  } else {
    Some(self.data_size)
  }
}

///|
pub fn VPKArchive::entries(self : VPKArchive) -> Array[VPKEntry] {
  self.entries.copy()
}

///|
pub fn VPKArchive::find(self : VPKArchive, path : String) -> VPKEntry? {
  self.lookup
  .get(path.replace_all(old="\\", new="/").to_lower())
  .map(i => self.entries[i])
}

///|
/// Only directory metadata is required. Embedded bytes can be supplied later by
/// the range loader; numbered archives are never eagerly read by this API.
pub fn read_vpk(
  bytes : Bytes,
  options~ : BinaryOptions = BinaryOptions::default(),
) -> VPKArchive raise {
  check_binary_options(options)
  if bytes.length() > options.max_bytes {
    raise InvalidBinary(0, "VPK directory exceeds byte budget")
  }
  let input = valve_bytes(bytes)
  if input.uint(4) != 0x55aa1234UL {
    raise InvalidBinary(0, "Expected Valve VPK signature")
  }
  let version = input.uint(4).to_int()
  if version != 1 && version != 2 {
    raise InvalidBinary(4, "Unsupported VPK version")
  }
  let size = input.count(options.max_bytes)
  let mut data_size = -1
  if version == 2 {
    data_size = input.count(2147483647)
    let archive_md5 = input.count(2147483647)
    let other_md5 = input.count(2147483647)
    let signature = input.count(2147483647)
    if archive_md5 % 28 != 0 ||
      (other_md5 != 0 && other_md5 != 48) ||
      (signature != 0 && signature < 8) {
      raise InvalidBinary(input.pos, "Invalid VPK v2 footer section sizes")
    }
  }
  let start = input.pos
  let tree = valve_bytes(input.take(size))
  let entries : Array[VPKEntry] = []
  let lookup : Map[String, Int32] = Map([])
  while true {
    let extension = tree.cstring(options.max_string_bytes)
    if extension.is_empty() {
      break
    }
    if extension != " " &&
      (
        extension.contains("/") ||
        extension.contains("\\") ||
        extension.contains(".")
      ) {
      raise InvalidBinary(tree.pos, "Invalid VPK extension")
    }
    while true {
      let directory = tree.cstring(options.max_string_bytes)
      if directory.is_empty() {
        break
      }
      while true {
        let name = tree.cstring(options.max_string_bytes)
        if name.is_empty() {
          break
        }
        if name.contains("/") || name.contains("\\") {
          raise InvalidBinary(tree.pos, "Invalid VPK leaf name")
        }
        let path = (if directory == " " { "" } else { directory + "/" }) +
          name +
          (if extension == " " { "" } else { "." + extension })
        if !resource_relative(path) || lookup.contains(path.to_lower()) {
          raise InvalidBinary(tree.pos, "Unsafe or duplicate VPK path")
        }
        let crc32 = tree.uint(4).to_uint()
        let preload_size = tree.uint(2).to_int()
        let archive = tree.uint(2).to_int()
        let offset = tree.count(2147483647)
        let length = tree.count(options.max_bytes)
        if length > options.max_bytes - preload_size ||
          offset > 2147483647 - length {
          raise InvalidBinary(tree.pos, "VPK entry range exceeds budget")
        }
        if tree.uint(2) != 65535UL {
          raise InvalidBinary(tree.pos, "Invalid VPK entry terminator")
        }
        let preload = tree.take(preload_size)
        if version == 2 &&
          archive == 32767 &&
          (offset > data_size || length > data_size - offset) {
          raise InvalidBinary(tree.pos, "VPK entry exceeds embedded section")
        }
        if entries.length() >= options.max_entries {
          raise InvalidBinary(tree.pos, "VPK entry count exceeds budget")
        }
        lookup[path.to_lower()] = entries.length()
        entries.push({ path, crc32, preload, archive, offset, length, })
      }
    }
  }
  if tree.remaining() != 0 {
    raise InvalidBinary(start + tree.pos, "Trailing VPK directory bytes")
  }
  {
    version,
    entries,
    lookup,
    directory: bytes,
    data_offset: start + size,
    data_size,
  }
}

///|
/// Loader arguments are archive index, absolute byte offset and byte count.
/// Index 0x7fff refers to the directory file. CRC covers preload plus body.
pub fn VPKArchive::read(
  self : VPKArchive,
  path : String,
  loader : (Int32, Int32, Int32) -> Bytes raise,
  verify_crc~ : Bool = true,
) -> Bytes raise {
  guard self.find(path) is Some(entry) else {
    raise InvalidReference("VPK path was not found: " + path)
  }
  let output = @buffer.Buffer()
  output.write_bytes(entry.preload[:])
  if entry.length > 0 {
    let base = if entry.archive == 32767 { self.data_offset } else { 0 }
    if entry.offset > 2147483647 - base - entry.length {
      raise InvalidBinary(0, "VPK absolute range overflow")
    }
    let offset = base + entry.offset
    let body = if entry.archive == 32767 &&
      offset <= self.directory.length() &&
      entry.length <= self.directory.length() - offset {
      self.directory[offset:offset + entry.length].to_owned()
    } else {
      loader(entry.archive, offset, entry.length)
    }
    if body.length() != entry.length {
      raise InvalidBinary(
        offset, "VPK range loader returned an incorrect length",
      )
    }
    output.write_bytes(body[:])
  }
  let bytes = output.to_bytes()
  if verify_crc && zip_crc32(bytes) != entry.crc32 {
    raise InvalidBinary(0, "VPK file CRC32 mismatch: " + entry.path)
  }
  bytes
}

///|
fn vpk_cstring(output : @buffer.Buffer, value : String) -> Unit {
  output.write_bytes(@utf8.encode(value[:])[:])
  output.write_byte(b'\x00')
}

///|
/// Deterministic embedded VPK v1. File names are case-insensitive, contents are
/// uncompressed and CRC32 is computed over the complete original byte stream.
pub fn create_vpk(
  files : Map[String, Bytes],
  preload_bytes~ : Int32 = 0,
  options~ : BinaryOptions = BinaryOptions::default(),
) -> Bytes raise {
  check_binary_options(options)
  if preload_bytes < 0 ||
    preload_bytes > 65535 ||
    files.length() > options.max_entries {
    raise InvalidBinary(0, "Invalid VPK creation limits")
  }
  let groups : Map[String, Map[String, Array[(String, String)]]] = Map([])
  let names : Map[String, Bool] = Map([])
  let mut total = 0
  for (path, bytes) in files {
    if !resource_relative(path) || names.contains(path.to_lower()) {
      raise InvalidReference("Unsafe or duplicate VPK output path")
    }
    names[path.to_lower()] = true
    if bytes.length() > options.max_bytes - total {
      raise InvalidBinary(0, "VPK aggregate byte budget exceeded")
    }
    total += bytes.length()
    let parts = path.split("/").map(x => x.to_owned()).collect()
    let leaf = parts[parts.length() - 1]
    let dot_parts = leaf.split(".").map(x => x.to_owned()).collect()
    let extension = if dot_parts.length() > 1 &&
      !dot_parts[dot_parts.length() - 1].is_empty() {
      dot_parts[dot_parts.length() - 1]
    } else {
      " "
    }
    let name = if extension == " " {
      leaf
    } else {
      leaf[:leaf.length() - extension.length() - 1].to_owned()
    }
    if name.is_empty() || (extension == " " && leaf == " ") {
      raise InvalidReference("VPK file name cannot be represented")
    }
    let directory = if parts.length() == 1 {
      " "
    } else {
      parts[:parts.length() - 1].to_owned().join("/")
    }
    let paths = groups.get(extension).unwrap_or(Map([]))
    groups[extension] = paths
    let leaves = paths.get(directory).unwrap_or([])
    paths[directory] = leaves
    leaves.push((name, path))
  }
  let tree = @buffer.Buffer()
  let data = @buffer.Buffer()
  let extensions = groups.keys().collect()
  extensions.sort_by((a, b) => a.lexical_compare(b))
  for extension in extensions {
    vpk_cstring(tree, extension)
    let directories = groups[extension].keys().collect()
    directories.sort_by((a, b) => a.lexical_compare(b))
    for directory in directories {
      vpk_cstring(tree, directory)
      let leaves = groups[extension][directory]
      leaves.sort_by((a, b) => a.0.lexical_compare(b.0))
      for (name, path) in leaves {
        let bytes = files[path]
        let preload = bytes.length().min(preload_bytes)
        vpk_cstring(tree, name)
        valve_uint(tree, zip_crc32(bytes).to_uint64(), 4)
        valve_uint(tree, preload.to_uint64(), 2)
        valve_uint(tree, 32767UL, 2)
        valve_uint(tree, data.length().to_uint64(), 4)
        valve_uint(tree, (bytes.length() - preload).to_uint64(), 4)
        valve_uint(tree, 65535UL, 2)
        tree.write_bytes(bytes[:preload])
        data.write_bytes(bytes[preload:])
      }
      tree.write_byte(b'\x00')
    }
    tree.write_byte(b'\x00')
  }
  tree.write_byte(b'\x00')
  if tree.length() + data.length() > options.max_bytes - 12 {
    raise InvalidBinary(0, "VPK output exceeds byte budget")
  }
  let output = @buffer.Buffer()
  valve_uint(output, 0x55aa1234UL, 4)
  valve_uint(output, 1UL, 4)
  valve_uint(output, tree.length().to_uint64(), 4)
  output.write_bytes(tree.to_bytes()[:])
  output.write_bytes(data.to_bytes()[:])
  output.to_bytes()
}