"""
Основные определения
"""
import asyncio
import types


# платформенная сопрограмма
async def coro():
    await asyncio.sleep(1)


def gen():
    yield 11


# Классическая сопрограмма
def classic_coro():
    for _ in range(10):
        yield from gen()
        x = yield "YIELD"
        print(x)


c = classic_coro()

print(next(c))
print(next(c))
c.send(1)
print(next(c))
c.send(2)
print(next(c))
c.send(3)


# Генераторные сопрограммы
@types.coroutine
def gen_coro():
    for _ in range(10):
        yield from asyncio.sleep(1)


async def test():
    await gen_coro()


# asyncio.run(test())

"""
В asyncio есть генератор, отдающий переданные ему сопрограммы, в порядке готовности, а не в порядке подачи.

Это выглядит как параллельное исполнение сопрограмм
"""

import random


async def test_coro(id_: int):
    await asyncio.sleep(random.randint(1, 5))
    return id_


async def task(id_: int):
    await asyncio.sleep(5)
    return id_


async def main_test():
    coros = []
    tasks = []

    for i in range(5):
        tasks.append(
            asyncio.create_task(task(i))
        )

    for i in range(5):
        coros.append(test_coro(i))

    for c in asyncio.as_completed(coros):
        print("ID: ", await c)

    # Таски выполняются в фоне, хоть и спят. 5 сек. Они спят асинхронно с test_coro корутинами.
    # Нам необязательно дожидаться, пока таска будет выполнена. Но если вы этого хотим, достаточно просто
    # вызвать таску с await если мы хотим передать ей управление в конкретной корутине
    for t in asyncio.as_completed(tasks):
        print("TASK ID: ", await t)


# asyncio.run(main_test())

"""
Если вы не можете переписать блокирующую функцию, как асинхронную. То её надо запустить в отдельном потоке или процессе.
"""

"""
В asyncio есть возможность использовать семафоры и потоки.

Семафор работает так: асинхронный контекстный менеджер семафора в asyncio не блокирует программу целиком. 
Он блокирует только конкретную сопрограмму, если счетчик семаформа обращается в ноль.

Также, можно вынести отдельную функцию в поток, если нет возможности сделать её асинхронной.
"""

import time


def dont_coro(id_: int):
    time.sleep(2)

    return id_


# Выполнение в отдельном потоке и асинхронно
async def test_thread():
    thread_tasks = []

    for i in range(5):
        thread_tasks.append(asyncio.create_task(asyncio.to_thread(dont_coro, i)))

    print("test")

    for _, dc in zip(range(10), asyncio.as_completed(thread_tasks)):
        print("T ID: ", await dc)
        await asyncio.sleep(1)
        print("HELLO FROM test_thread")


# asyncio.run(test_thread())

"""
Рекомендуется помещать перед ASGI приложением прокси-сервер, который обрабатывает все статические файлы, а также по
возможности, использовать CDN(система доставки контента)

В состав FastAPI входят скрипты для генерации проектов
"""


async def value():
    return random.randint(1, 10)


# Асинхронные генераторы
async def agen():
    for _ in range(5):
        yield await value()


async def main():
    async for v in agen():
        print(v)


# asyncio.run(main())

"""
Чтобы сделать так, чтобы Асинхронные генераторы стали поддерживать контекстные менеджеры, надо применить к ним
декоратор asynccontextmanager
"""


# Пример асинхронного включения:
async def acomp():
    print([x async for x in agen()])


# asyncio.run(acomp())


"""
Также можно определить асинхронное генераторное выражение (x async for x in agen()) где угодно в программе, 
но использовать его можно только в контексте асинхронного кода.
"""

"""
Чтобы контролировать исключения в функции

asyncio.gather()

Надо задать аргумент return_exceptions при вызове как return_exceptions=True

По умолчанию return_exceptions=False
"""

"""
Если вручную создать таски в через create_task, то надо дождаться, что они закончатся явно. 

Например через await или as_complited

"""

"""
У каждой задачи можно задать имя, это можно использовать как id задачи)))))))))))))))))

Пример:

"""


async def some_task(wait: int) -> int:
    print("some_task with wait: ", wait)
    await asyncio.sleep(wait)

    return 42


async def some_main():
    tasks = [
        asyncio.create_task(some_task(num), name=f"number_{num}") for num in range(5)
    ]

    for task in tasks:
        print("...")
        await task

        if task.get_name() == "number_4":
            print("NUM 4 IS RUNNING!")

            task.cancel()
            print("NUM 4 IS CANCELED!")


# asyncio.run(some_main())


"""
Задачи можно отменить с помощью метода .cancel() с обычными корутинами так не сделать.
"""

"""
Документация по Python 3.11+ рекомендует использовать TaskGroup() вместо gather()

TaskGroup - это асинхронный контекстный менеджер. Все таски в нем создаются, но await'тя после уже выхода из контекста

Пример:
"""


async def task(id_: int):
    await asyncio.sleep(1)
    print(f"Hello from task with id: {id_}")

    if id_ % 2 == 0:
        raise Exception

    return id_ * 100


# Можно выполнить хоть 500 задач, все равно они будут выполнены за ~1 секунду
async def task_group():
    results = []

    try:
        async with asyncio.TaskGroup() as tasks:
            for i in range(500):
                results.append(
                    tasks.create_task(
                        task(i)
                    )
                )

            print("inner context manager")
    except Exception as e:
        print(f"ERROR: {e}")

    """
        ПО КАКОЙ-ТО ПРИЧИНЕ, ИСКЛЮЧЕНИЕ ПРОБРАСЫВАЕТСЯ НЕ ТОЛЬКО В КОНТЕКСТНЫЙ МЕНЕДЖЕР, НО И В МЕТОД 

        .result()
    """

    try:
        # Получение результатов от задач
        print("outer context manager", [t.result() for t in results])
    except:
        ...


# asyncio.run(task_group())


"""
Есть также возможость запустить задачи через wait.

Интересный способ. По завершению wait вернет список выполненых задач и тех, которые были не выполнены. 
Например, из-за таймаута

wait имеет параметр return_when у которого есть 3 состояния:

    * FIRST_COMPLETED - Вернуть done, pending когда выполнена, хотя бы одна задача
    * FIRST_EXCEPTION - вернуть done, pending когда было хотя бы одно исключение
    * ALL_COMPLETED(По умолчанию) - done, pending, когда выполнены все задачи

Пример:
"""


async def wait_task(wait_time: int):
    await asyncio.sleep(wait_time)


async def wait_test():
    tasks = [
        asyncio.create_task(wait_task(1), name="d1"),
        asyncio.create_task(wait_task(2), name="d2"),
        asyncio.create_task(wait_task(3), name="d3"),
    ]

    done, pending = await asyncio.wait(tasks, timeout=2.1)

    print(
        f"{[d.get_name() for d in done]}, "
        f"{[p.get_name() for p in pending]}"
    )


# asyncio.run(wait_test())


"""
Для работы с файлами асинхронно можно юзать aiofiles это внешная библиотека
"""
