///|
/// A bounds-checked cursor over the UTF-8 byte encoding of a structured
/// field value.
///
/// All reads are checked against the end of the input; index arithmetic
/// never reaches the underlying array without a length check first. The
/// cursor's position is reported in UTF-8 byte offsets, which is exactly
/// the unit used for error offsets in [`SfError`].
pub struct Cursor {
  input : Bytes
  mut position : Int
}

///|
/// Creates a cursor over `input`.
pub fn Cursor::new(input : Bytes) -> Cursor {
  { input, position: 0 }
}

///|
/// The current UTF-8 byte offset.
pub fn Cursor::position(self : Cursor) -> Int {
  self.position
}

///|
/// Number of bytes remaining after the current position.
pub fn Cursor::remaining(self : Cursor) -> Int {
  self.input.length() - self.position
}

///|
/// Whether the cursor has reached the end of the input.
pub fn Cursor::is_end(self : Cursor) -> Bool {
  self.position >= self.input.length()
}

///|
/// The byte at the current position, or `None` at end of input.
pub fn Cursor::peek(self : Cursor) -> Byte? {
  if self.is_end() {
    return None
  }
  Some(self.input[self.position])
}

///|
/// The byte `n` bytes ahead of the current position, or `None` if that
/// offset is past the end of the input.
pub fn Cursor::peek_n(self : Cursor, n : Int) -> Byte? {
  let idx = self.position + n
  if n < 0 || idx >= self.input.length() {
    return None
  }
  Some(self.input[idx])
}

///|
/// Reads and consumes the byte at the current position, or `None` at end
/// of input.
pub fn Cursor::consume(self : Cursor) -> Byte? {
  if self.is_end() {
    return None
  }
  let b = self.input[self.position]
  self.position = self.position + 1
  Some(b)
}

///|
/// Consumes `b` if it is the next byte, returning whether it matched.
pub fn Cursor::consume_if(self : Cursor, b : Byte) -> Bool {
  if !self.is_end() && self.input[self.position] == b {
    self.position = self.position + 1
    return true
  }
  false
}

///|
/// Alias of [`Cursor::consume_if`] for call sites that read more naturally
/// with "expect".
pub fn Cursor::expect(self : Cursor, b : Byte) -> Bool {
  self.consume_if(b)
}

///|
/// Skips SP (0x20) characters. The Item grammar only permits SP, not HTAB.
pub fn Cursor::skip_spaces(self : Cursor) -> Unit {
  while !self.is_end() && self.input[self.position] == b' ' {
    self.position = self.position + 1
  }
}

///|
/// Skips OWS (SP / HTAB). Used between List and Dictionary members, where
/// the RFC allows tab characters.
pub fn Cursor::skip_ows(self : Cursor) -> Unit {
  while !self.is_end() && is_ows(self.input[self.position]) {
    self.position = self.position + 1
  }
}

///|
/// Returns a copy of the input bytes in the half-open range `[start, end)`.
/// Clamps out-of-range bounds.
pub fn Cursor::slice(self : Cursor, start : Int, end : Int) -> Bytes {
  let lo = start.max(0)
  let hi = end.min(self.input.length())
  if hi <= lo {
    return Bytes::new(0)
  }
  self.input.view(start=lo, end=hi).to_owned()
}

///|
/// The full input behind the cursor, copied.
pub fn Cursor::input(self : Cursor) -> Bytes {
  self.input
}

///|
/// The total length of the input, without copying.
pub fn Cursor::input_length(self : Cursor) -> Int {
  self.input.length()
}

///|
/// Saves the current position for later restoration.
pub fn Cursor::checkpoint(self : Cursor) -> Int {
  self.position
}

///|
/// Restores a previously saved position.
pub fn Cursor::restore(self : Cursor, pos : Int) -> Unit {
  if pos >= 0 && pos <= self.input.length() {
    self.position = pos
  }
}

///|
/// Reads the byte at absolute offset `offset`, or `None` out of bounds.
pub fn Cursor::at(self : Cursor, offset : Int) -> Byte? {
  if offset < 0 || offset >= self.input.length() {
    return None
  }
  Some(self.input[offset])
}

///|
/// Builds a short, lossy-decoded display context starting at byte `offset`
/// (see [`SfError::context`]). The slice is capped so errors never echo an
/// unbounded amount of input.
pub fn Cursor::context_string(
  self : Cursor,
  offset : Int,
  limit : Int,
) -> String {
  if self.input.length() == 0 {
    return ""
  }
  let lo = offset.min(self.input.length()).max(0)
  let hi = (lo + limit).min(self.input.length())
  let slice = self.input.view(start=lo, end=hi).to_owned()
  @utf8.decode_lossy(slice)
}