# Copyright (C) 2003  CAMP
# Please see the accompanying LICENSE file for further information.


import functools
import inspect
import sys
import time


def function_timer(func, *args, **kwargs):
    out = kwargs.pop('timeout', sys.stdout)
    t1 = time.time()
    r = func(*args, **kwargs)
    t2 = time.time()
    print(t2 - t1, file=out)
    return r


class Timer:
    """Timer object.

    Use like this::

        timer = Timer()
        timer.start('description')
        # do something
        timer.stop()

    or::

        with timer('description'):
            # do something

    To get a summary call::

        timer.write()

    """

    def __init__(self, print_levels=1000):
        self.timers = {}
        self.t0 = time.time()
        self.running = []
        self.print_levels = print_levels

    def print_info(self, calc):
        """Override to get to write info during calculator's initialize()."""

    def start(self, name):
        names = tuple(self.running + [name])
        self.timers[names] = self.timers.get(names, 0.0) - time.time()
        self.running.append(name)

    def stop(self, name=None):
        if name is None:
            name = self.running[-1]
        names = tuple(self.running)
        running = self.running.pop()
        if name != running:
            raise RuntimeError('Must stop timers by stack order.  '
                               'Requested stopping of %s but topmost is %s'
                               % (name, running))
        self.timers[names] += time.time()
        return names

    def __call__(self, name):
        """Context manager for timing a block of code.

        Example (t is a timer object)::

            with t('Add two numbers'):
                x = 2 + 2

            # same as this:
            t.start('Add two numbers')
            x = 2 + 2
            t.stop()
        """
        self.start(name)
        return self

    def __enter__(self):
        pass

    def __exit__(self, *args):
        self.stop()

    def get_time(self, *names):
        return self.timers[names]

    def write(self, out=sys.stdout):
        were_running = list(self.running)
        while self.running:
            self.stop()
        if len(self.timers) == 0:
            return

        t0 = time.time()
        tot = t0 - self.t0

        n = max(len(names[-1]) + len(names) for names in self.timers) + 1
        line = '-' * (n + 26) + '\n'
        out.write('%-*s    incl.     excl.\n' % (n, 'Timing:'))
        out.write(line)
        tother = tot

        inclusive = self.timers.copy()
        exclusive = self.timers.copy()
        keys = sorted(exclusive.keys())
        for names in keys:
            t = exclusive[names]
            if len(names) > 1:
                if len(names) < self.print_levels + 1:
                    exclusive[names[:-1]] -= t
            else:
                tother -= t
        exclusive[('Other',)] = tother
        inclusive[('Other',)] = tother
        keys.append(('Other',))
        for names in keys:
            t = exclusive[names]
            tinclusive = inclusive[names]
            r = t / tot
            p = 100 * r
            i = int(40 * r + 0.5)
            if i == 0:
                bar = '|'
            else:
                bar = '|%s|' % ('-' * (i - 1))
            level = len(names)
            if level > self.print_levels:
                continue
            name = (level - 1) * ' ' + names[-1] + ':'
            out.write('%-*s%9.3f %9.3f %5.1f%% %s\n' %
                      (n, name, tinclusive, t, p, bar))
        out.write(line)
        out.write('%-*s%9.3f %5.1f%%\n\n' % (n + 10, 'Total:', tot, 100.0))

        for name in were_running:
            self.start(name)

    def add(self, timer):
        for name, t in timer.timers.items():
            self.timers[name] = self.timers.get(name, 0.0) + t


class timer:
    """Decorator for timing a method call.

    Example::

        from ase.utils.timing import timer, Timer

        class A:
            def __init__(self):
                self.timer = Timer()

            @timer('Add two numbers')
            def add(self, x, y):
                return x + y

        """

    def __init__(self, name):
        self.name = name

    def __call__(self, method):
        if inspect.isgeneratorfunction(method):
            @functools.wraps(method)
            def new_method(slf, *args, **kwargs):
                gen = method(slf, *args, **kwargs)
                while True:
                    slf.timer.start(self.name)
                    try:
                        x = next(gen)
                    except StopIteration:
                        break
                    finally:
                        slf.timer.stop()
                    yield x
        else:
            @functools.wraps(method)
            def new_method(slf, *args, **kwargs):
                slf.timer.start(self.name)
                x = method(slf, *args, **kwargs)
                try:
                    slf.timer.stop()
                except IndexError:
                    pass
                return x
        return new_method
