"""@package grass.temporal

Temporal operator evaluation with PLY

(C) 2013 by the GRASS Development Team
This program is free software under the GNU General Public
License (>=v2). Read the file COPYING that comes with GRASS
for details.

:authors: Thomas Leppelt and Soeren Gebbert

.. code-block:: python

    >>> p = TemporalOperatorParser()
    >>> expression =  "{equal|equivalent|cover|in|meet|contain|overlap}"
    >>> p.parse(expression, optype = 'relation')
    >>> print((p.relations, p.temporal, p.function))
    (['equal', 'equivalent', 'cover', 'in', 'meet', 'contain', 'overlap'], None, None)

    >>> p = TemporalOperatorParser()
    >>> expression =  "{equal| during}"
    >>> p.parse(expression, optype = 'relation')
    >>> print((p.relations, p.temporal, p.function))
    (['equal', 'during'], None, None)
    >>> p = TemporalOperatorParser()
    >>> expression =  "{contains | starts}"
    >>> p.parse(expression)
    >>> print((p.relations, p.temporal, p.function))
    (['contains', 'starts'], None, None)
    >>> p = TemporalOperatorParser()
    >>> expression =  "{&&, during}"
    >>> p.parse(expression, optype = 'boolean')
    >>> print((p.relations, p.temporal, p.function, p.aggregate))
    (['during'], 'l', '&&', '&')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{||, equal | during}"
    >>> p.parse(expression, optype = 'boolean')
    >>> print((p.relations, p.temporal, p.function, p.aggregate))
    (['equal', 'during'], 'l', '||', '|')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{||, equal | during, &}"
    >>> p.parse(expression, optype = 'boolean')
    >>> print((p.relations, p.temporal, p.function, p.aggregate))
    (['equal', 'during'], 'l', '||', '&')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{&&, during, |}"
    >>> p.parse(expression, optype = 'boolean')
    >>> print((p.relations, p.temporal, p.function, p.aggregate))
    (['during'], 'l', '&&', '|')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{&&, during, |, r}"
    >>> p.parse(expression, optype = 'boolean')
    >>> print((p.relations, p.temporal, p.function, p.aggregate))
    (['during'], 'r', '&&', '|')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{&&, during, u}"
    >>> p.parse(expression, optype = 'boolean')
    >>> print((p.relations, p.temporal, p.function, p.aggregate))
    (['during'], 'u', '&&', '&')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{:, during, r}"
    >>> p.parse(expression, optype = 'select')
    >>> print((p.relations, p.temporal, p.function))
    (['during'], 'r', ':')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{!:, equal | contains, d}"
    >>> p.parse(expression, optype = 'select')
    >>> print((p.relations, p.temporal, p.function))
    (['equal', 'contains'], 'd', '!:')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{#, during, r}"
    >>> p.parse(expression, optype = 'hash')
    >>> print((p.relations, p.temporal, p.function))
    (['during'], 'r', '#')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{#, equal | contains}"
    >>> p.parse(expression, optype = 'hash')
    >>> print((p.relations, p.temporal, p.function))
    (['equal', 'contains'], 'l', '#')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{+, during, r}"
    >>> p.parse(expression, optype = 'raster')
    >>> print((p.relations, p.temporal, p.function))
    (['during'], 'r', '+')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{/, equal | contains}"
    >>> p.parse(expression, optype = 'raster')
    >>> print((p.relations, p.temporal, p.function))
    (['equal', 'contains'], 'l', '/')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{+, equal | contains,intersect}"
    >>> p.parse(expression, optype = 'raster')
    >>> print((p.relations, p.temporal, p.function))
    (['equal', 'contains'], 'i', '+')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{*, contains,disjoint}"
    >>> p.parse(expression, optype = 'raster')
    >>> print((p.relations, p.temporal, p.function))
    (['contains'], 'd', '*')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{~, equal,left}"
    >>> p.parse(expression, optype = 'overlay')
    >>> print((p.relations, p.temporal, p.function))
    (['equal'], 'l', '~')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{^, over,right}"
    >>> p.parse(expression, optype = 'overlay')
    >>> print((p.relations, p.temporal, p.function))
    (['overlaps', 'overlapped'], 'r', '^')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{&&, equal | during | contains | starts, &}"
    >>> p.parse(expression, optype = 'boolean')
    >>> print((p.relations, p.temporal, p.function, p.aggregate))
    (['equal', 'during', 'contains', 'starts'], 'l', '&&', '&')
    >>> p = TemporalOperatorParser()
    >>> expression =  "{&&, equal | during | contains | starts, &&&&&}"
    >>> p.parse(expression, optype = 'boolean')
    Traceback (most recent call last):
    SyntaxError: Unexpected syntax error in expression "{&&, equal | during | contains | starts, &&&&&}" at position 42 near &
    >>> p = TemporalOperatorParser()
    >>> expression =  "{+, starting}"
    >>> p.parse(expression)
    Traceback (most recent call last):
    SyntaxError: syntax error on line 1 position 4 near 'starting'
    >>> p = TemporalOperatorParser()
    >>> expression =  "{nope, start, |, l}"
    >>> p.parse(expression)
    Traceback (most recent call last):
    SyntaxError: syntax error on line 1 position 1 near 'nope'
    >>> p = TemporalOperatorParser()
    >>> expression =  "{++, start, |, l}"
    >>> p.parse(expression)
    Traceback (most recent call last):
    SyntaxError: Unexpected syntax error in expression "{++, start, |, l}" at position 2 near +
    >>> p = TemporalOperatorParser()
    >>> expression =  "{^, over, right}"
    >>> p.parse(expression, optype='rter')
    Traceback (most recent call last):
    SyntaxError: Unknown optype rter, must be one of ['select', 'boolean', 'raster', 'hash', 'relation', 'overlay']

"""
from __future__ import print_function

try:
    import ply.lex as lex
    import ply.yacc as yacc
except:
    pass

class TemporalOperatorLexer(object):
    """Lexical analyzer for the GRASS GIS temporal operator"""

    # Functions that defines topological relations.
    relations = {
        # temporal relations
        'equal'      : "EQUAL",
        'follows'    : "FOLLOWS",
        'precedes'   : "PRECEDES",
        'overlaps'   : "OVERLAPS",
        'overlapped' : "OVERLAPPED",
        'during'     : "DURING",
        'starts'     : "STARTS",
        'finishes'   : "FINISHES",
        'contains'   : "CONTAINS",
        'started'    : "STARTED",
        'finished'   : "FINISHED",
        'over'       : "OVER",
        # spatial relations
        'equivalent' : "EQUIVALENT",
        'cover'      : "COVER",
        'overlap'    : "OVERLAP",
        'in'         : "IN",
        'contain'    : "CONTAIN",
        'meet'       : "MEET"
        }

    # This is the list of token names.
    tokens = (
        'COMMA',
        'LEFTREF',
        'RIGHTREF',
        'UNION',
        'DISJOINT',
        'INTERSECT',
        'HASH',
        'OR',
        'AND',
        'DISOR',
        'XOR',
        'NOT',
        'MOD',
        'DIV',
        'MULT',
        'ADD',
        'SUB',
        'T_SELECT',
        'T_NOT_SELECT',
        'CLPAREN',
        'CRPAREN',
    )

    # Build the token list
    tokens = tokens + tuple(relations.values())

    # Regular expression rules for simple tokens
    t_T_SELECT       = r':'
    t_T_NOT_SELECT   = r'!:'
    t_COMMA          = r','
    t_LEFTREF        = '^[l|left]'
    t_RIGHTREF       = '^[r|right]'
    t_UNION          = '^[u|union]'
    t_DISJOINT       = '^[d|disjoint]'
    t_INTERSECT      = '^[i|intersect]'
    t_HASH           = r'\#'
    t_OR             = r'[\|]'
    t_AND            = r'[&]'
    t_DISOR          = r'\+'
    t_XOR            = r'\^'
    t_NOT            = r'\~'
    t_MOD            = r'[\%]'
    t_DIV            = r'[\/]'
    t_MULT           = r'[\*]'
    t_ADD            = r'[\+]'
    t_SUB            = r'[-]'
    t_CLPAREN        = r'\{'
    t_CRPAREN        = r'\}'

    # These are the things that should be ignored.
    t_ignore = ' \t\n'

    # Track line numbers.
    def t_newline(self, t):
        r'\n+'
        t.lineno += len(t.value)

    def t_NAME(self, t):
        r'[a-zA-Z_][a-zA-Z_0-9]*'
        return self.temporal_symbol(t)

    # Parse symbols
    def temporal_symbol(self, t):
        # Check for reserved words
        if t.value in TemporalOperatorLexer.relations.keys():
            t.type = TemporalOperatorLexer.relations.get(t.value)
        elif t.value == 'l' or t.value == 'left':
            t.value = 'l'
            t.type = 'LEFTREF'
        elif t.value == 'r' or t.value == 'right':
            t.value = 'r'
            t.type = 'RIGHTREF'
        elif t.value == 'u' or t.value == 'union':
            t.value = 'u'
            t.type = 'UNION'
        elif t.value == 'd' or t.value == 'disjoint':
            t.value = 'd'
            t.type = 'DISJOINT'
        elif t.value == 'i' or t.value == 'intersect':
            t.value = 'i'
            t.type = 'INTERSECT'
        else:
            self.t_error(t)
        return(t)

    # Handle errors.
    def t_error(self, t):
        raise SyntaxError("syntax error on line %d position %i near '%s'" %
                          (t.lineno, t.lexpos, t.value))

    # Build the lexer
    def build(self,**kwargs):
        self.lexer = lex.lex(module=self, optimize=False,
                             nowarn=True, debug=0, **kwargs)

    # Just for testing
    def test(self,data):
        self.name_list = {}
        print(data)
        self.lexer.input(data)
        while True:
             tok = self.lexer.token()
             if not tok: break
             print(tok)

###############################################################################

class TemporalOperatorParser(object):
    """The temporal operator class"""

    def __init__(self):
        self.lexer = TemporalOperatorLexer()
        self.lexer.build()
        self.parser = yacc.yacc(module=self, debug=0)
        self.relations = None   # Temporal relations (equals, contain, during, ...)
        self.temporal  = None   # Temporal operation (intersect, left, right, ...)
        self.function  = None   # Actual operation (+, -, /, *, ... )
        self.aggregate = None   # Aggregation function (|, &)

        self.optype_list = ["select", "boolean", "raster", "hash", "relation", "overlay"]

    def parse(self, expression, optype='relation'):
        """Parse the expression and fill the object variables

        :param expression:
        :param optype: The parameter optype can be of type:
                       - select   { :, during,   r}
                       - boolean  {&&, contains, |}
                       - raster   { *, equal,    |}
                       - overlay  { |, starts,   &}
                       - hash     { #, during,   l}
                       - relation {during}
        :return:
        """
        self.optype = optype

        if optype not in self.optype_list:
            raise SyntaxError("Unknown optype %s, must be one of %s"%(self.optype, str(self.optype_list)))
        self.expression = expression
        self.parser.parse(expression)

    # Error rule for syntax errors.
    def p_error(self, t):
        raise SyntaxError("Unexpected syntax error in expression"
                          " \"%s\" at position %i near %s"%(self.expression,
                                                            t.lexpos,
                                                            t.value))

    # Get the tokens from the lexer class
    tokens = TemporalOperatorLexer.tokens

    def p_relation_operator(self, t):
        # {during}
        # {during | equal | starts}
        """
        operator : CLPAREN relation CRPAREN
                 | CLPAREN relationlist CRPAREN
        """
        # Check for correct type.
        if not self.optype == 'relation':
            raise SyntaxError("Wrong optype \"%s\" must be \"relation\""%self.optype)
        else:
            # Set three operator components.
            if isinstance(t[2], list):
                self.relations = t[2]
            else:
                self.relations = [t[2]]
            self.temporal  = None
            self.function  = None

            t[0] = t[2]

    def p_relation_bool_operator(self, t):
        # {||, during}
        # {&&, during | equal | starts}
        """
        operator : CLPAREN OR  OR  COMMA relation     CRPAREN
                 | CLPAREN AND AND COMMA relation     CRPAREN
                 | CLPAREN OR  OR  COMMA relationlist CRPAREN
                 | CLPAREN AND AND COMMA relationlist CRPAREN
        """
        if not self.optype == 'boolean':
            raise SyntaxError("Wrong optype \"%s\" must be \"boolean\""%self.optype)
        else:
            # Set three operator components.
            if isinstance(t[5], list):
                self.relations = t[5]
            else:
                self.relations = [t[5]]
            self.temporal  = "l"
            self.function  = t[2] + t[3]
            self.aggregate = t[2]

            t[0] = t[2]

    def p_relation_bool_combi_operator(self, t):
        # {||, during, &}
        # {&&, during | equal | starts, |}
        """
        operator : CLPAREN OR  OR  COMMA relation     COMMA OR  CRPAREN
                 | CLPAREN OR  OR  COMMA relation     COMMA AND CRPAREN
                 | CLPAREN AND AND COMMA relation     COMMA OR  CRPAREN
                 | CLPAREN AND AND COMMA relation     COMMA AND CRPAREN
                 | CLPAREN OR  OR  COMMA relationlist COMMA OR  CRPAREN
                 | CLPAREN OR  OR  COMMA relationlist COMMA AND CRPAREN
                 | CLPAREN AND AND COMMA relationlist COMMA OR  CRPAREN
                 | CLPAREN AND AND COMMA relationlist COMMA AND CRPAREN
        """
        if not self.optype == 'boolean':
            raise SyntaxError("Wrong optype \"%s\" must be \"boolean\""%self.optype)
        else:
            # Set three operator components.
            if isinstance(t[5], list):
                self.relations = t[5]
            else:
                self.relations = [t[5]]
            self.temporal  = "l"
            self.function  = t[2] + t[3]
            self.aggregate = t[7]

            t[0] = t[2]

    def p_relation_bool_combi_operator2(self, t):
        # {||, during, left}
        # {&&, during | equal | starts, union}
        """
        operator : CLPAREN OR  OR  COMMA relation     COMMA temporal CRPAREN
                 | CLPAREN AND AND COMMA relation     COMMA temporal CRPAREN
                 | CLPAREN OR  OR  COMMA relationlist COMMA temporal CRPAREN
                 | CLPAREN AND AND COMMA relationlist COMMA temporal CRPAREN
        """
        if not self.optype == 'boolean':
            raise SyntaxError("Wrong optype \"%s\" must be \"boolean\""%self.optype)
        else:
            # Set three operator components.
            if isinstance(t[5], list):
                self.relations = t[5]
            else:
                self.relations = [t[5]]
            self.temporal  = t[7]
            self.function  = t[2] + t[3]
            self.aggregate = t[2]

            t[0] = t[2]

    def p_relation_bool_combi_operator3(self, t):
        # {||, during, |, left}
        # {&&, during | equal | starts, &, union}
        """
        operator : CLPAREN OR  OR  COMMA relation     COMMA OR  COMMA temporal CRPAREN
                 | CLPAREN OR  OR  COMMA relation     COMMA AND COMMA temporal CRPAREN
                 | CLPAREN AND AND COMMA relation     COMMA OR  COMMA temporal CRPAREN
                 | CLPAREN AND AND COMMA relation     COMMA AND COMMA temporal CRPAREN
                 | CLPAREN OR  OR  COMMA relationlist COMMA OR  COMMA temporal CRPAREN
                 | CLPAREN OR  OR  COMMA relationlist COMMA AND COMMA temporal CRPAREN
                 | CLPAREN AND AND COMMA relationlist COMMA OR  COMMA temporal CRPAREN
                 | CLPAREN AND AND COMMA relationlist COMMA AND COMMA temporal CRPAREN
        """
        if not self.optype == 'boolean':
            raise SyntaxError("Wrong optype \"%s\" must be \"relation\""%self.optype)
        else:
            # Set three operator components.
            if isinstance(t[5], list):
                self.relations = t[5]
            else:
                self.relations = [t[5]]
            self.temporal  = t[9]
            self.function  = t[2] + t[3]
            self.aggregate = t[7]

            t[0] = t[2]

    def p_select_relation_operator(self, t):
        # {!:}
        # { :, during}
        # {!:, during | equal | starts}
        # { :, during | equal | starts, l}
        """
        operator : CLPAREN select CRPAREN
                 | CLPAREN select COMMA relation     CRPAREN
                 | CLPAREN select COMMA relationlist CRPAREN
                 | CLPAREN select COMMA relation     COMMA temporal CRPAREN
                 | CLPAREN select COMMA relationlist COMMA temporal CRPAREN
        """
        if not self.optype == 'select':
            raise SyntaxError("Wrong optype \"%s\" must be \"select\""%self.optype)
        else:
            if len(t) == 4:
                # Set three operator components.
                self.relations = ['equal', 'equivalent']
                self.temporal  = "l"
                self.function  = t[2]
            elif len(t) == 6:
                if isinstance(t[4], list):
                    self.relations = t[4]
                else:
                    self.relations = [t[4]]
                self.temporal  = "l"
                self.function  = t[2]
            elif len(t) == 8:
                if isinstance(t[4], list):
                    self.relations = t[4]
                else:
                    self.relations = [t[4]]
                self.temporal  = t[6]
                self.function  = t[2]
            t[0] = t[2]

    def p_hash_relation_operator(self, t):
        # {#}
        # {#, during}
        # {#, during | equal | starts}
        # {#, during | equal | starts, l}
        """
        operator : CLPAREN HASH CRPAREN
                 | CLPAREN HASH COMMA relation     CRPAREN
                 | CLPAREN HASH COMMA relationlist CRPAREN
                 | CLPAREN HASH COMMA relation     COMMA temporal CRPAREN
                 | CLPAREN HASH COMMA relationlist COMMA temporal CRPAREN
        """
        if not self.optype == 'hash':
            raise SyntaxError("Wrong optype \"%s\" must be \"hash\""%self.optype)
        else:
            if len(t) == 4:
                # Set three operator components.
                self.relations = ['equal']
                self.temporal  = "l"
                self.function  = t[2]
            elif len(t) == 6:
                if isinstance(t[4], list):
                    self.relations = t[4]
                else:
                    self.relations = [t[4]]
                self.temporal  = "l"
                self.function  = t[2]
            elif len(t) == 8:
                if isinstance(t[4], list):
                    self.relations = t[4]
                else:
                    self.relations = [t[4]]
                self.temporal  = t[6]
                self.function  = t[2]
            t[0] = t[2]

    def p_raster_relation_operator(self, t):
        # {+}
        # {-, during}
        # {*, during | equal | starts}
        # {/, during | equal | starts, l}
        """
        operator : CLPAREN arithmetic CRPAREN
                 | CLPAREN arithmetic COMMA relation     CRPAREN
                 | CLPAREN arithmetic COMMA relationlist CRPAREN
                 | CLPAREN arithmetic COMMA relation     COMMA temporal CRPAREN
                 | CLPAREN arithmetic COMMA relationlist COMMA temporal CRPAREN
        """
        if not self.optype == 'raster':
            raise SyntaxError("Wrong optype \"%s\" must be \"raster\""%self.optype)
        else:
            if len(t) == 4:
                # Set three operator components.
                self.relations = ['equal']
                self.temporal  = "l"
                self.function  = t[2]
            elif len(t) == 6:
                if isinstance(t[4], list):
                    self.relations = t[4]
                else:
                    self.relations = [t[4]]
                self.temporal  = "l"
                self.function  = t[2]
            elif len(t) == 8:
                if isinstance(t[4], list):
                    self.relations = t[4]
                else:
                    self.relations = [t[4]]
                self.temporal  = t[6]
                self.function  = t[2]
            t[0] = t[2]

    def p_overlay_relation_operator(self, t):
        # {+}
        # {-, during}
        # {~, during | equal | starts}
        # {^, during | equal | starts, l}
        """
        operator : CLPAREN overlay CRPAREN
                 | CLPAREN overlay COMMA relation     CRPAREN
                 | CLPAREN overlay COMMA relationlist CRPAREN
                 | CLPAREN overlay COMMA relation     COMMA temporal CRPAREN
                 | CLPAREN overlay COMMA relationlist COMMA temporal CRPAREN
        """
        if not self.optype == 'overlay':
            raise SyntaxError("Wrong optype \"%s\" must be \"overlay\""%self.optype)
        else:
            if len(t) == 4:
                # Set three operator components.
                self.relations = ['equal']
                self.temporal  = "l"
                self.function  = t[2]
            elif len(t) == 6:
                if isinstance(t[4], list):
                    self.relations = t[4]
                else:
                    self.relations = [t[4]]
                self.temporal  = "l"
                self.function  = t[2]
            elif len(t) == 8:
                if isinstance(t[4], list):
                    self.relations = t[4]
                else:
                    self.relations = [t[4]]
                self.temporal  = t[6]
                self.function  = t[2]
            t[0] = t[2]

    def p_relation(self, t):
        # The list of relations. Temporal and spatial relations are supported
        """
        relation : EQUAL
                 | FOLLOWS
                 | PRECEDES
                 | OVERLAPS
                 | OVERLAPPED
                 | DURING
                 | STARTS
                 | FINISHES
                 | CONTAINS
                 | STARTED
                 | FINISHED
                 | EQUIVALENT
                 | COVER
                 | OVERLAP
                 | IN
                 | CONTAIN
                 | MEET
        """
        t[0] = t[1]

    def p_over(self, t):
        # The the over keyword
        """
        relation : OVER
        """
        over_list = ["overlaps", "overlapped"]
        t[0] = over_list

    def p_relationlist(self, t):
        # The list of relations.
        """
        relationlist : relation OR relation
                     | relation OR relationlist
        """
        rel_list = []
        rel_list.append(t[1])
        if isinstance(t[3], list):
            rel_list = rel_list + t[3]
        else:
            rel_list.append(t[3])
        t[0] =  rel_list

    def p_temporal_operator(self, t):
        # The list of relations.
        """
        temporal : LEFTREF
                 | RIGHTREF
                 | UNION
                 | DISJOINT
                 | INTERSECT
        """
        t[0] = t[1]

    def p_select_operator(self, t):
        # The list of relations.
        """
        select : T_SELECT
               | T_NOT_SELECT
        """
        t[0] = t[1]

    def p_arithmetic_operator(self, t):
        # The list of relations.
        """
        arithmetic : MOD
                   | DIV
                   | MULT
                   | ADD
                   | SUB
        """
        t[0] = t[1]

    def p_overlay_operator(self, t):
        # The list of relations.
        """
        overlay : AND
                | OR
                | XOR
                | DISOR
                | NOT
        """
        t[0] = t[1]

###############################################################################

if __name__ == "__main__":
    import doctest
    doctest.testmod()
