///|
const DOS_HEADER_OFFSET : Int32 = 0x3C

///|
const DOS_HEADER_SIZE : Int32 = 0x40

///|
const COFF_HEADER_SIZE : Int32 = 20

///|
const PE_SECTION_HEADER_SIZE : Int32 = 40

///|
const PE_OPTIONAL_MAGIC_PE32 : UInt16 = 0x010B

///|
const PE_OPTIONAL_MAGIC_PE32_PLUS : UInt16 = 0x020B

///|
/// Resource limits for parsing a PE image.
pub struct PeLimits {
  max_file_bytes : Int32
  max_sections : Int32
}

///|
pub fn PeLimits::default() -> PeLimits {
  PeLimits::{ max_file_bytes: 512 * 1024 * 1024, max_sections: 96, }
}

///|
pub struct PeSection {
  name : String
  virtual_size : UInt32
  virtual_address : UInt32
  raw_size : UInt32
  raw_pointer : UInt32
  characteristics : UInt32
}

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

///|
pub fn PeSection::virtual_size(self : PeSection) -> UInt32 {
  self.virtual_size
}

///|
pub fn PeSection::virtual_address(self : PeSection) -> UInt32 {
  self.virtual_address
}

///|
pub fn PeSection::raw_size(self : PeSection) -> UInt32 {
  self.raw_size
}

///|
pub fn PeSection::raw_pointer(self : PeSection) -> UInt32 {
  self.raw_pointer
}

///|
pub fn PeSection::characteristics(self : PeSection) -> UInt32 {
  self.characteristics
}

///|
pub fn PeSection::virtual_span(self : PeSection) -> UInt32 {
  if self.virtual_size > self.raw_size {
    self.virtual_size
  } else {
    self.raw_size
  }
}

///|
/// A PE section-number and section-relative offset mapped to RVA and VA.
pub struct RvaLocation {
  segment : UInt16
  section_offset : UInt32
  section_name : String
  rva : UInt32
  image_address : UInt64
}

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

///|
pub fn RvaLocation::section_offset(self : RvaLocation) -> UInt32 {
  self.section_offset
}

///|
pub fn RvaLocation::section_name(self : RvaLocation) -> String {
  self.section_name
}

///|
pub fn RvaLocation::rva(self : RvaLocation) -> UInt32 {
  self.rva
}

///|
pub fn RvaLocation::image_address(self : RvaLocation) -> UInt64 {
  self.image_address
}

///|
pub struct PeImage {
  machine : UInt16
  section_count : UInt16
  timestamp : UInt32
  optional_magic : UInt16
  image_base : UInt64
  sections : Array[PeSection]
}

///|
pub fn PeImage::machine(self : PeImage) -> UInt16 {
  self.machine
}

///|
pub fn PeImage::section_count(self : PeImage) -> UInt16 {
  self.section_count
}

///|
pub fn PeImage::timestamp(self : PeImage) -> UInt32 {
  self.timestamp
}

///|
pub fn PeImage::optional_magic(self : PeImage) -> UInt16 {
  self.optional_magic
}

///|
pub fn PeImage::image_base(self : PeImage) -> UInt64 {
  self.image_base
}

///|
pub fn PeImage::section_at(self : PeImage, index : Int32) -> PeSection? {
  if index < 0 || index >= self.sections.length() {
    None
  } else {
    Some(self.sections[index])
  }
}

///|
pub fn PeImage::map_segment_offset(
  self : PeImage,
  segment : UInt16,
  offset : UInt32,
) -> RvaLocation? {
  if segment == 0 || segment > self.section_count {
    return None
  }
  let section = self.sections[segment.to_int() - 1]
  let span = section.virtual_span()
  if span == 0U || offset >= span {
    return None
  }
  let rva_wide = section.virtual_address.to_uint64() + offset.to_uint64()
  if rva_wide > 0xFFFFFFFFUL {
    return None
  }
  if self.image_base > 0xFFFFFFFFFFFFFFFFUL - rva_wide {
    return None
  }
  let rva = rva_wide.to_uint()
  Some(RvaLocation::{
    segment,
    section_offset: offset,
    section_name: section.name,
    rva: UInt32(rva),
    image_address: self.image_base + rva_wide,
  })
}

///|
/// Parse the DOS, PE/COFF, optional-header, and section-table structures.
pub fn parse_pe_image(data : Bytes, limits : PeLimits) -> PeImage raise PeError {
  if data.length() > limits.max_file_bytes {
    raise PeLimitExceeded("PE file byte limit exceeded")
  }
  if data.length() < DOS_HEADER_SIZE {
    raise TooSmallPe(data.length())
  }
  if data.at(0) != b'M' || data.at(1) != b'Z' {
    raise BadDosMagic
  }
  let (pe_offset_raw, _) = read_pe_u32(data, DOS_HEADER_OFFSET, data.length())
  let pe_offset = pe_u32_to_int(pe_offset_raw, "PE header offset")
  if pe_offset < DOS_HEADER_SIZE {
    raise InvalidPeSection("PE header overlaps the DOS header")
  }
  let signature_end = checked_pe_add(pe_offset, 4, "PE signature")
  if signature_end > data.length() {
    raise TruncatedPe(signature_end)
  }
  if data.at(pe_offset) != b'P' ||
    data.at(pe_offset + 1) != b'E' ||
    data.at(pe_offset + 2) != 0 ||
    data.at(pe_offset + 3) != 0 {
    raise BadPeSignature
  }
  let coff_start = signature_end
  let coff_end = checked_pe_add(coff_start, COFF_HEADER_SIZE, "COFF header")
  if coff_end > data.length() {
    raise TruncatedPe(coff_end)
  }
  let (machine, cursor) = read_pe_u16(data, coff_start, coff_end)
  let (section_count, cursor) = read_pe_u16(data, cursor, coff_end)
  let (timestamp, cursor) = read_pe_u32(data, cursor, coff_end)
  let (_, cursor) = read_pe_u32(data, cursor, coff_end)
  let (_, cursor) = read_pe_u32(data, cursor, coff_end)
  let (optional_size_raw, cursor) = read_pe_u16(data, cursor, coff_end)
  let (_, cursor) = read_pe_u16(data, cursor, coff_end)
  if cursor != coff_end {
    raise InvalidPeSection("COFF header has trailing bytes")
  }
  if section_count.to_int() > limits.max_sections {
    raise PeLimitExceeded("PE section count limit exceeded")
  }
  let optional_size = optional_size_raw.to_int()
  let optional_start = coff_end
  let optional_end = checked_pe_add(
    optional_start, optional_size, "PE optional header",
  )
  if optional_end > data.length() {
    raise TruncatedPe(optional_end)
  }
  let (optional_magic, _) = read_pe_u16(data, optional_start, optional_end)
  let image_base = match optional_magic {
    PE_OPTIONAL_MAGIC_PE32 => {
      if optional_size < 32 {
        raise TruncatedPe(optional_end)
      }
      let (value, _) = read_pe_u32(data, optional_start + 28, optional_end)
      value.to_uint64()
    }
    PE_OPTIONAL_MAGIC_PE32_PLUS => {
      if optional_size < 32 {
        raise TruncatedPe(optional_end)
      }
      let (value, _) = read_pe_u64(data, optional_start + 24, optional_end)
      value
    }
    _ => raise UnsupportedPeOptionalHeader(optional_magic)
  }
  let section_table_end = checked_pe_add(
    optional_end,
    checked_pe_mul(
      section_count.to_int(),
      PE_SECTION_HEADER_SIZE,
      "PE sections",
    ),
    "PE section table",
  )
  if section_table_end > data.length() {
    raise TruncatedPe(section_table_end)
  }
  let sections = []
  let mut cursor = optional_end
  for _ in 0.. (PeSection, Int32) raise PeError {
  let section_end = checked_pe_add(start, PE_SECTION_HEADER_SIZE, "PE section")
  if section_end > end {
    raise TruncatedPe(section_end)
  }
  let name_bytes = data.exact_view(start~, end=start + 8).to_owned()
  let mut name_end = 0
  while name_end < 8 && name_bytes.at(name_end) != 0 {
    name_end = name_end + 1
  }
  let name = @utf8.decode_lossy(name_bytes.exact_view(end=name_end))
  let (virtual_size, cursor) = read_pe_u32(data, start + 8, section_end)
  let (virtual_address, cursor) = read_pe_u32(data, cursor, section_end)
  let (raw_size, cursor) = read_pe_u32(data, cursor, section_end)
  let (raw_pointer, cursor) = read_pe_u32(data, cursor, section_end)
  let (_, cursor) = read_pe_u32(data, cursor, section_end)
  let (_, cursor) = read_pe_u32(data, cursor, section_end)
  let (_, cursor) = read_pe_u16(data, cursor, section_end)
  let (_, cursor) = read_pe_u16(data, cursor, section_end)
  let (characteristics, cursor) = read_pe_u32(data, cursor, section_end)
  if cursor != section_end {
    raise InvalidPeSection("PE section header has trailing bytes")
  }
  (
    PeSection::{
      name,
      virtual_size,
      virtual_address,
      raw_size,
      raw_pointer,
      characteristics,
    },
    section_end,
  )
}

///|
fn read_pe_u16(
  data : Bytes,
  pos : Int32,
  end : Int32,
) -> (UInt16, Int32) raise PeError {
  if pos < 0 || pos + 2 > end || pos + 2 > data.length() {
    raise TruncatedPe(pos)
  }
  (
    ((data.at(pos + 1).to_int() << 8) | data.at(pos).to_int()).to_uint16(),
    pos + 2,
  )
}

///|
fn read_pe_u32(
  data : Bytes,
  pos : Int32,
  end : Int32,
) -> (UInt32, Int32) raise PeError {
  if pos < 0 || pos + 4 > end || pos + 4 > data.length() {
    raise TruncatedPe(pos)
  }
  (
    UInt32(
      (data.at(pos + 3).to_int().reinterpret_as_uint() << 24) |
      (data.at(pos + 2).to_int().reinterpret_as_uint() << 16) |
      (data.at(pos + 1).to_int().reinterpret_as_uint() << 8) |
      data.at(pos).to_int().reinterpret_as_uint(),
    ),
    pos + 4,
  )
}

///|
fn read_pe_u64(
  data : Bytes,
  pos : Int32,
  end : Int32,
) -> (UInt64, Int32) raise PeError {
  let (low, next) = read_pe_u32(data, pos, end)
  let (high, next) = read_pe_u32(data, next, end)
  ((high.to_uint64() << 32) | low.to_uint64(), next)
}

///|
fn pe_u32_to_int(value : UInt32, field : String) -> Int32 raise PeError {
  if value > 0x7FFFFFFFU {
    raise InvalidPeSection("\{field} exceeds Int range")
  }
  value.reinterpret_as_int()
}

///|
fn checked_pe_add(
  left : Int32,
  right : Int32,
  field : String,
) -> Int32 raise PeError {
  if left < 0 || right < 0 || left > 0x7FFFFFFF - right {
    raise InvalidPeSection("\{field} overflows Int range")
  }
  left + right
}

///|
fn checked_pe_mul(
  left : Int32,
  right : Int32,
  field : String,
) -> Int32 raise PeError {
  if left < 0 || right < 0 || (left != 0 && right > 0x7FFFFFFF / left) {
    raise InvalidPeSection("\{field} overflows Int range")
  }
  left * right
}