"""

A message class, built from 1 or more websocket frames.

"""

from __future__ import unicode_literals

import struct

from . import errors
from .opcode import Opcode
from .utf8validator import Utf8Validator


class Message(object):
    """Base class for a websocket message.

    :param opcode: A message opcode defined in
        :class:`~lomond.opcode.Opcode`.

    """

    __slots__ = ['opcode']
    _unpack16 = struct.Struct(b'!H').unpack

    def __init__(self, opcode):
        self.opcode = opcode

    def __repr__(self):
        return "<message {}>".format(Opcode.to_str(self.opcode))

    @classmethod
    def build(cls, frames):
        """Build a message from a sequence of frames."""
        opcode = frames[0].opcode
        payload = b''.join(frame.payload for frame in frames)
        if opcode == Opcode.CLOSE:
            return Close.from_payload(payload)
        elif opcode == Opcode.PING:
            return Ping(payload)
        elif opcode == Opcode.PONG:
            return Pong(payload)
        elif opcode == Opcode.BINARY:
            return Binary(payload)
        elif opcode == Opcode.TEXT:
            return Text.from_payload(payload)
        else:
            return Message(opcode)

    @property
    def is_text(self):
        """Check if the message is text."""
        return self.opcode == Opcode.TEXT

    @property
    def is_binary(self):
        """Check if the message is binary."""
        return self.opcode == Opcode.BINARY

    @property
    def is_close(self):
        """Check if this is a close message."""
        return self.opcode == Opcode.CLOSE

    @property
    def is_ping(self):
        """Check if this is a ping message."""
        return self.opcode == Opcode.PING

    @property
    def is_pong(self):
        """Check if this is a pong message."""
        return self.opcode == Opcode.PONG


class Binary(Message):
    """Binary application data.

    :param bytes data: The message data.

    """
    __slots__ = ['data']
    def __init__(self, data):
        self.data = data
        super(Binary, self).__init__(Opcode.BINARY)

    def __repr__(self):
        return "<message BINARY {!r}>".format(self.data)


class Text(Message):
    """Text application data.

    :param str text: The message text.

    """
    __slots__ = ['text']
    def __init__(self, text):
        self.text = text
        super(Text, self).__init__(Opcode.TEXT)

    @classmethod
    def from_payload(cls, payload):
        try:
            text = payload.decode('utf-8')
        except UnicodeDecodeError as error:
            raise errors.CriticalProtocolError(
                'payload contains invalid utf-8; {}',
                error
            )
        return cls(text)

    def __repr__(self):
        return "<message TEXT {!r}>".format(self.text)


class Close(Message):
    """Connection close control message.

    :param int code: Close code.
    :param str reason: Close reason.

    """
    __slots__ = ['code', 'reason']
    def __init__(self, code, reason):
        self.code = code
        self.reason = reason
        super(Close, self).__init__(Opcode.CLOSE)

    @classmethod
    def from_payload(cls, payload):
        """Decode the error 'code' and 'reason'."""
        code = None
        reason = ''
        if len(payload) == 1:
            raise errors.ProtocolError(
                'invalid close frame payload'
            )
        elif len(payload) >= 2:
            (code,) = cls._unpack16(payload[:2])
            reason_bytes = payload[2:]
            is_valid, _, _, _ = Utf8Validator().validate(reason_bytes)
            if not is_valid:
                raise errors.CriticalProtocolError(
                    'close frame contains invalid utf-8'
                )
            try:
                reason = reason_bytes.decode('utf-8')
            except UnicodeDecodeError as error:
                raise errors.CriticalProtocolError(
                    'invalid utf-8 in close reason ({})',
                    error
                )
        return cls(code, reason)

    def __repr__(self):
        return "<message CLOSE {}, {!r}>".format(self.code, self.reason)


class Ping(Message):
    """Ping message.

    :param bytes data: Ping data.

    """
    __slots__ = ['data']
    def __init__(self, data):
        self.data = data
        super(Ping, self).__init__(Opcode.PING)

    def __repr__(self):
        return "<message PING {!r}>".format(self.data)


class Pong(Message):
    """Pong message.

    :param bytes data: Pong data.

    """
    __slots__ = ['data']
    def __init__(self, data):
        self.data = data
        super(Pong, self).__init__(Opcode.PONG)

    def __repr__(self):
        return "<message PONG {!r}>".format(self.data)
