///|
priv struct SQLTokenV1 {
  kind : String
  value : String
}

///|
priv struct SQLParseWorkV1 {
  config : SQLConfigV1
  options : SQLVerificationOptionsV1
  tokens : Array[SQLTokenV1]
  refs : Array[Json]
  mut pos : Int32
  mut checks : Int32
  mut table : SQLTableV1?
  mut table_alias : String?
  mut resolution : String
  mut style : String
  mut stray_percent : Bool
}

///|
fn sql_charge_v1(work : SQLParseWorkV1) -> Unit raise {
  if work.checks >= work.options.max_checks {
    fail("budget:checks")
  }
  work.checks += 1
}

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

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

///|
fn sql_lex_v1(work : SQLParseWorkV1, source : String) -> Unit raise {
  let chars = source.to_array()
  let mut pos = 0
  let mut depth = 0
  while pos < chars.length() {
    sql_charge_v1(work)
    let c = chars[pos]
    if c == ' ' ||
      c == '\n' ||
      c == '\r' ||
      c == '\t' ||
      c == '\u000c' ||
      c == '\u000b' {
      pos += 1
      continue
    }
    if c == '\u0000' {
      fail("grammar:NUL")
    }
    if c == '-' && pos + 1 < chars.length() && chars[pos + 1] == '-' {
      pos += 2
      while pos < chars.length() && chars[pos] != '\n' {
        sql_charge_v1(work)
        if chars[pos] == '%' {
          work.stray_percent = true
        }
        pos += 1
      }
      continue
    }
    if c == '/' && pos + 1 < chars.length() && chars[pos + 1] == '*' {
      pos += 2
      let mut comments = 1
      while pos < chars.length() && comments > 0 {
        sql_charge_v1(work)
        if chars[pos] == '%' {
          work.stray_percent = true
        }
        if pos + 1 < chars.length() &&
          chars[pos] == '/' &&
          chars[pos + 1] == '*' {
          comments += 1
          pos += 2
        } else if pos + 1 < chars.length() &&
          chars[pos] == '*' &&
          chars[pos + 1] == '/' {
          comments -= 1
          pos += 2
        } else {
          pos += 1
        }
        if comments > work.config.limits.at("max_nesting") {
          fail("budget:comments")
        }
      }
      if comments != 0 {
        fail("grammar:unclosed-comment")
      }
      continue
    }
    let token = if c == '"' || c == '\'' {
      let quote = c
      pos += 1
      let builder = StringBuilder()
      let mut closed = false
      while pos < chars.length() {
        sql_charge_v1(work)
        let next = chars[pos]
        pos += 1
        if next == quote {
          if pos < chars.length() && chars[pos] == quote {
            builder.write_char(quote)
            pos += 1
          } else {
            closed = true
            break
          }
        } else {
          if next == '\u0000' || (quote == '\'' && next == '\\') {
            fail("unsupported:string-escape-or-NUL")
          }
          if next == '%' {
            work.stray_percent = true
          }
          builder.write_char(next)
        }
      }
      if !closed {
        fail("grammar:unclosed-quote")
      }
      {
        kind: if quote == '"' {
          "id"
        } else {
          "str"
        },
        value: builder.to_string(),
      }
    } else if c == '%' {
      pos += 1
      if pos < chars.length() && chars[pos] == 's' {
        pos += 1
        if pos < chars.length() && sql_ascii_tail_v1(chars[pos]) {
          fail("unsupported:bind-format")
        }
        { kind: "bind", value: "", }
      } else if pos < chars.length() && chars[pos] == '(' {
        pos += 1
        let builder = StringBuilder()
        if pos >= chars.length() || !sql_ascii_start_v1(chars[pos]) {
          fail("unsupported:named-bind")
        }
        while pos < chars.length() &&
              (
                sql_ascii_start_v1(chars[pos]) ||
                (chars[pos] >= '0' && chars[pos] <= '9')
              ) {
          sql_charge_v1(work)
          builder.write_char(chars[pos])
          pos += 1
        }
        let name = builder.to_string()
        if name.length() > 128 ||
          pos + 1 >= chars.length() ||
          chars[pos] != ')' ||
          chars[pos + 1] != 's' {
          fail("unsupported:named-bind")
        }
        pos += 2
        if pos < chars.length() && sql_ascii_tail_v1(chars[pos]) {
          fail("unsupported:bind-format")
        }
        { kind: "named", value: name, }
      } else {
        fail("unsupported:percent-format")
      }
    } else if sql_ascii_start_v1(c) {
      let builder = StringBuilder()
      while pos < chars.length() && sql_ascii_tail_v1(chars[pos]) {
        sql_charge_v1(work)
        builder.write_char(chars[pos])
        pos += 1
      }
      { kind: "word", value: builder.to_string().to_lower(), }
    } else if c >= '0' && c <= '9' {
      let builder = StringBuilder()
      while pos < chars.length() && chars[pos] >= '0' && chars[pos] <= '9' {
        sql_charge_v1(work)
        builder.write_char(chars[pos])
        pos += 1
      }
      { kind: "num", value: builder.to_string(), }
    } else {
      pos += 1
      let mut value = c.to_string()
      if (c == '<' || c == '>') && pos < chars.length() && chars[pos] == '=' {
        value += "="
        pos += 1
      }
      if !["=", "<", ">", "<=", ">=", ".", ",", "(", ")", "*", "-", ";"].contains(
          value,
        ) {
        fail("unsupported:token")
      }
      if value == "(" {
        depth += 1
        if depth > work.config.limits.at("max_nesting") {
          fail("budget:nesting")
        }
      }
      if value == ")" {
        depth -= 1
        if depth < 0 {
          fail("grammar:parenthesis")
        }
      }
      { kind: "symbol", value, }
    }
    if work.tokens.length() >= work.options.max_tokens ||
      work.tokens.length() >= work.config.limits.at("max_tokens") {
      fail("budget:tokens")
    }
    work.tokens.push(token)
  }
  if depth != 0 {
    fail("grammar:parenthesis")
  }
  work.tokens.push({ kind: "eof", value: "", })
}

///|
fn SQLParseWorkV1::peek(self : SQLParseWorkV1) -> SQLTokenV1 {
  self.tokens[self.pos]
}

///|
fn SQLParseWorkV1::take(self : SQLParseWorkV1) -> SQLTokenV1 raise {
  sql_charge_v1(self)
  let token = self.peek()
  if token.kind == "eof" {
    fail("grammar:unexpected-end")
  }
  self.pos += 1
  token
}

///|
fn SQLParseWorkV1::accept(self : SQLParseWorkV1, word : String) -> Bool raise {
  let token = self.peek()
  if (token.kind == "word" || token.kind == "symbol") && token.value == word {
    self.pos += 1
    sql_charge_v1(self)
    true
  } else {
    false
  }
}

///|
fn SQLParseWorkV1::expect(self : SQLParseWorkV1, word : String) -> Unit raise {
  if !self.accept(word) {
    fail("grammar:expected-" + word)
  }
}

///|
fn sql_reserved_v1(word : String) -> Bool {
  [
    "select", "from", "where", "and", "or", "null", "true", "false", "insert", "into",
    "update", "delete", "returning", "values", "set", "as", "order", "by", "asc",
    "desc", "collate", "join", "left", "right", "inner", "outer", "cross", "limit",
    "offset", "group", "having", "union", "with", "for", "on", "conflict", "default",
    "not", "is", "in", "between", "current_user", "session_user", "current_role",
    "user", "current_catalog", "current_schema", "current_date", "current_time",
    "current_timestamp", "localtime", "localtimestamp", "case", "when", "then", "else",
    "end", "exists", "any", "some", "all", "like", "ilike", "similar",
  ].contains(word)
}

///|
fn SQLParseWorkV1::identifier(self : SQLParseWorkV1) -> String raise {
  let token = self.take()
  if (token.kind != "id" && token.kind != "word") ||
    (token.kind == "word" && sql_reserved_v1(token.value)) {
    fail("grammar:identifier")
  }
  sql_identifier_v1(Json(token.value))
}