"""@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:: pycon

    >>> 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']

"""  # noqa: E501

from .ply import lex, yacc


class TemporalOperatorLexer:
    """Lexical analyzer for the GRASS 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 += 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) -> None:
        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 in {"l", "left"}:
            t.value = "l"
            t.type = "LEFTREF"
        elif t.value in {"r", "right"}:
            t.value = "r"
            t.type = "RIGHTREF"
        elif t.value in {"u", "union"}:
            t.value = "u"
            t.type = "UNION"
        elif t.value in {"d", "disjoint"}:
            t.value = "d"
            t.type = "DISJOINT"
        elif t.value in {"i", "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) -> None:
        self.lexer = lex.lex(module=self, debug=0, **kwargs)

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


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


class TemporalOperatorParser:
    """The temporal operator class"""

    def __init__(self) -> None:
        self.lexer = TemporalOperatorLexer()
        self.lexer.build()
        self.parser = yacc.yacc(module=self, debug=False)
        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}
        """
        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 self.optype != "relation":
            raise SyntaxError('Wrong optype "%s" must be "relation"' % self.optype)

        # 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 self.optype != "boolean":
            raise SyntaxError('Wrong optype "%s" must be "boolean"' % self.optype)

        # 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 self.optype != "boolean":
            raise SyntaxError('Wrong optype "%s" must be "boolean"' % self.optype)

        # 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 self.optype != "boolean":
            raise SyntaxError('Wrong optype "%s" must be "boolean"' % self.optype)

        # 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 self.optype != "boolean":
            raise SyntaxError('Wrong optype "%s" must be "relation"' % self.optype)

        # 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 self.optype != "select":
            raise SyntaxError('Wrong optype "%s" must be "select"' % self.optype)

        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 self.optype != "hash":
            raise SyntaxError('Wrong optype "%s" must be "hash"' % self.optype)

        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 self.optype != "raster":
            raise SyntaxError('Wrong optype "%s" must be "raster"' % self.optype)

        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 self.optype != "overlay":
            raise SyntaxError('Wrong optype "%s" must be "overlay"' % self.optype)

        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) -> None:
        # 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) -> None:
        # The over keyword
        """
        relation : OVER
        """
        over_list = ["overlaps", "overlapped"]
        t[0] = over_list

    def p_relationlist(self, t) -> None:
        # 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 += t[3]
        else:
            rel_list.append(t[3])
        t[0] = rel_list

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

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

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

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


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

if __name__ == "__main__":
    import doctest

    doctest.testmod()
