# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors
#
# SPDX-License-Identifier: MIT

from .. import socknumbers

from asyncio import BaseProtocol
from asyncio.transports import BaseTransport

class RecvmsgDatagramProtocol(BaseProtocol):
    """Callback interface similar to asyncio.DatagramProtocol, but dealing with
    recvmsg data."""

    def datagram_msg_received(self, data, ancdata, flags, address):
        """Called when some datagram is received."""

    def datagram_errqueue_received(self, data, ancdata, flags, address):
        """Called when some data is received from the error queue"""

    def error_received(self, exc):
        """Called when a send or receive operation raises an OSError."""

def _set_result_unless_cancelled(fut, result):
    """Helper setting the result only if the future was not cancelled."""
    if fut.cancelled():
        return
    fut.set_result(result)

class RecvmsgSelectorDatagramTransport(BaseTransport):
    """A simple loop-independent transport that largely mimicks
    DatagramTransport but interfaces a RecvmsgSelectorDatagramProtocol.

    This does not implement any flow control, based on the assumption that it's
    not needed, for CoAP has its own flow control mechanisms."""

    max_size = 4096  # Buffer size passed to recvmsg() -- should suffice for a full MTU package and ample ancdata

    def __init__(self, loop, sock, protocol, waiter):
        super().__init__(extra={'socket': sock})
        self.__sock = sock
        # Persisted outside of sock because when GC breaks a reference cycle,
        # it can happen that the sock gets closed before this; we have to hope
        # that no new file gets opened and registered in the meantime.
        self.__sock_fileno = sock.fileno()
        self._loop = loop
        self._protocol = protocol

        loop.call_soon(protocol.connection_made, self)
        # only start reading when connection_made() has been called
        import weakref
        # We could add error handling in here like this:
        # ```
        # self = s()
        # if self is None or self.__sock is None:
        #     # The read event happened briefly before .close() was called,
        #     # but late enough that the caller of close did not yield to let
        #     # the event out; when remove_reader was then called, the
        #     # pending event was not removed, so it fires now that the
        #     # socket is already closed. (Depending on the GC's whims, self
        #     # may or may not have been GC'd, but if it wasn't yet, the
        #     # closed state is indicated by the lack of a __sock.
        #     #
        #     # Thus, silently (preferably with an ICMP error, but really
        #     # can't do that)...
        #     return
        # ```
        # That was done tentatively while debugging errors flying out of
        # _read_ready, but it turned out that this was not the actual error
        # source. Thus, I'm not adding the handler and assuming that close's
        # remove_reader is not racing against callbacks, and thus that s() is
        # always valid while the transport is around (and the weakref is really
        # only used to break up the reference cycles to ensure the GC is not
        # needed here).
        def rr(s=weakref.ref(self)):
            s()._read_ready()
        loop.call_soon(loop.add_reader, self.__sock_fileno, rr)
        loop.call_soon(_set_result_unless_cancelled, waiter, None)

    def close(self):
        if self.__sock is None:
            return

        if not self._loop.is_closed():
            self._loop.call_soon(self._protocol.connection_lost, None)

        self._loop.remove_reader(self.__sock_fileno)
        self.__sock.close()
        self.__sock = None
        self._protocol = None
        self._loop = None

    def __del__(self):
        if self.__sock is not None:
            self.close()

    def _read_ready(self):
        if socknumbers.HAS_RECVERR:
            try:
                data, ancdata, flags, addr = self.__sock.recvmsg(self.max_size, 1024, socknumbers.MSG_ERRQUEUE)
            except (BlockingIOError, InterruptedError):
                pass
            except OSError as exc:
                if repr(exc) == "OSError('received malformed or improperly truncated ancillary data',)":
                    pass # workaround for https://bitbucket.org/pypy/pypy/issues/2649/recvmsg-with-empty-err-queue-raises-odd
                else:
                    self._protocol.error_received(exc)
            except Exception as exc:
                self._fatal_error(exc, 'Fatal read error on datagram transport')
            else:
                self._protocol.datagram_errqueue_received(data, ancdata, flags, addr)

        # copied and modified from _SelectorDatagramTransport
        try:
            data, ancdata, flags, addr = self.__sock.recvmsg(self.max_size, 1024) # TODO: find a way for the application to tell the trensport how much data is expected
        except (BlockingIOError, InterruptedError):
            pass
        except OSError as exc:
            self._protocol.error_received(exc)
        except Exception as exc:
            self._fatal_error(exc, 'Fatal read error on datagram transport')
        else:
            self._protocol.datagram_msg_received(data, ancdata, flags, addr)

    def sendmsg(self, data, ancdata, flags, address):
        try:
            self.__sock.sendmsg((data,), ancdata, flags, address)
            return
        except OSError as exc:
            self._protocol.error_received(exc)
            return
        except Exception as exc:
            self._fatal_error(exc,
                              'Fatal write error on datagram transport')
            return

async def create_recvmsg_datagram_endpoint(loop, factory, sock):
    """Create a datagram connection that uses recvmsg rather than recvfrom, and
    a RecvmsgDatagramProtocol protocol type.

    This is used like the create_datagram_endpoint method of an asyncio loop,
    but implemented in a generic way using the loop's add_reader method; thus,
    it's not a method of the loop but an independent function.

    Due to the way it is used in aiocoap, socket is not an optional argument
    here; it could be were this module ever split off into a standalone
    package.
    """
    sock.setblocking(False)

    protocol = factory()
    waiter = loop.create_future()
    transport = RecvmsgSelectorDatagramTransport(
            loop, sock, protocol, waiter)

    try:
        await waiter
    # see https://github.com/PyCQA/pycodestyle/issues/703
    except: # noqa: E722
        transport.close()
        raise

    return transport, protocol
