import binascii
import struct
from typing import Tuple


def dump_packet(prefix: str, data: bytes) -> None:
    try:
        data = to_string(data)
        print(prefix, ": ", data, sep="")
    except struct.error:
        data = binascii.b2a_hex(data).decode('utf8')
        print(prefix, " (not decoded): 0x", data, sep="")


def remaining_length(packet: bytes) -> Tuple[bytes, int]:
    l = min(5, len(packet))  # noqa: E741
    all_bytes = struct.unpack("!" + "B" * l, packet[:l])
    mult = 1
    rl = 0
    for i in range(1, l - 1):
        byte = all_bytes[i]

        rl += (byte & 127) * mult
        mult *= 128
        if byte & 128 == 0:
            packet = packet[i + 1:]
            break

    return (packet, rl)


def to_hex_string(packet: bytes) -> str:
    if not packet:
        return ""

    s = ""
    while len(packet) > 0:
        packet0 = struct.unpack("!B", packet[0])
        s = s+hex(packet0[0]) + " "
        packet = packet[1:]

    return s


def to_string(packet: bytes) -> str:
    if not packet:
        return ""

    packet0 = struct.unpack("!B%ds" % (len(packet)-1), bytes(packet))
    packet0 = packet0[0]
    cmd = packet0 & 0xF0
    if cmd == 0x00:
        # Reserved
        return "0x00"
    elif cmd == 0x10:
        # CONNECT
        (packet, rl) = remaining_length(packet)
        pack_format = "!H" + str(len(packet) - 2) + 's'
        (slen, packet) = struct.unpack(pack_format, packet)
        pack_format = "!" + str(slen) + 'sBBH' + str(len(packet) - slen - 4) + 's'
        (protocol, proto_ver, flags, keepalive, packet) = struct.unpack(pack_format, packet)
        kind = ("clean-session" if flags & 2 else "durable")
        s = f"CONNECT, proto={protocol}{proto_ver}, keepalive={keepalive}, {kind}"

        pack_format = "!H" + str(len(packet) - 2) + 's'
        (slen, packet) = struct.unpack(pack_format, packet)
        pack_format = "!" + str(slen) + 's' + str(len(packet) - slen) + 's'
        (client_id, packet) = struct.unpack(pack_format, packet)
        s = s + ", id=" + str(client_id)

        if flags & 4:
            pack_format = "!H" + str(len(packet) - 2) + 's'
            (slen, packet) = struct.unpack(pack_format, packet)
            pack_format = "!" + str(slen) + 's' + str(len(packet) - slen) + 's'
            (will_topic, packet) = struct.unpack(pack_format, packet)
            s = s + ", will-topic=" + str(will_topic)

            pack_format = "!H" + str(len(packet) - 2) + 's'
            (slen, packet) = struct.unpack(pack_format, packet)
            pack_format = "!" + str(slen) + 's' + str(len(packet) - slen) + 's'
            (will_message, packet) = struct.unpack(pack_format, packet)
            s = s + ", will-message=" + will_message

            s = s + ", will-qos=" + str((flags & 24) >> 3)
            s = s + ", will-retain=" + str((flags & 32) >> 5)

        if flags & 128:
            pack_format = "!H" + str(len(packet) - 2) + 's'
            (slen, packet) = struct.unpack(pack_format, packet)
            pack_format = "!" + str(slen) + 's' + str(len(packet) - slen) + 's'
            (username, packet) = struct.unpack(pack_format, packet)
            s = s + ", username=" + str(username)

        if flags & 64:
            pack_format = "!H" + str(len(packet) - 2) + 's'
            (slen, packet) = struct.unpack(pack_format, packet)
            pack_format = "!" + str(slen) + 's' + str(len(packet) - slen) + 's'
            (password, packet) = struct.unpack(pack_format, packet)
            s = s + ", password=" + str(password)

        if flags & 1:
            s = s + ", reserved=1"

        return s
    elif cmd == 0x20:
        # CONNACK
        if len(packet) == 4:
            (cmd, rl, resv, rc) = struct.unpack('!BBBB', packet)
            return "CONNACK, rl="+str(rl)+", res="+str(resv)+", rc="+str(rc)
        elif len(packet) == 5:
            (cmd, rl, flags, reason_code, proplen) = struct.unpack('!BBBBB', packet)
            return "CONNACK, rl="+str(rl)+", flags="+str(flags)+", rc="+str(reason_code)+", proplen="+str(proplen)
        else:
            return "CONNACK, (not decoded)"

    elif cmd == 0x30:
        # PUBLISH
        dup = (packet0 & 0x08) >> 3
        qos = (packet0 & 0x06) >> 1
        retain = (packet0 & 0x01)
        (packet, rl) = remaining_length(packet)
        pack_format = "!H" + str(len(packet) - 2) + 's'
        (tlen, packet) = struct.unpack(pack_format, packet)
        pack_format = "!" + str(tlen) + 's' + str(len(packet) - tlen) + 's'
        (topic, packet) = struct.unpack(pack_format, packet)
        s = "PUBLISH, rl=" + str(rl) + ", topic=" + str(topic) + ", qos=" + str(qos) + ", retain=" + str(retain) + ", dup=" + str(dup)
        if qos > 0:
            pack_format = "!H" + str(len(packet) - 2) + 's'
            (mid, packet) = struct.unpack(pack_format, packet)
            s = s + ", mid=" + str(mid)

        s = s + ", payload=" + str(packet)
        return s
    elif cmd == 0x40:
        # PUBACK
        if len(packet) == 5:
            (cmd, rl, mid, reason_code) = struct.unpack('!BBHB', packet)
            return "PUBACK, rl="+str(rl)+", mid="+str(mid)+", reason_code="+str(reason_code)
        else:
            (cmd, rl, mid) = struct.unpack('!BBH', packet)
            return "PUBACK, rl="+str(rl)+", mid="+str(mid)
    elif cmd == 0x50:
        # PUBREC
        if len(packet) == 5:
            (cmd, rl, mid, reason_code) = struct.unpack('!BBHB', packet)
            return "PUBREC, rl="+str(rl)+", mid="+str(mid)+", reason_code="+str(reason_code)
        else:
            (cmd, rl, mid) = struct.unpack('!BBH', packet)
            return "PUBREC, rl="+str(rl)+", mid="+str(mid)
    elif cmd == 0x60:
        # PUBREL
        dup = (packet0 & 0x08) >> 3
        (cmd, rl, mid) = struct.unpack('!BBH', packet)
        return "PUBREL, rl=" + str(rl) + ", mid=" + str(mid) + ", dup=" + str(dup)
    elif cmd == 0x70:
        # PUBCOMP
        (cmd, rl, mid) = struct.unpack('!BBH', packet)
        return "PUBCOMP, rl=" + str(rl) + ", mid=" + str(mid)
    elif cmd == 0x80:
        # SUBSCRIBE
        (packet, rl) = remaining_length(packet)
        pack_format = "!H" + str(len(packet) - 2) + 's'
        (mid, packet) = struct.unpack(pack_format, packet)
        s = "SUBSCRIBE, rl=" + str(rl) + ", mid=" + str(mid)
        topic_index = 0
        while len(packet) > 0:
            pack_format = "!H" + str(len(packet) - 2) + 's'
            (tlen, packet) = struct.unpack(pack_format, packet)
            pack_format = "!" + str(tlen) + 'sB' + str(len(packet) - tlen - 1) + 's'
            (topic, qos, packet) = struct.unpack(pack_format, packet)
            s = s + ", topic" + str(topic_index) + "=" + str(topic) + "," + str(qos)
        return s
    elif cmd == 0x90:
        # SUBACK
        (packet, rl) = remaining_length(packet)
        pack_format = "!H" + str(len(packet) - 2) + 's'
        (mid, packet) = struct.unpack(pack_format, packet)
        pack_format = "!" + "B" * len(packet)
        granted_qos = struct.unpack(pack_format, packet)

        s = "SUBACK, rl=" + str(rl) + ", mid=" + str(mid) + ", granted_qos=" + str(granted_qos[0])
        for i in range(1, len(granted_qos) - 1):
            s = s + ", " + str(granted_qos[i])
        return s
    elif cmd == 0xA0:
        # UNSUBSCRIBE
        (packet, rl) = remaining_length(packet)
        pack_format = "!H" + str(len(packet) - 2) + 's'
        (mid, packet) = struct.unpack(pack_format, packet)
        s = "UNSUBSCRIBE, rl=" + str(rl) + ", mid=" + str(mid)
        topic_index = 0
        while len(packet) > 0:
            pack_format = "!H" + str(len(packet) - 2) + 's'
            (tlen, packet) = struct.unpack(pack_format, packet)
            pack_format = "!" + str(tlen) + 's' + str(len(packet) - tlen) + 's'
            (topic, packet) = struct.unpack(pack_format, packet)
            s = s + ", topic" + str(topic_index) + "=" + str(topic)
        return s
    elif cmd == 0xB0:
        # UNSUBACK
        (cmd, rl, mid) = struct.unpack('!BBH', packet)
        return "UNSUBACK, rl=" + str(rl) + ", mid=" + str(mid)
    elif cmd == 0xC0:
        # PINGREQ
        (cmd, rl) = struct.unpack('!BB', packet)
        return "PINGREQ, rl=" + str(rl)
    elif cmd == 0xD0:
        # PINGRESP
        (cmd, rl) = struct.unpack('!BB', packet)
        return "PINGRESP, rl=" + str(rl)
    elif cmd == 0xE0:
        # DISCONNECT
        if len(packet) == 3:
            (cmd, rl, reason_code) = struct.unpack('!BBB', packet)
            return "DISCONNECT, rl="+str(rl)+", reason_code="+str(reason_code)
        else:
            (cmd, rl) = struct.unpack('!BB', packet)
            return "DISCONNECT, rl="+str(rl)
    elif cmd == 0xF0:
        # AUTH
        (cmd, rl) = struct.unpack('!BB', packet)
        return "AUTH, rl="+str(rl)
    raise ValueError(f"Unknown packet type {cmd}")
