"""AMQP Table Encoding/Decoding"""
import struct
import decimal
import calendar
from datetime import datetime

from pika import exceptions
from pika.compat import unicode_type, PY2, long, as_bytes


def encode_short_string(pieces, value):
    """Encode a string value as short string and append it to pieces list
    returning the size of the encoded value.

    :param list pieces: Already encoded values
    :param value: String value to encode
    :type value: str or unicode
    :rtype: int

    """
    encoded_value = as_bytes(value)
    length = len(encoded_value)

    # 4.2.5.3
    # Short strings, stored as an 8-bit unsigned integer length followed by zero
    # or more octets of data. Short strings can carry up to 255 octets of UTF-8
    # data, but may not contain binary zero octets.
    # ...
    # 4.2.5.5
    # The server SHOULD validate field names and upon receiving an invalid field
    # name, it SHOULD signal a connection exception with reply code 503 (syntax
    # error).
    # -> validate length (avoid truncated utf-8 / corrupted data), but skip null
    # byte check.
    if length > 255:
        raise exceptions.ShortStringTooLong(encoded_value)

    pieces.append(struct.pack('B', length))
    pieces.append(encoded_value)
    return 1 + length


if PY2:
    def decode_short_string(encoded, offset):
        """Decode a short string value from ``encoded`` data at ``offset``.
        """
        length = struct.unpack_from('B', encoded, offset)[0]
        offset += 1
        # Purely for compatibility with original python2 code. No idea what
        # and why this does.
        value = encoded[offset:offset + length]
        try:
            value = bytes(value)
        except UnicodeEncodeError:
            pass
        offset += length
        return value, offset

else:
    def decode_short_string(encoded, offset):
        """Decode a short string value from ``encoded`` data at ``offset``.
        """
        length = struct.unpack_from('B', encoded, offset)[0]
        offset += 1
        value = encoded[offset:offset + length].decode('utf8')
        offset += length
        return value, offset


def encode_table(pieces, table):
    """Encode a dict as an AMQP table appending the encded table to the
    pieces list passed in.

    :param list pieces: Already encoded frame pieces
    :param dict table: The dict to encode
    :rtype: int

    """
    table = table or {}
    length_index = len(pieces)
    pieces.append(None)  # placeholder
    tablesize = 0
    for (key, value) in table.items():
        tablesize += encode_short_string(pieces, key)
        tablesize += encode_value(pieces, value)

    pieces[length_index] = struct.pack('>I', tablesize)
    return tablesize + 4


def encode_value(pieces, value):
    """Encode the value passed in and append it to the pieces list returning
    the the size of the encoded value.

    :param list pieces: Already encoded values
    :param any value: The value to encode
    :rtype: int

    """

    if PY2:
        if isinstance(value, basestring):
            if isinstance(value, unicode_type):
                value = value.encode('utf-8')
            pieces.append(struct.pack('>cI', b'S', len(value)))
            pieces.append(value)
            return 5 + len(value)
    else:
        # support only str on Python 3
        if isinstance(value, str):
            value = value.encode('utf-8')
            pieces.append(struct.pack('>cI', b'S', len(value)))
            pieces.append(value)
            return 5 + len(value)
    if isinstance(value, bool):
        pieces.append(struct.pack('>cB', b't', int(value)))
        return 2
    if isinstance(value, long):
        pieces.append(struct.pack('>cq', b'l', value))
        return 9
    elif isinstance(value, int):
        pieces.append(struct.pack('>ci', b'I', value))
        return 5
    elif isinstance(value, decimal.Decimal):
        value = value.normalize()
        if value.as_tuple().exponent < 0:
            decimals = -value.as_tuple().exponent
            raw = int(value * (decimal.Decimal(10) ** decimals))
            pieces.append(struct.pack('>cBi', b'D', decimals, raw))
        else:
            # per spec, the "decimals" octet is unsigned (!)
            pieces.append(struct.pack('>cBi', b'D', 0, int(value)))
        return 6
    elif isinstance(value, datetime):
        pieces.append(struct.pack('>cQ', b'T',
                                  calendar.timegm(value.utctimetuple())))
        return 9
    elif isinstance(value, dict):
        pieces.append(struct.pack('>c', b'F'))
        return 1 + encode_table(pieces, value)
    elif isinstance(value, list):
        p = []
        for v in value:
            encode_value(p, v)
        piece = b''.join(p)
        pieces.append(struct.pack('>cI', b'A', len(piece)))
        pieces.append(piece)
        return 5 + len(piece)
    elif value is None:
        pieces.append(struct.pack('>c', b'V'))
        return 1
    else:
        raise exceptions.UnsupportedAMQPFieldException(pieces, value)


def decode_table(encoded, offset):
    """Decode the AMQP table passed in from the encoded value returning the
    decoded result and the number of bytes read plus the offset.

    :param str encoded: The binary encoded data to decode
    :param int offset: The starting byte offset
    :rtype: tuple

    """
    result = {}
    tablesize = struct.unpack_from('>I', encoded, offset)[0]
    offset += 4
    limit = offset + tablesize
    while offset < limit:
        key, offset = decode_short_string(encoded, offset)
        value, offset = decode_value(encoded, offset)
        result[key] = value
    return result, offset


def decode_value(encoded, offset):
    """Decode the value passed in returning the decoded value and the number
    of bytes read in addition to the starting offset.

    :param str encoded: The binary encoded data to decode
    :param int offset: The starting byte offset
    :rtype: tuple
    :raises: pika.exceptions.InvalidFieldTypeException

    """
    # slice to get bytes in Python 3 and str in Python 2
    kind = encoded[offset:offset + 1]
    offset += 1

    # Bool
    if kind == b't':
        value = struct.unpack_from('>B', encoded, offset)[0]
        value = bool(value)
        offset += 1

    # Short-Short Int
    elif kind == b'b':
        value = struct.unpack_from('>B', encoded, offset)[0]
        offset += 1

    # Short-Short Unsigned Int
    elif kind == b'B':
        value = struct.unpack_from('>b', encoded, offset)[0]
        offset += 1

    # Short Int
    elif kind == b'U':
        value = struct.unpack_from('>h', encoded, offset)[0]
        offset += 2

    # Short Unsigned Int
    elif kind == b'u':
        value = struct.unpack_from('>H', encoded, offset)[0]
        offset += 2

    # Long Int
    elif kind == b'I':
        value = struct.unpack_from('>i', encoded, offset)[0]
        offset += 4

    # Long Unsigned Int
    elif kind == b'i':
        value = struct.unpack_from('>I', encoded, offset)[0]
        offset += 4

    # Long-Long Int
    elif kind == b'L':
        value = long(struct.unpack_from('>q', encoded, offset)[0])
        offset += 8

    # Long-Long Unsigned Int
    elif kind == b'l':
        value = long(struct.unpack_from('>Q', encoded, offset)[0])
        offset += 8

    # Float
    elif kind == b'f':
        value = long(struct.unpack_from('>f', encoded, offset)[0])
        offset += 4

    # Double
    elif kind == b'd':
        value = long(struct.unpack_from('>d', encoded, offset)[0])
        offset += 8

    # Decimal
    elif kind == b'D':
        decimals = struct.unpack_from('B', encoded, offset)[0]
        offset += 1
        raw = struct.unpack_from('>i', encoded, offset)[0]
        offset += 4
        value = decimal.Decimal(raw) * (decimal.Decimal(10) ** -decimals)

    # Short String
    elif kind == b's':
        value, offset = decode_short_string(encoded, offset)

    # Long String
    elif kind == b'S':
        length = struct.unpack_from('>I', encoded, offset)[0]
        offset += 4
        value = encoded[offset:offset + length].decode('utf8')
        offset += length

    # Field Array
    elif kind == b'A':
        length = struct.unpack_from('>I', encoded, offset)[0]
        offset += 4
        offset_end = offset + length
        value = []
        while offset < offset_end:
            v, offset = decode_value(encoded, offset)
            value.append(v)

    # Timestamp
    elif kind == b'T':
        value = datetime.utcfromtimestamp(struct.unpack_from('>Q', encoded,
                                                             offset)[0])
        offset += 8

    # Field Table
    elif kind == b'F':
        (value, offset) = decode_table(encoded, offset)

    # Null / Void
    elif kind == b'V':
        value = None
    else:
        raise exceptions.InvalidFieldTypeException(kind)

    return value, offset

[((21, 20, 21, 35), 'pika.compat.as_bytes', 'as_bytes', ({(21, 29, 21, 34): 'value'}, {}), '(value)', False, 'from pika.compat import unicode_type, PY2, long, as_bytes\n'), ((87, 27, 87, 55), 'struct.pack', 'struct.pack', ({(87, 39, 87, 43): '""">I"""', (87, 45, 87, 54): 'tablesize'}, {}), "('>I', tablesize)", False, 'import struct\n'), ((36, 14, 36, 58), 'pika.exceptions.ShortStringTooLong', 'exceptions.ShortStringTooLong', ({(36, 44, 36, 57): 'encoded_value'}, {}), '(encoded_value)', False, 'from pika import exceptions\n'), ((38, 18, 38, 42), 'struct.pack', 'struct.pack', ({(38, 30, 38, 33): '"""B"""', (38, 35, 38, 41): 'length'}, {}), "('B', length)", False, 'import struct\n'), ((166, 16, 166, 57), 'struct.unpack_from', 'struct.unpack_from', ({(166, 35, 166, 39): '""">I"""', (166, 41, 166, 48): 'encoded', (166, 50, 166, 56): 'offset'}, {}), "('>I', encoded, offset)", False, 'import struct\n'), ((47, 17, 47, 57), 'struct.unpack_from', 'struct.unpack_from', ({(47, 36, 47, 39): '"""B"""', (47, 41, 47, 48): 'encoded', (47, 50, 47, 56): 'offset'}, {}), "('B', encoded, offset)", False, 'import struct\n'), ((63, 17, 63, 57), 'struct.unpack_from', 'struct.unpack_from', ({(63, 36, 63, 39): '"""B"""', (63, 41, 63, 48): 'encoded', (63, 50, 63, 56): 'offset'}, {}), "('B', encoded, offset)", False, 'import struct\n'), ((119, 22, 119, 53), 'struct.pack', 'struct.pack', ({(119, 34, 119, 39): '""">cq"""', (119, 41, 119, 45): "b'l'", (119, 47, 119, 52): 'value'}, {}), "('>cq', b'l', value)", False, 'import struct\n'), ((192, 16, 192, 57), 'struct.unpack_from', 'struct.unpack_from', ({(192, 35, 192, 39): '""">B"""', (192, 41, 192, 48): 'encoded', (192, 50, 192, 56): 'offset'}, {}), "('>B', encoded, offset)", False, 'import struct\n'), ((122, 22, 122, 53), 'struct.pack', 'struct.pack', ({(122, 34, 122, 39): '""">ci"""', (122, 41, 122, 45): "b'I'", (122, 47, 122, 52): 'value'}, {}), "('>ci', b'I', value)", False, 'import struct\n'), ((198, 16, 198, 57), 'struct.unpack_from', 'struct.unpack_from', ({(198, 35, 198, 39): '""">B"""', (198, 41, 198, 48): 'encoded', (198, 50, 198, 56): 'offset'}, {}), "('>B', encoded, offset)", False, 'import struct\n'), ((203, 16, 203, 57), 'struct.unpack_from', 'struct.unpack_from', ({(203, 35, 203, 39): '""">b"""', (203, 41, 203, 48): 'encoded', (203, 50, 203, 56): 'offset'}, {}), "('>b', encoded, offset)", False, 'import struct\n'), ((129, 26, 129, 66), 'struct.pack', 'struct.pack', ({(129, 38, 129, 44): '""">cBi"""', (129, 46, 129, 50): "b'D'", (129, 52, 129, 60): 'decimals', (129, 62, 129, 65): 'raw'}, {}), "('>cBi', b'D', decimals, raw)", False, 'import struct\n'), ((208, 16, 208, 57), 'struct.unpack_from', 'struct.unpack_from', ({(208, 35, 208, 39): '""">h"""', (208, 41, 208, 48): 'encoded', (208, 50, 208, 56): 'offset'}, {}), "('>h', encoded, offset)", False, 'import struct\n'), ((139, 22, 139, 45), 'struct.pack', 'struct.pack', ({(139, 34, 139, 38): '""">c"""', (139, 40, 139, 44): "b'F'"}, {}), "('>c', b'F')", False, 'import struct\n'), ((213, 16, 213, 57), 'struct.unpack_from', 'struct.unpack_from', ({(213, 35, 213, 39): '""">H"""', (213, 41, 213, 48): 'encoded', (213, 50, 213, 56): 'offset'}, {}), "('>H', encoded, offset)", False, 'import struct\n'), ((128, 31, 128, 50), 'decimal.Decimal', 'decimal.Decimal', ({(128, 47, 128, 49): '10'}, {}), '(10)', False, 'import decimal\n'), ((153, 14, 153, 69), 'pika.exceptions.UnsupportedAMQPFieldException', 'exceptions.UnsupportedAMQPFieldException', ({(153, 55, 153, 61): 'pieces', (153, 63, 153, 68): 'value'}, {}), '(pieces, value)', False, 'from pika import exceptions\n'), ((218, 16, 218, 57), 'struct.unpack_from', 'struct.unpack_from', ({(218, 35, 218, 39): '""">i"""', (218, 41, 218, 48): 'encoded', (218, 50, 218, 56): 'offset'}, {}), "('>i', encoded, offset)", False, 'import struct\n'), ((150, 22, 150, 45), 'struct.pack', 'struct.pack', ({(150, 34, 150, 38): '""">c"""', (150, 40, 150, 44): "b'V'"}, {}), "('>c', b'V')", False, 'import struct\n'), ((223, 16, 223, 57), 'struct.unpack_from', 'struct.unpack_from', ({(223, 35, 223, 39): '""">I"""', (223, 41, 223, 48): 'encoded', (223, 50, 223, 56): 'offset'}, {}), "('>I', encoded, offset)", False, 'import struct\n'), ((228, 21, 228, 62), 'struct.unpack_from', 'struct.unpack_from', ({(228, 40, 228, 44): '""">q"""', (228, 46, 228, 53): 'encoded', (228, 55, 228, 61): 'offset'}, {}), "('>q', encoded, offset)", False, 'import struct\n'), ((233, 21, 233, 62), 'struct.unpack_from', 'struct.unpack_from', ({(233, 40, 233, 44): '""">Q"""', (233, 46, 233, 53): 'encoded', (233, 55, 233, 61): 'offset'}, {}), "('>Q', encoded, offset)", False, 'import struct\n'), ((238, 21, 238, 62), 'struct.unpack_from', 'struct.unpack_from', ({(238, 40, 238, 44): '""">f"""', (238, 46, 238, 53): 'encoded', (238, 55, 238, 61): 'offset'}, {}), "('>f', encoded, offset)", False, 'import struct\n'), ((243, 21, 243, 62), 'struct.unpack_from', 'struct.unpack_from', ({(243, 40, 243, 44): '""">d"""', (243, 46, 243, 53): 'encoded', (243, 55, 243, 61): 'offset'}, {}), "('>d', encoded, offset)", False, 'import struct\n'), ((248, 19, 248, 59), 'struct.unpack_from', 'struct.unpack_from', ({(248, 38, 248, 41): '"""B"""', (248, 43, 248, 50): 'encoded', (248, 52, 248, 58): 'offset'}, {}), "('B', encoded, offset)", False, 'import struct\n'), ((250, 14, 250, 55), 'struct.unpack_from', 'struct.unpack_from', ({(250, 33, 250, 37): '""">i"""', (250, 39, 250, 46): 'encoded', (250, 48, 250, 54): 'offset'}, {}), "('>i', encoded, offset)", False, 'import struct\n'), ((252, 16, 252, 36), 'decimal.Decimal', 'decimal.Decimal', ({(252, 32, 252, 35): 'raw'}, {}), '(raw)', False, 'import decimal\n'), ((252, 40, 252, 59), 'decimal.Decimal', 'decimal.Decimal', ({(252, 56, 252, 58): '(10)'}, {}), '(10)', False, 'import decimal\n'), ((260, 17, 260, 58), 'struct.unpack_from', 'struct.unpack_from', ({(260, 36, 260, 40): '""">I"""', (260, 42, 260, 49): 'encoded', (260, 51, 260, 57): 'offset'}, {}), "('>I', encoded, offset)", False, 'import struct\n'), ((267, 17, 267, 58), 'struct.unpack_from', 'struct.unpack_from', ({(267, 36, 267, 40): '""">I"""', (267, 42, 267, 49): 'encoded', (267, 51, 267, 57): 'offset'}, {}), "('>I', encoded, offset)", False, 'import struct\n'), ((277, 42, 278, 68), 'struct.unpack_from', 'struct.unpack_from', ({(277, 61, 277, 65): '""">Q"""', (277, 67, 277, 74): 'encoded', (278, 61, 278, 67): 'offset'}, {}), "('>Q', encoded, offset)", False, 'import struct\n'), ((289, 14, 289, 56), 'pika.exceptions.InvalidFieldTypeException', 'exceptions.InvalidFieldTypeException', ({(289, 51, 289, 55): 'kind'}, {}), '(kind)', False, 'from pika import exceptions\n')]