// BQL subset parser.  Supported grammar:
//
//   SELECT  [, ]* [FROM postings|transactions|balances]
//     [WHERE ] [GROUP BY  [, *]]
//     [ORDER BY  [ASC|DESC] [, ...]] [LIMIT ]
//   BALANCES
//   JOURNAL  [LIMIT ]
//
// Expressions are columns, literals, and the functions year(), month(),
// root(), parent(), leaf() and the aggregate sum().

///|
priv enum Expr {
  ECol(String)
  EStr(String)
  ENum(@decimal.Decimal)
  EDate(@ast.Date)
  ENull
  EStar
  EBool(Bool)
  ECall(String, Array[Expr])
  EBinary(Expr, String, Expr)
}

///|
priv enum Cond {
  CTrue
  CNot(Cond)
  CAnd(Cond, Cond)
  COr(Cond, Cond)
  CCmp(Expr, String, Expr)
  CIsNull(Expr, Bool)
  CTruthy(Expr)
  CRegex(Expr, @string.Regex)
}

///|
priv struct OrderSpec {
  expr : Expr
  descending : Bool
}

///|
priv enum QuerySource {
  SrcPostings
  SrcBalances
  SrcClose(@ast.Date)
  SrcOpen(@ast.Date, @ast.Date?)
}

///|
priv struct SelectQuery {
  projections : Array[Expr]
  source : QuerySource
  filter : Cond
  having : Cond
  groups : Array[Expr]
  orders : Array[OrderSpec]
  limit : Int32?
}

///|
priv enum ParsedQuery {
  PqSelect(SelectQuery)
  PqBalances
  PqJournal(String, Int32?)
  PqError(String)
}

///|
priv struct QueryParser {
  tokens : Array[BTok]
  mut index : Int32
  mut error : String?
}

///|
fn QueryParser::new(tokens : Array[BTok]) -> QueryParser {
  { tokens, index: 0, error: None, }
}

///|
fn QueryParser::peek(self : QueryParser) -> BTok {
  if self.index < self.tokens.length() {
    self.tokens[self.index]
  } else {
    BEof
  }
}

///|
fn QueryParser::advance(self : QueryParser) -> BTok {
  let token = self.peek()
  if self.index < self.tokens.length() {
    self.index = self.index + 1
  }
  token
}

///|
fn QueryParser::at_end(self : QueryParser) -> Bool {
  self.peek() is BEof
}

///|
fn QueryParser::fail(self : QueryParser, message : String) -> Unit {
  match self.error {
    Some(_) => ()
    None => self.error = Some(message)
  }
}

///|
fn QueryParser::peek_word(self : QueryParser) -> String? {
  match self.peek() {
    BWord(word) => Some(word)
    _ => None
  }
}

///|
fn QueryParser::peek_is_keyword(self : QueryParser, keyword : String) -> Bool {
  match self.peek_word() {
    Some(word) => word.to_lower() == keyword
    None => false
  }
}

///|
fn QueryParser::match_keyword(self : QueryParser, keyword : String) -> Bool {
  if self.peek_is_keyword(keyword) {
    self.advance() |> ignore
    true
  } else {
    false
  }
}

///|
fn QueryParser::expect_keyword(self : QueryParser, keyword : String) -> Bool {
  if self.match_keyword(keyword) {
    true
  } else {
    self.fail("expected `" + keyword + "`")
    false
  }
}

///|
fn QueryParser::take_word(self : QueryParser) -> String? {
  match self.peek() {
    BWord(word) => {
      self.advance() |> ignore
      Some(word)
    }
    _ => None
  }
}

///|
fn QueryParser::peek_sym(self : QueryParser) -> String? {
  match self.peek() {
    BSym(symbol) => Some(symbol)
    _ => None
  }
}

///|
fn QueryParser::match_sym(self : QueryParser, symbol : String) -> Bool {
  if self.peek_sym() == Some(symbol) {
    self.advance() |> ignore
    true
  } else {
    false
  }
}

///|
fn QueryParser::expect_sym(self : QueryParser, symbol : String) -> Bool {
  if self.match_sym(symbol) {
    true
  } else {
    self.fail("expected `" + symbol + "`")
    false
  }
}

///|
fn QueryParser::take_int(self : QueryParser) -> Int32? {
  match self.peek() {
    BInt(value) => {
      self.advance() |> ignore
      Some(value)
    }
    BNum(_) => {
      self.fail("expected an integer")
      None
    }
    _ => {
      self.fail("expected an integer")
      None
    }
  }
}

///|
fn parse_primary(parser : QueryParser) -> Expr? {
  match parser.peek() {
    BWord(word) => {
      parser.advance() |> ignore
      let lower = word.to_lower()
      if parser.match_sym("(") {
        let args : Array[Expr] = []
        if !parser.match_sym(")") {
          let mut done = false
          while !done {
            if parser.match_sym("*") {
              args.push(EStar)
            } else {
              match parse_expr(parser) {
                Some(expr) => args.push(expr)
                None => return None
              }
            }
            if parser.match_sym(",") {
              continue
            }
            done = true
          }
          if !parser.expect_sym(")") {
            return None
          }
        }
        match lower {
          "year"
          | "month"
          | "root"
          | "parent"
          | "leaf"
          | "sum"
          | "count"
          | "meta" => Some(ECall(lower, args))
          _ => {
            parser.fail("unknown function `" + word + "`")
            None
          }
        }
      } else if lower == "null" {
        Some(ENull)
      } else if lower == "true" {
        Some(EBool(true))
      } else if lower == "false" {
        Some(EBool(false))
      } else {
        match lower {
          "date"
          | "flag"
          | "payee"
          | "narration"
          | "account"
          | "root"
          | "parent"
          | "leaf"
          | "units"
          | "currency"
          | "cost"
          | "value"
          | "position"
          | "tags"
          | "links"
          | "weight" => Some(ECol(lower))
          _ => {
            parser.fail("unsupported column `" + word + "`")
            None
          }
        }
      }
    }
    BStr(value) => {
      parser.advance() |> ignore
      Some(EStr(value))
    }
    BInt(value) => {
      parser.advance() |> ignore
      Some(ENum(@decimal.Decimal::from_int(value)))
    }
    BNum(number) => {
      parser.advance() |> ignore
      Some(ENum(number))
    }
    BDate(date) => {
      parser.advance() |> ignore
      Some(EDate(date))
    }
    BSym(symbol) =>
      if symbol == "(" {
        parser.advance() |> ignore
        let expr = parse_expr(parser)
        if !parser.expect_sym(")") {
          return None
        }
        expr
      } else if symbol == "-" || symbol == "+" {
        parser.advance() |> ignore
        match parse_primary(parser) {
          Some(expr) =>
            Some(EBinary(ENum(@decimal.Decimal::zero()), symbol, expr))
          None => None
        }
      } else if symbol == "*" {
        parser.advance() |> ignore
        Some(EStar)
      } else {
        parser.fail("expected an expression")
        None
      }
    BEof => {
      parser.fail("unexpected end of query")
      None
    }
  }
}

///|
fn parse_binary(parser : QueryParser, min_prec : Int32) -> Expr? {
  let mut left = match parse_primary(parser) {
    Some(e) => e
    None => return None
  }
  while true {
    let op = match parser.peek_sym() {
      Some(s) => s
      None => break
    }
    let prec = match op {
      "+" | "-" => 1
      "*" | "/" => 2
      _ => 0
    }
    if prec < min_prec || prec == 0 {
      break
    }
    parser.advance() |> ignore
    let right = match parse_binary(parser, prec + 1) {
      Some(e) => e
      None => return None
    }
    left = EBinary(left, op, right)
  }
  Some(left)
}

///|
fn parse_expr(parser : QueryParser) -> Expr? {
  parse_binary(parser, 1)
}

///|
fn parse_cond_atom(parser : QueryParser) -> Cond? {
  if parser.match_sym("(") {
    match parse_cond_or(parser) {
      Some(cond) => if parser.expect_sym(")") { Some(cond) } else { None }
      None => None
    }
  } else {
    match parse_expr(parser) {
      Some(left) =>
        if parser.match_sym("~") {
          match parser.advance() {
            BStr(pattern) => {
              let regex = @string.Regex("(?i:" + pattern + ")") catch {
                _ => {
                  parser.fail("invalid or unsupported regular expression")
                  return None
                }
              }
              Some(CRegex(left, regex))
            }
            _ => {
              parser.fail("regex requires a literal pattern")
              None
            }
          }
        } else if parser.match_keyword("is") {
          let negated = parser.match_keyword("not")
          if parser.expect_keyword("null") {
            Some(CIsNull(left, negated))
          } else {
            None
          }
        } else {
          match parser.peek_sym() {
            Some(symbol) =>
              if symbol == "=" ||
                symbol == "==" ||
                symbol == "!=" ||
                symbol == "<>" ||
                symbol == "<" ||
                symbol == "<=" ||
                symbol == ">" ||
                symbol == ">=" {
                parser.advance() |> ignore
                match parse_expr(parser) {
                  Some(right) => Some(CCmp(left, symbol, right))
                  None => None
                }
              } else {
                Some(CTruthy(left))
              }
            None => Some(CTruthy(left))
          }
        }
      None => None
    }
  }
}

///|
fn parse_cond_not(parser : QueryParser) -> Cond? {
  if parser.match_keyword("not") {
    match parse_cond_not(parser) {
      Some(cond) => Some(CNot(cond))
      None => None
    }
  } else {
    parse_cond_atom(parser)
  }
}

///|
fn parse_cond_and(parser : QueryParser) -> Cond? {
  match parse_cond_not(parser) {
    Some(left) => {
      let mut cond = left
      while parser.match_keyword("and") {
        match parse_cond_not(parser) {
          Some(right) => cond = CAnd(cond, right)
          None => return None
        }
      }
      Some(cond)
    }
    None => None
  }
}

///|
fn parse_cond_or(parser : QueryParser) -> Cond? {
  match parse_cond_and(parser) {
    Some(left) => {
      let mut cond = left
      while parser.match_keyword("or") {
        match parse_cond_and(parser) {
          Some(right) => cond = COr(cond, right)
          None => return None
        }
      }
      Some(cond)
    }
    None => None
  }
}

///|
fn parse_expr_list(parser : QueryParser) -> Array[Expr]? {
  let items : Array[Expr] = []
  match parse_expr(parser) {
    Some(expr) => items.push(expr)
    None => return None
  }
  while parser.match_sym(",") {
    match parse_expr(parser) {
      Some(expr) => items.push(expr)
      None => return None
    }
  }
  Some(items)
}

///|
fn parse_source(parser : QueryParser) -> QuerySource? {
  if parser.match_keyword("open") {
    if !parser.expect_keyword("on") {
      return None
    }
    let start = match parser.advance() {
      BDate(date) => date
      _ => {
        parser.fail("OPEN ON requires a date")
        return None
      }
    }
    if start.year == 1 && start.month == 1 && start.day == 1 {
      parser.fail("OPEN ON requires a date after 0001-01-01")
      return None
    }
    let mut close = None
    if parser.match_keyword("close") {
      if !parser.expect_keyword("on") {
        return None
      }
      close = match parser.advance() {
        BDate(date) => Some(date)
        _ => {
          parser.fail("CLOSE ON requires a date")
          return None
        }
      }
    }
    return Some(SrcOpen(start, close))
  }
  if parser.match_keyword("close") {
    if !parser.expect_keyword("on") {
      return None
    }
    return match parser.advance() {
      BDate(date) => Some(SrcClose(date))
      _ => {
        parser.fail("CLOSE ON requires a date")
        None
      }
    }
  }
  match parser.take_word() {
    Some(word) =>
      match word.to_lower() {
        "postings" => Some(SrcPostings)
        "balances" => Some(SrcBalances)
        _ => {
          parser.fail("unknown FROM source `" + word + "`")
          None
        }
      }
    None => {
      parser.fail("expected a FROM source")
      None
    }
  }
}

///|
fn parse_select(parser : QueryParser) -> ParsedQuery {
  let projections = match parse_expr_list(parser) {
    Some(items) => items
    None =>
      return PqError(
        match parser.error {
          Some(message) => message
          None => "expected a projection"
        },
      )
  }
  let mut source = SrcPostings
  if parser.match_keyword("from") {
    match parse_source(parser) {
      Some(found) => source = found
      None =>
        return PqError(
          match parser.error {
            Some(message) => message
            None => "invalid FROM clause"
          },
        )
    }
  }
  let mut filter = CTrue
  if parser.match_keyword("where") {
    match parse_cond_or(parser) {
      Some(cond) => filter = cond
      None =>
        return PqError(
          match parser.error {
            Some(message) => message
            None => "invalid WHERE clause"
          },
        )
    }
  }
  let groups : Array[Expr] = []
  if parser.match_keyword("group") {
    if !parser.expect_keyword("by") {
      return PqError(
        match parser.error {
          Some(message) => message
          None => "expected BY"
        },
      )
    }
    match parse_expr_list(parser) {
      Some(items) =>
        for item in items {
          groups.push(item)
        }
      None =>
        return PqError(
          match parser.error {
            Some(message) => message
            None => "invalid GROUP BY clause"
          },
        )
    }
  }
  let mut having = CTrue
  if parser.match_keyword("having") {
    match parse_cond_or(parser) {
      Some(cond) => having = cond
      None => return PqError("invalid HAVING clause")
    }
  }
  let orders : Array[OrderSpec] = []
  if parser.match_keyword("order") {
    if !parser.expect_keyword("by") {
      return PqError(
        match parser.error {
          Some(message) => message
          None => "expected BY"
        },
      )
    }
    let mut more = true
    while more {
      match parse_expr(parser) {
        Some(expr) => {
          let mut descending = false
          if parser.match_keyword("desc") {
            descending = true
          } else {
            parser.match_keyword("asc") |> ignore
          }
          orders.push({ expr, descending, })
        }
        None =>
          return PqError(
            match parser.error {
              Some(message) => message
              None => "invalid ORDER BY clause"
            },
          )
      }
      more = parser.match_sym(",")
    }
  }
  let mut limit = None
  if parser.match_keyword("limit") {
    match parser.take_int() {
      Some(value) => {
        if value < 0 {
          return PqError("LIMIT must be non-negative")
        }
        limit = Some(value)
      }
      None =>
        return PqError(
          match parser.error {
            Some(message) => message
            None => "invalid LIMIT"
          },
        )
    }
  }
  if !parser.at_end() {
    return PqError("unexpected input after query")
  }
  PqSelect({ projections, source, filter, having, groups, orders, limit, })
}

///|
fn parse_journal(parser : QueryParser) -> ParsedQuery {
  let account = match parser.peek() {
    BWord(word) => {
      parser.advance() |> ignore
      word
    }
    BStr(value) => {
      parser.advance() |> ignore
      value
    }
    _ => return PqError("JOURNAL expects an account name")
  }
  let mut limit = None
  if parser.match_keyword("limit") {
    match parser.take_int() {
      Some(value) => {
        if value < 0 {
          return PqError("LIMIT must be non-negative")
        }
        limit = Some(value)
      }
      None =>
        return PqError(
          match parser.error {
            Some(message) => message
            None => "invalid LIMIT"
          },
        )
    }
  }
  if !parser.at_end() {
    return PqError("unexpected input after JOURNAL")
  }
  PqJournal(account, limit)
}

///|
fn parse_query(text : String) -> ParsedQuery {
  let unsupported : Array[String] = []
  let tokens = lex_bql(text, unsupported~)
  if unsupported.length() > 0 {
    return PqError(unsupported[0])
  }
  let parser = QueryParser::new(tokens)
  if parser.at_end() {
    return PqError("empty query")
  }
  match parser.take_word() {
    Some(word) =>
      match word.to_lower() {
        "select" => parse_select(parser)
        "balances" =>
          if parser.at_end() {
            PqBalances
          } else {
            parse_balances(parser)
          }
        "journal" => parse_journal(parser)
        _ => PqError("unknown statement `" + word + "`")
      }
    None => PqError("query must start with SELECT, BALANCES or JOURNAL")
  }
}

///|
fn parse_balances(parser : QueryParser) -> ParsedQuery {
  let mut mode = "position"
  if parser.match_keyword("at") {
    mode = match parser.take_word() {
      Some(word) =>
        match word.to_lower() {
          "units" => "units"
          "cost" => "weight"
          _ => return PqError("BALANCES AT supports UNITS or COST")
        }
      None => return PqError("BALANCES AT expects a function")
    }
  }
  let tokens = [
    BWord("account"),
    BSym(","),
    BWord("sum"),
    BSym("("),
    BWord(mode),
    BSym(")"),
  ]
  for i in parser.index..<(parser.tokens.length() - 1) {
    tokens.push(parser.tokens[i])
  }
  for
    token in [
      BWord("group"),
      BWord("by"),
      BWord("account"),
      BWord("order"),
      BWord("by"),
      BWord("account"),
      BEof,
    ] {
    tokens.push(token)
  }
  parse_select(QueryParser::new(tokens))
}