import asyncio
import contextvars
import functools
import sys
from greenlet import greenlet, getcurrent


class GreenletBridge:
    stop = object()

    def __init__(self):
        self.bridge_greenlet = None

    def run(self):
        async def async_run():
            gl = getcurrent().parent
            coro = gl.switch()
            while gl and coro != self.stop:  # pragma: no branch
                try:
                    result = await coro
                except:  # noqa: E722
                    coro = gl.throw(*sys.exc_info())
                else:
                    coro = gl.switch(result)

        # get the asyncio loop
        try:
            loop = asyncio.get_event_loop()
        except RuntimeError:  # pragma: no cover
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)

        loop.run_until_complete(async_run())
        self.bridge_greenlet = None

    def start(self):
        if self.bridge_greenlet:
            return self.bridge_greenlet
        if asyncio.get_event_loop().is_running():
            # we shouldn't be here if a loop is already running!
            return greenlet.getcurrent()

        self.bridge_greenlet = greenlet(self.run)
        self.bridge_greenlet.switch()
        return self.bridge_greenlet

    def stop(self):
        if self.bridge_greenlet:
            self.bridge_greenlet.switch(self.stop)
            self.bridge_greenlet = None


bridge = GreenletBridge()


def async_(fn=None, *, with_context=False):
    """Convert a standard function to an async function that can be awaited.

    This function creates an async wrapper for a standard function, allowing
    callers to invoke the function as an awaitable. Example::

        def fn():
            pass

        async def main():
            await async_(fn)

    It is also possible to use this function as a decorator::

        @async_
        def fn():
            pass

        async def main():
            await fn()

    If you are using context variables, you can use the ``with_context`` option
    to ensure that the context is preserved when the function is called::

        @async_(with_context=True)
        def fn():
            pass

    :param fn: the standard function to convert to async.
    """
    if fn is None:
        return lambda fn: async_(fn, with_context=with_context)

    @functools.wraps(fn)
    async def decorator(*args, **kwargs):
        gl = greenlet(fn)

        async def run():
            coro = gl.switch(*args, **kwargs)
            while gl:
                try:
                    result = await coro
                except:  # noqa: E722
                    # this catches exceptions from async functions awaited in
                    # sync code, and re-raises them in the greenlet
                    coro = gl.throw(*sys.exc_info())
                else:
                    coro = gl.switch(result)
            return coro

        if with_context:
            gl.gr_context = contextvars.copy_context()
            try:
                result = await run()
            finally:
                # restore the context
                for var in gl.gr_context:
                    var.set(gl.gr_context[var])
        else:
            result = await run()

        return result

    return decorator


def await_(coro_or_fn):
    """Wait for an async function to complete in a standard function, without
    blocking the asyncio loop.

    This function can be used in two ways. First, as a replacement to the
    ``await`` keyword in a standard function::

        def func():
            await_(asyncio.sleep(1))

    Second, as a decorator to an async function, so that the function
    can be called as a standard function while still not blocking the asyncio
    loop::

        @await_
        async def func():
            await asyncio.sleep(1)

        def main():
            func()

    :param coro_or_fn: The coroutine or future to await, or when used as a
                       decorator, the async function to decorate.
    """
    if asyncio.iscoroutine(coro_or_fn) or asyncio.isfuture(coro_or_fn):
        # we were given an awaitable --> await it
        parent = greenlet.getcurrent().parent or bridge.start()
        if parent == greenlet.getcurrent():
            raise RuntimeError(
                'await_ cannot be called from the asyncio task')
        return parent.switch(coro_or_fn)
    else:
        # assume decorator usage
        @functools.wraps(coro_or_fn)
        def decorator(*args, **kwargs):
            return await_(coro_or_fn(*args, **kwargs))

        return decorator
