"""Cross-interpreter Channels High Level Module."""

import time
import _interpchannels as _channels
from concurrent.interpreters import _crossinterp

# aliases:
from _interpchannels import (
    ChannelError, ChannelNotFoundError, ChannelClosedError,  # noqa: F401
    ChannelEmptyError, ChannelNotEmptyError,  # noqa: F401
)
from concurrent.interpreters._crossinterp import (
    UNBOUND_ERROR, UNBOUND_REMOVE,
)


__all__ = [
    'UNBOUND', 'UNBOUND_ERROR', 'UNBOUND_REMOVE',
    'create', 'list_all',
    'SendChannel', 'RecvChannel',
    'ChannelError', 'ChannelNotFoundError', 'ChannelEmptyError',
    'ItemInterpreterDestroyed',
]


class ItemInterpreterDestroyed(ChannelError,
                               _crossinterp.ItemInterpreterDestroyed):
    """Raised from get() and get_nowait()."""


UNBOUND = _crossinterp.UnboundItem.singleton('queue', __name__)


def _serialize_unbound(unbound):
    if unbound is UNBOUND:
        unbound = _crossinterp.UNBOUND
    return _crossinterp.serialize_unbound(unbound)


def _resolve_unbound(flag):
    resolved = _crossinterp.resolve_unbound(flag, ItemInterpreterDestroyed)
    if resolved is _crossinterp.UNBOUND:
        resolved = UNBOUND
    return resolved


def create(*, unbounditems=UNBOUND):
    """Return (recv, send) for a new cross-interpreter channel.

    The channel may be used to pass data safely between interpreters.

    "unbounditems" sets the default for the send end of the channel.
    See SendChannel.send() for supported values.  The default value
    is UNBOUND, which replaces the unbound item when received.
    """
    unbound = _serialize_unbound(unbounditems)
    unboundop, = unbound
    cid = _channels.create(unboundop, -1)
    recv, send = RecvChannel(cid), SendChannel(cid)
    send._set_unbound(unboundop, unbounditems)
    return recv, send


def list_all():
    """Return a list of (recv, send) for all open channels."""
    channels = []
    for cid, unboundop, _ in _channels.list_all():
        chan = _, send = RecvChannel(cid), SendChannel(cid)
        if not hasattr(send, '_unboundop'):
            send._set_unbound(unboundop)
        else:
            assert send._unbound[0] == unboundop
        channels.append(chan)
    return channels


class _ChannelEnd:
    """The base class for RecvChannel and SendChannel."""

    _end = None

    def __new__(cls, cid):
        self = super().__new__(cls)
        if self._end == 'send':
            cid = _channels._channel_id(cid, send=True, force=True)
        elif self._end == 'recv':
            cid = _channels._channel_id(cid, recv=True, force=True)
        else:
            raise NotImplementedError(self._end)
        self._id = cid
        return self

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

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

    def __eq__(self, other):
        if isinstance(self, RecvChannel):
            if not isinstance(other, RecvChannel):
                return NotImplemented
        elif not isinstance(other, SendChannel):
            return NotImplemented
        return other._id == self._id

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

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

    @property
    def _info(self):
        return _channels.get_info(self._id)

    @property
    def is_closed(self):
        return self._info.closed


_NOT_SET = object()


class RecvChannel(_ChannelEnd):
    """The receiving end of a cross-interpreter channel."""

    _end = 'recv'

    def recv(self, timeout=None, *,
             _sentinel=object(),
             _delay=10 / 1000,  # 10 milliseconds
             ):
        """Return the next object from the channel.

        This blocks until an object has been sent, if none have been
        sent already.
        """
        if timeout is not None:
            timeout = int(timeout)
            if timeout < 0:
                raise ValueError(f'timeout value must be non-negative')
            end = time.time() + timeout
        obj, unboundop = _channels.recv(self._id, _sentinel)
        while obj is _sentinel:
            time.sleep(_delay)
            if timeout is not None and time.time() >= end:
                raise TimeoutError
            obj, unboundop = _channels.recv(self._id, _sentinel)
        if unboundop is not None:
            assert obj is None, repr(obj)
            return _resolve_unbound(unboundop)
        return obj

    def recv_nowait(self, default=_NOT_SET):
        """Return the next object from the channel.

        If none have been sent then return the default if one
        is provided or fail with ChannelEmptyError.  Otherwise this
        is the same as recv().
        """
        if default is _NOT_SET:
            obj, unboundop = _channels.recv(self._id)
        else:
            obj, unboundop = _channels.recv(self._id, default)
        if unboundop is not None:
            assert obj is None, repr(obj)
            return _resolve_unbound(unboundop)
        return obj

    def close(self):
        _channels.close(self._id, recv=True)


class SendChannel(_ChannelEnd):
    """The sending end of a cross-interpreter channel."""

    _end = 'send'

#    def __new__(cls, cid, *, _unbound=None):
#        if _unbound is None:
#            try:
#                op = _channels.get_channel_defaults(cid)
#                _unbound = (op,)
#            except ChannelNotFoundError:
#                _unbound = _serialize_unbound(UNBOUND)
#        self = super().__new__(cls, cid)
#        self._unbound = _unbound
#        return self

    def _set_unbound(self, op, items=None):
        assert not hasattr(self, '_unbound')
        if items is None:
            items = _resolve_unbound(op)
        unbound = (op, items)
        self._unbound = unbound
        return unbound

    @property
    def unbounditems(self):
        try:
            _, items = self._unbound
        except AttributeError:
            op, _ = _channels.get_queue_defaults(self._id)
            _, items = self._set_unbound(op)
        return items

    @property
    def is_closed(self):
        info = self._info
        return info.closed or info.closing

    def send(self, obj, timeout=None, *,
             unbounditems=None,
             ):
        """Send the object (i.e. its data) to the channel's receiving end.

        This blocks until the object is received.
        """
        if unbounditems is None:
            unboundop = -1
        else:
            unboundop, = _serialize_unbound(unbounditems)
        _channels.send(self._id, obj, unboundop, timeout=timeout, blocking=True)

    def send_nowait(self, obj, *,
                    unbounditems=None,
                    ):
        """Send the object to the channel's receiving end.

        If the object is immediately received then return True
        (else False).  Otherwise this is the same as send().
        """
        if unbounditems is None:
            unboundop = -1
        else:
            unboundop, = _serialize_unbound(unbounditems)
        # XXX Note that at the moment channel_send() only ever returns
        # None.  This should be fixed when channel_send_wait() is added.
        # See bpo-32604 and gh-19829.
        return _channels.send(self._id, obj, unboundop, blocking=False)

    def send_buffer(self, obj, timeout=None, *,
                    unbounditems=None,
                    ):
        """Send the object's buffer to the channel's receiving end.

        This blocks until the object is received.
        """
        if unbounditems is None:
            unboundop = -1
        else:
            unboundop, = _serialize_unbound(unbounditems)
        _channels.send_buffer(self._id, obj, unboundop,
                              timeout=timeout, blocking=True)

    def send_buffer_nowait(self, obj, *,
                           unbounditems=None,
                           ):
        """Send the object's buffer to the channel's receiving end.

        If the object is immediately received then return True
        (else False).  Otherwise this is the same as send().
        """
        if unbounditems is None:
            unboundop = -1
        else:
            unboundop, = _serialize_unbound(unbounditems)
        return _channels.send_buffer(self._id, obj, unboundop, blocking=False)

    def close(self):
        _channels.close(self._id, send=True)


# XXX This is causing leaks (gh-110318):
_channels._register_end_types(SendChannel, RecvChannel)
