###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

import abc
import six

__all__ = ('IWebSocketChannel',
           'IWebSocketChannelFrameApi',
           'IWebSocketChannelStreamingApi')


@six.add_metaclass(abc.ABCMeta)
class IWebSocketChannel(object):
    """
    A WebSocket channel is a bidirectional, full-duplex, ordered, reliable message channel
    over a WebSocket connection as specified in RFC6455.

    This interface defines a message-based API to WebSocket plus auxiliary hooks
    and methods.
    """

    @abc.abstractmethod
    def onConnect(self, requestOrResponse):
        """
        Callback fired during WebSocket opening handshake when a client connects (to a server with
        request from client) or when server connection established (by a client with response from
        server).

        :param requestOrResponse: Connection request (for servers) or response (for clients).
        :type requestOrResponse: Instance of :class:`autobahn.websocket.protocol.ConnectionRequest`
           or :class:`autobahn.websocket.protocol.ConnectionResponse`.

        :returns:
           When this callback is fired on a WebSocket server, you may return one of the
           following:

           1. ``None``: Connection accepted (no subprotocol)
           2. ``str``: Connection accepted with given subprotocol
           3. ``(subprotocol, headers)``: Connection accepted with given ``subprotocol`` (which
              also may be ``None``) and set the given HTTP ``headers`` (e.g. cookies). ``headers``
              must be a ``dict`` with ``str`` keys and values for the HTTP header values to set.

              If a given header value is a non-string iterable (e.g. list or tuple), a separate
              header line will be sent for each item in the iterable.

           If the client announced one or multiple subprotocols, the server MUST select
           one of the given list.
        """

    @abc.abstractmethod
    def onOpen(self):
        """
        Callback fired when the initial WebSocket opening handshake was completed.
        You now can send and receive WebSocket messages.
        """

    @abc.abstractmethod
    def sendMessage(self, payload, isBinary=False, fragmentSize=None, sync=False, doNotCompress=False):
        """
        Send a WebSocket message over the connection to the peer.

        :param payload: The message payload.
        :type payload: bytes
        :param isBinary: ``True`` when payload is binary, else the payload must be UTF-8 encoded text.
        :type isBinary: bool
        :param fragmentSize: Fragment message into WebSocket fragments of this size (the last frame
           potentially being shorter).
        :type fragmentSize: int
        :param sync: If ``True``, try to force data onto the wire immediately.

           .. warning::
              Do NOT use this feature for normal applications.
              Performance likely will suffer significantly.
              This feature is mainly here for use by Autobahn|Testsuite.
        :type sync: bool
        :param doNotCompress: Iff ``True``, never compress this message. This only applies to
                              Hybi-Mode and only when WebSocket compression has been negotiated on
                              the WebSocket connection. Use when you know the payload
                              incompressible (e.g. encrypted or already compressed).
        :type doNotCompress: bool
        """

    @abc.abstractmethod
    def onMessage(self, payload, isBinary):
        """
        Callback fired when a complete WebSocket message was received.

        :param payload: Message payload (UTF-8 encoded text or binary). Can also be empty when
           the WebSocket message contained no payload.
        :type payload: bytes
        :param isBinary: ``True`` iff payload is binary, else the payload is UTF-8 encoded text.
        :type isBinary: bool
        """

    @abc.abstractmethod
    def sendClose(self, code=None, reason=None):
        """
        Starts a WebSocket closing handshake tearing down the WebSocket connection.

        :param code: An optional close status code (``1000`` for normal close or ``3000-4999`` for
           application specific close).
        :type code: int
        :param reason: An optional close reason (a string that when present, a status
           code MUST also be present).
        :type reason: str
        """

    @abc.abstractmethod
    def onClose(self, wasClean, code, reason):
        """
        Callback fired when the WebSocket connection has been closed (WebSocket closing
        handshake has been finished or the connection was closed uncleanly).

        :param wasClean: ``True`` iff the WebSocket connection was closed cleanly.
        :type wasClean: bool
        :param code: Close status code (as sent by the WebSocket peer).
        :type code: int or None
        :param reason: Close reason (as sent by the WebSocket peer).
        :type reason: unicode or None
        """

    @abc.abstractmethod
    def sendPreparedMessage(self, preparedMsg):
        """
        Send a message that was previously prepared with :func:`autobahn.websocket.protocol.WebSocketFactory.prepareMessage`.

        :param prepareMessage: A previously prepared message.
        :type prepareMessage: Instance of :class:`autobahn.websocket.protocol.PreparedMessage`.
        """

    @abc.abstractmethod
    def sendPing(self, payload=None):
        """
        Send a WebSocket ping to the peer.

        A peer is expected to pong back the payload a soon as "practical". When more than
        one ping is outstanding at a peer, the peer may elect to respond only to the last ping.

        :param payload: An (optional) arbitrary payload of length **less than 126** octets.
        :type payload: bytes or None
        """

    @abc.abstractmethod
    def onPing(self, payload):
        """
        Callback fired when a WebSocket ping was received. A default implementation responds
        by sending a WebSocket pong.

        :param payload: Payload of ping (when there was any). Can be arbitrary, up to `125` octets.
        :type payload: bytes
        """

    @abc.abstractmethod
    def sendPong(self, payload=None):
        """
        Send a WebSocket pong to the peer.

        A WebSocket pong may be sent unsolicited. This serves as a unidirectional heartbeat.
        A response to an unsolicited pong is "not expected".

        :param payload: An (optional) arbitrary payload of length < 126 octets.
        :type payload: bytes
        """

    @abc.abstractmethod
    def onPong(self, payload):
        """
        Callback fired when a WebSocket pong was received. A default implementation does nothing.

        :param payload: Payload of pong (when there was any). Can be arbitrary, up to 125 octets.
        :type payload: bytes
        """


class IWebSocketChannelFrameApi(IWebSocketChannel):
    """
    Frame-based API to a WebSocket channel.
    """

    @abc.abstractmethod
    def onMessageBegin(self, isBinary):
        """
        Callback fired when receiving of a new WebSocket message has begun.

        :param isBinary: ``True`` if payload is binary, else the payload is UTF-8 encoded text.
        :type isBinary: bool
        """

    @abc.abstractmethod
    def onMessageFrame(self, payload):
        """
        Callback fired when a complete WebSocket message frame for a previously begun
        WebSocket message has been received.

        :param payload: Message frame payload (a list of chunks received).
        :type payload: list of bytes
        """

    @abc.abstractmethod
    def onMessageEnd(self):
        """
        Callback fired when a WebSocket message has been completely received (the last
        WebSocket frame for that message has been received).
        """

    @abc.abstractmethod
    def beginMessage(self, isBinary=False, doNotCompress=False):
        """
        Begin sending a new WebSocket message.

        :param isBinary: ``True`` if payload is binary, else the payload must be UTF-8 encoded text.
        :type isBinary: bool
        :param doNotCompress: If ``True``, never compress this message. This only applies to
           Hybi-Mode and only when WebSocket compression has been negotiated on the WebSocket
           connection. Use when you know the payload incompressible (e.g. encrypted or
           already compressed).
        :type doNotCompress: bool
        """

    @abc.abstractmethod
    def sendMessageFrame(self, payload, sync=False):
        """
        When a message has been previously begun, send a complete message frame in one go.

        :param payload: The message frame payload. When sending a text message, the payload must
                        be UTF-8 encoded already.
        :type payload: bytes
        :param sync: If ``True``, try to force data onto the wire immediately.

           .. warning::
              Do NOT use this feature for normal applications.
              Performance likely will suffer significantly.
              This feature is mainly here for use by Autobahn|Testsuite.
        :type sync: bool
        """

    @abc.abstractmethod
    def endMessage(self):
        """
        End a message previously begun message. No more frames may be sent (for that message).
        You have to begin a new message before sending again.
        """


class IWebSocketChannelStreamingApi(IWebSocketChannelFrameApi):
    """
    Streaming API to a WebSocket channel.
    """

    @abc.abstractmethod
    def onMessageFrameBegin(self, length):
        """
        Callback fired when receiving a new message frame has begun.
        A default implementation will prepare to buffer message frame data.

        :param length: Payload length of message frame which is subsequently received.
        :type length: int
        """

    @abc.abstractmethod
    def onMessageFrameData(self, payload):
        """
        Callback fired when receiving data within a previously begun message frame.
        A default implementation will buffer data for frame.

        :param payload: Partial payload for message frame.
        :type payload: bytes
        """

    @abc.abstractmethod
    def onMessageFrameEnd(self):
        """
        Callback fired when a previously begun message frame has been completely received.
        A default implementation will flatten the buffered frame data and
        fire `onMessageFrame`.
        """

    @abc.abstractmethod
    def beginMessageFrame(self, length):
        """
        Begin sending a new message frame.

        :param length: Length of the frame which is to be started. Must be less or equal **2^63**.
        :type length: int
        """

    @abc.abstractmethod
    def sendMessageFrameData(self, payload, sync=False):
        """
        Send out data when within a message frame (message was begun, frame was begun).
        Note that the frame is automatically ended when enough data has been sent.
        In other words, there is no ``endMessageFrame``, since you have begun the frame
        specifying the frame length, which implicitly defined the frame end. This is different
        from messages, which you begin *and* end explicitly , since a message can contain
        an unlimited number of frames.

        :param payload: Frame payload to send.
        :type payload: bytes
        :param sync: If ``True``, try to force data onto the wire immediately.

           .. warning::
              Do NOT use this feature for normal applications.
              Performance likely will suffer significantly.
              This feature is mainly here for use by Autobahn|Testsuite.
        :type sync: bool

        :returns: When the currently sent message frame is still incomplete, returns octets
           remaining to be sent. When the frame is complete, returns **0**. Otherwise the amount
           of unconsumed data in payload argument is returned.
        :rtype: int
        """
