// A small BQL tokenizer.  It is intentionally independent from the ledger
// lexer: BQL has operators, quoted strings and expressions on one line.

///|
priv enum BTok {
  BWord(String)
  BStr(String)
  BInt(Int32)
  BNum(@decimal.Decimal)
  BDate(@ast.Date)
  BSym(String)
  BEof
}

///|
fn bql_char(text : String, index : Int32) -> Char? {
  text.get_char(index)
}

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

///|
fn bql_is_word_char(c : Char) -> Bool {
  c.is_ascii_alphabetic() ||
  c.is_ascii_digit() ||
  c == '_' ||
  c == ':' ||
  c == '.' ||
  c == '-'
}

///|
fn bql_read_string(text : String, start : Int32) -> (String, Int32) {
  let quote = match bql_char(text, start) {
    Some(c) => c
    None => '"'
  }
  let sb = StringBuilder()
  let mut i = start + 1
  let len = text.length()
  while i < len {
    let c = match bql_char(text, i) {
      Some(c) => c
      None => break
    }
    if c == quote {
      if i + 1 < len && bql_char(text, i + 1) == Some(quote) {
        sb.write_char(quote)
        i = i + 2
        continue
      }
      return (sb.to_string(), i + 1)
    }
    if c == '\\' && i + 1 < len {
      let escaped = match bql_char(text, i + 1) {
        Some(c) => c
        None => '\\'
      }
      let decoded = match escaped {
        'n' => '\n'
        't' => '\t'
        'r' => '\r'
        other => other
      }
      sb.write_char(decoded)
      i = i + 1 + (if escaped.to_int() > 0xFFFF { 2 } else { 1 })
      continue
    }
    sb.write_char(c)
    i = i + (if c.to_int() > 0xFFFF { 2 } else { 1 })
  }
  (sb.to_string(), i)
}

///|
fn bql_read_number(text : String, start : Int32) -> (String, Int32) {
  let sb = StringBuilder()
  let mut i = start
  let len = text.length()
  while i < len {
    let c = match bql_char(text, i) {
      Some(c) => c
      None => break
    }
    if c.is_ascii_digit() ||
      c == '.' ||
      (
        c == '-' &&
        i - start < 10 &&
        text.length() >= start + 10 &&
        bql_char(text, start + 4) == Some('-') &&
        bql_char(text, start + 7) == Some('-')
      ) {
      sb.write_char(c)
      i = i + 1
    } else {
      break
    }
  }
  (sb.to_string(), i)
}

///|
fn bql_parse_int(raw : String) -> Int32 {
  let mut value = 0
  let mut i = 0
  let mut negative = false
  if raw.length() > 0 && bql_char(raw, 0) == Some('-') {
    negative = true
    i = 1
  }
  while i < raw.length() {
    let c = match bql_char(raw, i) {
      Some(c) => c
      None => break
    }
    let digit = c.to_int() - 48
    value = value * 10 + digit
    i = i + 1
  }
  if negative {
    -value
  } else {
    value
  }
}

///|
fn bql_has_dot(raw : String) -> Bool {
  for i in 0.. BTok {
  if raw.length() == 10 &&
    bql_char(raw, 4) == Some('-') &&
    bql_char(raw, 7) == Some('-') {
    match @ast.Date::parse(raw) {
      Some(date) => return BDate(date)
      None => ()
    }
  }
  match @decimal.Decimal::parse(raw) {
    Some(number) =>
      if bql_has_dot(raw) {
        BNum(number)
      } else {
        BInt(bql_parse_int(raw))
      }
    None => BWord(raw)
  }
}

///|
fn lex_bql(text : String, unsupported~ : Array[String] = []) -> Array[BTok] {
  let mut selects = 0
  let tokens : Array[BTok] = []
  let mut i = 0
  let len = text.length()
  while i < len {
    let c = match bql_char(text, i) {
      Some(c) => c
      None => {
        unsupported.push(bql_lex_error(text, i))
        break
      }
    }
    if c.to_int() > 0xFFFF {
      unsupported.push(bql_lex_error(text, i))
      i = i + 2
      continue
    }
    if c.is_ascii_whitespace() {
      i = i + 1
      continue
    }
    if c == '"' || c == '\'' {
      let (value, next) = bql_read_string(text, i)
      tokens.push(BStr(value))
      i = next
      continue
    }
    if c.is_ascii_digit() {
      let (raw, next) = bql_read_number(text, i)
      tokens.push(bql_number_token(raw))
      i = next
      continue
    }
    if bql_is_word_start(c) {
      let start = i
      let sb = StringBuilder()
      while i < len {
        let ch = match bql_char(text, i) {
          Some(ch) => ch
          None => break
        }
        if bql_is_word_char(ch) {
          sb.write_char(ch)
          i = i + 1
        } else {
          break
        }
      }
      let word = sb.to_string()
      let kind = match word.to_lower() {
        "select" => {
          selects = selects + 1
          if selects > 1 {
            "subquery"
          } else {
            ""
          }
        }
        "join" => "join"
        "pivot" => "pivot"
        _ => ""
      }
      if kind != "" {
        let mut line = 1
        let mut column = 1
        for offset in 0.. {
        let pair = StringBuilder()
        pair.write_char(c)
        pair.write_char(n)
        pair.to_string()
      }
      None => ""
    }
    if two == "<=" || two == ">=" || two == "!=" || two == "<>" || two == "==" {
      tokens.push(BSym(two))
      i = i + 2
    } else {
      let sym = StringBuilder()
      sym.write_char(c)
      tokens.push(BSym(sym.to_string()))
      i = i + 1
    }
  }
  tokens.push(BEof)
  tokens
}

///|
fn bql_lex_error(text : String, index : Int32) -> String {
  let mut line = 1
  let mut column = 1
  for offset in 0..