///|
/// Resource limits for the PDB Info stream.
pub struct PdbInfoLimits {
  max_string_bytes : Int32
  max_named_streams : Int32
  max_hash_capacity : Int32
  max_features : Int32
}

///|
pub fn PdbInfoLimits::default() -> PdbInfoLimits {
  PdbInfoLimits::{
    max_string_bytes: 1024 * 1024,
    max_named_streams: 4096,
    max_hash_capacity: 1_048_576,
    max_features: 256,
  }
}

///|
/// One name-to-stream-index entry in the PDB Info stream.
pub struct NamedStream {
  name : String
  stream_index : Int32
}

///|
pub fn NamedStream::name(self : NamedStream) -> String {
  self.name
}

///|
pub fn NamedStream::stream_index(self : NamedStream) -> Int32 {
  self.stream_index
}

///|
/// The fixed header and variable sections of the PDB Info stream.
pub struct PdbInfo {
  version : UInt32
  signature : UInt32
  age : UInt32
  guid : Bytes
  named_streams : Array[NamedStream]
  feature_codes : Array[UInt32]
}

///|
pub fn PdbInfo::version(self : PdbInfo) -> UInt32 {
  self.version
}

///|
pub fn PdbInfo::signature(self : PdbInfo) -> UInt32 {
  self.signature
}

///|
pub fn PdbInfo::age(self : PdbInfo) -> UInt32 {
  self.age
}

///|
pub fn PdbInfo::guid(self : PdbInfo) -> Bytes {
  self.guid
}

///|
pub fn PdbInfo::named_stream_count(self : PdbInfo) -> Int32 {
  self.named_streams.length()
}

///|
pub fn PdbInfo::named_streams(self : PdbInfo) -> Array[NamedStream] {
  self.named_streams.copy()
}

///|
pub fn PdbInfo::named_stream_index(self : PdbInfo, name : String) -> Int32? {
  for stream in self.named_streams {
    if stream.name == name {
      return Some(stream.stream_index)
    }
  }
  None
}

///|
pub fn PdbInfo::feature_codes(self : PdbInfo) -> Array[UInt32] {
  self.feature_codes.copy()
}

///|
pub fn PdbInfo::has_feature(self : PdbInfo, code : UInt32) -> Bool {
  for feature in self.feature_codes {
    if feature == code {
      return true
    }
  }
  false
}

///|
/// Format the PDB GUID using the conventional Windows field order.
pub fn PdbInfo::guid_string(self : PdbInfo) -> String {
  let guid = self.guid
  guid.at(3).to_hex() +
  guid.at(2).to_hex() +
  guid.at(1).to_hex() +
  guid.at(0).to_hex() +
  "-" +
  guid.at(5).to_hex() +
  guid.at(4).to_hex() +
  "-" +
  guid.at(7).to_hex() +
  guid.at(6).to_hex() +
  "-" +
  guid.at(8).to_hex() +
  guid.at(9).to_hex() +
  "-" +
  guid.at(10).to_hex() +
  guid.at(11).to_hex() +
  guid.at(12).to_hex() +
  guid.at(13).to_hex() +
  guid.at(14).to_hex() +
  guid.at(15).to_hex()
}

///|
/// Parse stream 1 of an already parsed MSF file.
pub fn MsfFile::pdb_info(
  self : MsfFile,
  limits : PdbInfoLimits,
) -> PdbInfo raise PdbInfoError {
  let info_bytes = self.stream(1) catch { _ => raise InfoStreamUnavailable }
  parse_pdb_info(info_bytes, self.stream_count(), limits)
}

///|
/// Parse the PDB Info stream bytes.
pub fn parse_pdb_info(
  data : Bytes,
  msf_stream_count : Int32,
  limits : PdbInfoLimits,
) -> PdbInfo raise PdbInfoError {
  if data.length() < 28 {
    raise TruncatedInfo(data.length())
  }
  if msf_stream_count <= 0 {
    raise InvalidInfoValue("MSF stream count must be positive")
  }
  let (version, cursor) = read_info_u32_le(data, 0, data.length())
  let (signature, cursor) = read_info_u32_le(data, cursor, data.length())
  let (age, cursor) = read_info_u32_le(data, cursor, data.length())
  let (guid, cursor) = read_info_bytes(data, cursor, data.length(), 16)
  let (string_buffer_size_raw, cursor) = read_info_u32_le(
    data,
    cursor,
    data.length(),
  )
  let string_buffer_size = info_uint_to_int(
    string_buffer_size_raw, "named stream string bytes",
  )
  if string_buffer_size > limits.max_string_bytes {
    raise LimitExceededInfo("named stream string byte limit exceeded")
  }
  let (string_buffer, cursor) = read_info_bytes(
    data,
    cursor,
    data.length(),
    string_buffer_size,
  )
  let (named_streams, cursor) = parse_named_stream_map(
    data, cursor, string_buffer, msf_stream_count, limits,
  )
  let feature_codes = parse_feature_codes(data, cursor, limits)
  PdbInfo::{ version, signature, age, guid, named_streams, feature_codes, }
}

///|
fn parse_named_stream_map(
  data : Bytes,
  start : Int32,
  string_buffer : Bytes,
  msf_stream_count : Int32,
  limits : PdbInfoLimits,
) -> (Array[NamedStream], Int32) raise PdbInfoError {
  let size_and_cursor = read_info_u32_le(data, start, data.length())
  let size_raw = size_and_cursor.0
  let mut cursor = size_and_cursor.1
  let capacity_and_cursor = read_info_u32_le(data, cursor, data.length())
  let capacity_raw = capacity_and_cursor.0
  cursor = capacity_and_cursor.1
  let size = info_uint_to_int(size_raw, "named stream count")
  let capacity = info_uint_to_int(capacity_raw, "named stream capacity")
  if size > limits.max_named_streams {
    raise LimitExceededInfo("named stream count limit exceeded")
  }
  if capacity > limits.max_hash_capacity || capacity < size {
    raise InvalidInfoValue("invalid named stream hash capacity")
  }
  let present = read_bit_vector(data, cursor, capacity)
  let present_words = present.0
  cursor = present.1
  let deleted = read_bit_vector(data, cursor, capacity)
  let deleted_words = deleted.0
  cursor = deleted.1
  if bit_count(present_words) != size {
    raise InvalidInfoValue("present bit count does not match table size")
  }
  for index in 0..= string_buffer.length() {
      raise InvalidNamedStream("key offset is outside the string buffer")
    }
    if stream_index < 0 || stream_index >= msf_stream_count {
      raise InvalidNamedStream("stream index is outside the MSF file")
    }
    let name_end = find_nul(string_buffer, key_offset)
    let name = decode_named_stream(
      string_buffer.exact_view(start=key_offset, end=name_end),
    )
    streams.push(NamedStream::{ name, stream_index, })
    cursor = next
  }
  (streams, cursor)
}

///|
fn read_bit_vector(
  data : Bytes,
  start : Int32,
  capacity : Int32,
) -> (Array[UInt32], Int32) raise PdbInfoError {
  let (word_count_raw, cursor) = read_info_u32_le(data, start, data.length())
  let word_count = info_uint_to_int(word_count_raw, "bit vector word count")
  let expected_words = be_div_ceil(capacity, 32)
  if word_count < 0 || word_count > expected_words {
    raise InvalidInfoValue("bit vector word count exceeds capacity")
  }
  let words = []
  let mut cursor = cursor
  for _ in 0.. Int32 {
  let mut total = 0
  for word in words {
    total = total + word.popcnt()
  }
  total
}

///|
fn parse_feature_codes(
  data : Bytes,
  start : Int32,
  limits : PdbInfoLimits,
) -> Array[UInt32] raise PdbInfoError {
  let remaining = data.length() - start
  if remaining % 4 != 0 {
    raise InvalidInfoValue("feature code bytes are not aligned to 4 bytes")
  }
  let count = remaining / 4
  if count > limits.max_features {
    raise LimitExceededInfo("feature code limit exceeded")
  }
  let codes = []
  let mut cursor = start
  for _ in 0.. (UInt32, Int32) raise PdbInfoError {
  read_u32_le(data, pos, end) catch {
    Truncated(_) => raise TruncatedInfo(pos)
    _ => raise InvalidInfoValue("invalid PDB Info stream offset")
  }
}

///|
fn read_info_bytes(
  data : Bytes,
  pos : Int32,
  end : Int32,
  length : Int32,
) -> (Bytes, Int32) raise PdbInfoError {
  if length < 0 || pos < 0 || pos + length > end {
    raise TruncatedInfo(pos)
  }
  (data.exact_view(start=pos, end=pos + length).to_owned(), pos + length)
}

///|
fn info_uint_to_int(value : UInt32, field : String) -> Int32 raise PdbInfoError {
  let wide = value.to_int64()
  if wide > 0x7FFFFFFFL {
    raise InvalidInfoValue("\{field} exceeds Int range")
  }
  wide.to_int()
}

///|
fn find_nul(data : Bytes, start : Int32) -> Int32 raise PdbInfoError {
  let mut index = start
  while index < data.length() {
    if data.at(index) == 0 {
      return index
    }
    index = index + 1
  }
  raise InvalidNamedStream("named stream is not NUL-terminated")
}

///|
fn decode_named_stream(data : BytesView) -> String raise PdbInfoError {
  @utf8.decode(data) catch {
    _ => raise InvalidNamedStream("named stream is not valid UTF-8")
  }
}