///|
/// Compute the case-insensitive PDB v1 string hash used by GSI tables.
pub fn hash_string_v1(value : String) -> UInt32 {
  let bytes = @utf8.encode(value)
  let length = bytes.length()
  let mut result = 0U
  let mut cursor = 0
  while cursor + 4 <= length {
    let word = UInt32(
      (bytes.at(cursor + 3).to_int().reinterpret_as_uint() << 24) |
      (bytes.at(cursor + 2).to_int().reinterpret_as_uint() << 16) |
      (bytes.at(cursor + 1).to_int().reinterpret_as_uint() << 8) |
      bytes.at(cursor).to_int().reinterpret_as_uint(),
    )
    result = result ^ word
    cursor = cursor + 4
  }
  if length - cursor >= 2 {
    let word = ((bytes.at(cursor + 1).to_int() << 8) | bytes.at(cursor).to_int()).reinterpret_as_uint()
    result = result ^ UInt32(word)
    cursor = cursor + 2
  }
  if length - cursor == 1 {
    result = result ^ UInt32(bytes.at(cursor).to_int().reinterpret_as_uint())
  }
  result = result | 0x20202020U
  result = result ^ (result >> 11)
  result ^ (result >> 16)
}

///|
/// The Globals/GSI stream and the symbol records it indexes.
pub struct GlobalsStream {
  gsi : GsiTable
  symbols : SymbolTable
  max_results : Int32
}

///|
pub fn GlobalsStream::gsi(self : GlobalsStream) -> GsiTable {
  self.gsi
}

///|
pub fn GlobalsStream::symbols(self : GlobalsStream) -> SymbolTable {
  self.symbols
}

///|
/// Return all symbol records whose exact names match `name`.
pub fn GlobalsStream::find_records_by_name(
  self : GlobalsStream,
  name : String,
) -> Array[SymbolRecord] raise SymbolError {
  if self.gsi.hash_records.length() == 0 {
    return []
  }
  let expanded_bucket = (hash_string_v1(name) % 4096U).reinterpret_as_int()
  let compressed = match self.gsi.bucket_map_index(expanded_bucket) {
    Some(value) => value
    None => raise InvalidGlobalIndex("expanded bucket is outside the GSI map")
  }
  if compressed < 0 {
    return []
  }
  let bucket_index = compressed
  if bucket_index >= self.gsi.buckets.length() {
    raise InvalidGlobalIndex("compressed bucket is outside the bucket table")
  }
  let start_bytes = self.gsi.buckets[bucket_index]
  let end_bytes = if bucket_index + 1 < self.gsi.buckets.length() {
    self.gsi.buckets[bucket_index + 1]
  } else {
    (self.gsi.hash_records.length() * 12).reinterpret_as_uint()
  }
  if start_bytes % 12U != 0U || end_bytes % 12U != 0U {
    raise InvalidGlobalIndex("GSI bucket is not aligned to a hash record")
  }
  let start = symbol_uint_to_int(start_bytes / 12U, "GSI bucket start")
  let end = symbol_uint_to_int(end_bytes / 12U, "GSI bucket end")
  if start < 0 || end < start || end > self.gsi.hash_records.length() {
    raise InvalidGlobalIndex("GSI bucket range is outside the hash records")
  }
  let matches = []
  for index in start.. value
      None => raise InvalidGlobalIndex("GSI hash record has no symbol offset")
    }
    let record = match
      self.symbols.record_at_offset(
        symbol_uint_to_int(symbol_offset, "GSI symbol offset"),
      ) {
      Some(value) => value
      None => raise InvalidGlobalIndex("GSI references a missing symbol record")
    }
    match record.name() {
      Some(record_name) =>
        if record_name == name {
          if matches.length() >= self.max_results {
            raise PublicsLimitExceeded("global name-match limit exceeded")
          }
          matches.push(record)
        }
      None => ()
    }
  }
  matches
}

///|
/// Read the Globals/GSI stream referenced by the DBI header.
pub fn MsfFile::globals_stream(
  self : MsfFile,
  dbi : DbiFile,
  limits : SymbolLimits,
) -> GlobalsStream raise SymbolError {
  let index = dbi.header().global_symbol_stream_index()
  if index == 0xFFFF {
    raise GlobalsStreamUnavailable(index)
  }
  let bytes = self.stream(index.to_int()) catch {
    _ => raise GlobalsStreamUnavailable(index)
  }
  let symbols = self.symbol_record_stream(dbi, limits)
  parse_globals_stream(bytes, symbols, limits)
}

///|
/// Parse a standalone Globals/GSI stream and its symbol table.
pub fn parse_globals_stream(
  data : Bytes,
  symbols : SymbolTable,
  limits : SymbolLimits,
) -> GlobalsStream raise SymbolError {
  if data.length() > limits.max_stream_bytes {
    raise PublicsLimitExceeded("Globals stream byte limit exceeded")
  }
  let (gsi, end) = parse_gsi_table(data, 0, limits)
  if end != data.length() {
    raise InvalidGlobalIndex("Globals stream has trailing bytes")
  }
  GlobalsStream::{ gsi, symbols, max_results: limits.max_public_symbols, }
}