# -*- coding: utf-8 -*-
"""
   profiling.tracing
   ~~~~~~~~~~~~~~~~~

   Profiles deterministically by :func:`sys.setprofile`.

   :copyright: (c) 2014-2016, What! Studio
   :license: BSD, see LICENSE for more details.

"""
from __future__ import absolute_import

import sys
import threading

from .timers import Timer
from .. import sortkeys
from ..profiler import Profiler
from ..stats import RecordingStatistics, VoidRecordingStatistics as void
from ..utils import deferral
from ..viewer import fmt, StatisticsTable


__all__ = ['TracingProfiler', 'TracingStatisticsTable']


TIMER_CLASS = Timer


class TracingStatisticsTable(StatisticsTable):

    columns = [
        ('FUNCTION', 'left', ('weight', 1), sortkeys.by_function),
        ('CALLS', 'right', (6,), sortkeys.by_own_hits),
        ('OWN', 'right', (6,), sortkeys.by_own_time),
        ('/CALL', 'right', (6,), sortkeys.by_own_time_per_call),
        ('%', 'left', (4,), None),
        ('DEEP', 'right', (6,), sortkeys.by_deep_time),
        ('/CALL', 'right', (6,), sortkeys.by_deep_time_per_call),
        ('%', 'left', (4,), None),
    ]
    order = sortkeys.by_deep_time

    def make_cells(self, node, stats):
        yield fmt.make_stat_text(stats)
        yield fmt.make_int_or_na_text(stats.own_hits)
        yield fmt.make_time_text(stats.own_time)
        yield fmt.make_time_text(stats.own_time_per_call)
        yield fmt.make_percent_text(stats.own_time, self.cpu_time)
        yield fmt.make_time_text(stats.deep_time)
        yield fmt.make_time_text(stats.deep_time_per_call)
        yield fmt.make_percent_text(stats.deep_time, self.cpu_time)


class TracingProfiler(Profiler):
    """The tracing profiler."""

    table_class = TracingStatisticsTable

    #: The CPU timer.  Usually it is an instance of :class:`profiling.tracing.
    #: timers.Timer`.
    timer = None

    #: The CPU time of profiling overhead.  It's the time spent in
    #: :meth:`_profile`.
    overhead = 0.0

    def __init__(self, base_frame=None, base_code=None,
                 ignored_frames=(), ignored_codes=(), timer=None):
        timer = timer or TIMER_CLASS()
        if not isinstance(timer, Timer):
            raise TypeError('Not a timer instance')
        base = super(TracingProfiler, self)
        base.__init__(base_frame, base_code, ignored_frames, ignored_codes)
        self.timer = timer
        self._times_entered = {}

    def _profile(self, frame, event, arg):
        """The callback function to register by :func:`sys.setprofile`."""
        # c = event.startswith('c_')
        if event.startswith('c_'):
            return
        time1 = self.timer()
        frames = self.frame_stack(frame)
        if frames:
            frames.pop()
        parent_stats = self.stats
        for f in frames:
            parent_stats = parent_stats.ensure_child(f.f_code, void)
        code = frame.f_code
        frame_key = id(frame)
        # if c:
        #     event = event[2:]
        #     code = mock_code(arg.__name__)
        #     frame_key = id(arg)
        # record
        time2 = self.timer()
        self.overhead += time2 - time1
        if event == 'call':
            time = time2 - self.overhead
            self.record_entering(time, code, frame_key, parent_stats)
        elif event == 'return':
            time = time1 - self.overhead
            self.record_leaving(time, code, frame_key, parent_stats)
        time3 = self.timer()
        self.overhead += time3 - time2

    def record_entering(self, time, code, frame_key, parent_stats):
        """Entered to a function call."""
        stats = parent_stats.ensure_child(code, RecordingStatistics)
        self._times_entered[(code, frame_key)] = time
        stats.own_hits += 1

    def record_leaving(self, time, code, frame_key, parent_stats):
        """Left from a function call."""
        try:
            stats = parent_stats.get_child(code)
            time_entered = self._times_entered.pop((code, frame_key))
        except KeyError:
            return
        time_elapsed = time - time_entered
        stats.deep_time += max(0, time_elapsed)

    def result(self):
        base = super(TracingProfiler, self)
        frozen_stats, cpu_time, wall_time = base.result()
        return (frozen_stats, cpu_time - self.overhead, wall_time)

    def run(self):
        if sys.getprofile() is not None:
            # NOTE: There's no threading.getprofile().
            # The profiling function will be stored at threading._profile_hook
            # but it's not documented.
            raise RuntimeError('Another profiler already registered')
        with deferral() as defer:
            self._times_entered.clear()
            self.overhead = 0.0
            sys.setprofile(self._profile)
            defer(sys.setprofile, None)
            threading.setprofile(self._profile)
            defer(threading.setprofile, None)
            self.timer.start(self)
            defer(self.timer.stop)
            yield