///|
enum TokenKind {
  Identifier(String)
  QuotedIdentifier(String)
  RawString(String)
  LiteralToken(Json)
  NumberToken(Int)
  Dot
  Star
  CurrentToken
  LParen
  RParen
  LBracket
  RBracket
  LBrace
  RBrace
  Comma
  Colon
  PipeToken
  OrToken
  AndToken
  NotToken
  EqToken
  NeToken
  LtToken
  LeToken
  GtToken
  GeToken
  Question
  Ampersand
  Eof
} derive(Eq, Debug)

///|
struct Token {
  kind : TokenKind
  offset : Int
} derive(Eq, Debug)

///|
fn is_space(c : Char) -> Bool {
  c == ' ' || c == '\t' || c == '\r' || c == '\n'
}

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

///|
fn is_identifier_start(c : Char) -> Bool {
  (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
}

///|
fn is_identifier_continue(c : Char) -> Bool {
  is_identifier_start(c) || is_digit(c)
}

///|
fn parse_int_text(text : String, offset : Int) -> Int raise JmesPathError {
  let chars = text.to_array()
  let mut value = 0
  let mut i = 0
  let mut negative = false
  if chars.length() > 0 && chars[0] == '-' {
    negative = true
    i = 1
  }
  while i < chars.length() {
    let digit = chars[i].to_int() - '0'.to_int()
    if value > 214748364 {
      fail("syntax", offset, "integer index is out of range")
    }
    value = value * 10 + digit
    i += 1
  }
  if negative {
    -value
  } else {
    value
  }
}

///|
fn decode_json_string(raw : String, offset : Int) -> String raise JmesPathError {
  let wrapped = "\"" + raw + "\""
  let parsed = @json.parse(wrapped) catch {
    _ => fail("syntax", offset, "invalid quoted identifier escape")
  }
  guard parsed is String(value) else {
    fail("syntax", offset, "invalid quoted identifier")
  }
  value
}

///|
fn lex(source : String, limits : Limits) -> Array[Token] raise JmesPathError {
  let chars = source.to_array()
  if chars.length() > limits.max_expression_chars {
    fail("limit_exceeded", 0, "expression exceeds max_expression_chars")
  }
  let tokens : Array[Token] = []
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if is_space(c) {
      i += 1
      continue
    }
    let start = i
    if is_identifier_start(c) {
      i += 1
      while i < chars.length() && is_identifier_continue(chars[i]) {
        i += 1
      }
      tokens.push({
        kind: Identifier(String::from_array(chars[start:i])),
        offset: start,
      })
      continue
    }
    if is_digit(c) ||
      (c == '-' && i + 1 < chars.length() && is_digit(chars[i + 1])) {
      i += 1
      while i < chars.length() && is_digit(chars[i]) {
        i += 1
      }
      let text = String::from_array(chars[start:i])
      tokens.push({
        kind: NumberToken(parse_int_text(text, start)),
        offset: start,
      })
      continue
    }
    match c {
      '"' => {
        i += 1
        let content_start = i
        let mut escaped = false
        while i < chars.length() {
          if !escaped && chars[i] == '"' {
            break
          }
          if !escaped && chars[i] == '\\' {
            escaped = true
          } else {
            escaped = false
          }
          i += 1
        }
        if i >= chars.length() {
          fail("syntax", start, "unterminated quoted identifier")
        }
        let raw = String::from_array(chars[content_start:i])
        tokens.push({
          kind: QuotedIdentifier(decode_json_string(raw, start)),
          offset: start,
        })
        i += 1
      }
      '\'' => {
        i += 1
        let b = StringBuilder()
        let mut closed = false
        while i < chars.length() {
          if chars[i] == '\'' {
            closed = true
            i += 1
            break
          }
          if chars[i] == '\\' {
            let mut end = i
            while end < chars.length() && chars[end] == '\\' {
              end += 1
            }
            let count = end - i
            if end < chars.length() && chars[end] == '\'' && count % 2 == 1 {
              for j = 0; j < count - 1; j = j + 1 {
                b.write_char('\\')
              }
              b.write_char('\'')
              i = end + 1
            } else {
              for j = 0; j < count; j = j + 1 {
                b.write_char('\\')
              }
              i = end
            }
          } else {
            b.write_char(chars[i])
            i += 1
          }
        }
        if !closed {
          fail("syntax", start, "unterminated raw string")
        }
        tokens.push({ kind: RawString(b.to_string()), offset: start, })
      }
      '`' => {
        i += 1
        let b = StringBuilder()
        let mut closed = false
        while i < chars.length() {
          if chars[i] == '`' {
            closed = true
            i += 1
            break
          }
          if chars[i] == '\\' && i + 1 < chars.length() && chars[i + 1] == '`' {
            b.write_char('`')
            i += 2
          } else {
            b.write_char(chars[i])
            i += 1
          }
        }
        if !closed {
          fail("syntax", start, "unterminated JSON literal")
        }
        let value = @json.parse(b.to_string()) catch {
          _ => fail("syntax", start, "invalid JSON literal")
        }
        tokens.push({ kind: LiteralToken(value), offset: start, })
      }
      '.' => {
        tokens.push({ kind: Dot, offset: start, })
        i += 1
      }
      '*' => {
        tokens.push({ kind: Star, offset: start, })
        i += 1
      }
      '@' => {
        tokens.push({ kind: CurrentToken, offset: start, })
        i += 1
      }
      '(' => {
        tokens.push({ kind: LParen, offset: start, })
        i += 1
      }
      ')' => {
        tokens.push({ kind: RParen, offset: start, })
        i += 1
      }
      '[' => {
        tokens.push({ kind: LBracket, offset: start, })
        i += 1
      }
      ']' => {
        tokens.push({ kind: RBracket, offset: start, })
        i += 1
      }
      '{' => {
        tokens.push({ kind: LBrace, offset: start, })
        i += 1
      }
      '}' => {
        tokens.push({ kind: RBrace, offset: start, })
        i += 1
      }
      ',' => {
        tokens.push({ kind: Comma, offset: start, })
        i += 1
      }
      ':' => {
        tokens.push({ kind: Colon, offset: start, })
        i += 1
      }
      '?' => {
        tokens.push({ kind: Question, offset: start, })
        i += 1
      }
      '&' =>
        if i + 1 < chars.length() && chars[i + 1] == '&' {
          tokens.push({ kind: AndToken, offset: start, })
          i += 2
        } else {
          tokens.push({ kind: Ampersand, offset: start, })
          i += 1
        }
      '|' =>
        if i + 1 < chars.length() && chars[i + 1] == '|' {
          tokens.push({ kind: OrToken, offset: start, })
          i += 2
        } else {
          tokens.push({ kind: PipeToken, offset: start, })
          i += 1
        }
      '!' =>
        if i + 1 < chars.length() && chars[i + 1] == '=' {
          tokens.push({ kind: NeToken, offset: start, })
          i += 2
        } else {
          tokens.push({ kind: NotToken, offset: start, })
          i += 1
        }
      '=' =>
        if i + 1 < chars.length() && chars[i + 1] == '=' {
          tokens.push({ kind: EqToken, offset: start, })
          i += 2
        } else {
          fail("syntax", start, "expected ==")
        }
      '<' =>
        if i + 1 < chars.length() && chars[i + 1] == '=' {
          tokens.push({ kind: LeToken, offset: start, })
          i += 2
        } else {
          tokens.push({ kind: LtToken, offset: start, })
          i += 1
        }
      '>' =>
        if i + 1 < chars.length() && chars[i + 1] == '=' {
          tokens.push({ kind: GeToken, offset: start, })
          i += 2
        } else {
          tokens.push({ kind: GtToken, offset: start, })
          i += 1
        }
      _ => fail("syntax", start, "unexpected character")
    }
  }
  tokens.push({ kind: Eof, offset: chars.length(), })
  tokens
}