#!/usr/bin/python

# Copyright 2015 Huawei Devices USA Inc. All rights reserved.
#
# 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.
#
#
# Authors:
#       Chuk Orakwue <chuk.orakwue@huawei.com>

import os
import sys
import re
import abc
from six import with_metaclass

try:
    from logbook import Logger
except ImportError:
    import logging
    logging.basicConfig()
    from logging import getLogger as Logger

from .parsers import PARSERS
from .task import Task
from .event import Event, EventList
from .common import (
    ConstantBase,
    is_list_like,
    ParserError,
)

__all__ = ['Ftrace']

log = Logger('Ftrace')

class Filetype(ConstantBase):
    UNKNOWN = ()
    FTRACE = ()
    SYSTRACE = ()

#------------------------------------------------------------------------------
# FTraceComponent

class FTraceComponent(with_metaclass(abc.ABCMeta)):
    """Abstract Base Class for FTrace Components APIs"""

    _initialized = False

    def __repr__(self):
        return "{}".format(self.__class__.__name__)

    def _initialize(self):
        raise NotImplementedError

#------------------------------------------------------------------------------
# FTrace

class Ftrace(object):

    _APIS = {}
    _TRACER_PATTERN = re.compile(r"""#\s+tracer:\s+(?P<tracer>.+)""")
    _BUFFER_PATTERN = re.compile(
        r"""
        #\s+entries-in-buffer/entries-written:\s+
        (?P<entries_in>\d+)/(?P<entries_written>\d+)\s+.+
        """,
        re.X)

    _LINE_PATTERN = re.compile(
        r"""
        (?P<name>.+) # task name
        \D
        (?P<pid>\d+) # pid
        \s+
        \D*(?P<tgid>\d*)\D* # tgid
        \s*
        \[(?P<cpu>\d+)\] # cpu#
        \s+
        (?P<irqs_off>[d|X|\.]) # irqs-off
        (?P<need_resched>[N|n|p|\.]) # need-resched
        (?P<irq_type>[H|h|s|.]) # hardirq/softirq
        (?P<preempt_depth>[0-9|.]) # preempt-depth
        \s+
        (?P<timestamp>\d+\W{1}\d+)
        \W{1}\s+
        (?P<tracepoint>\w+) # tracepoint
        \W{1}\s+
        (?P<data>.+) # payload
        """,
        re.X|re.M
    )

    def __init__(self, filepath, tracepoints=None):
        """
        Parser for ftrace output.

        Params:
        -------
        filepath : str
            Path of file to parse
        tracepoints : str or list-like (optional)
            List of tracepoints to parse - nothing more!
        """
        self.filepath = filepath

        self._initial_tps = tracepoints if (is_list_like(tracepoints) or tracepoints is None) else [tracepoints]
        self.filetype = self._check_filetype()

        self.duration = 0.0
        self._raw_start_timestamp = None
        self.events = None
        self.interval = None
        self.tracepoints = set()
        self.seen_cpus = set()

        # tracer metadata
        self.tracer = None
        self.entries_in = 0
        self.entries_written = 0
        self.filedir, self.filename = os.path.split(self.filepath)

        success = self._parse_file()
        if success:
            self.interval = self.events.interval
            self._initiate_apis()

    def __repr__(self):
        return "Trace(filepath={}, tracer={}, lost_entries={})".format(
            self.filepath, self.tracer, self.num_lost_events
            )

    @property
    def buffer_overflowed(self):
        """
        Returns True/False if buffer overflow occurred .
        """
        return self.entries_in == self.entries_written

    @property
    def num_lost_events(self):
        """
        Returns number of lost events
        """
        return self.entries_written - self.entries_in

    def _parse_file(self):
        """
        Parse input file (lazily), return True if successful, False otherwise.
        """
        try:
            self.events = EventList(self._parse_lines())
            return True
        except Exception as e:
            log.exception(e)
            return False

    def _parse_lines(self):
        """
        Parse systrace lines in file.
        """
        num_events = 0
        log.info("Parsing {filename}.".format(filename=self.filename))
        for line in self._line_gen():
            match = re.match(self._LINE_PATTERN, line)
            if match:
                match_dict = match.groupdict()
                match_dict['raw_timestamp'] = float(match_dict['timestamp'])
                match_dict['timestamp'] = float(match_dict['timestamp'])
                if self._raw_start_timestamp is None:
                    self._raw_start_timestamp = match_dict['raw_timestamp']
                # Normalize timestamp
                match_dict['timestamp'] -= self._raw_start_timestamp
                match_dict['task'] = Task(**match_dict)

                parsed_data = self._parse_data(
                    match_dict['tracepoint'],
                    match_dict['data'],
                )
                match_dict['data']= parsed_data
                event = Event(**match_dict)
                # Special treatment, adjust timestamp
                if event.tracepoint in ('bus_update_request'):
                    event = event._replace(data=event.data._replace(timestamp=event.data.timestamp - self._raw_start_timestamp))
                    event = event._replace(timestamp=event.data.timestamp)
                # add to seen cpus
                self.seen_cpus.add(event.cpu)
                if self._initial_tps is None or event.tracepoint in self._initial_tps:
                    self.tracepoints.add(event.tracepoint)
                    yield event
                    num_events +=1
                if num_events % 10000 == 0: # Every 10000 lines, dump
                    sys.stdout.write('.')
                    
        # Properly calculate duration (even if _initial_tps is used)
        self.duration = event.timestamp

    def _line_gen(self):
        """
        Generator that yields ftrace lines in file.
        """
        yield_trace = False
        with open(self.filepath, 'rU') as f:
            num_lines = os.fstat(f.fileno()).st_size
            while True:
                line = f.readline().strip()
                if self.filetype is Filetype.SYSTRACE:
                    line = line.rstrip() # line[:-3]
                if self.tracer is None and 'tracer:' in line:
                    self.tracer = self._check_tracer(line)
                if not (self.entries_in or self.entries_written) and \
                    'entries-in-buffer' in line:
                    self.entries_in, self.entries_written = \
                        self._check_buffer_entries(line)
                if not yield_trace and 'TASK-PID' in line:
                    yield_trace = True
                    _ = f.readline()
                    continue
                if yield_trace:
                    yield line
                if not line and f.tell() == num_lines:
                    break

    def _parse_data(self, tracepoint, data):
        """
        Parse payload(data) for tracepoint - if we have it.
        """
        rv = data
        try:
            rv = PARSERS[tracepoint](data)
        except Exception as e:
            rv = PARSERS[tracepoint](data)
        except ParserError as e:
            log.exception(e)
            log.warn('Error parsing {tp} with {data}'.format(tp=tracepoint, data=data))
        finally:
            return rv if rv else data

    def _check_tracer(self, line):
        """
        Return tracer (typically 'nop')
        """
        match = re.match(self._TRACER_PATTERN, line.strip())
        if match:
            return match.groupdict()['tracer']
        return None

    def _check_buffer_entries(self, line):
        """
        Return tuple of (entries-in-buffer, entries-written)
        """
        match = re.match(self._BUFFER_PATTERN, line)
        if match:
            mgd = match.groupdict()
            return int(mgd['entries_in']), int(mgd['entries_written'])
        return 0, 0

    def _check_filetype(self):
        """
        Return file type.
        """
        if self.filepath.endswith('.html'):
            return Filetype.SYSTRACE
        elif self.filepath.endswith('.txt'):
            return Filetype.FTRACE
        return Filetype.UNKNOWN

    def _initiate_apis(self):
        """Start initialized all registered apis after parsing events in file"""
        for name, cls in self._APIS.items():
            setattr(self, name, cls(self))

def register_api(name):
    """Decorator for registering api methods"""
    def wrapped(cls):
        Ftrace._APIS[name] = cls
        log.info("Registering {name} api to trace".format(name=name))
        return cls
    return wrapped
