"""Event filter expression parser."""

import binascii
import codecs
import re

from dfdatetime import posix_time as dfdatetime_posix_time
from dfdatetime import time_elements as dfdatetime_time_elements

from plaso.containers import artifacts
from plaso.filters import expressions
from plaso.filters import logger
from plaso.lib import errors


class Token:
    """An event filter expression parser token.

    Attributes:
      actions (list[str]): list of method names in the EventFilterExpressionParser
          to call.
      next_state (str): next state we transition to if this Token matches.
      state (str): parser state within the token should be applied or None if
          the token should be applied regardless of the parser state.
    """

    _ACTION_SEPARATOR = ","

    def __init__(self, state, regex, actions, next_state):
        """Initializes an event filter expressions parser token.

        Args:
          state (str): parser state within the token should be applied or None if
              the token should be applied regardless of the parser state.
          regex (str): regular expression to try and match from the current point.
          actions (list[str]): list of method names in the
              EventFilterExpressionParser to call.
          next_state (str): next state we transition to if this Token matches.
        """
        super().__init__()
        self._regex = re.compile(regex, re.DOTALL | re.I | re.M | re.S | re.U)
        self.actions = []
        self.next_state = next_state
        self.state = state

        if actions:
            self.actions = actions.split(self._ACTION_SEPARATOR)

    def CompareExpression(self, expression):
        """Compares the token against an expression string.

        Args:
          expression (str): expression string.

        Returns:
          re.Match: the regular expression match object if the expression string
              matches the token or None if no match.
        """
        return self._regex.match(expression)


class EventFilterExpressionParser:
    """Event filter expression parser.

    Examples of valid syntax:
      size is 40
      (name contains "Program Files" AND hash.md5 is "123abc")
      @imported_modules (num_symbols = 14 AND symbol.name is "FindWindow")
    """

    _OPERATORS_WITH_NEGATION = frozenset(["contains", "equals", "inset", "is"])

    _STATE_ARGUMENT = "ARGUMENT"
    _STATE_ATTRIBUTE = "ATTRIBUTE"
    _STATE_BINARY_OPERATOR = "BINARY"
    _STATE_DATETIME = "DATETIME"
    _STATE_NEGATION_OPERATOR = "CHECKNOT"
    _STATE_CONTINUE = "CONTINUE"
    _STATE_INITIAL = "INITIAL"
    _STATE_OPERATOR = "OPERATOR"
    _STATE_PATH = "PATH"
    _STATE_STRING_DOUBLE_QUOTE = "STRING_DOUBLE_QUOTE"
    _STATE_STRING_SINGLE_QUOTE = "STRING_SINGLE_QUOTE"

    _TOKENS = [
        # Operators and related tokens
        Token(_STATE_INITIAL, r"[^\s\(\)]", "_PushState,_PushBack", _STATE_ATTRIBUTE),
        Token(_STATE_INITIAL, r"\(", "_PushState,_AddBracketOpen", None),
        Token(_STATE_INITIAL, r"\)", "_AddBracketClose", _STATE_BINARY_OPERATOR),
        # Double quoted string
        Token(_STATE_STRING_DOUBLE_QUOTE, '"', "_PopState,_StringFinish", None),
        Token(_STATE_STRING_DOUBLE_QUOTE, r"\\x(..)", "HexEscape", None),
        Token(_STATE_STRING_DOUBLE_QUOTE, r"\\(.)", "_StringEscape", None),
        Token(_STATE_STRING_DOUBLE_QUOTE, r'[^\\"]+', "_StringExpand", None),
        # Single quoted string
        Token(_STATE_STRING_SINGLE_QUOTE, "'", "_PopState,_StringFinish", None),
        Token(_STATE_STRING_SINGLE_QUOTE, r"\\x(..)", "HexEscape", None),
        Token(_STATE_STRING_SINGLE_QUOTE, r"\\(.)", "_StringEscape", None),
        Token(_STATE_STRING_SINGLE_QUOTE, r"[^\\\']+", "_StringExpand", None),
        # Date and time definition
        Token(_STATE_DATETIME, r"\)", "_PopState,_AddArgumentDateTime", None),
        Token(_STATE_DATETIME, r"(\d+)", "_SetDateTimeDecimalInteger", _STATE_DATETIME),
        Token(
            _STATE_DATETIME, '"', "_PushState,_StringStart", _STATE_STRING_DOUBLE_QUOTE
        ),
        Token(
            _STATE_DATETIME, "'", "_PushState,_StringStart", _STATE_STRING_SINGLE_QUOTE
        ),
        # Path definition
        Token(_STATE_PATH, r"\)", "_PopState,_AddArgumentPath", None),
        Token(_STATE_PATH, '"', "_PushState,_StringStart", _STATE_STRING_DOUBLE_QUOTE),
        Token(_STATE_PATH, "'", "_PushState,_StringStart", _STATE_STRING_SINGLE_QUOTE),
        # Basic expression
        Token(_STATE_ATTRIBUTE, r"[\w._0-9]+", "_SetAttribute", _STATE_OPERATOR),
        Token(_STATE_OPERATOR, "not ", "_NegateExpression", None),
        Token(
            _STATE_OPERATOR, r"(\w+|[<>!=]=?)", "_SetOperator", _STATE_NEGATION_OPERATOR
        ),
        Token(_STATE_NEGATION_OPERATOR, "not", "_NegateExpression", _STATE_ARGUMENT),
        Token(_STATE_NEGATION_OPERATOR, r"\s+", None, None),
        Token(_STATE_NEGATION_OPERATOR, r"([^not])", "_PushBack", _STATE_ARGUMENT),
        Token(
            _STATE_ARGUMENT, r"(\d+\.\d+)", "_AddArgumentFloatingPoint", _STATE_ARGUMENT
        ),
        Token(
            _STATE_ARGUMENT,
            r"(0x\d+)",
            "_AddArgumentHexadecimalInteger",
            _STATE_ARGUMENT,
        ),
        Token(_STATE_ARGUMENT, r"(\d+)", "_AddArgumentDecimalInteger", _STATE_ARGUMENT),
        Token(
            _STATE_ARGUMENT, '"', "_PushState,_StringStart", _STATE_STRING_DOUBLE_QUOTE
        ),
        Token(
            _STATE_ARGUMENT, "'", "_PushState,_StringStart", _STATE_STRING_SINGLE_QUOTE
        ),
        Token(_STATE_ARGUMENT, r"DATETIME\(", "_PushState", _STATE_DATETIME),
        Token(_STATE_ARGUMENT, r"PATH\(", "_PushState", _STATE_PATH),
        # When the last parameter from arg_list has been pushed
        # State where binary operators are supported (AND, OR)
        Token(
            _STATE_BINARY_OPERATOR,
            r"(?i)(and|or|\&\&|\|\|)",
            "_AddBinaryOperator",
            _STATE_INITIAL,
        ),
        # - We can also skip spaces
        Token(_STATE_BINARY_OPERATOR, r"\s+", None, None),
        # - But if it's not "and" or just spaces we have to go back
        Token(_STATE_BINARY_OPERATOR, ".", "_PushBack,_PopState", None),
        # Skip whitespace.
        Token(None, r"\s+", None, None),
    ]

    def __init__(self):
        """Initializes an event filter expression parser."""
        super().__init__()
        self._buffer = ""
        self._datetime_value = None
        self._current_expression = None
        self._error = 0
        self._flags = 0
        self._have_negate_keyword = False
        self._processed_buffer = ""
        self._stack = []
        self._state = self._STATE_INITIAL
        self._state_stack = []
        self._string = None

    # The parser token callback methods use a specific function interface.
    # pylint: disable=redundant-returns-doc,useless-return

    def _AddArgument(self, value):
        """Adds an argument to the current expression.

        Args:
          value (object): argument value.

        Returns:
          str: state or None if the argument could not be added to the current
              expression.

        Raises:
          ParseError: if the operator does not support negation.
        """
        logger.debug(f"Storing argument: {value!s}")

        if self._have_negate_keyword:
            operator = self._current_expression.operator
            if operator and operator.lower() not in self._OPERATORS_WITH_NEGATION:
                raise errors.ParseError(
                    f"Operator: {operator:s} does not support negation (not)."
                )

        # This expression is complete
        if self._current_expression.AddArgument(value):
            self._stack.append(self._current_expression)
            self._current_expression = expressions.EventExpression()
            # We go to the BINARY state, to find if there's an AND or OR operator
            return self._STATE_BINARY_OPERATOR

        return None

    def _AddArgumentDateTime(self, **unused_kwargs):
        """Adds a date and time argument to the current expression.

        Note that this function is used as a callback by _GetNextToken.

        Returns:
          str: state or None if the argument could not be added to the current
              expression.

        Raises:
          ParseError: if datetime value does not contain a valid POSIX timestamp
              in microseconds or ISO 8601 date and time string.
        """
        if isinstance(self._datetime_value, int):
            date_time = dfdatetime_posix_time.PosixTimeInMicroseconds(
                timestamp=self._datetime_value
            )

        elif isinstance(self._datetime_value, str):
            try:
                date_time = dfdatetime_time_elements.TimeElementsInMicroseconds()
                date_time.CopyFromStringISO8601(self._datetime_value)
            except ValueError:
                raise errors.ParseError(
                    f"unsupported ISO 8601 string: {self._datetime_value:s}."
                )

        else:
            raise errors.ParseError(
                f"unsupported datetime value: {self._datetime_value!s}."
            )

        self._datetime_value = None
        return self._AddArgument(date_time)

    def _AddArgumentDecimalInteger(self, string="", **unused_kwargs):
        """Adds a decimal integer argument to the current expression.

        Note that this function is used as a callback by _GetNextToken.

        Args:
          string (Optional[str]): argument string that contains an integer value
              formatted in decimal.

        Returns:
          str: state or None if the argument could not be added to the current
              expression.

        Raises:
          ParseError: if string does not contain a valid integer.
        """
        try:
            int_value = int(string)
        except (TypeError, ValueError):
            raise errors.ParseError(f"{string:s} is not a valid integer.")
        return self._AddArgument(int_value)

    def _AddArgumentFloatingPoint(self, string="", **unused_kwargs):
        """Adds a floating-point argument to the current expression.

        Note that this function is used as a callback by _GetNextToken.

        Args:
          string (Optional[str]): argument string that contains a floating-point
              value.

        Returns:
          str: state or None if the argument could not be added to the current
              expression.

        Raises:
          ParseError: if string does not contain a valid floating-point number.
        """
        try:
            float_value = float(string)
        except (TypeError, ValueError):
            raise errors.ParseError(f"{string:s} is not a valid float.")
        return self._AddArgument(float_value)

    def _AddArgumentHexadecimalInteger(self, string="", **unused_kwargs):
        """Adds a hexadecimal integer argument to the current expression.

        Note that this function is used as a callback by _GetNextToken.

        Args:
          string (Optional[str]): argument string that contains an integer value
              formatted in hexadecimal.

        Returns:
          str: state or None if the argument could not be added to the current
              expression.

        Raises:
          ParseError: if string does not contain a valid base16 formatted integer.
        """
        try:
            int_value = int(string, 16)
        except (TypeError, ValueError):
            raise errors.ParseError(f"{string:s} is not a valid base16 integer.")
        return self._AddArgument(int_value)

    def _AddArgumentPath(self, **unused_kwargs):
        """Adds a path argument to the current expression.

        Note that this function is used as a callback by _GetNextToken.

        Returns:
          str: state or None if the argument could not be added to the current
              expression.
        """
        value = artifacts.PathArtifact(path=self._string)
        return self._AddArgument(value)

    def _AddBinaryOperator(self, string=None, **unused_kwargs):
        """Adds a binary operator to the stack.

        Note that this function is used as a callback by _GetNextToken.

        Args:
          string (str): operator, such as "and", "or", "&&" or "||".

        Returns:
          str: next state, which is None.
        """
        expression = expressions.BinaryExpression(operator=string)
        self._stack.append(expression)

        return None

    def _AddBracketClose(self, **unused_kwargs):
        """Adds a closing bracket to the stack.

        Note that this function is used as a callback by _GetNextToken.

        Returns:
          str: next state, which is None.
        """
        self._stack.append(")")

        return None

    def _AddBracketOpen(self, **unused_kwargs):
        """Adds an opening bracket to the stack.

        Note that this function is used as a callback by _GetNextToken.

        Returns:
          str: next state, which is None.
        """
        self._stack.append("(")

        return None

    def _CombineBinaryExpressions(self, operator):
        """Combines the binary expressions on the stack.

        Args:
          operator (str): operator, such as "and" or "or".
        """
        operator_lower = operator.lower()

        item_index = 1
        number_of_items = len(self._stack) - 1
        while item_index < number_of_items:
            item = self._stack[item_index]
            if (
                isinstance(item, expressions.BinaryExpression)
                and item.operator.lower() == operator_lower
                and not item.args
            ):
                previous_item = self._stack[item_index - 1]
                next_item = self._stack[item_index + 1]

                if isinstance(previous_item, expressions.Expression) and isinstance(
                    next_item, expressions.Expression
                ):
                    item.AddOperands(previous_item, next_item)

                    self._stack.pop(item_index + 1)
                    self._stack.pop(item_index - 1)

                    item_index -= 2
                    number_of_items -= 2

            item_index += 1
            if item_index == 0:
                item_index += 1

    def _CombineParenthesis(self):
        """Combines parenthesis (braces) expressions on the stack."""
        item_index = 1
        number_of_items = len(self._stack) - 1
        while item_index < number_of_items:
            item = self._stack[item_index]
            previous_item = self._stack[item_index - 1]
            next_item = self._stack[item_index + 1]

            if (
                previous_item == "("
                and next_item == ")"
                and isinstance(item, expressions.Expression)
            ):
                self._stack.pop(item_index + 1)
                self._stack.pop(item_index - 1)

                item_index -= 2
                number_of_items -= 2

            item_index += 1
            if item_index == 0:
                item_index += 1

    def _GetNextToken(self):
        """Determines the next parser token based on the expression.

        Returns:
          Token: parser token or None if the buffer is empty.

        Raises:
          ParseError: if no token matched the expression.
        """
        if not self._buffer:
            return None

        supported_states = (None, self._state)
        for token in self._TOKENS:
            if token.state not in supported_states:
                continue

            match = token.CompareExpression(self._buffer)
            if not match:
                continue

            # The match consumes the data off the buffer (the handler can put it back
            # if it likes)
            match_end_offset = match.end()
            match_buffer = self._buffer[:match_end_offset]
            self._buffer = self._buffer[match_end_offset:]
            self._processed_buffer = "".join([self._processed_buffer, match_buffer])

            next_state = token.next_state
            for action in token.actions:
                callback = getattr(self, action, self._NoOperation)

                # Allow a callback to skip other callbacks.
                possible_next_state = callback(string=match.group(0), match=match)
                if possible_next_state == self._STATE_CONTINUE:
                    continue

                # Override the state from the Token
                if possible_next_state:
                    next_state = possible_next_state

            # Update the next state
            if next_state:
                self._state = next_state

            return token

        number_of_bytes = len(self._processed_buffer)
        raise errors.ParseError(
            f"No token match for parser state: {self._state:s} at position "
            f"{number_of_bytes:d}: {self._processed_buffer:s} <---> "
            f"{self._buffer:s}"
        )

    def _NegateExpression(self, **unused_kwargs):
        """Reverses the logic of (negates) the current expression.

        Raises:
          ParseError: when the negation keyword (not) is expressed more than once,
              used after an argument or before an operator that does not support
              negation.
        """
        if self._have_negate_keyword:
            raise errors.ParseError(
                "Negation keyword (not) can only be expressed once."
            )

        if self._current_expression.args:
            raise errors.ParseError(
                "Negation keyword (not) cannot be used after an argument."
            )

        operator = self._current_expression.operator
        if operator and operator.lower() not in self._OPERATORS_WITH_NEGATION:
            raise errors.ParseError(
                f"Operator: {operator:s} does not support negation."
            )

        self._have_negate_keyword = True

        logger.debug("Negating expression")
        self._current_expression.Negate()

    def _NoOperation(self, **kwarg):
        """No operation.

        Note that this function is used as a callback by _GetNextToken.
        """
        logger.debug(f"Default handler: {kwarg!s}")

    def _PopState(self, **unused_kwargs):
        """Pops the previous state from the stack.

        Returns:
          str: next state, which is the previous state on the stack.

        Raises:
          ParseError: if the stack is empty.
        """
        try:
            self._state = self._state_stack.pop()
        except IndexError:
            number_of_bytes = len(self._processed_buffer)
            raise errors.ParseError(
                f"Tried to pop state from an empty stack - possible recursion error "
                f"at position {number_of_bytes:d}: {self._processed_buffer:s} <---> "
                f"{self._buffer:s}"
            )

        logger.debug(f"Returned state to {self._state:s}")
        return self._state

    def _PushBack(self, string="", **unused_kwargs):
        """Pushes the string from processed buffer back onto the buffer.

        Note that this function is used as a callback by _GetNextToken.

        Args:
          string (Optional[str]): string.

        Returns:
          str: next state, which is None.
        """
        self._buffer = "".join([string, self._buffer])
        self._processed_buffer = self._processed_buffer[: -len(string)]

        return None

    def _PushState(self, **unused_kwargs):
        """Pushes the current state on the state stack.

        Note that this function is used as a callback by _GetNextToken.

        Returns:
          str: next state, which is None.
        """
        logger.debug(f"Storing state {self._state:s}")
        self._state_stack.append(self._state)

        return None

    def _Reduce(self):
        """Reduces the expression stack into a single expression.

        Returns:
          Expression: remaining expression on the stack.

        Raises:
          ParseError: if the current state is unsupported or the remaining number
              of items on the stack is not 1.
        """
        if self._state not in (self._STATE_BINARY_OPERATOR, self._STATE_INITIAL):
            number_of_bytes = len(self._processed_buffer)
            raise errors.ParseError(
                f"Unsupported initial state: {self._state:s} - premature end of "
                f"expression at position {number_of_bytes:d}: "
                f"{self._processed_buffer:s} <---> {self._buffer:s}"
            )

        number_of_items = len(self._stack)
        while number_of_items > 1:
            # Precedence order
            self._CombineParenthesis()
            self._CombineBinaryExpressions("and")
            self._CombineBinaryExpressions("or")

            # No change
            if len(self._stack) == number_of_items:
                break

            number_of_items = len(self._stack)

        if number_of_items != 1:
            number_of_bytes = len(self._processed_buffer)
            raise errors.ParseError(
                f"Unsupported event filter expression at position "
                f"{number_of_bytes:d}: {self._processed_buffer:s} <---> "
                f"{self._buffer:s}"
            )

        return self._stack[0]

    def _Reset(self):
        """Resets the parser."""
        self._buffer = ""
        self._current_expression = expressions.EventExpression()
        self._error = 0
        self._flags = 0
        self._have_negate_keyword = False
        self._processed_buffer = ""
        self._stack = []
        self._state = self._STATE_INITIAL
        self._state_stack = []
        self._string = None

    def _SetAttribute(self, string="", **unused_kwargs):
        """Sets the attribute in the current expression.

        Note that this function is used as a callback by _GetNextToken.

        Args:
          string (Optional[str]): attribute.

        Returns:
          str: next state, which is the operator state.
        """
        logger.debug(f"Storing attribute {string:s}")

        self._current_expression.SetAttribute(string)

        self._have_negate_keyword = False

        return self._STATE_OPERATOR

    def _SetDateTimeDecimalInteger(self, string="", **unused_kwargs):
        """Sets a decimal integer argument to the datetime value.

        Note that this function is used as a callback by _GetNextToken.

        Args:
          string (Optional[str]): argument string that contains an integer value
              formatted in decimal.

        Returns:
          str: state.

        Raises:
          ParseError: if string does not contain a valid integer.
        """
        try:
            self._datetime_value = int(string)
        except (TypeError, ValueError):
            raise errors.ParseError(f"{string:s} is not a valid integer.")

        return self._STATE_DATETIME

    def _SetOperator(self, string="", **unused_kwargs):
        """Sets the operator in the current expression.

        Note that this function is used as a callback by _GetNextToken.

        Args:
          string (Optional[str]): operator.

        Returns:
          str: next state, which is None.
        """
        logger.debug(f"Storing operator {repr(string)!s}")
        self._current_expression.SetOperator(string)

        return None

    def _StringEscape(self, string="", match="", **unused_kwargs):
        """Escapes backslashes found inside an expression string.

        Backslashes followed by anything other than [\\'"rnbt.ws] will raise
        an Error.

        Note that this function is used as a callback by _GetNextToken.

        Args:
          string (Optional[str]): expression string.
          match (Optional[re.MatchObject]): the regular expression match object,
              where match.group(1) contains the escaped code.

        Returns:
          str: next state, which is None.

        Raises:
          ParseError: when the escaped string is not one of [\\'"rnbt].
        """
        if match.group(1) not in "\\'\"rnbt\\.ws":
            raise errors.ParseError(f"Invalid escape character {string:s}.")

        decoded_string = codecs.decode(string, "unicode_escape")
        return self._StringExpand(string=decoded_string)

    def _StringExpand(self, string="", **unused_kwargs):
        """Expands the internal string with the expression string.

        Note that this function is used as a callback by _GetNextToken.

        Args:
          string (Optional[str]): expression string.

        Returns:
          str: next state, which is None.
        """
        self._string = "".join([self._string, string])

        return None

    def _StringFinish(self, **unused_kwargs):
        """Finishes parsing a string.

        Note that this function is used as a callback by _GetNextToken.

        Returns:
          str: next state, or None when the state is not the attribute or argument
              state.
        """
        if self._state == self._STATE_ATTRIBUTE:
            return self._SetAttribute(string=self._string)

        if self._state == self._STATE_ARGUMENT:
            return self._AddArgument(self._string)

        if self._state == self._STATE_DATETIME:
            self._datetime_value = self._string
            return self._STATE_DATETIME

        if self._state == self._STATE_PATH:
            return self._STATE_PATH

        return None

    def _StringStart(self, **unused_kwargs):
        """Initializes parsing a string.

        Note that this function is used as a callback by _GetNextToken.

        Returns:
          str: next state, which is None.
        """
        self._string = ""

        return None

    def HexEscape(self, string, match, **unused_kwargs):
        """Converts a hex escaped string.

        Note that this function is used as a callback by _GetNextToken.

        Returns:
          str: next state, which is None.

        Raises:
          ParseError: if the string is not hex escaped.
        """
        logger.debug(f"HexEscape matched {string:s}.")
        hex_string = match.group(1)
        try:
            hex_string = binascii.unhexlify(hex_string)
            hex_string = hex_string.decode("utf-8")
            self._string += hex_string
        except (TypeError, binascii.Error):
            raise errors.ParseError(f"Invalid hex escape {hex_string!s}.")

        return None

    def Parse(self, expression):
        """Parses an event filter expression.

        Args:
          expression (str): event filter expression.

        Returns:
          Expression: expression.
        """
        if not expression:
            return expressions.IdentityExpression()

        self._Reset()
        self._buffer = expression

        token = self._GetNextToken()
        while token:
            token = self._GetNextToken()

        return self._Reduce()
