///|
/// A bounds-checked little-endian reader over a `Bytes`.
///
/// PE is a little-endian, offset-based format: every header field is a fixed
/// width at a fixed place, and a malicious or truncated file will happily claim
/// a length that runs off the end. All reads therefore go through `need`, which
/// turns that into a precise `OutOfBounds` error.

///|
priv struct Reader {
  bytes : Bytes
  mut pos : Int32
}

///|
fn Reader::new(bytes : Bytes) -> Reader {
  { bytes, pos: 0, }
}

///|
/// A reader positioned at `pos` without validating it; the first read reports
/// an out-of-bounds error if the position is unusable.
fn Reader::at(bytes : Bytes, pos : Int32) -> Reader {
  { bytes, pos, }
}

///|
/// Position a reader at ``pos``, failing if that is already past the end.
fn Reader::seek(bytes : Bytes, pos : Int32) -> Reader raise PeError {
  if pos < 0 || pos > bytes.length() {
    raise PeError::OutOfBounds(pos, 0, bytes.length())
  }
  { bytes, pos, }
}

///|
fn Reader::position(self : Reader) -> Int32 {
  self.pos
}

///|
fn Reader::remaining(self : Reader) -> Int32 {
  self.bytes.length() - self.pos
}

///|
fn Reader::need(self : Reader, count : Int32) -> Unit raise PeError {
  if count < 0 || self.pos + count > self.bytes.length() {
    raise PeError::OutOfBounds(self.pos, count, self.remaining())
  }
}

///|
fn Reader::u16(self : Reader) -> Int32 raise PeError {
  self.need(2)
  let b0 = self.bytes[self.pos].to_int()
  let b1 = self.bytes[self.pos + 1].to_int()
  self.pos = self.pos + 2
  b0 | (b1 << 8)
}

///|
/// Little-endian 32-bit value.
///
/// The result is computed in `Int64` and range-checked, so a file cannot smuggle
/// a value that behaves differently on a 32-bit and a 64-bit target: anything
/// above `0x7fffffff` is reported as malformed rather than silently wrapping.
fn Reader::u32(self : Reader) -> Int32 raise PeError {
  self.need(4)
  let b0 = self.bytes[self.pos].to_int64()
  let b1 = self.bytes[self.pos + 1].to_int64()
  let b2 = self.bytes[self.pos + 2].to_int64()
  let b3 = self.bytes[self.pos + 3].to_int64()
  let value = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
  if value > 2147483647 {
    raise PeError::Malformed(
      "32-bit field exceeds the supported 2 GiB range",
      self.pos,
    )
  }
  self.pos = self.pos + 4
  value.to_int()
}

///|
/// Little-endian 32-bit value without the 2 GiB guard.
///
/// Section characteristics legitimately use the top bit (`0x80000000` is
/// `IMAGE_SCN_MEM_WRITE`), so they cannot go through `u32`. The returned value
/// is a raw bit pattern: it may read as negative on a 32-bit target and as a
/// large positive number on a 64-bit one, which is why every consumer uses bit
/// tests rather than arithmetic.
fn Reader::u32_raw(self : Reader) -> Int32 raise PeError {
  self.need(4)
  let b0 = self.bytes[self.pos].to_int()
  let b1 = self.bytes[self.pos + 1].to_int()
  let b2 = self.bytes[self.pos + 2].to_int()
  let b3 = self.bytes[self.pos + 3].to_int()
  self.pos = self.pos + 4
  b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
}

///|
/// Little-endian 64-bit value, returned as `Int64`.
fn Reader::u64(self : Reader) -> Int64 raise PeError {
  self.need(8)
  let b0 = self.bytes[self.pos].to_int64()
  let b1 = self.bytes[self.pos + 1].to_int64()
  let b2 = self.bytes[self.pos + 2].to_int64()
  let b3 = self.bytes[self.pos + 3].to_int64()
  let b4 = self.bytes[self.pos + 4].to_int64()
  let b5 = self.bytes[self.pos + 5].to_int64()
  let b6 = self.bytes[self.pos + 6].to_int64()
  let b7 = self.bytes[self.pos + 7].to_int64()
  let value = b0 |
    (b1 << 8) |
    (b2 << 16) |
    (b3 << 24) |
    (b4 << 32) |
    (b5 << 40) |
    (b6 << 48) |
    (b7 << 56)
  self.pos = self.pos + 8
  value
}

///|
fn Reader::skip(self : Reader, count : Int32) -> Unit raise PeError {
  self.need(count)
  self.pos = self.pos + count
}

///|
/// Read `count` bytes as a NUL-trimmed ASCII string.
fn Reader::ascii(self : Reader, count : Int32) -> String raise PeError {
  self.need(count)
  let sb = StringBuilder()
  let mut i = 0
  while i < count {
    let value = self.bytes[self.pos + i].to_int()
    if value == 0 {
      break
    }
    if value < 128 {
      sb.write_char(Int32::unsafe_to_char(value))
    } else {
      sb.write_char('?')
    }
    i = i + 1
  }
  self.pos = self.pos + count
  sb.to_string()
}

///|
/// Read a NUL-terminated ASCII string, failing if no terminator is found within
/// `limit` bytes.
fn Reader::cstring(self : Reader, limit : Int32) -> String raise PeError {
  let sb = StringBuilder()
  let mut i = 0
  while i < limit {
    if self.pos + i >= self.bytes.length() {
      raise PeError::OutOfBounds(self.pos + i, 1, 0)
    }
    let value = self.bytes[self.pos + i].to_int()
    if value == 0 {
      self.pos = self.pos + i + 1
      return sb.to_string()
    }
    if value < 128 {
      sb.write_char(Int32::unsafe_to_char(value))
    } else {
      sb.write_char('?')
    }
    i = i + 1
  }
  raise PeError::Malformed(
    "unterminated string (no NUL within " + limit.to_string() + " bytes)",
    self.pos,
  )
}