#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright 2014 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.
"""This file contains SkyDrive error log file parser in plaso."""

import logging

import pyparsing

from plaso.events import time_events
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import manager
from plaso.parsers import text_parser


__author__ = 'Francesco Picasso (francesco.picasso@gmail.com)'


class SkyDriveLogErrorEvent(time_events.TimestampEvent):
  """Convenience class for a SkyDrive error log line event."""
  DATA_TYPE = 'skydrive:error:line'

  def __init__(self, timestamp, module, source_code, text, detail):
    """Initializes the event object.

    Args:
      timestamp: Milliseconds since epoch in UTC.
      module: The module name that generated the log line.
      source_code: Logging source file and line number.
      text: The error text message.
      detail: The error details.
    """
    super(SkyDriveLogErrorEvent, self).__init__(
        timestamp, eventdata.EventTimestamp.ADDED_TIME)
    self.module = module
    self.source_code = source_code
    self.text = text
    self.detail = detail


class SkyDriveLogErrorParser(text_parser.PyparsingMultiLineTextParser):
  """Parse SkyDrive error log files."""

  NAME = 'skydrive_log_error'
  DESCRIPTION = u'Parser for OneDrive (or SkyDrive) error log files.'

  ENCODING = 'utf-8'

  # Common SDE (SkyDriveError) structures.
  INTEGER_CAST = text_parser.PyParseIntCast
  HYPHEN = text_parser.PyparsingConstants.HYPHEN
  TWO_DIGITS = text_parser.PyparsingConstants.TWO_DIGITS
  TIME_MSEC = text_parser.PyparsingConstants.TIME_MSEC
  MSEC = pyparsing.Word(pyparsing.nums, max=3).setParseAction(INTEGER_CAST)
  COMMA = pyparsing.Literal(u',').suppress()
  DOT = pyparsing.Literal(u'.').suppress()
  IGNORE_FIELD = pyparsing.CharsNotIn(u',').suppress()

  # Header line timestamp (2013-07-25-160323.291).
  SDE_HEADER_TIMESTAMP = pyparsing.Group(
      text_parser.PyparsingConstants.DATE.setResultsName('date') + HYPHEN +
      TWO_DIGITS.setResultsName('hh') + TWO_DIGITS.setResultsName('mm') +
      TWO_DIGITS.setResultsName('ss') + DOT +
      MSEC.setResultsName('ms')).setResultsName('hdr_timestamp')

  # Line timestamp (07-25-13,16:06:31.820).
  SDE_TIMESTAMP = (
      TWO_DIGITS.setResultsName('month') + HYPHEN +
      TWO_DIGITS.setResultsName('day') + HYPHEN +
      TWO_DIGITS.setResultsName('year_short') + COMMA +
      TIME_MSEC.setResultsName('time')).setResultsName('timestamp')

  # Header start.
  SDE_HEADER_START = (pyparsing.Literal(u'######').suppress() +
      pyparsing.Literal(u'Logging started.').setResultsName('log_start'))

  # Multiline entry end marker, matched from right to left.
  SDE_ENTRY_END = pyparsing.StringEnd() | SDE_HEADER_START | SDE_TIMESTAMP

  # SkyDriveError line pyparsing structure.
  SDE_LINE = (
      SDE_TIMESTAMP + COMMA +
      IGNORE_FIELD + COMMA + IGNORE_FIELD + COMMA + IGNORE_FIELD + COMMA +
      pyparsing.CharsNotIn(u',').setResultsName('module') + COMMA +
      pyparsing.CharsNotIn(u',').setResultsName('source_code') + COMMA +
      IGNORE_FIELD + COMMA + IGNORE_FIELD + COMMA + IGNORE_FIELD + COMMA +
      pyparsing.Optional(pyparsing.CharsNotIn(u',').setResultsName('text')) +
      COMMA + pyparsing.SkipTo(SDE_ENTRY_END).setResultsName('detail'))

  # SkyDriveError header pyparsing structure.
  SDE_HEADER = (
      SDE_HEADER_START +
      pyparsing.Literal(u'Version=').setResultsName('ver_str') +
      pyparsing.Word(pyparsing.nums + u'.').setResultsName('ver_num') +
      pyparsing.Literal(u'StartSystemTime:').suppress() +
      SDE_HEADER_TIMESTAMP +
      pyparsing.Literal(u'StartLocalTime:').setResultsName('lt_str') +
      pyparsing.SkipTo(pyparsing.lineEnd).setResultsName('details'))

  # Define the available log line structures.
  LINE_STRUCTURES = [
      ('logline', SDE_LINE),
      ('header', SDE_HEADER),
  ]

  def __init__(self):
    """Initializes a parser object."""
    super(SkyDriveLogErrorParser, self).__init__()
    self.use_local_zone = False

  def VerifyStructure(self, parser_context, line):
    """Verify that this file is a SkyDrive Error log file.

    Args:
      parser_context: A parser context object (instance of ParserContext).
      line: A single line from the text file.

    Returns:
      True if this is the correct parser, False otherwise.
    """
    try:
      parsed_structure = self.SDE_HEADER.parseString(line)
    except pyparsing.ParseException:
      logging.debug(u'Not a SkyDrive Error log file')
      return False
    timestamp = self._GetTimestampFromHeader(parsed_structure.hdr_timestamp)
    if not timestamp:
      logging.debug(
          u'Not a SkyDrive Error log file, invalid timestamp {0:s}'.format(
              parsed_structure.timestamp))
      return False
    return True

  def ParseRecord(self, parser_context, key, structure):
    """Parse each record structure and return an EventObject if applicable.

    Args:
      parser_context: A parser context object (instance of ParserContext).
      key: An identification string indicating the name of the parsed
           structure.
      structure: A pyparsing.ParseResults object from a line in the
                 log file.

    Returns:
      An event object (instance of EventObject) or None.
    """
    if key == 'logline':
      return self._ParseLine(structure)
    elif key == 'header':
      return self._ParseHeader(structure)
    else:
      logging.warning(
          u'Unable to parse record, unknown structure: {0:s}'.format(key))

  def _ParseLine(self, structure):
    """Parse a logline and store appropriate attributes."""
    timestamp = self._GetTimestampFromLine(structure.timestamp)
    if not timestamp:
      logging.debug(u'SkyDriveLogError invalid timestamp {0:s}'.format(
          structure.timestamp))
      return
    # Replace newlines with spaces in structure.detail to preserve output.
    return SkyDriveLogErrorEvent(
        timestamp, structure.module, structure.source_code,
        structure.text, structure.detail.replace(u'\r\n', u' '))

  def _ParseHeader(self, structure):
    """Parse header lines and store appropriate attributes.

    [u'Logging started.', u'Version=', u'17.0.2011.0627',
    [2013, 7, 25], 16, 3, 23, 291, u'StartLocalTime', u'<details>']

    Args:
      structure: The parsed structure.

    Returns:
      timestamp: The event or none.
    """
    timestamp = self._GetTimestampFromHeader(structure.hdr_timestamp)
    if not timestamp:
      logging.debug(
          u'SkyDriveLogError invalid timestamp {0:d}'.format(
              structure.hdr_timestamp))
      return
    text = u'{0:s} {1:s} {2:s}'.format(
        structure.log_start, structure.ver_str, structure.ver_num)
    detail = u'{0:s} {1:s}'.format(structure.lt_str, structure.details)
    return SkyDriveLogErrorEvent(
        timestamp, None, None, text, detail)

  def _GetTimestampFromHeader(self, structure):
    """Gets a timestamp from the structure.

    The following is an example of the timestamp structure expected
    [[2013, 7, 25], 16, 3, 23, 291]

    Args:
      structure: The parsed structure, which should be a timestamp.

    Returns:
      timestamp: A plaso timelib timestamp event or 0.
    """
    year, month, day = structure.date
    hour = structure.get('hh', 0)
    minute = structure.get('mm', 0)
    second = structure.get('ss', 0)
    microsecond = structure.get('ms', 0) * 1000

    return timelib.Timestamp.FromTimeParts(
        year, month, day, hour, minute, second, microseconds=microsecond)

  def _GetTimestampFromLine(self, structure):
    """Gets a timestamp from string from the structure

    The following is an example of the timestamp structure expected
    [7, 25, 13, [16, 3, 24], 649]

    Args:
      structure: The parsed structure.

    Returns:
      timestamp: A plaso timelib timestamp event or 0.
    """
    hour, minute, second = structure.time[0]
    microsecond = structure.time[1] * 1000
    # TODO: Verify if timestamps are locale dependent.
    year = structure.get('year_short', 0)
    month = structure.get('month', 0)
    day = structure.get('day', 0)
    if year < 0 or not month or not day:
      return 0

    year += 2000

    return timelib.Timestamp.FromTimeParts(
        year, month, day, hour, minute, second, microseconds=microsecond)


manager.ParsersManager.RegisterParser(SkyDriveLogErrorParser)
