///|
/// A lexical error, always carrying the position where it was detected.
pub(all) suberror LexError {
  UnexpectedChar(Position, Char)
  UnterminatedString(Position)
  UnterminatedRawString(Position)
  InvalidEscape(Position, Char)
  InvalidUnicodeEscape(Position)
  InvalidNumber(Position, String)
} derive(@debug.Debug)

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

///|
fn describe(position : Position) -> String {
  "line " +
  position.line.to_string() +
  ", column " +
  position.column.to_string()
}

///|
/// A human readable explanation of the error.
pub fn LexError::message(self : LexError) -> String {
  match self {
    UnexpectedChar(position, c) =>
      "unexpected character '" + c.to_string() + "' at " + describe(position)
    UnterminatedString(position) =>
      "unterminated string at " + describe(position)
    UnterminatedRawString(position) =>
      "unterminated raw string at " + describe(position)
    InvalidEscape(position, c) =>
      "invalid escape '\\" + c.to_string() + "' at " + describe(position)
    InvalidUnicodeEscape(position) =>
      "invalid unicode escape at " + describe(position)
    InvalidNumber(position, text) =>
      "invalid number '" + text + "' at " + describe(position)
  }
}

///|
fn is_digit(c : Char) -> Bool {
  c >= '0' && c <= '9'
}

///|
fn hex_value(c : Char) -> Int32? {
  if c >= '0' && c <= '9' {
    Some(c.to_int() - '0'.to_int())
  } else if c >= 'a' && c <= 'f' {
    Some(c.to_int() - 'a'.to_int() + 10)
  } else if c >= 'A' && c <= 'F' {
    Some(c.to_int() - 'A'.to_int() + 10)
  } else {
    None
  }
}

///|
priv struct Scanner {
  source : Array[Char]
  mut offset : Int32
  mut line : Int32
  mut column : Int32
}

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

///|
fn Scanner::position(self : Scanner) -> Position {
  { offset: self.offset, line: self.line, column: self.column, }
}

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

///|
fn Scanner::peek_at(self : Scanner, distance : Int32) -> Char? {
  self.source.get(self.offset + distance)
}

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

///|
fn Scanner::skip_trivia(self : Scanner) -> Unit {
  while true {
    match self.peek() {
      Some(c) =>
        if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
          let _ = self.advance()
        } else if c == '#' {
          while true {
            match self.peek() {
              Some(ch) =>
                if ch == '\n' {
                  break
                } else {
                  let _ = self.advance()
                }
              None => break
            }
          }
        } else {
          break
        }
      None => break
    }
  }
}

///|
fn Scanner::next_token(self : Scanner) -> Token raise LexError {
  self.skip_trivia()
  let position = self.position()
  match self.peek() {
    None => Token::new(Eof, "", position)
    Some(c) =>
      if is_ident_start(c) {
        self.scan_identifier(position)
      } else if is_digit(c) {
        self.scan_number(position)
      } else if c == '"' {
        self.scan_string(position)
      } else if c == '`' {
        self.scan_raw_string(position)
      } else {
        self.scan_symbol(c, position)
      }
  }
}

///|
fn Scanner::scan_identifier(self : Scanner, position : Position) -> Token {
  let buffer = StringBuilder()
  while true {
    match self.peek() {
      Some(c) =>
        if is_ident_continue(c) {
          buffer.write_char(c)
          let _ = self.advance()
        } else {
          break
        }
      None => break
    }
  }
  let text = buffer.to_string()
  let kind = match keyword_kind(text) {
    Some(kind) => kind
    None => Ident
  }
  Token::new(kind, text, position)
}

///|
fn Scanner::scan_number(
  self : Scanner,
  position : Position,
) -> Token raise LexError {
  let buffer = StringBuilder()
  match self.peek() {
    Some('0') => {
      buffer.write_char('0')
      let _ = self.advance()
    }
    Some(c) =>
      if c >= '1' && c <= '9' {
        while true {
          match self.peek() {
            Some(d) =>
              if is_digit(d) {
                buffer.write_char(d)
                let _ = self.advance()
              } else {
                break
              }
            None => break
          }
        }
      } else {
        raise InvalidNumber(position, buffer.to_string())
      }
    None => raise InvalidNumber(position, buffer.to_string())
  }
  if self.peek() == Some('.') {
    match self.peek_at(1) {
      Some(d) =>
        if is_digit(d) {
          buffer.write_char('.')
          let _ = self.advance()
          while true {
            match self.peek() {
              Some(ch) =>
                if is_digit(ch) {
                  buffer.write_char(ch)
                  let _ = self.advance()
                } else {
                  break
                }
              None => break
            }
          }
        } else {
          raise InvalidNumber(position, buffer.to_string() + ".")
        }
      None => raise InvalidNumber(position, buffer.to_string() + ".")
    }
  }
  if self.peek() == Some('e') || self.peek() == Some('E') {
    let marker = match self.peek() {
      Some(ch) => ch
      None => 'e'
    }
    buffer.write_char(marker)
    let _ = self.advance()
    if self.peek() == Some('+') || self.peek() == Some('-') {
      let sign = match self.peek() {
        Some(ch) => ch
        None => '+'
      }
      buffer.write_char(sign)
      let _ = self.advance()
    }
    let mut digits = 0
    while true {
      match self.peek() {
        Some(ch) =>
          if is_digit(ch) {
            buffer.write_char(ch)
            let _ = self.advance()
            digits = digits + 1
          } else {
            break
          }
        None => break
      }
    }
    if digits == 0 {
      raise InvalidNumber(position, buffer.to_string())
    }
  }
  match self.peek() {
    Some(c) =>
      if is_ident_continue(c) {
        raise InvalidNumber(position, buffer.to_string())
      }
    None => ()
  }
  Token::new(NumberLit, buffer.to_string(), position)
}

///|
fn Scanner::scan_string(
  self : Scanner,
  position : Position,
) -> Token raise LexError {
  let _ = self.advance()
  let buffer = StringBuilder()
  while true {
    match self.peek() {
      None => raise UnterminatedString(position)
      Some('"') => {
        let _ = self.advance()
        break
      }
      Some('\\') => self.scan_escape(buffer)
      Some('\n') => raise UnterminatedString(position)
      Some(c) => {
        buffer.write_char(c)
        let _ = self.advance()
      }
    }
  }
  Token::new(StringLit, buffer.to_string(), position)
}

///|
fn Scanner::scan_escape(
  self : Scanner,
  buffer : StringBuilder,
) -> Unit raise LexError {
  let position = self.position()
  let _ = self.advance()
  match self.peek() {
    None => raise UnterminatedString(position)
    Some(c) => {
      let _ = self.advance()
      match c {
        '"' => buffer.write_char('"')
        '\\' => buffer.write_char('\\')
        '/' => buffer.write_char('/')
        'b' => buffer.write_char((8).to_char().unwrap())
        'f' => buffer.write_char((12).to_char().unwrap())
        'n' => buffer.write_char('\n')
        'r' => buffer.write_char('\r')
        't' => buffer.write_char('\t')
        'u' => self.scan_unicode_escape(buffer, position)
        _ => raise InvalidEscape(position, c)
      }
    }
  }
}

///|
fn Scanner::read_hex4(
  self : Scanner,
  position : Position,
) -> Int32 raise LexError {
  let mut value = 0
  for _i = 0; _i < 4; _i = _i + 1 {
    match self.peek() {
      Some(c) =>
        match hex_value(c) {
          Some(digit) => {
            value = value * 16 + digit
            let _ = self.advance()
          }
          None => raise InvalidUnicodeEscape(position)
        }
      None => raise InvalidUnicodeEscape(position)
    }
  }
  value
}

///|
fn Scanner::scan_unicode_escape(
  self : Scanner,
  buffer : StringBuilder,
  position : Position,
) -> Unit raise LexError {
  let high = self.read_hex4(position)
  if high >= 0xD800 && high <= 0xDBFF {
    if self.peek() == Some('\\') && self.peek_at(1) == Some('u') {
      let _ = self.advance()
      let _ = self.advance()
      let low = self.read_hex4(position)
      if low >= 0xDC00 && low <= 0xDFFF {
        let code = 0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00)
        buffer.write_char(code.to_char().unwrap())
        return
      }
    }
    raise InvalidUnicodeEscape(position)
  }
  if high >= 0xDC00 && high <= 0xDFFF {
    raise InvalidUnicodeEscape(position)
  }
  buffer.write_char(high.to_char().unwrap())
}

///|
fn Scanner::scan_raw_string(
  self : Scanner,
  position : Position,
) -> Token raise LexError {
  let _ = self.advance()
  let buffer = StringBuilder()
  while true {
    match self.peek() {
      None => raise UnterminatedRawString(position)
      Some('`') => {
        let _ = self.advance()
        break
      }
      Some(c) => {
        buffer.write_char(c)
        let _ = self.advance()
      }
    }
  }
  Token::new(RawStringLit, buffer.to_string(), position)
}

///|
fn Scanner::scan_symbol(
  self : Scanner,
  c : Char,
  position : Position,
) -> Token raise LexError {
  let _ = self.advance()
  match c {
    '{' => Token::new(LBrace, "{", position)
    '}' => Token::new(RBrace, "}", position)
    '[' => Token::new(LBracket, "[", position)
    ']' => Token::new(RBracket, "]", position)
    '(' => Token::new(LParen, "(", position)
    ')' => Token::new(RParen, ")", position)
    ',' => Token::new(Comma, ",", position)
    ';' => Token::new(Semicolon, ";", position)
    '.' => Token::new(Dot, ".", position)
    '|' => Token::new(Pipe, "|", position)
    '&' => Token::new(Ampersand, "&", position)
    '+' => Token::new(Plus, "+", position)
    '-' => Token::new(Minus, "-", position)
    '*' => Token::new(Star, "*", position)
    '/' => Token::new(Slash, "/", position)
    '%' => Token::new(Percent, "%", position)
    ':' =>
      if self.peek() == Some('=') {
        let _ = self.advance()
        Token::new(Assign, ":=", position)
      } else {
        Token::new(Colon, ":", position)
      }
    '=' =>
      if self.peek() == Some('=') {
        let _ = self.advance()
        Token::new(EqualEqual, "==", position)
      } else {
        Token::new(Equal, "=", position)
      }
    '!' =>
      if self.peek() == Some('=') {
        let _ = self.advance()
        Token::new(NotEqual, "!=", position)
      } else {
        raise UnexpectedChar(position, '!')
      }
    '<' =>
      if self.peek() == Some('=') {
        let _ = self.advance()
        Token::new(LessEqual, "<=", position)
      } else {
        Token::new(Less, "<", position)
      }
    '>' =>
      if self.peek() == Some('=') {
        let _ = self.advance()
        Token::new(GreaterEqual, ">=", position)
      } else {
        Token::new(Greater, ">", position)
      }
    _ => raise UnexpectedChar(position, c)
  }
}

///|
/// Splits a Rego source text into tokens, ending with one `Eof` token.
///
/// Comments and whitespace are skipped. String tokens carry their decoded
/// value; raw string tokens carry their contents verbatim.
pub fn tokenize(source : String) -> Array[Token] raise LexError {
  let scanner = Scanner::new(source)
  let tokens : Array[Token] = []
  while true {
    let token = scanner.next_token()
    let done = token.kind == Eof
    tokens.push(token)
    if done {
      break
    }
  }
  tokens
}