from functools import wraps
from copy import deepcopy


def no_side_effects_decorator(func):
    @wraps(func)
    def inner(*args, **kwargs):
        if isinstance(args[0], (list, dict, set)):
            copeed_args = tuple([args[0].copy(), *args[1:]])
        else:
            copeed_args = args
        return func(*copeed_args, **kwargs)
    return inner


@no_side_effects_decorator
def add_element(data, element):
    data.append(element)
    return data
'''
def add_element(data, key, value=None):
    data[key] = value
    return data
'''


def add_args(func):
    @wraps(func)
    def inner(*args, **kwargs):
        modifed_args = ('begin', ) + args + ('end', )
        return func(*modifed_args, **kwargs)
    return inner

@add_args
def concatenate(*args):
    """
    Возвращает конкатенацию переданных строк
    """
    return ', '.join(args)


def explicit_args(func):
    @wraps(func)
    def inner(*args, **kwargs):
        if len(args):
            print('Вы не можете передать позиционные аргументы. Используйте именованный способ передачи значений')
            return
        else:
            return func(*args, **kwargs)
    return inner


@explicit_args
def add(a: int, b: int) -> int:
    '''Возвращает сумму двух чисел'''
    return a + b


def reverse(func):
    @wraps(func)
    def inner(*args, **kwargs):
        reversed_args = args[::-1]
        return func(*reversed_args)
    return inner


def monkey_patching(func):
    @wraps(func)
    def inner(*args, **kwargs):
        patched_args = ('Monkey', )*len(args)
        patched_kwargs = {key:'patching' for key, _ in kwargs.items()}
        return func(*patched_args, **patched_kwargs)
    return inner


@monkey_patching
def info_kwargs(**kwargs):
    """Выводит информацию о переданных kwargs"""
    for k, v in sorted(kwargs.items()):
        print(f'{k} = {v}')


def counting_calls(func):
    @wraps(func)
    def inner(*args, **kwargs):
        inner.call_count += 1
        inner.calls.append({'args': args, 'kwargs':kwargs})
        return func(*args, **kwargs)        
    setattr(inner, 'call_count', 0)
    setattr(inner, 'calls', [])
    return inner


@counting_calls
def add(a: int, b: int) -> int:
    '''Возвращает сумму двух чисел'''
    return a + b


def check_count_args(func):
    @wraps(func)
    def inner(*args, **kwargs):
        if (len(args) + len(kwargs)) == 2:
            return func(*args, **kwargs)
        elif (len(args) + len(kwargs)) < 2:
            print("Not enough arguments")
        else:
            print("Too many arguments")
    return inner


def cache_result(func):
    cache = {}
    @wraps(func)
    def inner(*args, **kwargs):
        nonlocal cache
        key = (args, tuple(kwargs.items()))
        if key in cache.keys():
            print(f'[FROM CACHE] Вызов {inner.__name__} = {cache[key]}')
            return cache[key]
        else:
            res = func(*args, **kwargs)
            cache[key] = res
            return res
    return inner


@cache_result
def multiply(a, b):
    return a * b


def test(*args, **kwargs):
    pass

def main():

    print(add(10, b=20))
    print(add(7, 5))
    print(add(12, 45))
    print('Количество вызовов =', add.call_count)
    print(add.calls[2])

    print(add(b=11, a=22))
    print(add.calls[3])

    # print(multiply(4, 5))  # Вызываем 1й раз функцию с аргументами 4 и 5. Идет сохранение результата

    # print(multiply(4, 5))  # При повторном вызове достаем из кеша

    # print(multiply(5, 8))  # Впервые вызывает с аргументами 5 и 8
    # print(multiply(5, 8))  # Достаем из кеша результат вызова multiply(5, 8)
    # print(multiply(5, 8))  # Вновь достаем из кеша

    # print(multiply(-3, 7))  # Впервые вычисляем результат вызова multiply(-3, 7), сохраняем в кеше
    # print(multiply(-3, 7))  # Достаем из кеша multiply(-3, 7)
    

    # info_kwargs(first_name="John", last_name="Doe", age=33)
    # info_kwargs(c=43, b= 32, a=32)
    # print(info_kwargs.__name__)
    # print(info_kwargs.__doc__.strip())

    # print(add(10, 20))

    # print(concatenate('hello', 'world', 'my', 'name is', 'Artem'))
    # print(concatenate('my', 'name is', 'Artem'))
    # print(concatenate.__name__)
    # print(concatenate.__doc__.strip())

    # my_list = [1, 2, 3]
    # print('Результат вызова =', add_element(my_list, 4))
    # print('Результат вызова =', add_element(my_list, 5))
    # my_dict = {1: 'Hello', 2: 'World'}
    # print('Результат вызова =', add_element(my_dict, 4, 'four'))
    # print(my_list)
    # print(add_element.__name__)
    # test(my_list)




if __name__ == '__main__':
    main()