///|
/// A byte source the deserializer reads from: an immutable `Bytes` with a
/// mutable cursor. Reads are bounds-checked against the current region limit
/// and fail with `InputEndedUnexpectedly` on underrun.
///
/// The evolution machinery uses `push_region`/`pop_region` to fence reads to
/// a byte range (an evolved record's generation chunk) and `seek` to jump
/// between chunk start positions.
pub struct BinaryInput {
  priv mut data : Bytes
  priv mut pos : Int32
  priv mut limit : Int32
  /// Allocated lazily on first `push_region` — plain version-0
  /// deserialization (the common case) never touches it.
  priv mut region_stack : Array[(Int32, Int32)]?
}

///|
/// Create an input reading from `data`, starting at position 0.
pub fn BinaryInput::new(data : Bytes) -> BinaryInput {
  let len = data.length()
  { data, pos: 0, limit: len, region_stack: None, }
}

///|
/// Number of bytes already consumed.
pub fn BinaryInput::position(self : BinaryInput) -> Int32 {
  self.pos
}

///|
/// Number of bytes left to read.
pub fn BinaryInput::remaining(self : BinaryInput) -> Int32 {
  self.limit - self.pos
}

///|
/// True when the whole input has been consumed.
pub fn BinaryInput::is_empty(self : BinaryInput) -> Bool {
  self.remaining() == 0
}

///|
fn BinaryInput::ensure(
  self : BinaryInput,
  count : Int32,
) -> Unit raise DesertFailure {
  if self.limit - self.pos < count {
    raise InputEndedUnexpectedly
  }
}

///|
/// Move the cursor to an absolute position within the current region.
/// Only used by the evolution machinery to jump to a chunk's start.
pub fn BinaryInput::seek(
  self : BinaryInput,
  target : Int32,
) -> Unit raise DesertFailure {
  if target < 0 || target > self.limit {
    raise InputEndedUnexpectedly
  }
  self.pos = target
}

///|
/// Advance the cursor by `count` bytes; negative counts fail cleanly
/// instead of wrapping into a trap on a later view construction.
pub fn BinaryInput::skip_bytes(
  self : BinaryInput,
  count : Int32,
) -> Unit raise DesertFailure {
  if count < 0 {
    raise DeserializationFailure("Negative skip count: \{count}")
  }
  self.ensure(count)
  self.pos = self.pos + count
}

///|
/// Fence reads into `[start, start + size)`: the cursor jumps to `start`,
/// the region end becomes the read limit, and the previous cursor state is
/// pushed onto the region stack.
pub fn BinaryInput::push_region(
  self : BinaryInput,
  start : Int32,
  size : Int32,
) -> Unit {
  match self.region_stack {
    Some(stack) => stack.push((self.pos, self.limit))
    None => {
      let stack : Array[(Int32, Int32)] = [(self.pos, self.limit)]
      self.region_stack = Some(stack)
    }
  }
  self.pos = start
  self.limit = start + size
}

///|
/// End the current region: restores the previous cursor state and returns
/// the region's remaining `(start, size)` range so the caller can resume the
/// same chunk later.
pub fn BinaryInput::pop_region(self : BinaryInput) -> (Int32, Int32) {
  let remaining_start = self.pos
  let remaining_size = self.limit - self.pos
  match self.region_stack {
    Some(stack) =>
      match stack.pop() {
        Some((pos, limit)) => {
          self.pos = pos
          self.limit = limit
        }
        None => ()
      }
    None => ()
  }
  (remaining_start, remaining_size)
}

///|
/// Read a single raw byte.
pub fn BinaryInput::read_u8(self : BinaryInput) -> Byte raise DesertFailure {
  self.ensure(1)
  let b = self.data[self.pos]
  self.pos = self.pos + 1
  b
}

///|
/// Read `count` raw bytes as a zero-copy view.
pub fn BinaryInput::read_bytes_view(
  self : BinaryInput,
  count : Int32,
) -> BytesView raise DesertFailure {
  if count < 0 {
    raise DeserializationFailure("Negative byte count: \{count}")
  }
  self.ensure(count)
  let view = self.data.exact_view(start=self.pos, end=self.pos + count)
  self.pos = self.pos + count
  view
}

///|
/// Read `count` raw bytes into an owned `Bytes`.
pub fn BinaryInput::read_bytes(
  self : BinaryInput,
  count : Int32,
) -> Bytes raise DesertFailure {
  self.read_bytes_view(count).to_owned()
}

///|
/// Read a 1-byte signed integer, sign-extended into an `Int`.
pub fn BinaryInput::read_i8(self : BinaryInput) -> Int32 raise DesertFailure {
  let v = self.read_u8().to_int()
  if v >= 128 {
    v - 256
  } else {
    v
  }
}

///|
/// Read a 2-byte big-endian unsigned integer.
pub fn BinaryInput::read_u16(self : BinaryInput) -> UInt16 raise DesertFailure {
  self.ensure(2)
  let v = Bytes::unsafe_read_uint16_be(self.data, self.pos)
  self.pos = self.pos + 2
  v
}

///|
/// Read a 2-byte big-endian signed integer, sign-extended into an `Int`.
pub fn BinaryInput::read_i16(self : BinaryInput) -> Int32 raise DesertFailure {
  let v = self.read_u16().to_int()
  if v >= 0x8000 {
    v - 0x10000
  } else {
    v
  }
}

///|
/// Read a 4-byte big-endian unsigned integer.
pub fn BinaryInput::read_u32(self : BinaryInput) -> UInt32 raise DesertFailure {
  self.ensure(4)
  let v = Bytes::unsafe_read_uint32_be(self.data, self.pos)
  self.pos = self.pos + 4
  v
}

///|
/// Read a 4-byte big-endian signed integer.
pub fn BinaryInput::read_i32(self : BinaryInput) -> Int32 raise DesertFailure {
  self.read_u32().reinterpret_as_int()
}

///|
/// Read an 8-byte big-endian unsigned integer.
pub fn BinaryInput::read_u64(self : BinaryInput) -> UInt64 raise DesertFailure {
  self.ensure(8)
  let v = Bytes::unsafe_read_uint64_be(self.data, self.pos)
  self.pos = self.pos + 8
  v
}

///|
/// Read an 8-byte big-endian signed integer.
pub fn BinaryInput::read_i64(self : BinaryInput) -> Int64 raise DesertFailure {
  self.read_u64().reinterpret_as_int64()
}

///|
/// Read a 4-byte IEEE-754 float (big-endian bit pattern).
pub fn BinaryInput::read_f32(self : BinaryInput) -> Float raise DesertFailure {
  Float::reinterpret_from_uint(self.read_u32())
}

///|
/// Read an 8-byte IEEE-754 double (big-endian bit pattern).
pub fn BinaryInput::read_f64(self : BinaryInput) -> Double raise DesertFailure {
  self.read_u64().reinterpret_as_double()
}

///|
/// Read a base-128 varint of an unsigned 32-bit value. Fails with
/// `DeserializationFailure` if the encoding exceeds 5 bytes.
pub fn BinaryInput::read_var_u32(
  self : BinaryInput,
) -> UInt32 raise DesertFailure {
  let mut result = 0U
  let mut shift = 0
  for _i in 0..<5 {
    let byte = self.read_u8().to_uint()
    result = result | ((byte & 0x7F) << shift)
    if byte < 0x80 {
      return result
    }
    shift = shift + 7
  }
  raise DeserializationFailure("var_u32 too long")
}

///|
/// Read a zigzag-mapped signed varint (symmetric with `write_var_i32`).
pub fn BinaryInput::read_var_i32(
  self : BinaryInput,
) -> Int32 raise DesertFailure {
  let raw = self.read_var_u32()
  let inv = if (raw & 1U) == 1U { 0xFFFFFFFFU } else { 0U }
  ((raw >> 1) ^ inv).reinterpret_as_int()
}

///|
/// Point the input at new data and clear any region state, reusing the
/// allocated object. Session state lives in the `DeserializationContext`
/// and is reset separately.
pub fn BinaryInput::reset_input(self : BinaryInput, data : Bytes) -> Unit {
  self.data = data
  self.pos = 0
  self.limit = data.length()
  self.region_stack = None
}