#!/usr/bin/env python3

"""
** Makes the communication medium and its use independent. **
-------------------------------------------------------------

Whether it is tcp sockets, files, mailboxes or a drive like dropbox,
we want to be able to use all these media indifferently...
That's the role of this module.
It allows to add an abstraction layer to unify the communication API.
"""

import abc
import socket

from raisin.serialization import deserialize, serialize
from raisin.serialization.iter_tools import anticipate
from raisin.serialization.core import size2tag, tag2size


class AbstractConn:
    """
    ** Allows for easier communication. **

    It is the basic abstract class that allows to make information
    carriers of very different nature communicate in the same way.
    """

    @abc.abstractmethod
    def send(self, raw, dest=None):
        """
        ** Sends a data stream to the recipient. **

        Must be overwritten.

        Parameters
        ----------
        raw : iterator
            A generator that yields byte strings. The division is arbitrary.
            On reception, the packets received are not necessarily split in the same way.
            This flow can typically be generated by the ``raisin.serialization.serialize`` function.
        dest : str, default=None
            The address or addresses of the recipient(s) of the message.
            By default (None), the message is sent to the main recipient.
        """
        raise NotImplementedError('this abstract method must be overwritten')

    def send_obj(self, obj, dest=None, **kwargs):
        """
        ** Sends an object to the recipient. **

        Can be overwritten.

        Parameters
        ----------
        obj
            It is the serialisable object that is sent to the recipient(s).
        dest : str, default=None
            Same as ``AbstractConn.send``.
        **kwargs : dict
            These are the parameters passed to the ``raisin.serialization.serialize`` function.
        """
        self.send(serialize(obj, **kwargs), dest=dest)

    @abc.abstractmethod
    def recv(self):
        """
        ** Recovers the raw data sent by the sender. **

        Must be overwritten.

        Yields
        ------
        bytes
            The packets of bytes emitted by the ``AbstractConn.send``
            method of the sender. Packet decoding may differ, but the order is preserved.

        raises
        ------
        ConnectionError
            If the connection is broken.
        """
        raise NotImplementedError('this abstract method must be overwritten')

    def recv_obj(self, **kwargs):
        """
        ** Recover and deserialize the sent object. **

        Can be overwritten.

        Parameters
        ----------
        **kwargs : dict
            These are the parameters passed to the ``raisin.serialization.deserialize`` function.

        Returns
        -------
        obj
            The emitted object.
        """
        return deserialize(self.recv(), **kwargs)

    @abc.abstractmethod
    def close(self):
        """
        ** Definitely close the connection. **

        Must be overwritten.
        """
        raise NotImplementedError('this abstract method must be overwritten')

    def __del__(self):
        self.close()


class SocketAbstractConn(AbstractConn):
    """
    ** Adds an abstraction layer to TCP sockets. **

    Attributes
    ----------
    conn : socket.socket
        A socket capable of reading and sending data.
    """

    def __init__(self, conn):
        self.conn = conn
        self._recv_pack = b'' # buffer memory

    def send(self, raw, dest=None):
        """
        ** Implementation of ``AbstactConn.send``. **
        """
        for is_end, pack in anticipate(raw):
            signed_pack = bytes([is_end]) + size2tag(len(pack)) + pack
            self.conn.sendall(signed_pack)

    def recv(self):
        """
        ** Implementation of ``AbstraConn.recv``. **
        """
        def gen():
            while True:
                try:
                    yield self.conn.recv(4096)
                except OSError as err:
                    raise ConnectionError('communication seems to be interrupted') from err
        iter_pack = iter(gen())
        while True:
            # check for len and last pack
            while not self._recv_pack:
                self._recv_pack += next(iter_pack)
            is_end = bool(self._recv_pack[0])
            self._recv_pack = self._recv_pack[1:]
            size, self._recv_pack, _ = tag2size(pack=self._recv_pack, gen=iter_pack)
            while len(self._recv_pack) < size:
                self._recv_pack += next(iter_pack)
            yield self._recv_pack[:size]
            self._recv_pack = self._recv_pack[size:]
            if is_end:
                break

    def close(self):
        """
        ** Implementation of ``AbstractConn.close``. **
        """
        try:
            self.conn.shutdown(socket.SHUT_RDWR)
        except OSError:
            pass
        self.conn.close()
