import itertools
import functools
from time import perf_counter, sleep
import sys


def do_twice(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        for _ in itertools.repeat(None, 2):
            func(*args, **kwargs)
            return func(*args, **kwargs)
    return wrapper


def timer(func):
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = perf_counter()
        value = func(*args, **kwargs)
        end_time = perf_counter()
        run_time = end_time - start_time
        print(f'Finished {func.__name__!r} in {run_time:.4f} secs')
        return value
    return wrapper_timer


def debug(func):
    """ Print the function signature and return the value """
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f'{k}={v!r}' for k, v in kwargs.items()]
        signature = ', '.join(args_repr + kwargs_repr)
        print(f'Calling {func.__name__}({signature})')
        value = func(*args, **kwargs)
        print(f'{func.__name__!r} returned {value!r}')
        return value
    return wrapper_debug


def slow_down(func):
    @functools.wraps(func)
    def wrapper_slow_down(*args, **kwargs):
        sleep(1)
        return func(*args, **kwargs)
    return wrapper_slow_down


def repeat(num: int):
    def decorator_repeat(func: callable):
        @functools.wraps(func)
        def wrapper_repeat(*args: list, **kwargs: dict):
            for _ in range(num):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat
    return decorator_repeat


def repeatN(_func=None, *, num=2):
    def decorator_repeatN(func):
        @functools.wraps(func)
        def wrapper_repeatN(*args, **kwargs):
            for _ in range(num):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeatN

    if _func is None:
        return decorator_repeatN
    else:
        return decorator_repeatN(_func)


def repeat_partial(func=None, *, num=2):
    if func is None:
        return functools.partial(repeat_partial, num=num)

    @functools.wraps(func)
    def repeat_partial_wrapper(*args, **kwargs):
        for _ in range(num):
            value = func(*args, **kwargs)
        return value
    return repeat_partial_wrapper


# def count_calls(func: callable):
#     @functools.wraps(func)
#     def wrapper_count_calls(*args, **kwargs):
#         wrapper_count_calls.num_calls += 1
#         print(f'Call {wrapper_count_calls.num_calls} of {func.__name__!r}')
#         return func(*args, **kwargs)
#     wrapper_count_calls.num_calls = 0
#     print(wrapper_count_calls.num_calls)
#     return wrapper_count_calls


# def count_calls(func: callable):
#     num_calls = 0
#     print(num_calls)
#     @functools.wraps(func)
#     def wrapper_count_calls(*args, **kwargs):
#         nonlocal num_calls
#         num_calls += 1
#         print(f'Call {num_calls} of {func.__name__}')
#         return func(*args, **kwargs)
#     return wrapper_count_calls


def count_calls(func=None, *, num=0):
    if func is None:
        return functools.partial(count_calls, num=num)

    @functools.wraps(func)
    def wrapper_count_calls(*args, **kwargs):
        wrapper_count_calls.num += 1
        print(f'Call {wrapper_count_calls.num} of {func.__name__}')
        return func(*args, **kwargs)
    wrapper_count_calls.num = num
    return wrapper_count_calls


class Counter(object):
    """docstring for Counter"""
    def __init__(self, func):
        print('start init')
        super(Counter, self).__init__()
        functools.update_wrapper(self, func)
        self.func = func
        self.num_calls = 0
        print('finished init')

    def __call__(self, *args, **kwargs):
        print('start call')
        self.num_calls += 1
        print(f'Call {self.num_calls} of {self.func.__name__!r}')
        print('finished call')
        return self.func(*args, **kwargs)


# class slowDown(object):
#     """docstring for slowDown"""
#     def __init__(self, rate):
#         if callable(rate):
#             self.func = rate
#             self.rate = 1
#         else:
#             self.rate = rate

#     def __get__(self, obj, type=None):
#         return functools.partial(self, obj)

#     def __call__(self, *args, **kwargs):
#         if not hasattr(self, 'func'):
#             self.func = args[0]
#             return self
#         sleep(self.rate)
#         self.func(*args, **kwargs)


class slowDown(object):
    """docstring for Slow_Down"""
    def __init__(self, rate=1):
        print('init')
        self.rate = rate

    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print('wrapper')
            sleep(self.rate)
            func(*args, **kwargs)
        print('call finished')
        print(self, func)
        return wrapper


# class slowDown(object):
#     """docstring for slowDown"""
#     def __init__(self, rate):
#         if callable(rate):
#             self.func = rate
#             self.rate = 1
#             print(f'no args, {locals()}, func={self.func}, rate={self.rate},\
#                   self={self}')
#         else:
#             self.rate = rate
#             print(f'args set, rate={self.rate}')

#     def __get__(self, obj, type=None):
#         print(f'get called, self={self}, obj={obj}, type={type}')
#         return functools.partial(self, obj)

#     def __call__(self, *args, **kwargs):
#         print(f'call called, rate={self.rate}, args={args}'
#               f', kwargs={kwargs} ,self={self}')
#         print(f'locals = {locals()}')
#         if not hasattr(self, 'func'):
#             self.func = args[0]
#             print(f'args set, setting self.func to {self.func}')
#             return self
#         sleep(self.rate)
#         self.func(*args, **kwargs)
