/*
 Metamodel (grammar) for the SieveExpertBot file format.
 For details, see: https://github.com/igordejanovic/textX
*/

SieveModel: statements*=Statement;

Statement: Action | Branching;
Branching: if_=IfClause elif_*=ElifClause (else_=ElseClause)?;
IfClause: 'if' expr=Expression '{' statements*=Statement '}';
ElifClause: 'elif' expr=Expression '{' statements*=Statement '}';
ElseClause: 'else' '{' statements*=Statement '}';

Expression: conj=Conjunction ('||' conj=Conjunction)*;
Conjunction: cond=Condition ('&&' cond=Condition)*;
Condition:
    neg?=Negation (
        match=StringMatch
        | match=NumericMatch
        | match=IpRangeMatch
        | match=DateMatch
        | match=ExistMatch
        | match=ListMatch
        | match=BoolMatch
        | ( '(' match=Expression ')' )
    )
;

Variable: value=/\$[a-z0-9_]+/;

StringMatch: SingleStringMatch | MultiStringMatch;
SingleStringMatch: key=Key op=SingleStringOperator value=SingleStringValue;
MultiStringMatch: key=Key op=MultiStringOperator value=StringValueList;

SingleStringOperator
  : '=='           // compares two whole strings with each other
  | '!='           // test for string inequality
  | ':contains'    // sub-string match
  | '=~'           // match strings according to regular expression
  | '!~'           // inverse match with regular expression
;

MultiStringOperator
  : ':in'          // tests if a string is in a list of strings
  | ':containsany' // sub-string match against multiple substrings
  | ':regexin'     // match a string against at least one of a list of regex patterns
;


NumericMatch: SingleNumericMatch | MultiNumericMatch;
SingleNumericMatch: key=Key op=SingleNumericOperator value=SingleNumericValue;
MultiNumericMatch: key=Key op=MultiNumericOperator value=NumericValueList;

SingleNumericOperator
  : '=='        // equal
  | '!='        // not equal
  | '<='        // less than or equal
  | '>='        // greater than or equal
  | '<'         // less than
  | '>'         // greater than
;

MultiNumericOperator
  : ':in'       // tests if number is in a list of numbers
;


IpRangeMatch: key=Key '<<' range=IpRange;
IpRange: SingleIpRange | IpRangeList;
SingleIpRange: value=STRING;
IpRangeList: '[' values+=SingleIpRange[','] ']' ;

DateMatch: key=Key op=DateOperator date=SingleStringValue;
DateOperator: ':before' | ':after';

ExistMatch: op=ExistOperator key=Key;
ExistOperator: ':exists' | ':notexists';

ListMatch: key=Key op=ListOperator value=ListValue;
ListOperator
  : ':equals'         // lists are equal, including order
  | ':setequals'      // lists contain the same elements, ignoring order and repeating values
  | ':overlaps'       // lists contain one or more common values
  | ':subsetof'       // key is a proper subset of value
  | ':supersetof'     // key is a proper superset of value
;

Bool: BOOL | Variable;
BoolMatch: key=Key op=BoolOperator value=Bool;
BoolOperator: '==' | '!=';


Negation: '!';

TypedValue: STRICTFLOAT | INT | BOOL | STRING | Variable;
ListValue: '[' values*=TypedValue[','] ']' ;

KeyLiteral: /[a-z0-9_\.]+/;
Key: KeyLiteral | Variable;

SingleStringValue: value=STRING | value=Variable ;
StringValueList: '[' values+=SingleStringValue[','] ']' ;

SingleNumericValue: value=NUMBER | value=Variable ;
NumericValueList: '[' values+=SingleNumericValue[','] ']' ;

AssignOperator
  : '='
  | '+='
  | '-='
;

PathValue: SinglePathValue | PathValueList ;
SinglePathValue: value=STRING ;
PathValueList: '[' values+=SinglePathValue[','] ']' ;

Action: action=DropAction
    | ( action=KeepAction
        | action=PathAction
        | action=AddAction
        | action=AddForceAction
        | action=UpdateAction
        | action=RemoveAction
        | action=AppendAction
        | action=AppendForceAction
        | action=VarSetAction
      );

DropAction: 'drop';
KeepAction: 'keep';
PathAction: 'path' path=PathValue;
AddAction: 'add' key=Key operator=AssignOperator value=TypedValue;          // add key/value without overwriting existing key
AddForceAction: 'add!' key=Key operator=AssignOperator value=TypedValue;    // add key/value, overwriting existing key
UpdateAction: 'update' key=Key operator=AssignOperator value=TypedValue;    // update key/value, do not create if not exists
RemoveAction: 'remove' key=Key;
AppendAction: 'append' key=Key value=TypedValue;                            // append an element to a list if it exists
AppendForceAction: 'append!' key=Key value=TypedValue;                      // append an element to a list; converts a single item to a list with one element
                                                                            // or creates a new list if the key is not included yet
VarSetAction: var=Variable '=' key=KeyLiteral ;                                    // set variable to value given in key
Comment: /(#|\/\/).*$/ ;
