import asyncio
from queue import Queue

from clarabot_nano.common.zmq.inproc_client import AioInprocClient, InprocClient
from clarabot_nano.common.zmq_utils import polling_send_multipart
from clarabot_nano.utility.thread import AbstractThread


class AbstractZmqSocketThread(AbstractThread):
    """
    For platform independent performance and compatibility we purposefully avoid using the zmq.asyncio and the
    aiozmq event-loop solutions. (neither promises good performance on Windows)

    For low latency event communication with a logic thread this uses and inproc PAIR socket.
    """

    def __init__(self, inproc_socket_name):
        AbstractThread.__init__(self)
        self.inproc_name = f"inproc://{inproc_socket_name}"
        self._inproc_queue = Queue()

    def create_inproc_client(self):
        """
        Create a client object that will be used for receiving and sending messages from the logic thread.
        """
        raise NotImplementedError()

    def _thread(self):
        """
        Socket thread: setup the sockets and start a processing loop.
        """
        sockets = []
        try:
            self._thread_create_sockets(sockets)
            self._thread_socket_loop(*sockets)
        finally:
            for s in sockets:
                s.close(500)

    def _thread_create_sockets(self, sockets: list):
        """
        Create the sockets that will be passed to ._thread_socket_loop(...)
        """
        raise NotImplementedError()

    def _thread_socket_loop(self, *args):
        """
        Socket thread: main processing loop.
        """
        raise NotImplementedError()

    def _thread_socket_send_to_logic(self, data, inproc_socket, poll):
        """
        This must implement the pushing of a remote message to the logic thread for processing.
        """
        raise NotImplementedError()


class AbstractBiPairZmqSocketThread(AbstractZmqSocketThread):
    """
    Abstract base for creating a socket thread which communicates with the logic thread by using the PAIR socket for
    bi-directional buffering. (hence the name: *BiPair*)
    """

    def create_inproc_client(self):
        return InprocClient(self._inproc_queue, self.inproc_name)

    def _thread_create_sockets(self, sockets: list):
        raise NotImplementedError()

    def _thread_socket_loop(self, *args):
        raise NotImplementedError()

    def _thread_socket_send_to_logic(self, data, inproc_socket, poll):
        polling_send_multipart(inproc_socket, data, poll)


class AbstractAioZmqSocketThread(AbstractZmqSocketThread):
    """
    This abstract base uses thread-safe fifo queue for pushing a message to the logic thread, while managing a
    asyncio.Event object for notification. This provides a sufficient compatibility layer between the zmq thread
    and asyncio event-loop.
    Since the InprocClient will be created and used solely in the asyncio thread the normal send implementation
    is usable.
    """

    def __init__(self, loop, inproc_bind_name):
        self._loop = loop
        AbstractZmqSocketThread.__init__(self, inproc_bind_name)
        self._async_queue = Queue()
        self._async_event = asyncio.Event()

    def create_inproc_client(self):
        """
        This must be called from the asyncio thread.
        """
        return AioInprocClient(self._async_queue, self._async_event, self._inproc_queue, self.inproc_name)

    def _thread_create_sockets(self, sockets: list):
        raise NotImplementedError()

    def _thread_socket_loop(self, *args):
        raise NotImplementedError()

    def _thread_socket_send_to_logic(self, data, inproc_socket, poll):
        self._async_queue.put(data)
        self._loop.call_soon_threadsafe(self._async_event.set)
