///|
/// A small cursor over an IFC physical-file string.
priv struct Cursor {
  source : String
  mut offset : Int
  mut line : Int
  mut column : Int
}

///|
fn Cursor::new(source : String) -> Cursor {
  { source, offset: 0, line: 1, column: 1 }
}

///|
fn Cursor::position(self : Cursor) -> SourcePosition {
  make_position(self.offset, self.line, self.column)
}

///|
fn Cursor::eof(self : Cursor) -> Bool {
  self.offset >= self.source.length()
}

///|
fn Cursor::peek(self : Cursor) -> Char? {
  self.source.get_char(self.offset)
}

///|
fn Cursor::advance(self : Cursor) -> Char? {
  match self.source.get_char(self.offset) {
    None => None
    Some(character) => {
      self.offset += 1
      if character == '\n' {
        self.line += 1
        self.column = 1
      } else {
        self.column += 1
      }
      Some(character)
    }
  }
}

///|
fn Cursor::skip_space(self : Cursor) -> Unit {
  for ;; {
    match self.peek() {
      Some(' ' | '\t' | '\r' | '\n') => ignore(self.advance())
      Some('/') =>
        if self.offset + 1 < self.source.length() &&
          self.source[self.offset + 1] == '/' {
          ignore(self.advance())
          ignore(self.advance())
          for ;; {
            match self.peek() {
              None => break
              Some('\n') => break
              Some(_) => ignore(self.advance())
            }
          }
        } else {
          break
        }
      Some('-') =>
        if self.offset + 1 < self.source.length() &&
          self.source[self.offset + 1] == '-' {
          ignore(self.advance())
          ignore(self.advance())
          for ;; {
            match self.peek() {
              None => break
              Some('\n') => break
              Some(_) => ignore(self.advance())
            }
          }
        } else {
          break
        }
      _ => break
    }
  }
}

///|
fn is_identifier_start(character : Char) -> Bool {
  character == '_' || character.is_ascii_alphabetic()
}

///|
fn is_identifier_continue(character : Char) -> Bool {
  is_identifier_start(character) || character.is_ascii_digit()
}

///|
fn is_symbol_continue(character : Char) -> Bool {
  is_identifier_continue(character) || character == '-'
}

///|
fn Cursor::read_symbol(self : Cursor) -> String raise ParseError {
  self.skip_space()
  let start = self.offset
  guard self.peek() is Some(character) && is_symbol_continue(character) else {
    raise expected(self.offset, self.line, self.column, "symbol")
  }
  ignore(self.advance())
  for ;; {
    match self.peek() {
      Some(character) if is_symbol_continue(character) => ignore(self.advance())
      _ => break
    }
  }
  self.source[start:self.offset].to_owned()
}

///|
fn Cursor::read_until(
  self : Cursor,
  delimiter : Char,
) -> String raise ParseError {
  let start = self.offset
  let mut escaped = false
  for ;; {
    match self.advance() {
      None => raise unexpected_end(self.offset, self.line, self.column)
      Some(character) =>
        if escaped {
          escaped = false
        } else if character == '\\' {
          escaped = true
        } else if character == delimiter {
          return self.source[start:self.offset - 1].to_owned()
        }
    }
  }
}