# Credit to @stelar7, this python implementation is a port of his original javascript implementation

from base64 import b32decode, b32encode
from io import BytesIO
MAX_KNOWN_VERSION = 20


class Base32:
    @staticmethod
    def decode(b32string):
        s = Base32.add_padding(b32string)
        return b32decode(s)

    @staticmethod
    def encode(bytes_list):
        encoded_string = b32encode(bytes(bytes_list))
        encoded_string = str(encoded_string, "utf-8").strip("=")
        return encoded_string

    # python's builtin decoder is strict with padding
    @staticmethod
    def add_padding(b32String):
        length = len(b32String)
        padding = 0

        if (length % 8 > 0):
            padding = 8 - (length % 8)
            b32String += "=" * padding
        return b32String


faction_code_to_id = {
    "DE": 0,
    "FR": 1,
    "IO": 2,
    "NX": 3,
    "PZ": 4,
    "SI": 5,
    "BW": 6,
    "SH": 7,
    "BC": 10,
    "MT": 9
}
id_to_faction_code = {
    0: "DE",
    1: "FR",
    2: "IO",
    3: "NX",
    4: "PZ",
    5: "SI",
    6: "BW",
    7: "SH",
    10: "BC",
    9: "MT"
}


class DeckCode:
    @staticmethod
    def encode_deck(deck):

        result = [17]
        cards_3 = []
        cards_2 = []
        cards_1 = []
        cards_other = []
        #remove non-collectable and created deck code 
        deck = DeckCode.remove_invalid_cards(deck)
        if not DeckCode.is_valid_card_codes_and_count(deck):
            raise Exception("Deck contains invalid card codes")
        else:
            for card, count in deck.items():
                if count == 3:
                    cards_3.append(card)
                elif count == 2:
                    cards_2.append(card)
                elif count == 1:
                    cards_1.append(card)
                else:
                    cards_other.append((card, count))

        grouped_cards_3 = DeckCode.group_by_faction(cards_3)
        grouped_cards_2 = DeckCode.group_by_faction(cards_2)
        grouped_cards_1 = DeckCode.group_by_faction(cards_1)

        grouped_cards_3 = DeckCode.sort_card_groups(grouped_cards_3)
        grouped_cards_2 = DeckCode.sort_card_groups(grouped_cards_2)
        grouped_cards_1 = DeckCode.sort_card_groups(grouped_cards_1)
        cards_other = sorted(cards_other)

        # encode
        result = [*result, *VarIntTransformer.encodeGroupOf(grouped_cards_3)]
        result = [*result, *VarIntTransformer.encodeGroupOf(grouped_cards_2)]
        result = [*result, *VarIntTransformer.encodeGroupOf(grouped_cards_1)]

        result = [*result, *VarIntTransformer.encodeNOfs(cards_other)]

        return Base32.encode(result)

    @staticmethod
    def group_by_faction(card_list):
        new_list = []
        while len(card_list) > 0:
            card = card_list.pop(0)
            set_num, faction, _ = DeckCode.parse_card_code(card)

            faction_set = []
            faction_set.append(card)

            temp_list = card_list[:]
            for card in temp_list:
                current_set_num, current_faction, _ = DeckCode.parse_card_code(
                    card)
                if current_set_num == set_num and current_faction == faction:
                    faction_set.append(card)
                    card_list.remove(card)
            faction_set.sort()
            new_list.append(faction_set)

        return new_list

    @staticmethod
    def sort_card_groups(card_groups):
        s = sorted(card_groups, key=lambda x: len(x))
        return s

    @staticmethod
    def parse_card_code(card_code):
        set = card_code[:2]
        faction = card_code[2:4]
        card_id = card_code[4:]
        return set, faction, card_id

    @staticmethod
    def is_valid_card_codes_and_count(deck):
        for card, count in deck.items():
            code = card
            if len(code) != 7:
                print(f"deck code length is {len(code)}")
                return False

            # check set code is numeric
            for char in code[:2]:
                if not char.isdigit():
                    print(f"code is not digit: {char}")
                    return False

            faction = faction_code_to_id.get(code[2:4], -1)
            if faction < 0:
                print(f"faction code not in faction code list: {code[2:4]}")
                return False

            for char in code[4:]:
                if not char.isdigit():
                    print(f"card number contains non digit: {char}")
                    return False

            if count < 1:
                print("less than one card: {count}")
                return False

        return True

    @staticmethod
    def remove_invalid_cards(deck):
        # When you remove continue, it will not print error messages
        newDeck = deck.copy()
        for card, count in deck.items():
            code = card
            if len(code) != 7:
                print(f"remove_invalid_cards deck code length is {len(code)}")
                del newDeck[card]
                continue

            # check set code is numeric
            for char in code[:2]:
                if not char.isdigit():
                    print(f"remove_invalid_cards code is not digit: {char}")
                    del newDeck[card]
                    continue

            faction = faction_code_to_id.get(code[2:4], -1)
            if faction < 0:
                print(f"remove_invalid_cards faction code not in faction code list: {code[2:4]}")
                del newDeck[card]
                continue

            for char in code[4:]:
                if not char.isdigit():
                    print(f"remove_invalid_cards card number contains non digit: {char}")
                    del newDeck[card]
                    continue

            if count < 1:
                print("remove_invalid_cards less than one card: {count}")
                del newDeck[card]
                continue
        return newDeck

    @staticmethod
    def decode_deck(string):
        result = {}
        data = Base32.decode(string)
        byte_list = BytesIO(data)
        version = VarIntTransformer.popVarInt(byte_list)     
        if version > MAX_KNOWN_VERSION:
            raise ValueError("Please update to the latest version of twisted_fate")
        for i in range(3, 0, -1):
            numGroupOfs = VarIntTransformer.popVarInt(byte_list)
            for __ in range(numGroupOfs):
                numOfsInThisGroup = VarIntTransformer.popVarInt(byte_list)
                setNum = VarIntTransformer.popVarInt(byte_list)
                faction = VarIntTransformer.popVarInt(byte_list)
                for ___ in range(numOfsInThisGroup):
                    card = VarIntTransformer.popVarInt(byte_list)

                    setString = str(setNum).zfill(2)
                    factionString = id_to_faction_code[faction]
                    cardString = str(card).zfill(3)

                    card_code = setString + factionString + cardString
                    result[card_code] = i

        while len(bytearray(byte_list)) > 0:
            fpc = VarIntTransformer.popVarInt(byte_list)
            fps = VarIntTransformer.popVarInt(byte_list)
            fpf = VarIntTransformer.popVarInt(byte_list)
            fpn = VarIntTransformer.popVarInt(byte_list)

            fpss = str(fps).zfill(2)
            fpfs = id_to_faction_code[fpf]
            fpns = str(fpn).zfill(3)

            card_code = fpss + fpfs + fpns
            result[card_code] = fpc
        return result


class VarIntTransformer:
    @staticmethod
    def popVarInt(stream):
        #data = BytesIO(_bytes)
        shift = 0
        result = 0
        while True:
            c = stream.read(1)
            if c == b"" or c == "":
                raise EOFError("Unexpected EOF while reading varint")
            i = ord(c)
            result |= (i & 0x7f) << shift
            shift += 7
            if not (i & 0x80):
                break

        return result

    @staticmethod
    def getVarInt(value):
        value = int(value)
        AllButMSB = 0x7F
        JustMSB = 0x80

        buff = [10]
        current_index = 0

        if value == 0:
            return [0]

        while value != 0:
            byte_val = value & AllButMSB
            value >>= 7

            if value != 0:
                byte_val |= JustMSB

            try:
                buff[current_index] = byte_val
            except IndexError:
                buff.append(byte_val)

            current_index += 1

        return buff[:current_index]

    @staticmethod
    def encodeNOfs(nOfs):
        _bytes = []
        for card, count in nOfs:
            _bytes = [*_bytes, *VarIntTransformer.getVarInt(count)]

            setNum, factionCode, cardNum = DeckCode.parse_card_code(card)
            factionNum = faction_code_to_id[factionCode]

            _bytes = [*_bytes, *VarIntTransformer.getVarInt(setNum)]
            _bytes = [*_bytes, *VarIntTransformer.getVarInt(factionNum)]
            _bytes = [*_bytes, *VarIntTransformer.getVarInt(cardNum)]

        return _bytes

    @staticmethod
    def encodeGroupOf(groupOf):
        _bytes = []

        _bytes = [*_bytes, *VarIntTransformer.getVarInt(len(groupOf))]

        for cardList in groupOf:
            _bytes = [*_bytes, *VarIntTransformer.getVarInt(len(cardList))]

            current_card_code = cardList[0]
            current_set_num, current_faction_code, _ = DeckCode.parse_card_code(
                current_card_code)
            current_faction_num = faction_code_to_id[current_faction_code]

            _bytes = [*_bytes, *VarIntTransformer.getVarInt(current_set_num)]
            _bytes = [
                *_bytes, *VarIntTransformer.getVarInt(current_faction_num)
            ]

            for card in cardList:
                sequenceNumber = card[4:]
                _bytes = [
                    *_bytes, *VarIntTransformer.getVarInt(sequenceNumber)
                ]

        return _bytes
