///|
const PUBLICS_HEADER_SIZE : Int32 = 28

///|
const GSI_HEADER_SIZE : Int32 = 16

///|
const GSI_BITMAP_WORDS : Int32 = 129

///|
const GSI_BUCKET_COUNT : Int32 = 4097

///|
const GSI_SIGNATURE : UInt32 = 0xFFFFFFFFU

///|
const GSI_VERSION : UInt32 = 0xEFFE0000U + 19990810U

///|
/// The fixed header before a Publics or Globals GSI hash table.
pub struct GsiHashHeader {
  signature : UInt32
  version : UInt32
  hash_record_bytes : UInt32
  declared_bucket_count : UInt32
}

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

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

///|
pub fn GsiHashHeader::hash_record_bytes(self : GsiHashHeader) -> UInt32 {
  self.hash_record_bytes
}

///|
pub fn GsiHashHeader::declared_bucket_count(self : GsiHashHeader) -> UInt32 {
  self.declared_bucket_count
}

///|
/// One GSI hash record. On-disk offsets are stored one greater than zero.
pub struct GsiHashRecord {
  raw_offset : UInt32
  reference_count : UInt32
}

///|
pub fn GsiHashRecord::raw_offset(self : GsiHashRecord) -> UInt32 {
  self.raw_offset
}

///|
pub fn GsiHashRecord::symbol_offset(self : GsiHashRecord) -> UInt32? {
  if self.raw_offset == 0U {
    None
  } else {
    Some(self.raw_offset - 1U)
  }
}

///|
pub fn GsiHashRecord::reference_count(self : GsiHashRecord) -> UInt32 {
  self.reference_count
}

///|
/// A parsed GSI hash table, including its compressed bucket map.
pub struct GsiTable {
  header : GsiHashHeader
  hash_records : Array[GsiHashRecord]
  bitmap : Array[UInt32]
  buckets : Array[UInt32]
  bucket_map : Array[Int32]
}

///|
pub fn GsiTable::header(self : GsiTable) -> GsiHashHeader {
  self.header
}

///|
pub fn GsiTable::hash_record_count(self : GsiTable) -> Int32 {
  self.hash_records.length()
}

///|
pub fn GsiTable::hash_record_at(
  self : GsiTable,
  index : Int32,
) -> GsiHashRecord? {
  if index < 0 || index >= self.hash_records.length() {
    None
  } else {
    Some(self.hash_records[index])
  }
}

///|
pub fn GsiTable::hash_records(self : GsiTable) -> Array[GsiHashRecord] {
  self.hash_records.copy()
}

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

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

///|
pub fn GsiTable::bucket_count(self : GsiTable) -> Int32 {
  self.buckets.length()
}

///|
pub fn GsiTable::bucket_map_index(
  self : GsiTable,
  expanded_bucket : Int32,
) -> Int32? {
  if expanded_bucket < 0 || expanded_bucket >= self.bucket_map.length() {
    None
  } else {
    Some(self.bucket_map[expanded_bucket])
  }
}

///|
/// The fixed 28-byte header at the start of the Publics stream.
pub struct PublicsHeader {
  symbol_hash : UInt32
  address_map_bytes : UInt32
  thunk_count : UInt32
  thunk_size : UInt32
  thunk_table_section : UInt16
  thunk_table_offset : UInt32
  section_count : UInt32
}

///|
pub fn PublicsHeader::symbol_hash(self : PublicsHeader) -> UInt32 {
  self.symbol_hash
}

///|
pub fn PublicsHeader::address_map_bytes(self : PublicsHeader) -> UInt32 {
  self.address_map_bytes
}

///|
pub fn PublicsHeader::thunk_count(self : PublicsHeader) -> UInt32 {
  self.thunk_count
}

///|
pub fn PublicsHeader::thunk_size(self : PublicsHeader) -> UInt32 {
  self.thunk_size
}

///|
pub fn PublicsHeader::thunk_table_section(self : PublicsHeader) -> UInt16 {
  self.thunk_table_section
}

///|
pub fn PublicsHeader::thunk_table_offset(self : PublicsHeader) -> UInt32 {
  self.thunk_table_offset
}

///|
pub fn PublicsHeader::section_count(self : PublicsHeader) -> UInt32 {
  self.section_count
}

///|
pub struct PublicSectionOffset {
  offset : UInt32
  section : UInt16
}

///|
pub fn PublicSectionOffset::offset(self : PublicSectionOffset) -> UInt32 {
  self.offset
}

///|
pub fn PublicSectionOffset::section(self : PublicSectionOffset) -> UInt16 {
  self.section
}

///|
/// A Publics address-map entry resolved to its S_PUB32 symbol.
pub struct ResolvedPublicSymbol {
  symbol_offset : UInt32
  flags : UInt32
  segment : UInt16
  offset : UInt32
  name : String
}

///|
pub fn ResolvedPublicSymbol::symbol_offset(
  self : ResolvedPublicSymbol,
) -> UInt32 {
  self.symbol_offset
}

///|
pub fn ResolvedPublicSymbol::flags(self : ResolvedPublicSymbol) -> UInt32 {
  self.flags
}

///|
pub fn ResolvedPublicSymbol::segment(self : ResolvedPublicSymbol) -> UInt16 {
  self.segment
}

///|
pub fn ResolvedPublicSymbol::offset(self : ResolvedPublicSymbol) -> UInt32 {
  self.offset
}

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

///|
/// The Publics stream, its GSI table, and address-resolved S_PUB32 records.
pub struct PublicsStream {
  header : PublicsHeader
  gsi : GsiTable
  address_map : Array[UInt32]
  thunk_map : Array[UInt32]
  section_offsets : Array[PublicSectionOffset]
  public_symbols : Array[ResolvedPublicSymbol]
}

///|
pub fn PublicsStream::header(self : PublicsStream) -> PublicsHeader {
  self.header
}

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

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

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

///|
pub fn PublicsStream::section_offsets(
  self : PublicsStream,
) -> Array[PublicSectionOffset] {
  self.section_offsets.copy()
}

///|
pub fn PublicsStream::public_symbol_count(self : PublicsStream) -> Int32 {
  self.public_symbols.length()
}

///|
pub fn PublicsStream::public_symbol_at(
  self : PublicsStream,
  index : Int32,
) -> ResolvedPublicSymbol? {
  if index < 0 || index >= self.public_symbols.length() {
    None
  } else {
    Some(self.public_symbols[index])
  }
}

///|
pub fn PublicsStream::find_public_exact(
  self : PublicsStream,
  segment : UInt16,
  offset : UInt32,
) -> ResolvedPublicSymbol? {
  let mut low = 0
  let mut high = self.public_symbols.length()
  while low < high {
    let middle = low + (high - low) / 2
    let item = self.public_symbols[middle]
    let comparison = compare_public_address(
      item.segment,
      item.offset,
      segment,
      offset,
    )
    if comparison < 0 {
      low = middle + 1
    } else {
      high = middle
    }
  }
  if low < self.public_symbols.length() {
    let item = self.public_symbols[low]
    if item.segment == segment && item.offset == offset {
      Some(item)
    } else {
      None
    }
  } else {
    None
  }
}

///|
pub fn PublicsStream::find_public_nearest(
  self : PublicsStream,
  segment : UInt16,
  offset : UInt32,
) -> ResolvedPublicSymbol? {
  let mut low = 0
  let mut high = self.public_symbols.length()
  while low < high {
    let middle = low + (high - low) / 2
    let item = self.public_symbols[middle]
    if compare_public_address(item.segment, item.offset, segment, offset) <= 0 {
      low = middle + 1
    } else {
      high = middle
    }
  }
  if low == 0 {
    return None
  }
  let item = self.public_symbols[low - 1]
  if item.segment == segment {
    Some(item)
  } else {
    None
  }
}

///|
/// Read the Publics stream referenced by DBI and resolve its address map.
pub fn MsfFile::publics_stream(
  self : MsfFile,
  dbi : DbiFile,
  limits : SymbolLimits,
) -> PublicsStream raise SymbolError {
  let index = dbi.header().public_symbol_stream_index()
  if index == 0xFFFF {
    raise PublicsStreamUnavailable(index)
  }
  let bytes = self.stream(index.to_int()) catch {
    _ => raise PublicsStreamUnavailable(index)
  }
  let symbols = self.symbol_record_stream(dbi, limits)
  parse_publics_stream(bytes, symbols, limits)
}

///|
pub fn parse_publics_stream(
  data : Bytes,
  symbols : SymbolTable,
  limits : SymbolLimits,
) -> PublicsStream raise SymbolError {
  if data.length() > limits.max_stream_bytes {
    raise PublicsLimitExceeded("Publics stream byte limit exceeded")
  }
  if data.length() < PUBLICS_HEADER_SIZE {
    raise TruncatedPublics(data.length())
  }
  let (header, after_header) = parse_publics_header(data)
  let (gsi, after_gsi) = parse_gsi_table(data, after_header, limits)
  let mut cursor = after_gsi
  let address_count_raw = header.address_map_bytes / 4U
  let address_count = symbol_uint_to_int(
    address_count_raw, "Publics address-map count",
  )
  if address_count > limits.max_public_symbols {
    raise PublicsLimitExceeded("Publics address-map count limit exceeded")
  }
  let address_end = checked_symbol_add(
    cursor,
    checked_symbol_mul(address_count, 4, "address map"),
    "address map",
  )
  if address_end > data.length() {
    raise TruncatedPublics(address_end)
  }
  let address_map = []
  for _ in 0.. limits.max_public_symbols {
    raise PublicsLimitExceeded("Publics thunk count limit exceeded")
  }
  let thunk_end = checked_symbol_add(
    cursor,
    checked_symbol_mul(thunk_count, 4, "thunk map"),
    "thunk map",
  )
  if thunk_end > data.length() {
    raise TruncatedPublics(thunk_end)
  }
  let thunk_map = []
  for _ in 0.. data.length() {
      raise TruncatedPublics(section_end)
    }
    raise InvalidPublicsHeader("Publics stream has trailing bytes")
  }
  let section_offsets = []
  for _ in 0.. (PublicsHeader, Int32) raise SymbolError {
  let (symbol_hash, cursor) = read_symbol_u32(data, 0, PUBLICS_HEADER_SIZE)
  let (address_map_bytes, cursor) = read_symbol_u32(
    data,
    cursor,
    PUBLICS_HEADER_SIZE,
  )
  if address_map_bytes % 4U != 0U {
    raise InvalidPublicsHeader("address-map byte count is not uint32-aligned")
  }
  let (thunk_count, cursor) = read_symbol_u32(data, cursor, PUBLICS_HEADER_SIZE)
  let (thunk_size, cursor) = read_symbol_u32(data, cursor, PUBLICS_HEADER_SIZE)
  let (thunk_table_section, cursor) = read_symbol_u16(
    data,
    cursor,
    PUBLICS_HEADER_SIZE,
  )
  let (padding, cursor) = read_symbol_u16(data, cursor, PUBLICS_HEADER_SIZE)
  if padding != 0 {
    raise InvalidPublicsHeader("Publics header padding is not zero")
  }
  let (thunk_table_offset, cursor) = read_symbol_u32(
    data,
    cursor,
    PUBLICS_HEADER_SIZE,
  )
  let (section_count, cursor) = read_symbol_u32(
    data,
    cursor,
    PUBLICS_HEADER_SIZE,
  )
  (
    PublicsHeader::{
      symbol_hash,
      address_map_bytes,
      thunk_count,
      thunk_size,
      thunk_table_section,
      thunk_table_offset,
      section_count,
    },
    cursor,
  )
}

///|
fn parse_gsi_table(
  data : Bytes,
  start : Int32,
  limits : SymbolLimits,
) -> (GsiTable, Int32) raise SymbolError {
  let header_end = checked_symbol_add(start, GSI_HEADER_SIZE, "GSI header")
  if header_end > data.length() {
    raise TruncatedPublics(header_end)
  }
  let (signature, cursor0) = read_symbol_u32(data, start, header_end)
  if signature != GSI_SIGNATURE {
    raise InvalidGsi("GSI signature must be 0xFFFFFFFF")
  }
  let (version, cursor1) = read_symbol_u32(data, cursor0, header_end)
  if version != GSI_VERSION {
    raise UnsupportedGsiVersion(version)
  }
  let (hash_record_bytes, cursor2) = read_symbol_u32(data, cursor1, header_end)
  let (declared_bucket_count, cursor3) = read_symbol_u32(
    data, cursor2, header_end,
  )
  let mut cursor = cursor3
  if hash_record_bytes % 8U != 0U {
    raise InvalidGsi("GSI hash-record byte count is not aligned")
  }
  let hash_record_count = symbol_uint_to_int(
    hash_record_bytes / 8U,
    "GSI hash-record count",
  )
  if hash_record_count > limits.max_hash_records {
    raise PublicsLimitExceeded("GSI hash-record count limit exceeded")
  }
  let records_end = checked_symbol_add(
    cursor,
    checked_symbol_mul(hash_record_count, 8, "GSI hash records"),
    "GSI hash records",
  )
  if records_end > data.length() {
    raise TruncatedPublics(records_end)
  }
  let hash_records = []
  for _ in 0.. 0 {
    let bitmap_end = checked_symbol_add(
      cursor,
      checked_symbol_mul(GSI_BITMAP_WORDS, 4, "GSI bitmap"),
      "GSI bitmap",
    )
    if bitmap_end > data.length() {
      raise TruncatedPublics(bitmap_end)
    }
    for _ in 0.. data.length() {
      raise TruncatedPublics(buckets_end)
    }
    for _ in 0.. Array[ResolvedPublicSymbol] raise SymbolError {
  let resolved = []
  let mut previous : (UInt16, UInt32)? = None
  for symbol_offset in address_map {
    let offset = symbol_uint_to_int(symbol_offset, "public symbol offset")
    let record = match symbols.record_at_offset(offset) {
      Some(value) => value
      None =>
        raise InvalidPublicSymbol("address map references a missing symbol")
    }
    let public = match record.details() {
      Public(value) => value
      _ => raise InvalidPublicSymbol("address map does not reference S_PUB32")
    }
    match previous {
      Some((segment, previous_offset)) =>
        if segment > public.segment ||
          (segment == public.segment && previous_offset > public.offset) {
          raise InvalidPublicSymbol("address map is not sorted by address")
        }
      None => ()
    }
    previous = Some((public.segment, public.offset))
    resolved.push(ResolvedPublicSymbol::{
      symbol_offset,
      flags: public.flags,
      segment: public.segment,
      offset: public.offset,
      name: public.name,
    })
  }
  resolved
}

///|
fn compare_public_address(
  left_segment : UInt16,
  left_offset : UInt32,
  right_segment : UInt16,
  right_offset : UInt32,
) -> Int32 {
  if left_segment < right_segment {
    -1
  } else if left_segment > right_segment {
    1
  } else if left_offset < right_offset {
    -1
  } else if left_offset > right_offset {
    1
  } else {
    0
  }
}