#!/usr/bin/env python
"""Parser for past and future linear temporal logic.

The syntax is based on that of SPIN.
"""
# Copyright 2014 by California Institute of Technology
# All rights reserved. Licensed under BSD-3.
#
# the parser is based on that of `tulip`
import astutils

from omega.logic.ast import Nodes


TABMODULE = 'omega.logic.ltl_parsetab'


class Lexer(astutils.Lexer):
    """Token rules to build LTL lexer."""

    def __init__(self):
        self.reserved = {
            'VARIABLE': 'VARIABLE',
            'VARIABLES': 'VARIABLES',
            'CONSTANT': 'CONSTANT',
            'CONSTANTS': 'CONSTANTS',
            'ite': 'ITE',
            'X': 'NEXT',
            'FALSE': 'FALSE',
            'False': 'FALSE',
            'false': 'FALSE',
            'TRUE': 'TRUE',
            'True': 'TRUE',
            'true': 'TRUE',
            'LET': 'LET',
            'IN': 'IN_EXPR',
            'IF': 'IF',
            'THEN': 'THEN',
            'ELSE': 'ELSE',
            'U': 'UNTIL',
            'W': 'WEAK_UNTIL',
            'V': 'RELEASE',
            'S': 'SINCE',  # as in NuSMV
            'T': 'TRIGGER'}
        self.values = {'next': 'X'}
        self.delimiters = [
            'LPAREN',
            'RPAREN',
            'DQUOTES',
            'COMMA']
        self.operators = [
            'NOT',
            'AND',
            'OR',
            'XOR',
            'IMPLIES',
            'EQUIV',
            'EQUALS',
            'NEQUALS',
            'LT',
            'LE',
            'GT',
            'GE',
            'PLUS',
            'MINUS',
            'TIMES',
            'DIV',
            'MOD',
            'TRUNCATE',
            'PREVIOUS',
            'WEAK_PREVIOUS',
            'HISTORICALLY',
            'ALWAYS',
            'EVENTUALLY',
            'ONCE',
            'PRIME',
            'DOTS',
            'AT',
            'FORALL',
            'EXISTS',
            'RENAME',
            'IN',
            'COLON',
            'DEF']
        self.misc = ['NAME', 'NUMBER']
        super().__init__()

    def t_NAME(self, t):
        r"""
        [A-Za-z_]
        [A-Za-z0-9_]*
        """
        t.value = self.values.get(t.value, t.value)
        t.type = self.reserved.get(t.value, 'NAME')
        return t

    def t_AND(self, t):
        r"""
          \&\&
        | \&
        | /\\
        """
        t.value = '/\\'
        return t

    def t_OR(self, t):
        r"""
          \|\|
        | \|
        | \\/
        """
        t.value = r'\/'
        return t

    def t_NEQUALS(self, t):
        r"""
          !=
        | /=
        | \#
        """
        return t

    def t_NOT(self, t):
        r"""
          \~
        | !
        """
        t.value = '~'
        return t

    def t_IMPLIES(self, t):
        r"""
          =>
        | \->
        """
        t.value = '=>'
        return t

    def t_EQUIV(self, t):
        r"""
          <=>
        | <\->
        """
        t.value = '<=>'
        return t

    def t_LE(self, t):
        r"""
          =<
        | <=
        """
        return t

    t_DEF = r' == '
    # quantifiers
    t_FORALL = r' \\A '
    t_EXISTS = r' \\E '
    t_RENAME = r' \\S '  # for arbitrary
        # substitution (compose)
    # conjoin and quantify existentially
    t_COLON = r' : '
    # set theory
    t_IN = r' \\in '
    # Boolean
    t_XOR = r' \^ '
    # comparators
    t_EQUALS = r' = '
    t_LT = r' < '
    t_GT = r' >= '
    t_GE = r' > '
    # delimiters
    t_LPAREN = r' \( '
    t_RPAREN = r' \) '
    t_COMMA = r' , '
    t_DQUOTES = r' " '
    # arithmetic
    t_PLUS = r' \+ '
    t_MINUS = r' \- '
    t_TIMES = r' \* '
    t_DIV = r' / '
    t_MOD = r' % '
    t_TRUNCATE = r' <<>> '
    # temporal
    t_PREVIOUS = r' \- \- X '
    t_WEAK_PREVIOUS = r' \- X '
    t_HISTORICALLY = r' \- \[ \] '
    t_ONCE = r' \- <> '
    t_ALWAYS = r' \[ \] '
    t_EVENTUALLY = r' <> '
    t_DOTS = r' \. \. '
    t_PRIME = r" ' "
    # other
    t_AT = r' @ '
    t_NUMBER = r' \d+ '
    t_ignore = ''.join(['\x20', '\t'])

    def t_comment(self, t):
        r' \\ \* [^\n]* \n '
        return

    def t_multiline_comment(self, t):
        r"""
        \( \*
        [\s\S]*?
        \* \)
        """
        return

    def t_newline(self, t):
        r' \n+ '
        t.lexer.lineno += t.value.count('\n')


class Parser(astutils.Parser):
    """Production rules to build parser.

    The language recognized by the
    parser includes logic and arithmetic.
    """

    def __init__(self, *arg, **kw):
        self.tabmodule = TABMODULE
        self.start = 'start'
        # lowest to highest
        # based on precedence in `spin.y`
        self.precedence = (
            ('nonassoc',
                'REDUCE_LIST'),
            ('nonassoc',
                'DEF'),
            ('nonassoc',
                'LET_IN',
                'IF_THEN_ELSE',
                'CONJ_LIST'),
            ('left',
                'COLON'),
            ('left',
                'EQUIV'),
            ('left',
                'IMPLIES'),
            ('left',
                'XOR'),
            ('left',
                'OR'),
            ('left',
                'AND'),
            ('left',
                'ALWAYS',
                'EVENTUALLY',
                'HISTORICALLY',
                'ONCE'),
            ('left',
                'UNTIL',
                'WEAK_UNTIL',
                'RELEASE',
                'SINCE',
                'TRIGGER'),
            ('left',
                'EQUALS',
                'NEQUALS'),
            ('left',
                'LT',
                'LE',
                'GT',
                'GE',
                'IN'),
            ('left',
                'PLUS',
                'MINUS'),
            ('left',
                'TIMES',
                'DIV',
                'MOD'),
            ('right',
                'NOT',
                'UMINUS'),
            ('left',
                'RENAME'),
            ('left',
                'FORALL',
                'EXISTS'),
            ('right',
                'NEXT',
                'WEAK_PREVIOUS',
                'PREVIOUS'),
            ('nonassoc',
                'DOTS'),
            ('left',
                'PRIME'))
        self.nodes = Nodes
        lexer = Lexer()
        super().__init__(
            lexer=lexer,
            *arg, **kw)

    def p_start(self, p):
        """start : module
                 | expr
        """
        p[0] = p[1]

    def p_module(self, p):
        """module : units"""
        p[0] = p[1]

    def p_units_iter(self, p):
        """units : units unit"""
        p[1].append(p[2])
        p[0] = p[1]

    def p_units_end(self, p):
        """units : unit"""
        p[0] = [p[1]]

    def p_unit(self, p):
        """unit : def
                | var_decl
                | const_decl
        """
        p[0] = p[1]

    def p_variable_declaration(self, p):
        """var_decl : VARIABLE list
                    | VARIABLES list
        """
        p[0] = self.nodes.Operator(p[1], p[2])

    def p_constant_declaration(self, p):
        """const_decl : CONSTANT list
                      | CONSTANTS list
        """
        p[0] = self.nodes.Operator(p[1], p[2])

    def p_defs_iter(self, p):
        """defs : defs def"""
        p[1].append(p[2])
        p[0] = p[1]

    def p_defs_end(self, p):
        """defs : def"""
        p[0] = [p[1]]

    def p_operator_definition(self, p):
        """def : NAME DEF expr"""
        name = p[1]
        u = self.nodes.Terminal(name, dtype='opname')
        p[0] = self.nodes.Binary('==', u, p[3])

    def p_let_in(self, p):
        """expr : LET defs IN_EXPR expr %prec LET_IN"""
        p[0] = self.nodes.Operator(p[1], p[2], p[4])

    def p_nullary(self, p):
        """expr : TRUE
                | FALSE
        """
        p[0] = self.nodes.Bool(p[1])

    def p_postfix_next(self, p):
        """expr : expr PRIME"""
        p[0] = self.nodes.Unary('X', p[1])

    def p_unary(self, p):
        """expr : NOT expr
                | ALWAYS expr
                | EVENTUALLY expr
                | NEXT expr
                | WEAK_PREVIOUS expr
                | PREVIOUS expr
                | HISTORICALLY expr
                | ONCE expr
        """
        p[0] = self.nodes.Unary(p[1], p[2])

    def p_binary_connective(self, p):
        """expr : expr AND expr
                | expr OR expr
                | expr XOR expr
                | expr IMPLIES expr
                | expr EQUIV expr
                | expr UNTIL expr
                | expr WEAK_UNTIL expr
                | expr RELEASE expr
                | expr SINCE expr
                | expr TRIGGER expr
        """
        p[0] = self.nodes.Binary(p[2], p[1], p[3])

    def p_binary_predicate(self, p):
        """expr : expr EQUALS expr
                | expr NEQUALS expr
                | expr LT expr
                | expr LE expr
                | expr GT expr
                | expr GE expr
        """
        p[0] = self.nodes.Comparator(p[2], p[1], p[3])

    def p_binary_function(self, p):
        """expr : expr TIMES expr
                | expr DIV expr
                | expr MOD expr
                | expr PLUS expr
                | expr MINUS expr
        """
        p[0] = self.nodes.Arithmetic(p[2], p[1], p[3])

    # CAUTION: indentation ignored: use parentheses
    def p_junction_list(self, p):
        """expr : junc_list  %prec REDUCE_LIST"""
        p[0] = p[1]

    def p_junction_list_iter(self, p):
        """junc_list : junc_list AND expr
                     | junc_list OR expr
        """
        p[0] = self.nodes.Binary(p[2], p[1], p[3])

    def p_conjunction_list_end(self, p):
        """junc_list : AND expr
                     | OR expr  %prec CONJ_LIST
        """
        p[0] = p[2]

    def p_truncator(self, p):
        """expr : expr TRUNCATE number"""
        # keep separate to allow overriding
        p[0] = self.nodes.Arithmetic(p[2], p[1], p[3])

    def p_if_then_else(self, p):
        ("""expr : IF expr """
         """           THEN expr """
         """           ELSE expr """
         """    %prec IF_THEN_ELSE""")
        p[0] = self.nodes.Operator(
            'ite', p[2], p[4], p[6])

    def p_ternary_conditional(self, p):
        ("""expr : ITE LPAREN expr """
         """           COMMA expr """
         """           COMMA expr RPAREN""")
        p[0] = self.nodes.Operator(
            p[1], p[3], p[5], p[7])

    def p_quantifier(self, p):
        """expr : FORALL list COLON expr
                | EXISTS list COLON expr
        """
        params = self.nodes.Operator('params', *p[2])
        p[0] = self.nodes.Operator(p[1], params, p[4])

    def p_substitute(self, p):
        """expr : RENAME pairs COLON expr"""
        p[0] = self.nodes.Operator(p[1], p[2], p[4])

    def p_in_range(self, p):
        """expr : expr IN expr"""
        p[0] = self.nodes.Binary(p[2], p[1], p[3])

    def p_range(self, p):
        """expr : number DOTS number"""
        p[0] = self.nodes.Binary(p[2], p[1], p[3])

    def p_varlist_iter(self, p):
        """list : list COMMA expr"""
        p[1].append(p[3])
        p[0] = p[1]

    def p_varlist_end(self, p):
        """list : expr"""
        p[0] = [p[1]]

    def p_pairs_iter(self, p):
        """pairs : pairs COMMA pair"""
        p[1].append(p[3])
        p[0] = p[1]

    def p_pairs_end(self, p):
        """pairs : pair"""
        p[0] = [p[1]]

    def p_pair(self, p):
        """pair : expr DIV expr"""
        p[0] = (p[1], p[3])

    def p_paren(self, p):
        """expr : LPAREN expr RPAREN"""
        p[0] = p[2]

    def p_var(self, p):
        """expr : var"""
        p[0] = p[1]

    def p_var_unprimed(self, p):
        """var : NAME"""
        p[0] = self.nodes.Var(p[1])

    def p_bdd_node(self, p):
        """expr : AT number"""
        p[0] = self.nodes.Operator(p[1], p[2])

    def p_number_expr(self, p):
        """expr : number"""
        p[0] = p[1]

    def p_number(self, p):
        """number : NUMBER"""
        p[0] = self.nodes.Num(p[1])

    def p_negative_number(self, p):
        """number : MINUS NUMBER %prec UMINUS"""
        p[0] = self.nodes.Num('-' + p[2])

    def p_string(self, p):
        """expr : DQUOTES NAME DQUOTES"""
        p[0] = self.nodes.Str('"' + p[2] + '"')


def _rewrite_tables(outputdir='./'):
    astutils.rewrite_tables(
        Parser, TABMODULE, outputdir)


if __name__ == '__main__':
    import logging
    log = logging.getLogger('astutils')
    log.setLevel(logging.DEBUG)
    log.addHandler(logging.StreamHandler())
    _rewrite_tables()
