"""The event filter expression parser filter classes."""

import abc
import codecs
import re

from dfdatetime import interface as dfdatetime_interface

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


class Filter:
  """Filter interface.

  Attributes:
    args (list[object]): arguments provided to the filter.
  """

  def __init__(self, arguments=None):
    """Initializes a filter.

    Implementations expanders are provided by subclassing ValueExpander.

    Args:
      arguments (Optional[object]): arguments.
    """
    logger.debug(f'Adding {arguments!s}')

    super().__init__()
    self.args = arguments or []

  def _CopyValueToString(self, value):
    """Copies an event filter value to a string.

    Args:
      value (list|int|bytes|str): value to convert.

    Returns:
      str: string representation of the argument.
    """
    if isinstance(value, list):
      value = [self._CopyValueToString(item) for item in value]
      return ''.join(value)

    if isinstance(value, int):
      value = f'{value:d}'

    if not isinstance(value, str):
      return codecs.decode(value, 'utf8', 'ignore')
    return value

  @abc.abstractmethod
  def Matches(self, event, event_data, event_data_stream, event_tag):
    """Determines if the event, data and tag match the filter.

    Args:
      event (EventObject): event to compare against the filter.
      event_data (EventData): event data to compare against the filter.
      event_data_stream (EventDataStream): event data stream.
      event_tag (EventTag): event tag to compare against the filter.

    Returns:
      bool: True if the event, data and tag match the filter, False otherwise.
    """


class AndFilter(Filter):
  """A filter that performs a boolean AND on the arguments.

  Note that if no conditions are passed, all objects will pass.
  """

  def Matches(self, event, event_data, event_data_stream, event_tag):
    """Determines if the event, data and tag match the filter.

    Args:
      event (EventObject): event to compare against the filter.
      event_data (EventData): event data to compare against the filter.
      event_data_stream (EventDataStream): event data stream.
      event_tag (EventTag): event tag to compare against the filter.

    Returns:
      bool: True if the event, data and tag match the filter, False otherwise.
    """
    for sub_filter in self.args:
      match = sub_filter.Matches(
          event, event_data, event_data_stream, event_tag)
      if not match:
        return False
    return True


class OrFilter(Filter):
  """A filter that performs a boolean OR on the arguments.

  Note that if no conditions are passed, all objects will pass.
  """

  def Matches(self, event, event_data, event_data_stream, event_tag):
    """Determines if the event, data and tag match the filter.

    Args:
      event (EventObject): event to compare against the filter.
      event_data (EventData): event data to compare against the filter.
      event_data_stream (EventDataStream): event data stream.
      event_tag (EventTag): event tag to compare against the filter.

    Returns:
      bool: True if the event, data and tag match the filter, False otherwise.
    """
    if not self.args:
      return True

    for sub_filter in self.args:
      match = sub_filter.Matches(
          event, event_data, event_data_stream, event_tag)
      if match:
        return True
    return False


class Operator(Filter):
  """Interface for filters that represent operators."""

  @abc.abstractmethod
  def Matches(self, event, event_data, event_data_stream, event_tag):
    """Determines if the event, data and tag match the filter.

    Args:
      event (EventObject): event to compare against the filter.
      event_data (EventData): event data to compare against the filter.
      event_data_stream (EventDataStream): event data stream.
      event_tag (EventTag): event tag to compare against the filter.

    Returns:
      bool: True if the event, data and tag match the filter, False otherwise.
    """


class IdentityFilter(Operator):
  """A filter which always evaluates to True."""

  def Matches(self, event, event_data, event_data_stream, event_tag):
    """Determines if the event, data and tag match the filter.

    Args:
      event (EventObject): event to compare against the filter.
      event_data (EventData): event data to compare against the filter.
      event_data_stream (EventDataStream): event data stream.
      event_tag (EventTag): event tag to compare against the filter.

    Returns:
      bool: True if the event, data and tag match the filter, False otherwise.
    """
    return True


class BinaryOperator(Operator):
  """Interface for binary operators.

  Attributes:
    left_operand (object): left hand operand.
    right_operand (object): right hand operand.
  """

  def __init__(self, arguments=None, **kwargs):
    """Initializes a binary operator.

    Args:
      arguments (Optional[list[str, object]]): operands of the filter.

    Raises:
      InvalidNumberOfOperands: if the number of operands provided is not
          supported.
    """
    number_of_arguments = len(arguments)
    if number_of_arguments != 2:
      class_name = self.__class__.__name__
      raise errors.InvalidNumberOfOperands(
          f'{class_name:s} only supports 2 operands, provided were '
          f'{number_of_arguments:d} operands.')

    super().__init__(arguments=arguments, **kwargs)
    self.left_operand = arguments[0]
    self.right_operand = arguments[1]

  @abc.abstractmethod
  def Matches(self, event, event_data, event_data_stream, event_tag):
    """Determines if the event, data and tag match the filter.

    Args:
      event (EventObject): event to compare against the filter.
      event_data (EventData): event data to compare against the filter.
      event_data_stream (EventDataStream): event data stream.
      event_tag (EventTag): event tag to compare against the filter.

    Returns:
      bool: True if the event, data and tag match the filter, False otherwise.
    """


class GenericBinaryOperator(BinaryOperator):
  """Shared functionality for common binary operators."""

  # Attributes that are stored in the event attribute container.
  _EVENT_ATTRIBUTE_NAMES = frozenset(['timestamp', 'timestamp_desc'])

  _UNSUPPORTED_ATTRIBUTE_NAMES = frozenset([
      'message', 'parser', 'source', 'source_long', 'source_short',
      'sourcetype'])

  def __init__(self, arguments=None, **kwargs):
    """Initializes a generic binary operator.

    Args:
      arguments (Optional[list[str, object]]): operands of the filter.
    """
    super().__init__(arguments=arguments, **kwargs)
    self._bool_value = True

  @abc.abstractmethod
  def _CompareValue(self, event_value, filter_value):
    """Compares two values with the operator.

    Args:
      event_value (object): value retrieved from the event.
      filter_value (object): value defined by the filter.

    Returns:
      bool: True if the values match according to the operator, False otherwise.
    """

  def _GetValue(
      self, attribute_name, event, event_data, event_data_stream, event_tag):
    """Retrieves the value of a specific event, data or tag attribute.

    Args:
      attribute_name (str): name of the attribute to retrieve the value from.
      event (EventObject): event to retrieve the value from.
      event_data (EventData): event data to retrieve the value from.
      event_data_stream (EventDataStream): event data stream.
      event_tag (EventTag): event tag to retrieve the value from.

    Returns:
      object: attribute value or None if not available.
    """
    if attribute_name in self._UNSUPPORTED_ATTRIBUTE_NAMES:
      logger.warning(
          f'Expansion of {attribute_name:s} in event filter no longer '
          f'supported')

    if attribute_name in self._EVENT_ATTRIBUTE_NAMES:
      attribute_value = getattr(event, attribute_name, None)
      if attribute_value is not None:
        # Make sure timestamp attribute values are (dfDateTime) date time
        # objects.
        # TODO: remove when timestamp values are (de)serialized as dfDateTime
        # objects.
        if attribute_name == 'timestamp' and not isinstance(attribute_value, (
            dfdatetime_interface.DateTimeValues,
            value_types.DateTimeValueType)):
          attribute_value = value_types.DateTimeValueType(attribute_value)

    elif (event_data_stream and
          attribute_name in event_data_stream.GetAttributeNames()):
      attribute_value = getattr(event_data_stream, attribute_name, None)

    elif attribute_name == 'tag':
      attribute_value = getattr(event_tag, 'labels', None)

    else:
      attribute_value = getattr(event_data, attribute_name, None)

    return attribute_value

  def FlipBool(self):
    """Negates the internal boolean value attribute."""
    logger.debug('Negative matching.')
    self._bool_value = not self._bool_value

  def Matches(self, event, event_data, event_data_stream, event_tag):
    """Determines if the event, data and tag match the filter.

    Args:
      event (EventObject): event to compare against the filter.
      event_data (EventData): event data to compare against the filter.
      event_data_stream (EventDataStream): event data stream.
      event_tag (EventTag): event tag to compare against the filter.

    Returns:
      bool: True if the event, data and tag match the filter, False otherwise.
    """
    value = self._GetValue(
        self.left_operand, event, event_data, event_data_stream, event_tag)

    if value and self._CompareValue(value, self.right_operand):
      return self._bool_value
    return not self._bool_value


class EqualsOperator(GenericBinaryOperator):
  """Equals (==) operator."""

  def _CompareValue(self, event_value, filter_value):
    """Compares if two values are equal.

    Args:
      event_value (object): value retrieved from the event.
      filter_value (object): value defined by the filter.

    Returns:
      bool: True if the values are equal, False otherwise.
    """
    return event_value == filter_value


class NotEqualsOperator(GenericBinaryOperator):
  """Not equals (!=) operator."""

  def _CompareValue(self, event_value, filter_value):
    """Compares if two values are not equal.

    Args:
      event_value (object): value retrieved from the event.
      filter_value (object): value defined by the filter.

    Returns:
      bool: True if the values are not equal, False otherwise.
    """
    return event_value != filter_value


class LessThanOperator(GenericBinaryOperator):
  """Less than (<) operator."""

  def _CompareValue(self, event_value, filter_value):
    """Compares if the event value is less than the second.

    Args:
      event_value (object): value retrieved from the event.
      filter_value (object): value defined by the filter.

    Returns:
      bool: True if the event value is less than the second, False otherwise.
    """
    return event_value < filter_value


class LessEqualOperator(GenericBinaryOperator):
  """Less than or equals (<=) operator."""

  def _CompareValue(self, event_value, filter_value):
    """Compares if the event value is less than or equals the second.

    Args:
      event_value (object): value retrieved from the event.
      filter_value (object): value defined by the filter.

    Returns:
      bool: True if the event value is than or equals the second, False
          otherwise.
    """
    return event_value <= filter_value


class GreaterThanOperator(GenericBinaryOperator):
  """Greater than (>) operator."""

  def _CompareValue(self, event_value, filter_value):
    """Compares if the event value is greater than the second.

    Args:
      event_value (object): value retrieved from the event.
      filter_value (object): value defined by the filter.

    Returns:
      bool: True if the event value is greater than the second, False otherwise.
    """
    return event_value > filter_value


class GreaterEqualOperator(GenericBinaryOperator):
  """Greater than or equals (>=) operator."""

  def _CompareValue(self, event_value, filter_value):
    """Compares if the event value is greater than or equals the second.

    Args:
      event_value (object): value retrieved from the event.
      filter_value (object): value defined by the filter.

    Returns:
      bool: True if the event value is greater than or equals the second, False
          otherwise.
    """
    return event_value >= filter_value


class Contains(GenericBinaryOperator):
  """Operator to determine if a value contains another value."""

  def _CompareValue(self, event_value, filter_value):
    """Compares if the second value is part of the first.

    Note that this method will do a case insensitive comparison if the first
    value is a string.

    Args:
      event_value (object): value retrieved from the event.
      filter_value (object): value defined by the filter.

    Returns:
      bool: True if the second value is part of the first, False otherwise.
    """
    try:
      if isinstance(filter_value, artifacts.PathArtifact):
        return filter_value.ContainedIn(event_value)

      if isinstance(event_value, str):
        return filter_value.lower() in event_value.lower()

      return filter_value in event_value
    except (AttributeError, TypeError):
      return False


# TODO: Change to an N-ary Operator?
class InSet(GenericBinaryOperator):
  """Operator to determine if a value is part of another value."""

  def _CompareValue(self, event_value, filter_value):
    """Compares if the event value is part of the second.

    Note that this method will do a case insensitive string comparison if
    the event value is a string.

    Args:
      event_value (object): value retrieved from the event.
      filter_value (object): value defined by the filter.

    Returns:
      bool: True if the event value is part of the second, False otherwise.
    """
    if event_value in filter_value:
      return True

    # event_value might be an iterable
    # first we need to skip strings or we'll do silly things
    if isinstance(event_value, (bytes, str)):
      return False

    try:
      for value in event_value:
        if value not in filter_value:
          return False
      return True
    except TypeError:
      return False


# TODO: is GenericBinaryOperator the most suitable super class here?
# Would BinaryOperator be a better fit?
class Regexp(GenericBinaryOperator):
  """Operator to determine if a value matches a regular expression.

  Attributes:
    compiled_re (???): compiled regular expression.
  """

  def __init__(self, arguments=None, **kwargs):
    """Initializes a regular expression operator.

    This operator uses case sensitive comparison.

    Args:
      arguments (Optional[object]): operands of the filter.

    Raises:
      ValueError: if the regular expression is malformed.
    """
    super().__init__(arguments=arguments, **kwargs)

    # Note that right_operand is not necessarily a string.
    logger.debug(f'Compiled: {self.right_operand!s}')

    try:
      expression = self._CopyValueToString(self.right_operand)
      compiled_re = re.compile(expression, re.DOTALL)
    except re.error:
      raise ValueError(
          f'Regular expression "{self.right_operand!s}" is malformed.')

    self.compiled_re = compiled_re

  def _CompareValue(self, event_value, filter_value):
    """Compares if the event value matches a regular expression.

    Args:
      event_value (object): value retrieved from the event.
      filter_value (object): value defined by the filter.

    Returns:
      bool: True if the event value matches the regular expression, False
          otherwise.
    """
    try:
      string_value = self._CopyValueToString(event_value)
      if self.compiled_re.search(string_value):
        return True
    except TypeError:
      pass

    return False


class RegexpInsensitive(Regexp):
  """Operator to determine if a value matches a regular expression."""

  def __init__(self, arguments=None, **kwargs):
    """Initializes a regular expression operator.

    This operator uses case insensitive comparison.

    Args:
      arguments (Optional[object]): operands of the filter.

    Raises:
      ValueError: if the regular expression is malformed.
    """
    super().__init__(arguments=arguments, **kwargs)

    # Note that right_operand is not necessarily a string.
    logger.debug(f'Compiled: {self.right_operand!s}')

    try:
      expression = self._CopyValueToString(self.right_operand)
      compiled_re = re.compile(expression, re.I | re.DOTALL)
    except re.error:
      raise ValueError(
          f'Regular expression "{self.right_operand!s}" is malformed.')

    self.compiled_re = compiled_re
