// Expression sub-language: a lexer + Pratt parser for the contents of
// {{ ... }} and tag conditions.
//
// Precedence (low -> high): or, and, not, comparison, +/-, * / %, unary -,
// postfix (member `.`, index `[]`, filter `|`), primary.

///|
/// A token in the expression sub-language.
pub(all) enum ExprToken {
  EIdent(String)
  EInt(Int)
  EStr(String)
  EOp(String)
  EDot
  ELparen
  ERparen
  ELbracket
  ERbracket
  EComma
  EPipe
  EEof
} derive(Debug)

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

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

///|
fn is_ident_part(c : Char) -> Bool {
  is_ident_start(c) || is_digit(c)
}

///|
/// Lex an expression string into tokens.
pub fn expr_lex(s : String) -> Array[ExprToken] {
  let chars = s.to_array()
  let n = chars.length()
  let toks : Array[ExprToken] = []
  let mut i = 0
  while i < n {
    let c = chars[i]
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      i = i + 1
    } else if is_digit(c) {
      let mut j = i
      while j < n && is_digit(chars[j]) {
        j = j + 1
      }
      let mut v = 0
      for k = i; k < j; k = k + 1 {
        v = v * 10 + (chars[k].to_int() - '0'.to_int())
      }
      toks.push(EInt(v))
      i = j
    } else if is_ident_start(c) {
      let mut j = i
      while j < n && is_ident_part(chars[j]) {
        j = j + 1
      }
      toks.push(EIdent(s[i:j].to_owned()))
      i = j
    } else if c == '"' {
      let mut j = i + 1
      let buf = StringBuilder::new()
      while j < n && chars[j] != '"' {
        buf.write_char(chars[j])
        j = j + 1
      }
      toks.push(EStr(buf.to_string()))
      i = j + 1
    } else if c == '.' {
      toks.push(EDot)
      i = i + 1
    } else if c == '(' {
      toks.push(ELparen)
      i = i + 1
    } else if c == ')' {
      toks.push(ERparen)
      i = i + 1
    } else if c == '[' {
      toks.push(ELbracket)
      i = i + 1
    } else if c == ']' {
      toks.push(ERbracket)
      i = i + 1
    } else if c == ',' {
      toks.push(EComma)
      i = i + 1
    } else if c == '|' {
      toks.push(EPipe)
      i = i + 1
    } else {
      let two = if i + 1 < n { s[i:i + 2].to_owned() } else { "" }
      if two == "==" || two == "!=" || two == "<=" || two == ">=" {
        toks.push(EOp(two))
        i = i + 2
      } else if c == '<' ||
        c == '>' ||
        c == '+' ||
        c == '-' ||
        c == '*' ||
        c == '/' ||
        c == '%' {
        toks.push(EOp(s[i:i + 1].to_owned()))
        i = i + 1
      } else {
        i = i + 1
      }
    }
  }
  toks.push(EEof)
  toks
}

///|
priv struct EP {
  toks : Array[ExprToken]
  mut i : Int
}

///|
fn EP::peek(self : EP) -> ExprToken {
  if self.i < self.toks.length() {
    self.toks[self.i]
  } else {
    EEof
  }
}

///|
fn EP::advance(self : EP) -> Unit {
  self.i = self.i + 1
}

///|
fn EP::at_ident(self : EP, kw : String) -> Bool {
  match self.peek() {
    EIdent(k) => k == kw
    _ => false
  }
}

///|
fn EP::at_op(self : EP, ops : Array[String]) -> Bool {
  match self.peek() {
    EOp(o) => ops.contains(o)
    _ => false
  }
}

///|
fn EP::cur_op(self : EP) -> String {
  match self.peek() {
    EOp(o) => o
    _ => ""
  }
}

///|
fn EP::cur_ident(self : EP) -> String {
  match self.peek() {
    EIdent(k) => k
    _ => ""
  }
}

///|
fn EP::peek_is_pipe(self : EP) -> Bool {
  match self.peek() {
    EPipe => true
    _ => false
  }
}

///|
fn EP::peek_is_dot(self : EP) -> Bool {
  match self.peek() {
    EDot => true
    _ => false
  }
}

///|
fn EP::peek_is_lparen(self : EP) -> Bool {
  match self.peek() {
    ELparen => true
    _ => false
  }
}

///|
fn EP::peek_is_lbracket(self : EP) -> Bool {
  match self.peek() {
    ELbracket => true
    _ => false
  }
}

///|
fn EP::peek_is_comma(self : EP) -> Bool {
  match self.peek() {
    EComma => true
    _ => false
  }
}

///|
/// Parse a token list into an `Expr`.
pub fn expr_parse(toks : Array[ExprToken]) -> Expr {
  let p = EP::{ toks, i: 0 }
  p.parse_or()
}

///|
fn EP::parse_or(self : EP) -> Expr {
  let mut left = self.parse_and()
  while self.at_ident("or") {
    self.advance()
    left = EBinOp("or", left, self.parse_and())
  }
  left
}

///|
fn EP::parse_and(self : EP) -> Expr {
  let mut left = self.parse_not()
  while self.at_ident("and") {
    self.advance()
    left = EBinOp("and", left, self.parse_not())
  }
  left
}

///|
fn EP::parse_not(self : EP) -> Expr {
  if self.at_ident("not") {
    self.advance()
    ENot(self.parse_not())
  } else {
    self.parse_is()
  }
}

///|
/// Parse `expr is test_name` (and chains via precedence).
fn EP::parse_is(self : EP) -> Expr {
  let mut left = self.parse_cmp()
  while self.at_ident("is") {
    self.advance()
    let name = self.cur_ident()
    self.advance()
    left = EIs(left, name)
  }
  left
}

///|
fn EP::parse_cmp(self : EP) -> Expr {
  let mut left = self.parse_add()
  while self.at_op(["==", "!=", "<", ">", "<=", ">="]) {
    let op = self.cur_op()
    self.advance()
    left = EBinOp(op, left, self.parse_add())
  }
  left
}

///|
fn EP::parse_add(self : EP) -> Expr {
  let mut left = self.parse_mul()
  while self.at_op(["+", "-"]) {
    let op = self.cur_op()
    self.advance()
    left = EBinOp(op, left, self.parse_mul())
  }
  left
}

///|
fn EP::parse_mul(self : EP) -> Expr {
  let mut left = self.parse_unary()
  while self.at_op(["*", "/", "%"]) {
    let op = self.cur_op()
    self.advance()
    left = EBinOp(op, left, self.parse_unary())
  }
  left
}

///|
fn EP::parse_unary(self : EP) -> Expr {
  if self.at_op(["-"]) {
    self.advance()
    EBinOp("-", EInt(0), self.parse_unary())
  } else {
    self.parse_postfix()
  }
}

///|
fn EP::parse_postfix(self : EP) -> Expr {
  let mut e = self.parse_primary()
  let mut cont = true
  while cont {
    if self.peek_is_dot() {
      self.advance()
      let field = self.cur_ident()
      self.advance()
      e = EMember(e, field)
    } else if self.peek_is_lbracket() {
      self.advance()
      let index = self.parse_or()
      self.advance() // consume ']'
      e = EIndex(e, index)
    } else if self.peek_is_pipe() {
      self.advance()
      let name = self.cur_ident()
      self.advance()
      let args : Array[Expr] = []
      if self.peek_is_lparen() {
        self.advance()
        args.push(self.parse_or())
        while self.peek_is_comma() {
          self.advance()
          args.push(self.parse_or())
        }
        self.advance() // consume ')'
      }
      e = EFilter(e, name, args)
    } else {
      cont = false
    }
  }
  e
}

///|
fn EP::parse_primary(self : EP) -> Expr {
  match self.peek() {
    EInt(v) => {
      self.advance()
      EInt(v)
    }
    EStr(s) => {
      self.advance()
      EStr(s)
    }
    EIdent(name) => {
      self.advance()
      if name == "true" {
        EBool(true)
      } else if name == "false" {
        EBool(false)
      } else if name == "super" {
        if self.peek_is_lparen() {
          self.advance()
          self.advance() // consume ')'
        }
        ESuper
      } else {
        EVar(name)
      }
    }
    ELparen => {
      self.advance()
      let e = self.parse_or()
      self.advance()
      e
    }
    _ => EInt(0)
  }
}