"""Subinterpreters High Level Module."""

import threading
import weakref
import _interpreters

# aliases:
from _interpreters import (
    InterpreterError, InterpreterNotFoundError, NotShareableError,
    is_shareable,
)
from ._queues import (
    create as create_queue,
    Queue, QueueEmpty, QueueFull,
)


__all__ = [
    'get_current', 'get_main', 'create', 'list_all', 'is_shareable',
    'Interpreter',
    'InterpreterError', 'InterpreterNotFoundError', 'ExecutionFailed',
    'NotShareableError',
    'create_queue', 'Queue', 'QueueEmpty', 'QueueFull',
]


_EXEC_FAILURE_STR = """
{superstr}

Uncaught in the interpreter:

{formatted}
""".strip()

class ExecutionFailed(InterpreterError):
    """An unhandled exception happened during execution.

    This is raised from Interpreter.exec() and Interpreter.call().
    """

    def __init__(self, excinfo):
        msg = excinfo.formatted
        if not msg:
            if excinfo.type and excinfo.msg:
                msg = f'{excinfo.type.__name__}: {excinfo.msg}'
            else:
                msg = excinfo.type.__name__ or excinfo.msg
        super().__init__(msg)
        self.excinfo = excinfo

    def __str__(self):
        try:
            formatted = self.excinfo.errdisplay
        except Exception:
            return super().__str__()
        else:
            return _EXEC_FAILURE_STR.format(
                superstr=super().__str__(),
                formatted=formatted,
            )


def create():
    """Return a new (idle) Python interpreter."""
    id = _interpreters.create(reqrefs=True)
    return Interpreter(id, _ownsref=True)


def list_all():
    """Return all existing interpreters."""
    return [Interpreter(id, _whence=whence)
            for id, whence in _interpreters.list_all(require_ready=True)]


def get_current():
    """Return the currently running interpreter."""
    id, whence = _interpreters.get_current()
    return Interpreter(id, _whence=whence)


def get_main():
    """Return the main interpreter."""
    id, whence = _interpreters.get_main()
    assert whence == _interpreters.WHENCE_RUNTIME, repr(whence)
    return Interpreter(id, _whence=whence)


_known = weakref.WeakValueDictionary()

class Interpreter:
    """A single Python interpreter.

    Attributes:

    "id" - the unique process-global ID number for the interpreter
    "whence" - indicates where the interpreter was created

    If the interpreter wasn't created by this module
    then any method that modifies the interpreter will fail,
    i.e. .close(), .prepare_main(), .exec(), and .call()
    """

    _WHENCE_TO_STR = {
       _interpreters.WHENCE_UNKNOWN: 'unknown',
       _interpreters.WHENCE_RUNTIME: 'runtime init',
       _interpreters.WHENCE_LEGACY_CAPI: 'legacy C-API',
       _interpreters.WHENCE_CAPI: 'C-API',
       _interpreters.WHENCE_XI: 'cross-interpreter C-API',
       _interpreters.WHENCE_STDLIB: '_interpreters module',
    }

    def __new__(cls, id, /, _whence=None, _ownsref=None):
        # There is only one instance for any given ID.
        if not isinstance(id, int):
            raise TypeError(f'id must be an int, got {id!r}')
        id = int(id)
        if _whence is None:
            if _ownsref:
                _whence = _interpreters.WHENCE_STDLIB
            else:
                _whence = _interpreters.whence(id)
        assert _whence in cls._WHENCE_TO_STR, repr(_whence)
        if _ownsref is None:
            _ownsref = (_whence == _interpreters.WHENCE_STDLIB)
        try:
            self = _known[id]
            assert hasattr(self, '_ownsref')
        except KeyError:
            self = super().__new__(cls)
            _known[id] = self
            self._id = id
            self._whence = _whence
            self._ownsref = _ownsref
            if _ownsref:
                # This may raise InterpreterNotFoundError:
                _interpreters.incref(id)
        return self

    def __repr__(self):
        return f'{type(self).__name__}({self.id})'

    def __hash__(self):
        return hash(self._id)

    def __del__(self):
        self._decref()

    # for pickling:
    def __reduce__(self):
        return (type(self), (self._id,))

    # gh-135729: Globals might be destroyed by the time this is called, so we
    # need to keep references ourself
    def _decref(self, *,
                InterpreterNotFoundError=InterpreterNotFoundError,
                _interp_decref=_interpreters.decref,
                ):
        if not self._ownsref:
            return
        self._ownsref = False
        try:
            _interp_decref(self._id)
        except InterpreterNotFoundError:
            pass

    @property
    def id(self):
        return self._id

    @property
    def whence(self):
        return self._WHENCE_TO_STR[self._whence]

    def is_running(self):
        """Return whether or not the identified interpreter is running."""
        return _interpreters.is_running(self._id)

    # Everything past here is available only to interpreters created by
    # interpreters.create().

    def close(self):
        """Finalize and destroy the interpreter.

        Attempting to destroy the current interpreter results
        in an InterpreterError.
        """
        return _interpreters.destroy(self._id, restrict=True)

    def prepare_main(self, ns=None, /, **kwargs):
        """Bind the given values into the interpreter's __main__.

        The values must be shareable.
        """
        ns = dict(ns, **kwargs) if ns is not None else kwargs
        _interpreters.set___main___attrs(self._id, ns, restrict=True)

    def exec(self, code, /):
        """Run the given source code in the interpreter.

        This is essentially the same as calling the builtin "exec"
        with this interpreter, using the __dict__ of its __main__
        module as both globals and locals.

        There is no return value.

        If the code raises an unhandled exception then an ExecutionFailed
        exception is raised, which summarizes the unhandled exception.
        The actual exception is discarded because objects cannot be
        shared between interpreters.

        This blocks the current Python thread until done.  During
        that time, the previous interpreter is allowed to run
        in other threads.
        """
        excinfo = _interpreters.exec(self._id, code, restrict=True)
        if excinfo is not None:
            raise ExecutionFailed(excinfo)

    def _call(self, callable, args, kwargs):
        res, excinfo = _interpreters.call(self._id, callable, args, kwargs, restrict=True)
        if excinfo is not None:
            raise ExecutionFailed(excinfo)
        return res

    def call(self, callable, /, *args, **kwargs):
        """Call the object in the interpreter with given args/kwargs.

        Nearly all callables, args, kwargs, and return values are
        supported.  All "shareable" objects are supported, as are
        "stateless" functions (meaning non-closures that do not use
        any globals).  This method will fall back to pickle.

        If the callable raises an exception then the error display
        (including full traceback) is sent back between the interpreters
        and an ExecutionFailed exception is raised, much like what
        happens with Interpreter.exec().
        """
        return self._call(callable, args, kwargs)

    def call_in_thread(self, callable, /, *args, **kwargs):
        """Return a new thread that calls the object in the interpreter.

        The return value and any raised exception are discarded.
        """
        t = threading.Thread(target=self._call, args=(callable, args, kwargs))
        t.start()
        return t
