import ast
import curio
import curio.monitor
import code
import inspect
import sys
import types
import warnings
import threading
import signal

assert sys.version_info >= (3, 8), "console requires Python 3.8+"


class CurioIOInteractiveConsole(code.InteractiveConsole):

    def __init__(self, locals_):
        super().__init__(locals_)
        self.compile.compiler.flags |= ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
        self.requests = curio.UniversalQueue()
        self.response = curio.UniversalQueue()

    def runcode(self, code):
        # This coroutine is handed from the thread running the REPL to the
        # task runner in the main thread.
        async def run_it():
            func = types.FunctionType(code, self.locals)
            try:
                # We restore the default REPL signal handler for running normal code
                hand = signal.signal(signal.SIGINT, signal.default_int_handler)
                try:
                    coro = func()
                finally:
                    signal.signal(signal.SIGINT, hand)
            except BaseException as ex:  # noqa: BLE001
                await self.response.put((None, ex))
                return
            if not inspect.iscoroutine(coro):
                await self.response.put((coro, None))
                return

            # For a coroutine... We're going to try and do some magic to intercept
            # Control-C in an Event/Task.
            async def watch_ctrl_c(evt, repl_task):
                await evt.wait()
                await repl_task.cancel()

            evt = curio.UniversalEvent()
            try:
                hand = signal.signal(signal.SIGINT, lambda signo, frame: evt.set())
                repl_task = await curio.spawn(coro)
                watch_task = await curio.spawn(watch_ctrl_c, evt, repl_task)
                try:
                    result = await repl_task.join()
                    response = (result, None)
                except SystemExit:
                    raise
                except BaseException as e:  # noqa: BLE001
                    await repl_task.wait()
                    response = (None, e.__cause__)
                await watch_task.cancel()
            finally:
                signal.signal(signal.SIGINT, hand)
            await self.response.put(response)

        self.requests.put(run_it())
        # Get the result here...
        result, exc = self.response.get()
        if exc is not None:
            try:
                raise exc
            except BaseException:  # noqa: BLE001
                self.showtraceback()
        else:
            return result

    # Task that runs in the main thread, executing input fed to it from above
    async def runmain(self):
        try:
            hand = signal.signal(signal.SIGINT, signal.SIG_IGN)
            while True:
                coro = await self.requests.get()
                if coro is None:
                    break
                await coro
        finally:
            signal.signal(signal.SIGINT, hand)


def run_repl(console):
    try:
        banner = (
            f"curio REPL {sys.version} on {sys.platform}\n"
            f'Use "await" directly instead of "curio.run()".\n'
            f'Type "help", "copyright", "credits" or "license" '
            f"for more information.\n"
            f'{getattr(sys, "ps1", ">>> ")}import curio'
        )
        console.interact(banner=banner, exitmsg="exiting curio REPL...")
    finally:
        warnings.filterwarnings(
            "ignore", message=r"^coroutine .* was never awaited$", category=RuntimeWarning
        )
        console.requests.put(None)


if __name__ == "__main__":
    repl_locals = {
        "curio": curio,
        "ps": curio.monitor.ps,
        "where": curio.monitor.where,
    }
    for key in ("__name__", "__package__", "__loader__", "__spec__", "__builtins__", "__file__"):
        repl_locals[key] = locals()[key]

    console = CurioIOInteractiveConsole(repl_locals)
    threading.Thread(target=run_repl, args=[console], daemon=True).start()
    curio.run(console.runmain)
