#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright 2012 The Plaso Project Authors.
# Please see the AUTHORS file for details on individual authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""An extension of the objectfilter to provide plaso specific options."""

import datetime
import logging

from plaso.formatters import manager as formatters_manager

# TODO: Changes this so it becomes an attribute instead of having backend
# load a front-end library.
from plaso.frontend import presets

from plaso.lib import limit
from plaso.lib import objectfilter
from plaso.lib import timelib
from plaso.lib import utils


class DictObject(object):
  """A simple object representing a dict object.

  To filter against an object that is stored as a dictionary the dict
  is converted into a simple object. Since keys can contain spaces
  and/or other symbols they are stripped out to make filtering work
  like it is another object.

  Example dict:
    {'A value': 234,
     'this (my) key_': 'value',
     'random': True,
    }

  This object would then allow access to object.thismykey that would access
  the key 'this (my) key_' inside the dict.
  """

  def __init__(self, dict_object):
    """Initialize the object and build a secondary dict."""
    # TODO: Move some of this code to a more value typed system.
    self._dict_object = dict_object

    self._dict_translated = {}
    for key, value in dict_object.items():
      self._dict_translated[self._StripKey(key)] = value

  def _StripKey(self, key):
    """Return a stripped version of the dict key without symbols."""
    try:
      return str(key).lower().translate(None, ' (){}+_=-<>[]')
    except UnicodeEncodeError:
      pass

  def __getattr__(self, attr):
    """Return back entries from the dictionary."""
    if attr in self._dict_object:
      return self._dict_object.get(attr)

    # Special case of getting all the key/value pairs.
    if attr == '__all__':
      ret = []
      for key, value in self._dict_translated.items():
        ret.append(u'{}:{}'.format(key, value))
      return u' '.join(ret)

    test = self._StripKey(attr)
    if test in self._dict_translated:
      return self._dict_translated.get(test)


class PlasoValueExpander(objectfilter.AttributeValueExpander):
  """An expander that gives values based on object attribute names."""

  def __init__(self):
    """Initialize an attribue value expander."""
    super(PlasoValueExpander, self).__init__()
    self._formatters_manager = formatters_manager.EventFormatterManager

  def _GetMessage(self, obj):
    """Return a properly formatted message string."""
    ret = u''

    try:
      ret, _ = self._formatters_manager.GetMessageStrings(obj)
    except KeyError as exception:
      logging.warning(u'Unable to correctly assemble event: {0:s}'.format(
          exception))

    return ret

  def _GetSources(self, obj):
    """Return a properly formatted source strings."""
    try:
      source_short, source_long = self._formatters_manager.GetSourceStrings(obj)
    except KeyError as exception:
      logging.warning(u'Unable to correctly assemble event: {0:s}'.format(
          exception))

    return source_short, source_long

  def _GetValue(self, obj, attr_name):
    ret = getattr(obj, attr_name, None)

    if ret:
      if isinstance(ret, dict):
        ret = DictObject(ret)

      if attr_name == 'tag':
        return ret.tags

      return ret

    # Check if this is a message request and we have a regular EventObject.
    if attr_name == 'message' :
      return self._GetMessage(obj)

    # Check if this is a source_short request.
    if attr_name in ('source', 'source_short'):
      source_short, _ = self._GetSources(obj)
      return source_short

    # Check if this is a source_long request.
    if attr_name in ('source_long', 'sourcetype'):
      _, source_long = self._GetSources(obj)
      return source_long

  def _GetAttributeName(self, path):
    return path[0].lower()


class PlasoExpression(objectfilter.BasicExpression):
  """A Plaso specific expression."""
  # A simple dictionary used to swap attributes so other names can be used
  # to reference some core attributes (implementation specific).
  swap_source = {
    'date': 'timestamp',
    'datetime': 'timestamp',
    'time': 'timestamp',
    'description_long': 'message',
    'description': 'message',
    'description_short': 'message_short',
  }

  def Compile(self, filter_implementation):
    self.attribute = self.swap_source.get(self.attribute, self.attribute)
    arguments = [self.attribute]
    op_str = self.operator.lower()
    operator = filter_implementation.OPS.get(op_str, None)

    if not operator:
      raise objectfilter.ParseError(u'Unknown operator {0:s} provided.'.format(
          self.operator))

    # Plaso specific implementation - if we are comparing a timestamp
    # to a value, we use our specific implementation that compares
    # timestamps in a "human readable" format.
    if self.attribute == 'timestamp':
      args = []
      for arg in self.args:
        args.append(DateCompareObject(arg))
      self.args = args

    for arg in self.args:
      if isinstance(arg, DateCompareObject):
        if 'Less' in str(operator):
          TimeRangeCache.SetUpperTimestamp(arg.data)
        else:
          TimeRangeCache.SetLowerTimestamp(arg.data)
    arguments.extend(self.args)
    expander = filter_implementation.FILTERS['ValueExpander']
    ops = operator(arguments=arguments, value_expander=expander)
    if not self.bool_value:
      if hasattr(ops, 'FlipBool'):
        ops.FlipBool()

    return ops


class ParserList(objectfilter.GenericBinaryOperator):
  """Matches when a parser is inside a predefined list of parsers."""

  def __init__(self, *children, **kwargs):
    """Construct the parser list and retrieve a list of available parsers."""
    super(ParserList, self).__init__(*children, **kwargs)
    self.compiled_list = presets.categories.get(
        self.right_operand.lower(), [])

  def Operation(self, x, unused_y):
    """Return a bool depending on the parser list contains the parser."""
    if self.left_operand != 'parser':
      raise objectfilter.MalformedQueryError(
        u'Unable to use keyword "inlist" for other than parser.')

    if x in self.compiled_list:
      return True

    return False


class PlasoAttributeFilterImplementation(objectfilter.BaseFilterImplementation):
  """Does field name access on the lowercase version of names.

  Useful to only access attributes and properties with Google's python naming
  style.
  """

  FILTERS = {}
  FILTERS.update(objectfilter.BaseFilterImplementation.FILTERS)
  FILTERS.update({'ValueExpander': PlasoValueExpander})
  OPS = objectfilter.OP2FN
  OPS.update({'inlist': ParserList,})


class DateCompareObject(object):
  """A specific class created for date comparison.

  This object takes a date representation, whether that is a direct integer
  datetime object or a string presenting the date, and uses that for comparing
  against timestamps stored in microseconds in in microseconds since
  Jan 1, 1970 00:00:00 UTC.

  This makes it possible to use regular comparison operators for date,
  irrelevant of the format the date comes in, since plaso stores all timestamps
  in the same format, which is an integer/long, it is a simple manner of
  changing the input into the same format (int) and compare that.
  """

  def __init__(self, data):
    """Take a date object and use that for comparison.

    Args:
      data: A string, datetime object or an integer that
      represents the time to compare against. Time should be stored
      as microseconds since UTC in Epoch format.

    Raises:
      ValueError: if the date string is invalid.
    """
    self.text = utils.GetUnicodeString(data)
    if type(data) in (int, long):
      self.data = data
    elif type(data) == float:
      self.data = long(data)
    elif type(data) in (str, unicode):
      try:
        self.data = timelib.Timestamp.FromTimeString(
            utils.GetUnicodeString(data))
      except ValueError as exception:
        raise ValueError(u'Wrongly formatted date string: {0:s} - {1:s}'.format(
            data, exception))
    elif type(data) == datetime.datetime:
      self.data = timelib.Timestamp.FromPythonDatetime(data)
    elif isinstance(DateCompareObject, data):
      self.data = data.data
    else:
      raise ValueError(u'Unsupported type: {0:s}.'.format(type(data)))

  def __cmp__(self, x):
    """A simple comparison operation."""
    try:
      x_date = DateCompareObject(x)
      return cmp(self.data, x_date.data)
    except ValueError:
      return False

  def __le__(self, x):
    """Less or equal comparison."""
    return self.data <= x

  def __ge__(self, x):
    """Greater or equal comparison."""
    return self.data >= x

  def __eq__(self, x):
    """Check if equal."""
    return x == self.data

  def __ne__(self, x):
    """Check if not equal."""
    return x != self.data

  def __str__(self):
    """Return a string representation of the object."""
    return self.text


class BaseParser(objectfilter.Parser):
  """Plaso version of the Parser."""

  expression_cls = PlasoExpression


class TrueObject(object):
  """A simple object that always returns true for all comparison.

  This object is used for testing certain conditions inside filter queries.
  By returning true for all comparisons this object can be used to evaluate
  specific portions of a filter query.
  """

  def __init__(self, txt=''):
    """Save the text object so it can be used when comparing text."""
    self.txt = txt

  def __getattr__(self, unused_attr):
    """Return a TrueObject for every attribute request."""
    return self

  def __eq__(self, unused_x):
    """Return true for tests of equality."""
    return True

  def __gt__(self, unused_x):
    """Return true for checks for greater."""
    return True

  def __ge__(self, unused_x):
    """Return true for checks for greater or equal."""
    return True

  def __lt__(self, unused_x):
    """Return true for checks of less."""
    return True

  def __le__(self, unused_x):
    """Return true for checks of less or equal."""
    return True

  def __ne__(self, unused_x):
    """Return true for all not equal comparisons."""
    return True

  def __iter__(self):
    """Return a generator so a test for the in keyword can be used."""
    yield self

  def __str__(self):
    """Return a string to make regular expression searches possible.

    Returns:
      A string that containes the original query with some of the matches
      expanded, perhaps several times.
    """
    # Regular expressions in pfilter may include the following escapes:
    #     "\\'\"rnbt\.ws":
    txt = self.txt
    if r'\.' in self.txt:
      txt += self.txt.replace(r'\.', ' _ text _ ')

    if r'\b' in self.txt:
      txt += self.txt.replace(r'\b', ' ')

    if r'\s' in self.txt:
      txt += self.txt.replace(r'\s', ' ')

    return txt


class MockTestFilter(object):
  """A mock test filter object used to test certain portion of test queries.

  The logic behind this object is that a single attribute can be isolated
  for comparison. That is to say all calls to attributes will lead to a TRUE
  response, except those attributes that are specifically stated in the
  constructor. This way it is simple to test for instance whether or not
  to include a parser at all, before actually running the tool. The same applies
  to filtering out certain filenames, etc.
  """

  def __init__(self, query, **kwargs):
    """Constructor, only valid attribute is the parser one."""
    self.attributes = kwargs
    self.txt = query

  def __getattr__(self, attr):
    """Return TrueObject for all requests except for stored attributes."""
    if attr in self.attributes:
      return self.attributes.get(attr, None)

    # TODO: Either delete this entire object (MockTestFilter) or implement
    # a false object and return the correct one depending on whether we
    # are looking for a true or negative response (eg "not" keyword included).
    return TrueObject(self.txt)


class TimeRangeCache(object):
  """A class that stores timeranges from filters."""

  @classmethod
  def ResetTimeConstraints(cls):
    """Resets the time constraints."""
    if hasattr(cls, '_lower'):
      del cls._lower
    if hasattr(cls, '_upper'):
      del cls._upper

  @classmethod
  def SetLowerTimestamp(cls, timestamp):
    """Sets the lower bound timestamp."""
    if not hasattr(cls, '_lower'):
      cls._lower = timestamp
      return

    if timestamp < cls._lower:
      cls._lower = timestamp

  @classmethod
  def SetUpperTimestamp(cls, timestamp):
    """Sets the upper bound timestamp."""
    if not hasattr(cls, '_upper'):
      cls._upper = timestamp
      return

    if timestamp > cls._upper:
      cls._upper = timestamp

  @classmethod
  def GetTimeRange(cls):
    """Return the first and last timestamp of filter range."""
    first = getattr(cls, '_lower', 0)
    last = getattr(cls, '_upper', limit.MAX_INT64)

    if first < last:
      return first, last
    else:
      return last, first


def GetMatcher(query, quiet=False):
  """Return a filter match object for a given query."""
  matcher = None
  try:
    parser = BaseParser(query).Parse()
    matcher = parser.Compile(PlasoAttributeFilterImplementation)
  except objectfilter.ParseError as exception:
    if not quiet:
      logging.error(u'Filter <{0:s}> malformed: {1:s}'.format(
          query, exception))

  return matcher
