///|
/// A position in the source text. `offset` counts characters from the start of
/// the file, while `line` and `column` are one-based and meant for humans.
pub(all) struct Position {
  offset : Int32
  line : Int32
  column : Int32
} derive(Eq, @debug.Debug)

///|
pub extend Position with Eq::{equal, not_equal}

///|
pub extend Position with @debug.Debug::{to_repr}

///|
pub fn Position::new(offset : Int32, line : Int32, column : Int32) -> Position {
  { offset, line, column, }
}

///|
/// The lexical categories of Rego.
///
/// The variants that share a name with a built-in constructor or a core type
/// carry a suffix, so `SomeKeyword` and `StringLit` cannot be confused with
/// `Some` or `String`.
pub(all) enum TokenKind {
  Eof
  Ident
  NumberLit
  StringLit
  RawStringLit
  Package
  Import
  As
  Default
  Else
  Not
  SomeKeyword
  Every
  In
  If
  Contains
  With
  TrueKeyword
  FalseKeyword
  NullKeyword
  LBrace
  RBrace
  LBracket
  RBracket
  LParen
  RParen
  Comma
  Colon
  Semicolon
  Dot
  Pipe
  Ampersand
  Assign
  Equal
  EqualEqual
  NotEqual
  Less
  LessEqual
  Greater
  GreaterEqual
  Plus
  Minus
  Star
  Slash
  Percent
} derive(Eq, @debug.Debug)

///|
pub extend TokenKind with Eq::{equal, not_equal}

///|
pub extend TokenKind with @debug.Debug::{to_repr}

///|
/// A short name used in error messages and tests.
pub fn TokenKind::name(self : TokenKind) -> String {
  match self {
    Eof => "end of file"
    Ident => "identifier"
    NumberLit => "number"
    StringLit => "string"
    RawStringLit => "raw string"
    Package => "package"
    Import => "import"
    As => "as"
    Default => "default"
    Else => "else"
    Not => "not"
    SomeKeyword => "some"
    Every => "every"
    In => "in"
    If => "if"
    Contains => "contains"
    With => "with"
    TrueKeyword => "true"
    FalseKeyword => "false"
    NullKeyword => "null"
    LBrace => "{"
    RBrace => "}"
    LBracket => "["
    RBracket => "]"
    LParen => "("
    RParen => ")"
    Comma => ","
    Colon => ":"
    Semicolon => ";"
    Dot => "."
    Pipe => "|"
    Ampersand => "&"
    Assign => ":="
    Equal => "="
    EqualEqual => "=="
    NotEqual => "!="
    Less => "<"
    LessEqual => "<="
    Greater => ">"
    GreaterEqual => ">="
    Plus => "+"
    Minus => "-"
    Star => "*"
    Slash => "/"
    Percent => "%"
  }
}

///|
/// A token and where it came from.
///
/// `text` is the meaningful text: the identifier or keyword as written, the
/// numeric literal, the decoded value of a string, the contents of a raw
/// string, or the operator itself.
pub(all) struct Token {
  kind : TokenKind
  text : String
  position : Position
} derive(Eq, @debug.Debug)

///|
pub extend Token with Eq::{equal, not_equal}

///|
pub extend Token with @debug.Debug::{to_repr}

///|
pub fn Token::new(
  kind : TokenKind,
  text : String,
  position : Position,
) -> Token {
  { kind, text, position, }
}

///|
/// Whether a character may start an identifier.
pub fn is_ident_start(c : Char) -> Bool {
  (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
}

///|
/// Whether a character may continue an identifier.
pub fn is_ident_continue(c : Char) -> Bool {
  is_ident_start(c) || (c >= '0' && c <= '9')
}

///|
/// The keyword kind for one identifier, or `None` for a plain identifier.
pub fn keyword_kind(text : String) -> TokenKind? {
  if text == "package" {
    Some(Package)
  } else if text == "import" {
    Some(Import)
  } else if text == "as" {
    Some(As)
  } else if text == "default" {
    Some(Default)
  } else if text == "else" {
    Some(Else)
  } else if text == "not" {
    Some(Not)
  } else if text == "some" {
    Some(SomeKeyword)
  } else if text == "every" {
    Some(Every)
  } else if text == "in" {
    Some(In)
  } else if text == "if" {
    Some(If)
  } else if text == "contains" {
    Some(Contains)
  } else if text == "with" {
    Some(With)
  } else if text == "true" {
    Some(TrueKeyword)
  } else if text == "false" {
    Some(FalseKeyword)
  } else if text == "null" {
    Some(NullKeyword)
  } else {
    None
  }
}