# Copyright 2011, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#     * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


"""Stream of WebSocket protocol with the framing used by IETF HyBi 00 and
Hixie 75. For Hixie 75 this stream doesn't perform closing handshake.
"""


from mod_pywebsocket import common
from mod_pywebsocket._stream_base import BadOperationException
from mod_pywebsocket._stream_base import ConnectionTerminatedException
from mod_pywebsocket._stream_base import InvalidFrameException
from mod_pywebsocket._stream_base import StreamBase
from mod_pywebsocket._stream_base import UnsupportedFrameException
from mod_pywebsocket import util


class StreamHixie75(StreamBase):
    """Stream of WebSocket messages."""

    def __init__(self, request, enable_closing_handshake=False):
        """Construct an instance.

        Args:
            request: mod_python request.
            enable_closing_handshake: to let StreamHixie75 perform closing
                                      handshake as specified in HyBi 00, set
                                      this option to True.
        """

        StreamBase.__init__(self, request)

        self._logger = util.get_class_logger(self)

        self._enable_closing_handshake = enable_closing_handshake

        self._request.client_terminated = False
        self._request.server_terminated = False

    def send_message(self, message, end=True, binary=False):
        """Send message.

        Args:
            message: unicode string to send.
            binary: not used in hixie75.

        Raises:
            BadOperationException: when called on a server-terminated
                connection.
        """

        if not end:
            raise BadOperationException(
                'StreamHixie75 doesn\'t support send_message with end=False')

        if binary:
            raise BadOperationException(
                'StreamHixie75 doesn\'t support send_message with binary=True')

        if self._request.server_terminated:
            raise BadOperationException(
                'Requested send_message after sending out a closing handshake')

        self._write(''.join(['\x00', message.encode('utf-8'), '\xff']))

    def _read_payload_length_hixie75(self):
        """Reads a length header in a Hixie75 version frame with length.

        Raises:
            ConnectionTerminatedException: when read returns empty string.
        """

        length = 0
        while True:
            b_str = self._read(1)
            b = ord(b_str)
            length = length * 128 + (b & 0x7f)
            if (b & 0x80) == 0:
                break
        return length

    def receive_message(self):
        """Receive a WebSocket frame and return its payload an unicode string.

        Returns:
            payload unicode string in a WebSocket frame.

        Raises:
            ConnectionTerminatedException: when read returns empty
                string.
            BadOperationException: when called on a client-terminated
                connection.
        """

        if self._request.client_terminated:
            raise BadOperationException(
                'Requested receive_message after receiving a closing '
                'handshake')

        while True:
            # Read 1 byte.
            # mp_conn.read will block if no bytes are available.
            # Timeout is controlled by TimeOut directive of Apache.
            frame_type_str = self.receive_bytes(1)
            frame_type = ord(frame_type_str)
            if (frame_type & 0x80) == 0x80:
                # The payload length is specified in the frame.
                # Read and discard.
                length = self._read_payload_length_hixie75()
                if length > 0:
                    _ = self.receive_bytes(length)
                # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the
                # /client terminated/ flag and abort these steps.
                if not self._enable_closing_handshake:
                    continue

                if frame_type == 0xFF and length == 0:
                    self._request.client_terminated = True

                    if self._request.server_terminated:
                        self._logger.debug(
                            'Received ack for server-initiated closing '
                            'handshake')
                        return None

                    self._logger.debug(
                        'Received client-initiated closing handshake')

                    self._send_closing_handshake()
                    self._logger.debug(
                        'Sent ack for client-initiated closing handshake')
                    return None
            else:
                # The payload is delimited with \xff.
                bytes = self._read_until('\xff')
                # The WebSocket protocol section 4.4 specifies that invalid
                # characters must be replaced with U+fffd REPLACEMENT
                # CHARACTER.
                message = bytes.decode('utf-8', 'replace')
                if frame_type == 0x00:
                    return message
                # Discard data of other types.

    def _send_closing_handshake(self):
        if not self._enable_closing_handshake:
            raise BadOperationException(
                'Closing handshake is not supported in Hixie 75 protocol')

        self._request.server_terminated = True

        # 5.3 the server may decide to terminate the WebSocket connection by
        # running through the following steps:
        # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the
        # start of the closing handshake.
        self._write('\xff\x00')

    def close_connection(self, unused_code='', unused_reason=''):
        """Closes a WebSocket connection.

        Raises:
            ConnectionTerminatedException: when closing handshake was
                not successfull.
        """

        if self._request.server_terminated:
            self._logger.debug(
                'Requested close_connection but server is already terminated')
            return

        if not self._enable_closing_handshake:
            self._request.server_terminated = True
            self._logger.debug('Connection closed')
            return

        self._send_closing_handshake()
        self._logger.debug('Sent server-initiated closing handshake')

        # TODO(ukai): 2. wait until the /client terminated/ flag has been set,
        # or until a server-defined timeout expires.
        #
        # For now, we expect receiving closing handshake right after sending
        # out closing handshake, and if we couldn't receive non-handshake
        # frame, we take it as ConnectionTerminatedException.
        message = self.receive_message()
        if message is not None:
            raise ConnectionTerminatedException(
                'Didn\'t receive valid ack for closing handshake')
        # TODO: 3. close the WebSocket connection.
        # note: mod_python Connection (mp_conn) doesn't have close method.

    def send_ping(self, body):
        raise BadOperationException(
            'StreamHixie75 doesn\'t support send_ping')


# vi:sts=4 sw=4 et
