"""
Smartglass message un-/packing

Also handles de/encryption internally, this means:
You feed plaintext data (wrapped in :class:`.XStruct`) and you get plaintext data

**Note on encryption**
Depending on the packet-type, acquiring the `Initialization Vector` and
encrypting the `protected payload` happens differently:

* ConnectRequest: The `IV` is randomly chosen from calculated
  Elliptic Curve (which happens in :class:`Crypto`) and is delivered
  inside the `unprotected payload` section of the ConnectRequest message.
* Messages: The `IV` is generated by encrypting the first 16 bytes of
  the `unencrypted` header.

**Note on padding**
If message has `protected payload` it might need padding according
to PKCS#7 (e.g. padding is in whole bytes, the value of each added byte is
the number of bytes that are added, i.e. N bytes, each of value N are
added. thx wikipedia).
"""
from construct import Int16ub
from xbox.sg.enum import PacketType
from xbox.sg.crypto import PKCS7Padding
from xbox.sg.packet import simple, message
from xbox.sg.utils.struct import flatten


class PackerError(Exception):
    """
    Custom exceptions for usage in packer module
    """
    pass


def unpack(buf, crypto=None):
    """
    Unpacks messages from Smartglass CoreProtocol.

    For messages that require decryption, a Crypto instance needs to be passed
    as well.

    The decryption happens in :class:`CryptoTunnel`.

    Args:
        buf (bytes): A byte string to be deserialized into a message.
        crypto (Crypto): Instance of :class:`Crypto`.

    Raises:
        PackerError: On various errors, instance of :class:`PackerError`.

    Returns:
        Container: The deserialized message, instance of :class:`Container`.
    """
    msg_struct = None
    pkt_type = PacketType(Int16ub.parse(buf[:2]))

    if pkt_type not in PacketType:
        raise PackerError("Invalid packet type")

    if pkt_type in simple.pkt_types:
        msg_struct = simple.struct
    elif pkt_type == PacketType.Message:
        msg_struct = message.struct

    return msg_struct.parse(buf, _crypto=crypto)


def pack(msg, crypto=None):
    """
    Packs messages for Smartglass CoreProtocol.

    For messages that require encryption, a Crypto instance needs to be passed
    as well.

    Args:
        msg (XStructObj): A serializable message, instance of
                          :class:`XStructObj`.
        crypto (Crypto): Instance of :class:`Crypto`.

    Returns:
        bytes: The serialized bytes.
    """
    container = flatten(msg.container)
    packed_unprotected = b''
    packed_protected = b''

    if container.get('unprotected_payload', None):
        packed_unprotected = msg.subcon.unprotected_payload.build(
            container.unprotected_payload, **container
        )

    if container.get('protected_payload', None):
        packed_protected = msg.subcon.protected_payload.build(
            container.protected_payload, **container
        )

    container.header.unprotected_payload_length = len(packed_unprotected)
    container.header.protected_payload_length = len(packed_protected)
    packed_header = msg.subcon.header.build(
        container.header, **container
    )

    if container.get('protected_payload', None) and packed_protected:
        connect_types = [PacketType.ConnectRequest, PacketType.ConnectResponse]
        if container.header.pkt_type in connect_types:
            iv = container.unprotected_payload.iv
        elif container.header.pkt_type == PacketType.Message:
            iv = crypto.generate_iv(packed_header[:16])
        else:
            raise PackerError("Incompatible packet type for encryption")

        if PKCS7Padding.size(len(packed_protected), 16) > 0:
            packed_protected = PKCS7Padding.pad(packed_protected, 16)

        packed_protected = crypto.encrypt(iv, packed_protected)
        buffer = packed_header + packed_unprotected + packed_protected
        return buffer + crypto.hash(buffer)
    else:
        return packed_header + packed_unprotected


def payload_length(msg):
    """
    Calculates the packed length in bytes of the given message.

    Args:
        msg (XStructObj): A serializable message, instance of
                          :class:`XStructObj`.

    Returns:
        int: The packed message length in bytes.
    """
    container = flatten(msg.container)
    packed_unprotected = b''
    packed_protected = b''

    if container.get('unprotected_payload', None):
        packed_unprotected = msg.subcon.unprotected_payload.build(
            container.unprotected_payload, **container
        )

    if container.get('protected_payload', None):
        packed_protected = msg.subcon.protected_payload.build(
            container.protected_payload, **container
        )

    return len(packed_unprotected + packed_protected)
