// Bounds-checked reading of primitive values.
//
// Every function takes a cursor and returns the decoded value plus the
// advanced cursor, so a caller cannot observe a half-consumed field. Nothing
// here indexes a buffer without going through `Bounds::require` first: a
// truncated or corrupt blob has to come back as `UnexpectedEnd` or
// `OutOfBounds`, never as a wild read.

///|
/// Reinterprets the low 32 bits of an `Int64` as an unsigned word.
///
/// Values are accumulated in `Int64` so the high bit never turns the number
/// negative mid-calculation; this masks the truncation back down.
fn u32_of_int64(v : Int64) -> UInt32 {
  (v & 0xFFFF_FFFFL).to_int().reinterpret_as_uint()
}

///|
/// A buffer together with the range of it that may be read.
///
/// `start` and `limit` are absolute offsets into `bytes`. Keeping them
/// absolute means a sub-blob (the struct block inside a larger blob, say) can
/// be described without copying, while error messages still report a position
/// the caller can find in the original file.
pub(all) struct Bounds {
  /// Bytes of the blob.
  bytes : Bytes
  /// First readable offset.
  start : Int64
  /// One past the last readable offset.
  limit : Int64
}

///|
/// Wraps a whole buffer as readable from 0 to its length.
pub fn Bounds::of_bytes(bytes : Bytes) -> Bounds {
  Bounds::{ bytes, start: 0L, limit: bytes.length().to_int64(), }
}

///|
/// Restricts a buffer to `[start, limit)`, rejecting a range that runs off the
/// end of the underlying bytes.
pub fn Bounds::of_range(
  bytes : Bytes,
  start : Int64,
  limit : Int64,
) -> Bounds raise DtbError {
  let total = bytes.length().to_int64()
  if start < 0L || limit < start || limit > total {
    raise OutOfBounds(
      what="sub-range",
      offset=start,
      size=limit - start,
      limit=total,
    )
  }
  Bounds::{ bytes, start, limit, }
}

///|
/// Number of readable bytes.
pub fn Bounds::length(self : Bounds) -> Int64 {
  self.limit - self.start
}

///|
/// Fails unless `[offset, offset + size)` lies inside the readable range.
///
/// Parsers call this before trusting any length field that came out of the
/// file, so a corrupt length is reported with the field name that carried it.
pub fn Bounds::require(
  self : Bounds,
  offset : Int64,
  size : Int64,
  what : String,
) -> Unit raise DtbError {
  if size < 0L {
    raise OutOfBounds(what~, offset~, size~, limit=self.limit)
  }
  if offset < self.start || offset > self.limit || size > self.limit - offset {
    raise OutOfBounds(what~, offset~, size~, limit=self.limit)
  }
}

///|
/// Returns the byte at an absolute offset, after checking that it is readable.
fn Bounds::byte(self : Bounds, offset : Int64) -> Byte raise DtbError {
  self.require(offset, 1L, "byte")
  self.bytes.at(offset.to_int())
}

///|
/// Decodes an unsigned 8-bit integer.
pub fn Bounds::read_u8(
  self : Bounds,
  cur : Cursor,
) -> (UInt32, Cursor) raise DtbError {
  let v = self.byte(cur.pos).to_int().reinterpret_as_uint()
  (v, Cursor::{ pos: cur.pos + 1L, endian: cur.endian, })
}

///|
/// Decodes an unsigned 16-bit integer in the cursor's byte order.
pub fn Bounds::read_u16(
  self : Bounds,
  cur : Cursor,
) -> (UInt32, Cursor) raise DtbError {
  self.require(cur.pos, 2L, "u16")
  let base = cur.pos.to_int()
  let b0 = self.bytes.at(base).to_int().to_int64()
  let b1 = self.bytes.at(base + 1).to_int().to_int64()
  let v = match cur.endian {
    Little => b0 | (b1 << 8)
    Big => (b0 << 8) | b1
  }
  (u32_of_int64(v), Cursor::{ pos: cur.pos + 2L, endian: cur.endian, })
}

///|
/// Decodes an unsigned 32-bit integer in the cursor's byte order.
///
/// Accumulated in `Int64` so that a value with bit 31 set does not go negative
/// before it is reinterpreted.
pub fn Bounds::read_u32(
  self : Bounds,
  cur : Cursor,
) -> (UInt32, Cursor) raise DtbError {
  self.require(cur.pos, 4L, "u32")
  let base = cur.pos.to_int()
  let b0 = self.bytes.at(base).to_int().to_int64()
  let b1 = self.bytes.at(base + 1).to_int().to_int64()
  let b2 = self.bytes.at(base + 2).to_int().to_int64()
  let b3 = self.bytes.at(base + 3).to_int().to_int64()
  let v = match cur.endian {
    Little => b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
    Big => (b0 << 24) | (b1 << 16) | (b2 << 8) | b3
  }
  (u32_of_int64(v), Cursor::{ pos: cur.pos + 4L, endian: cur.endian, })
}

///|
/// Decodes an unsigned 64-bit integer in the cursor's byte order.
///
/// Returned as `Int64` so all 64 bits survive; callers that need the unsigned
/// reading mask at the use site, which is exactly where they have to think
/// about it anyway.
pub fn Bounds::read_u64(
  self : Bounds,
  cur : Cursor,
) -> (Int64, Cursor) raise DtbError {
  self.require(cur.pos, 8L, "u64")
  let (a, next) = self.read_u32(cur)
  let (b, next) = self.read_u32(next)
  let (hi, lo) = match cur.endian {
    Little => (b, a)
    Big => (a, b)
  }
  ((hi.to_int64() << 32) | (lo.to_int64() & 0xFFFF_FFFFL), next)
}

///|
/// Copies `n` bytes starting at `offset` into a fresh buffer.
pub fn Bounds::slice(
  self : Bounds,
  offset : Int64,
  n : Int64,
  what : String,
) -> Bytes raise DtbError {
  self.require(offset, n, what)
  let out = Buffer::Buffer(size_hint=n.to_int())
  for i in 0.. (String, Int64) raise DtbError {
  if limit > self.limit {
    raise OutOfBounds(
      what="string",
      offset~,
      size=limit - offset,
      limit=self.limit,
    )
  }
  let start = offset.to_int()
  let mut end = start
  let stop = limit.to_int()
  while end < stop && self.bytes.at(end).to_int() != 0 {
    end = end + 1
  }
  if end >= stop {
    raise UnexpectedEnd(
      offset~,
      needed=(end - start + 1).to_int64(),
      available=(stop - start).to_int64(),
    )
  }
  let out = Buffer::Buffer(size_hint=end - start)
  for i in start.. raise BadName(offset~) }
  (text, (end + 1).to_int64())
}