// Formula grammar for improvise.
//
// A formula has the form:   TARGET = EXPR [WHERE filter]
// See parser.rs for the tree walker that produces a Formula AST.
//
// Identifier rules (bare_ident / pipe_quoted) mirror `bare_name` and
// `pipe_quoted` in src/persistence/improv.pest: bare identifiers are
// alphanumeric plus `_` and `-`, with no internal spaces; multi-word
// names must be pipe-quoted.

// Auto-skip horizontal whitespace between tokens in non-atomic rules.
WHITESPACE = _{ " " | "\t" }

// ---- top-level ----------------------------------------------------------

formula = { SOI ~ target ~ "=" ~ expr ~ where_clause? ~ EOI }

// The target keeps its raw text (including pipes, if any) — we capture
// the span directly rather than walking into its children.
target = { identifier }

where_clause = { ^"WHERE" ~ identifier ~ "=" ~ filter_value }

// ---- expressions --------------------------------------------------------

// Used by parse_expr() — forces a standalone expression to consume the
// whole input, so `1 + 2 3` fails instead of silently dropping " 3".
expr_eoi = { SOI ~ expr ~ EOI }

expr = { add_expr }

add_expr = { mul_expr ~ (add_op ~ mul_expr)* }
add_op   = { "+" | "-" }

mul_expr = { pow_expr ~ (mul_op ~ pow_expr)* }
mul_op   = { "*" | "/" }

pow_expr = { unary ~ (pow_op ~ unary)? }
pow_op   = { "^" }

unary       = { unary_minus | primary }
unary_minus = { "-" ~ primary }

primary = {
      number
    | agg_call
    | if_expr
    | paren_expr
    | ref_expr
}

paren_expr = { "(" ~ expr ~ ")" }

// Aggregates with optional inline WHERE filter inside the parens.
agg_call     = { agg_func ~ "(" ~ expr ~ inline_where? ~ ")" }
agg_func     = { ^"SUM" | ^"AVG" | ^"MIN" | ^"MAX" | ^"COUNT" }
inline_where = { ^"WHERE" ~ identifier ~ "=" ~ filter_value }

// IF(cond, then, else).  Comparison is a standalone rule because comparison
// operators are not valid in general expressions — only inside an IF condition.
if_expr    = { ^"IF" ~ "(" ~ comparison ~ "," ~ expr ~ "," ~ expr ~ ")" }
comparison = { expr ~ cmp_op ~ expr }
cmp_op     = { "!=" | "<=" | ">=" | "<" | ">" | "=" }

// A reference to an item.  `SUM` and `IF` without parens fall through to
// this rule because agg_call / if_expr require a "(" and otherwise fail.
ref_expr = { identifier }

// ---- identifiers --------------------------------------------------------
//
// Mirror of improv.pest's bare_name / pipe_quoted.

identifier = ${ pipe_quoted | bare_ident }

// Backslash escapes inside pipes: \| literal pipe, \\ backslash, \n newline.
pipe_quoted = @{ "|" ~ ("\\" ~ ANY | !"|" ~ ANY)* ~ "|" }

bare_ident = @{
    (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | "-")*
}

// ---- literal values -----------------------------------------------------

filter_value = { string | pipe_quoted | bare_ident }

string = @{ "\"" ~ (!"\"" ~ ANY)* ~ "\"" }

number = @{
      ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT*)?
    | "." ~ ASCII_DIGIT+
}
