///|
/// A bounds-checked reader for OPC UA Binary Encoding.
pub(all) struct UaReader {
  data : BytesView
  mut offset : Int32
  limits : UaLimits
}

///|
/// Create a reader with default limits.
pub fn UaReader::new(data : BytesView) -> UaReader {
  { data, offset: 0, limits: UaLimits::default(), }
}

///|
/// Create a reader from owned bytes.
pub fn UaReader::from_bytes(data : Bytes) -> UaReader {
  UaReader::new(data.exact_view())
}

///|
/// Create a reader with explicit limits.
pub fn UaReader::with_limits(data : BytesView, limits : UaLimits) -> UaReader {
  { data, offset: 0, limits, }
}

///|
/// Current byte offset.
pub fn UaReader::position(self : UaReader) -> Int32 {
  self.offset
}

///|
/// Remaining bytes.
pub fn UaReader::remaining(self : UaReader) -> Int32 {
  self.data.length() - self.offset
}

///|
fn UaReader::take(self : UaReader, count : Int32) -> BytesView raise UaError {
  let remaining = self.remaining()
  if count < 0 || count > remaining {
    raise Truncated(count, remaining)
  }
  let start = self.offset
  self.offset = self.offset + count
  self.data[start:self.offset]
}

///|
/// Read an unsigned byte.
pub fn UaReader::read_u8(self : UaReader) -> UInt32 raise UaError {
  self.take(1)[0].to_uint()
}

///|
/// Read a signed byte.
pub fn UaReader::read_sbyte(self : UaReader) -> Int32 raise UaError {
  let value = self.read_u8().reinterpret_as_int()
  if value >= 0x80 {
    value - 0x100
  } else {
    value
  }
}

///|
/// Read a Boolean. OPC UA accepts zero and one only.
pub fn UaReader::read_bool(self : UaReader) -> Bool raise UaError {
  match self.read_u8() {
    0 => false
    1 => true
    value => raise InvalidValue("Boolean must be 0 or 1, got \{value}")
  }
}

///|
/// Read a little-endian Int16.
pub fn UaReader::read_int16(self : UaReader) -> Int32 raise UaError {
  let value = self.take(2).unsafe_read_uint16_le(0).to_int()
  if value >= 0x8000 {
    value - 0x10000
  } else {
    value
  }
}

///|
/// Read a little-endian UInt16.
pub fn UaReader::read_uint16(self : UaReader) -> UInt16 raise UaError {
  self.take(2).unsafe_read_uint16_le(0)
}

///|
/// Read a little-endian Int32.
pub fn UaReader::read_int32(self : UaReader) -> Int32 raise UaError {
  self.take(4).unsafe_read_uint32_le(0).reinterpret_as_int()
}

///|
/// Read a little-endian UInt32.
pub fn UaReader::read_uint32(self : UaReader) -> UInt32 raise UaError {
  self.take(4).unsafe_read_uint32_le(0)
}

///|
/// Read a little-endian Int64.
pub fn UaReader::read_int64(self : UaReader) -> Int64 raise UaError {
  self.take(8).unsafe_read_uint64_le(0).reinterpret_as_int64()
}

///|
/// Read a little-endian UInt64.
pub fn UaReader::read_uint64(self : UaReader) -> UInt64 raise UaError {
  self.take(8).unsafe_read_uint64_le(0)
}

///|
/// Read a little-endian Float.
pub fn UaReader::read_float(self : UaReader) -> Float raise UaError {
  Float::reinterpret_from_uint(self.read_uint32())
}

///|
/// Read a little-endian Double.
pub fn UaReader::read_double(self : UaReader) -> Double raise UaError {
  self.read_uint64().reinterpret_as_double()
}

///|
/// Read a nullable OPC UA String.
pub fn UaReader::read_string(self : UaReader) -> String? raise UaError {
  let length = self.read_int32()
  if length == -1 {
    return None
  }
  if length < -1 {
    raise InvalidLength(length)
  }
  if length > self.limits.max_byte_length {
    raise LimitExceeded(length, self.limits.max_byte_length)
  }
  let payload = self.take(length)
  Some(@utf8.decode(payload) catch { _ => raise InvalidUtf8 })
}

///|
/// Read a String and reject null.
pub fn UaReader::read_required_string(self : UaReader) -> String raise UaError {
  match self.read_string() {
    Some(value) => value
    None => raise InvalidValue("String must not be null")
  }
}

///|
/// Read a nullable ByteString.
pub fn UaReader::read_bytes(self : UaReader) -> Bytes? raise UaError {
  let length = self.read_int32()
  if length == -1 {
    return None
  }
  if length < -1 {
    raise InvalidLength(length)
  }
  if length > self.limits.max_byte_length {
    raise LimitExceeded(length, self.limits.max_byte_length)
  }
  Some(self.take(length).to_owned())
}

///|
/// Read a ByteString and reject null.
pub fn UaReader::read_required_bytes(self : UaReader) -> Bytes raise UaError {
  match self.read_bytes() {
    Some(value) => value
    None => raise InvalidValue("ByteString must not be null")
  }
}