"""General utility methods"""

# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

import asyncio
import sys
import inspect
from typing import Callable, Awaitable, Any, Union


def check_ipython() -> None:
    # original from vaex/asyncio.py
    IPython = sys.modules.get('IPython')
    if IPython:
        IPython_version = tuple(map(int, IPython.__version__.split('.')))  # type: ignore
        if IPython_version < (7, 0, 0):
            raise RuntimeError(f'You are using IPython {IPython.__version__} '  # type: ignore
                               'while we require 7.0.0+, please update IPython')


def check_patch_tornado() -> None:
    """If tornado is imported, add the patched asyncio.Future to its tuple of acceptable Futures"""
    # original from vaex/asyncio.py
    if 'tornado' in sys.modules:
        import tornado.concurrent  # type: ignore
        if asyncio.Future not in tornado.concurrent.FUTURES:
            tornado.concurrent.FUTURES = \
                tornado.concurrent.FUTURES + (asyncio.Future, )  # type: ignore


def just_run(coro: Awaitable) -> Any:
    """Make the coroutine run, even if there is an event loop running (using nest_asyncio)"""
    # original from vaex/asyncio.py
    loop = asyncio._get_running_loop()
    if loop is None:
        had_running_loop = False
        try:
            loop = asyncio.get_event_loop()
        except RuntimeError:
            # we can still get 'There is no current event loop in ...'
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
    else:
        had_running_loop = True
    if had_running_loop:
        # if there is a running loop, we patch using nest_asyncio
        # to have reentrant event loops
        check_ipython()
        import nest_asyncio
        nest_asyncio.apply()
        check_patch_tornado()
    return loop.run_until_complete(coro)


def run_sync(coro: Callable) -> Callable:
    """Runs a coroutine and blocks until it has executed.

    An event loop is created if no one already exists. If an event loop is
    already running, this event loop execution is nested into the already
    running one if `nest_asyncio` is set to True.

    Parameters
    ----------
    coro : coroutine
        The coroutine to be executed.

    Returns
    -------
    result :
        Whatever the coroutine returns.
    """
    def wrapped(*args, **kwargs):
        return just_run(coro(*args, **kwargs))
    wrapped.__doc__ = coro.__doc__
    return wrapped


async def ensure_async(obj: Union[Awaitable, Any]) -> Any:
    """Convert a non-awaitable object to a coroutine if needed,
    and await it if it was not already awaited.
    """
    if inspect.isawaitable(obj):
        try:
            result = await obj
        except RuntimeError as e:
            if str(e) == 'cannot reuse already awaited coroutine':
                # obj is already the coroutine's result
                return obj
            raise
        return result
    # obj doesn't need to be awaited
    return obj
