import os
import json
from simple_websocket.ws import ConnectionClosed
from math import ceil
from random import randint
import random
import threading
from time import sleep
import traceback
from flask import Flask, render_template, request
from flask_sock import Sock
from threading import Timer, Thread
import logging

log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
app = Flask(__name__)
sock = Sock(app)
app.config['TEMPLATES_AUTO_RELOAD'] = True
app.jinja_env.auto_reload = True
app.config.from_object(__name__)
if os.path.isdir("/home/runner") == True:
    # on replit
    serverHost = 'ws${location.protocol.replace("http","").replace(":","")}://${location.hostname}/sock'  # ${location.hostname}
    startCode = os.environ["startKey"]
    loginKey = os.environ["loginKey"]
else:
    # not on replit
    serverHost = "ws://${location.hostname}/sock"  # ${location.hostname}
    startCode = "pls"
    loginKey = "pls"

cards = {
    "red": {
        2: [
            "2+", "aussetzen", "richtungswechsel", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "komunist"
        ]
    },
    "green": {
        2: [
            "2+", "aussetzen", "richtungswechsel", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "komunist"
        ]
    },
    "yellow": {
        2: [
            "2+", "aussetzen", "richtungswechsel", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "komunist"
        ]
    },
    "blue": {
        2: [
            "2+", "aussetzen", "richtungswechsel", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "komunist"
        ]
    },
    "": {
        4: ["farbwechsel4+", "farbwechsel"]
    }
}

deck = []
for color in cards.keys():
    for numbers in cards[color].keys():
        for i in range(numbers):
            for cardType in cards[color][numbers]:
                deck.append(color + "_" + cardType)
allDeck = []

for x in range(40):
    allDeck += deck

noCardLimit = True
startCardCount = 2

class gameMaster:

    def __init__(self):

        self.twoPlusAdder = 0

        self.scoreboard = {}

        self.events = {}

        self.specialAction = ""
        self.nextPlayerEvents = []

        self.status = "waiting_for_players"
        self.currentPlayer = 0
        self.direction = 1

        self.tables = []
        self.otherTables = []

        self.players = []
        self.playerNames = {}
        self.playerClasses = {}
        self.watchers = []
        self.wonPlayers = []

        self.playerDeck = {}
        self.availableCards = allDeck
        self.lyingCards = []

        self.cardCache = ""

    def sendTable(self, dat):
        for value in self.tables:
            value.send_message(json.dumps(dat))
        for value in self.otherTables:
            value.send_message(json.dumps(dat))

    def sendWatcher(self, dat):
        for x in self.watchers:
            x.send_message(json.dumps(dat))

    def setCards(self):
        self.availableCards = allDeck
        iteration = 0
        for playerId in self.players:
            self.playerDeck[playerId] = []
            for i in range(startCardCount):
                choice = random.randint(0, len(self.availableCards) - 1)
                self.playerDeck[playerId].append(self.availableCards[choice])
                self.availableCards.pop(choice)
                iteration += 1

            self.addEvent(
                playerId, {
                    "type": "stats",
                    "dat": {
                        "type": "deck",
                        "dat": self.playerDeck[playerId]
                    }
                })
        self.status = "playing"
        self.addEvents({"type": "status", "dat": "playing"})

    def start(self):
        self.setCards()
        choice = random.randint(0, len(self.availableCards) - 1)
        self.lyingCards = []
        self.addCard(self.availableCards[choice])
        self.availableCards.pop(choice)

        self.addEvents({"type": "action", "dat": "startGame"})

        self.easyEvents("cardRedo")
        self.askPlayer()

    def playerHas2x(self, playerId):
        for x in self.playerDeck[playerId]:
            if x.endswith("2+"):
                return True

    def askPlayer(self):
        playerId = self.players[self.currentPlayer]
        self.addEvents({
            "type": "stats",
            "dat": {
                "type": "currentPlayer",
                "dat": self.currentPlayer
            }
        })
        self.sendTable({"type": "currentPlayer", "dat": self.currentPlayer})
        self.sendWatcher({
            "type": "stats",
            "dat": {
                "type": "currentPlayer",
                "dat": self.currentPlayer
            }
        })
        # run next Player events
        if self.nextPlayerEvents != [] and self.specialAction == "":
            # doing 4+ and 2+
            for x in self.nextPlayerEvents:
                if x == "4+":
                    self.drawCard(playerId)
                    self.drawCard(playerId)
                    self.drawCard(playerId)
                    self.drawCard(playerId)
                    self.playerClasses[playerId].send_message(
                        json.dumps({
                            "type": "message",
                            "dat": "Du Hast 4 Karten gezogen"
                        }))
                if x == "2+":
                    self.drawCard(playerId)
                    self.drawCard(playerId)
                    self.playerClasses[playerId].send_message(
                        json.dumps({
                            "type": "message",
                            "dat": "Du Hast 2 Karten gezogen"
                        }))

            # doing Aussetzen
            np = 0
            for x in self.nextPlayerEvents:
                if x == "aussetzen":
                    np += 1
            self.nextPlayerEvents = []
            if np != 0:
                print("next player")
                self.nextPlayer(np)
                return

            # while self.nextPlayerEvents != []:
            #    if self.nextPlayerEvents[0] == "4+":
            #        self.drawCard(playerId)
            #        self.drawCard(playerId)
            #        self.drawCard(playerId)
            #        self.drawCard(playerId)
            #        self.playerClasses[playerId].send_message(
            #            json.dumps({
            #                "type": "message",
            #                "dat": "Du Hast 4 Karten gezogen"
            #            }))
            #    elif self.nextPlayerEvents[0] == "2+":
            #        self.twoPlusAdder += 2
            #        if self.playerHas2x(playerId):
            #            self.addEvent(playerId, {
            #                "type": "status",
            #                "dat": "2+_Desicion"
            #            })
            #            self.addEvent(
            #                playerId, {
            #                    "type": "action",
            #                    "dat": "slect2+Card",
            #                    "dat2": self.twoPlusAdder
            #                })
            #            self.nextPlayerEvents = self.nextPlayerEvents[1:]
            #            return
            #        else:
            #            for x in range(self.twoPlusAdder):
            #                self.drawCard(playerId)
            #            self.playerClasses[playerId].send_message(
            #                json.dumps({
            #                    "type":
            #                    "message",
            #                    "dat":
            #                    "Du Hast " + str(self.twoPlusAdder) +
            #                    " Karten gezogen"
            #                }))
            #            self.twoPlusAdder = 0
            #    elif self.nextPlayerEvents[0] == "aussetzen":
            #        print("aussetzen")
            #        self.nextPlayerEvents = self.nextPlayerEvents[1:]
            #        self.nextPlayer()
            #        return
            #    else:
            #        print("error: unknown playerEvent   : " +
            #              self.nextPlayerEvents[0])
            #    self.nextPlayerEvents = self.nextPlayerEvents[1:]
        # special cards appied to the current player (after the next player is allready selected so actually to the next player)
        if self.lyingCards[-1].endswith("4+"):
            if self.specialAction.startswith("winAussetzen"):
                self.nextPlayerEvents.append("4+")
            else:
                self.drawCard(playerId)
                self.drawCard(playerId)
                self.drawCard(playerId)
                self.drawCard(playerId)
                self.playerClasses[playerId].send_message(
                    json.dumps({
                        "type": "message",
                        "dat": "Du Hast 4 Karten gezogen"
                    }))
        if self.lyingCards[-1].endswith("2+"):
            if self.specialAction.startswith("winAussetzen"):
                self.nextPlayerEvents.append("2+")
            else:
                self.twoPlusAdder += 2
                if self.playerHas2x(playerId):
                    self.addEvent(playerId, {
                        "type": "status",
                        "dat": "2+_Desicion"
                    })
                    self.addEvent(
                        playerId, {
                            "type": "action",
                            "dat": "slect2+Card",
                            "dat2": self.twoPlusAdder
                        })
                else:
                    for x in range(self.twoPlusAdder):
                        self.drawCard(playerId)
                    self.playerClasses[playerId].send_message(
                        json.dumps({
                            "type":
                            "message",
                            "dat":
                            "Du Hast " + str(self.twoPlusAdder) +
                            " Karten gezogen"
                        }))
                    self.twoPlusAdder = 0
                    self.addEvent(playerId, {
                        "type": "status",
                        "dat": "slectCard"
                    })
                    self.addEvent(playerId, {
                        "type": "action",
                        "dat": "slectCard"
                    })
        else:
            self.twoPlusAdder = 0
            self.addEvent(playerId, {"type": "status", "dat": "slectCard"})
            self.addEvent(playerId, {"type": "action", "dat": "slectCard"})

    def withdraw2x(self, playerId):
        for x in range(self.twoPlusAdder):
            self.drawCard(playerId)
        self.playerClasses[playerId].send_message(
            json.dumps({
                "type":
                "message",
                "dat":
                "Du Hast " + str(self.twoPlusAdder) + " Karten gezogen"
            }))
        self.twoPlusAdder = 0
        # if self.nextPlayerEvents == []:
        self.addEvent(playerId, {"type": "status", "dat": "slectCard"})
        self.addEvent(playerId, {"type": "action", "dat": "slectCard"})

    def playerWin(self, playerId):
        print(f"player {playerId} Finished!")

        name = "[" + str(playerId) + "]"
        if (playerId in self.playerNames):
            name = self.playerNames[playerId]
        self.wonPlayers.append(name)

        # message everyone that the player has won/finished
        if len(self.tables) > 0:
            self.sendTable({"type": "playerFinished", "dat": name})
        else:
            self.addEvents({
                "type": "action",
                "dat": {
                    "type": "playerFinished",
                    "dat": name
                }
            })
            self.sendWatcher({
                "type": "action",
                "dat": {
                    "type": "playerFinished",
                    "dat": name
                }
            })

        # tell player he won. Automatically reconnects thus automatically removing all informations of the player in the game
        self.playerClasses[playerId].send_message(
            json.dumps({
                "type": "action",
                "dat": {
                    "type": "won",
                    "dat": str(len(self.wonPlayers))
                }
            }))
        self.playerClasses[playerId].handle_close(True)

    def isThisCombo(self,
                    cards,
                    comboCards,
                    playerId,
                    specificColour=False,
                    topCardColor="green"):
        if len(comboCards) != len(cards):
            return False
        for x in range(len(cards)):
            if specificColour:
                if cards[x] not in comboCards:
                    return False
                comboCards.remove(cards[x])
            else:
                if cards[x].split("_")[1] not in comboCards:
                    return False
                # remove one crad that has the same type ignoring color
                i = 0
                while True:
                    if comboCards[i] == cards[x].split("_")[1]:
                        comboCards.remove(comboCards[i])
                        break
                    i += 1

        if not specificColour and cards[0].split(
                "_")[0] != self.lyingCards[-1].split("_")[0]:
            self.addEvent(playerId, {
                "type": "message",
                "dat": "du darfst diese Karte nicht legen!"
            })
            return False

        coulourCard = ""
        if specificColour:
            for x in cards:
                if x.split("_")[0] == topCardColor:
                    coulourCard = x
        for x in cards:
            if specificColour:
                if x != coulourCard:
                    self.easyEvents("cardAdd", x)
                    self.addCard(x)
                    self.playerDeck[playerId].remove(x)
            else:
                i = 0
                while True:
                    if self.playerDeck[playerId][i].split("_")[0] == x.split(
                            "_")[0]:
                        self.easyEvents("cardAdd",
                                        self.playerDeck[playerId][i])
                        self.addCard(self.playerDeck[playerId][i])
                        self.playerDeck[playerId].remove(
                            self.playerDeck[playerId][i])
                        break
                    i += 1
                pass
            sleep(0.1)
        if specificColour:
            self.easyEvents("cardAdd", coulourCard)
            self.addCard(coulourCard)
            self.playerDeck[playerId].remove(coulourCard)
        return True

    def layCard(self, card, playerId, topCardColor=None):
        if playerId != self.players[self.currentPlayer]:
            self.playerClasses[playerId].send_message(
                json.dumps({
                    "type": "message",
                    "dat": "Du bist zurzeit nicht am zug!"
                }))
            return
        if len(card) == 1:
            if (card[0] in self.playerDeck[playerId]):
                c = card[0].split("_")
                lc = self.lyingCards[-1].split("_")

                # if card is the right type
                ok = True
                if c[0] == "":
                    pass
                else:
                    if c[0] != lc[0] and c[1] != lc[1]:
                        ok = False

                # then
                if ok:
                    if c[1] == "richtungswechsel":
                        self.direction = self.direction * -1
                    if c[1] == "komunist":
                        self.playerDeck[playerId].remove(card[0])
                        self.addEvent(playerId, {
                            "type": "action",
                            "dat": "removeCard",
                            "dat2": card[0]
                        })

                        if len(self.playerDeck[playerId]) == 0:
                            self.playerWin(playerId)

                        self.addCard(card[0])
                        self.easyEvents("cardAdd", card[0])

                        self.messageAll("Karten werden neu gemischt.....")

                        threading.Thread(target=self.komunist,
                                         daemon=True).start()
                        return
                    if c[1] == "farbwechsel" or c[1] == "farbwechsel4+":
                        self.cardCache = card[0]
                        self.playerClasses[playerId].send_message(
                            json.dumps({
                                "type": "status",
                                "dat": "waiting_for_color"
                            }))
                        self.playerClasses[playerId].send_message(
                            json.dumps({
                                "type": "action",
                                "dat": "select_color"
                            }))
                    else:
                        self.addCard(card[0])
                        self.playerDeck[playerId].remove(card[0])
                        self.addEvent(playerId, {
                            "type": "action",
                            "dat": "removeCard",
                            "dat2": card[0]
                        })
                        self.easyEvents("cardAdd", card[0])

                        if len(self.playerDeck[playerId]) == 0:
                            self.playerWin(playerId)

                        if self.specialAction.startswith("winAussetzen"):
                            if card[0].endswith("_aussetzen"):
                                self.nextPlayerEvents.append("aussetzen")
                            self.nextPlayer()
                            return

                        if (card[0].endswith("_aussetzen")):
                            self.nextPlayer(2)

                        elif (card[0].endswith("_richtungswechsel")):
                            self.askPlayer()
                        else:
                            self.nextPlayer()

                else:
                    self.playerClasses[playerId].send_message(
                        json.dumps({
                            "type": "message",
                            "dat": "Du darfst diese Karte nicht legen!"
                        }))
            else:
                self.playerClasses[playerId].send_message(
                    json.dumps({
                        "type": "message",
                        "dat": "Du hast diese Karte garnicht"
                    }))
                self.addEvent(
                    playerId, {
                        "type": "stats",
                        "dat": {
                            "type": "deck",
                            "dat": self.playerDeck[playerId]
                        }
                    })
        else:
            # check if has card
            for x in card:
                if x not in self.playerDeck[playerId]:
                    self.playerClasses[playerId].send_message(
                        json.dumps({
                            "type":
                            "message",
                            "dat":
                            "Fehler! Du besitzt mindestens eine dieser Karten nicht!"
                        }))
                    return

            if self.isThisCombo(card, [
                    "yellow_richtungswechsel", "blue_richtungswechsel",
                    "green_richtungswechsel", "red_richtungswechsel"
            ], playerId, True, topCardColor):
                if len(self.playerDeck[playerId]) == 0:
                    self.playerWin(playerId)
                    return
                self.playerClasses[
                    playerId].currentAction = "specificSelect_winRichtungswechsel"

                allPlayers = {}
                for x in self.players:
                    if x != playerId:
                        allPlayers[self.playerNames[x]] = x
                if len(allPlayers) == 0:
                    self.playerWin(playerId)
                    return
                self.playerClasses[playerId].send_message(
                    json.dumps({
                        "type": "action",
                        "dat": {
                            "type": "specificSelect",
                            "dat": {
                                "title":
                                "Mit welchem spieler willst du tauschen?",
                                "options": allPlayers,
                                "type": "text"
                            }
                        }
                    }))
                self.playerClasses[playerId].send_message(
                    json.dumps({
                        "type": "message",
                        "dat": "Karten-combo gelegt"
                    }))
            elif (not self.specialAction.startswith("winAussetzen")
                  ) and self.isThisCombo(card, [
                      "yellow_aussetzen", "blue_aussetzen", "green_aussetzen",
                      "red_aussetzen"
                  ], playerId, True, topCardColor):
                if len(self.playerDeck[playerId]) == 0:
                    self.playerWin(playerId)
                    return
                self.specialAction = "winAussetzen_4"
                self.playerClasses[playerId].send_message(
                    json.dumps({
                        "type":
                        "message",
                        "dat":
                        "Karten-combo gelegt du kannst nun 4 mal Karten hintereinander legen"
                    }))
                self.nextPlayer()
            elif self.specialAction.startswith("winAussetzen"):
                self.playerClasses[playerId].send_message(
                    json.dumps({
                        "type":
                        "message",
                        "dat":
                        "Windows-aussetzen-combo kann nicht addiert/kombiniert werden"
                    }))
            elif self.isThisCombo(card, ["9", "6"], playerId, False):
                if len(self.playerDeck[playerId]) == 0:
                    self.playerWin(playerId)
                    return
                self.playerClasses[
                    playerId].currentAction = "specificSelect_96"
                self.playerClasses[playerId].i = 4
                self.playerClasses[playerId].send_message(
                    json.dumps({
                        "type": "action",
                        "dat": {
                            "type": "specificSelect",
                            "dat": {
                                "title":
                                f"Welche karte möchtest du haben (noch 4 versuch(e))",
                                "options": "_deck",
                                "type": "card",
                                "cancel": True
                            }
                        }
                    }))
            elif self.isThisCombo(card, ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], playerId, False):
                self.playerWin(playerId)
                return
            elif self.isThisCombo(card, [
                    "yellow_komunist", "blue_komunist", "green_komunist",
                    "red_komunist"
            ], playerId, True, topCardColor):
                if len(self.playerDeck[playerId]) == 0:
                    self.playerWin(playerId)
                    self.komunist()
                    return
                self.komunist(True, playerId)
            else:
                print(card)
                self.playerClasses[playerId].send_message(
                    json.dumps({
                        "type": "message",
                        "dat": "Karten-combo zurzeit nicht verfügbar!"
                    }))

            master.addEvent(
                playerId, {
                    "type": "stats",
                    "dat": {
                        "type": "deck",
                        "dat": master.playerDeck[playerId]
                    }
                })

    def messageAll(self, msg):
        if len(self.tables) > 0:
            self.sendTable({"type": "message", "dat": msg})
        else:
            self.addEvents({"type": "message", "dat": msg})
            self.sendWatcher({"type": "message", "dat": msg})

    def komunist(self, windows=False, windowsPlayer=-1):
        toMix = []
        for key, value in self.playerDeck.items():
            self.playerDeck[key] = []
            toMix += value
            sleep(0.2)
            self.addEvent(key, {
                "type": "stats",
                "dat": {
                    "type": "deck",
                    "dat": []
                }
            })
            self.easyEvents("playerCardCount")

        for s in range(5):
            sleep(0.2)
            self.messageAll(f"mischen... {s+1}/5")

        cards = ceil(len(toMix) / len(self.players))

        if windows == False:
            for x in range(cards):
                for player in self.players:
                    if len(toMix) != 0:
                        choice = random.randint(0, len(toMix) - 1)
                        self.playerDeck[player].append(toMix[choice])
                        toMix.pop(choice)
                    else:
                        choice = random.randint(0,
                                                len(self.availableCards) - 1)
                        self.playerDeck[player].append(
                            self.availableCards[choice])
                        self.availableCards.pop(choice)
        else:
            toggle = True
            for x in range(cards):
                for player in self.players:
                    if windowsPlayer != player or toggle:
                        if len(toMix) != 0:
                            choice = random.randint(0, len(toMix) - 1)
                            self.playerDeck[player].append(toMix[choice])
                            toMix.pop(choice)
                        else:
                            choice = random.randint(
                                0,
                                len(self.availableCards) - 1)
                            self.playerDeck[player].append(
                                self.availableCards[choice])
                            self.availableCards.pop(choice)
                    toggle = not toggle
        # update players of their deck
        for player in self.players:
            self.addEvent(
                player, {
                    "type": "stats",
                    "dat": {
                        "type": "deck",
                        "dat": self.playerDeck[player]
                    }
                })
        self.easyEvents("playerCardCount")
        self.nextPlayer()

    def selectColor(self, playerId, color):
        if self.players[self.currentPlayer] == playerId:
            self.addCard(color + self.cardCache)
            self.playerDeck[playerId].remove(self.cardCache)
            self.addEvent(playerId, {
                "type": "action",
                "dat": "removeCard",
                "dat2": self.cardCache
            })
            self.easyEvents("cardAdd", color + self.cardCache)

            if len(self.playerDeck[playerId]) == 0:
                self.playerWin(playerId)

            self.cardCache = ""
            self.nextPlayer()
        else:
            self.playerClasses[playerId].send_message(
                json.dumps({
                    "type": "message",
                    "dat": "Du bist zurzeit nicht am zug!"
                }))

    def nextPlayerWithoutAsking(self):
        self.currentPlayer += self.direction
        if self.currentPlayer >= len(self.players):
            self.currentPlayer = 0
        if self.currentPlayer < 0:
            self.currentPlayer = len(self.players) - 1

    def nextPlayer(self, count=1):
        if len(self.players) <= 1:
            self.gameEnd()
            return
        self.addEvent(self.players[self.currentPlayer], {
            "type": "status",
            "dat": "playing"
        })
        if self.specialAction.startswith("winAussetzen"):
            if int(self.specialAction.split("_")[1]) <= 0:
                self.specialAction = ""
                self.nextPlayer()
                return
            self.specialAction = self.specialAction.split("_")[0] + "_" + str(
                int(self.specialAction.split("_")[1]) - 1)
        else:
            for x in range(count):
                self.nextPlayerWithoutAsking()
        self.askPlayer()

    def easyEvents(self, etype, dat=None):
        if etype == "playerlist":
            self.addEvents({
                "type": "stats",
                "dat": {
                    "type": "players",
                    "dat": {
                        "id": self.players,
                        "name": self.playerNames
                    }
                }
            })
            self.sendTable({
                "type": "players",
                "dat": {
                    "id": self.players,
                    "name": self.playerNames
                }
            })
            self.sendWatcher({
                "type": "stats",
                "dat": {
                    "type": "players",
                    "dat": {
                        "id": self.players,
                        "name": self.playerNames
                    }
                }
            })
        if etype == "cardList":
            for playerId in self.players:
                self.addEvent(
                    playerId, {
                        "type": "stats",
                        "dat": {
                            "type": "deck",
                            "dat": self.playerDeck[playerId]
                        }
                    })
        if etype == "disconnect":
            self.easyEvents("playerlist")
        if etype == "cardRedo":
            self.addEvents({
                "type": "stats",
                "dat": {
                    "type": "lyingCards",
                    "dat": self.lyingCards
                }
            })
            self.sendTable({"type": "lyingCards", "dat": self.lyingCards})
            self.sendWatcher({
                "type": "stats",
                "dat": {
                    "type": "lyingCards",
                    "dat": self.lyingCards
                }
            })

            counts = {}
            for x in self.playerDeck.keys():
                counts[x] = len(self.playerDeck[x])
            self.addEvents({
                "type": "stats",
                "dat": {
                    "type": "playerCardCount",
                    "dat": counts
                }
            })

            self.updateTable()
        if etype == "cardAdd":
            self.addEvents({
                "type": "action",
                "dat": {
                    "type": "lyingCards",
                    "dat": dat
                }
            })
            self.sendTable({"type": "lyingCardsAdd", "dat": dat})
            self.sendWatcher({
                "type": "action",
                "dat": {
                    "type": "lyingCards",
                    "dat": dat
                }
            })

            counts = {}
            for x in self.playerDeck.keys():
                counts[x] = len(self.playerDeck[x])
            self.addEvents({
                "type": "stats",
                "dat": {
                    "type": "playerCardCount",
                    "dat": counts
                }
            })
            self.updateTable()
        if etype == "playerCardCount":
            counts = {}
            for x in self.playerDeck.keys():
                counts[x] = len(self.playerDeck[x])
            self.addEvents({
                "type": "stats",
                "dat": {
                    "type": "playerCardCount",
                    "dat": counts
                }
            })
            self.sendTable({"type": "playerCardCount", "dat": counts})
            self.sendWatcher({
                "type": "stats",
                "dat": {
                    "type": "playerCardCount",
                    "dat": counts
                }
            })

    def updateTable(self):
        counts = {}
        for x in self.playerDeck.keys():
            counts[x] = len(self.playerDeck[x])
        self.sendTable({"type": "playerCardCount", "dat": counts})
        self.sendWatcher({
            "type": "stats",
            "dat": {
                "type": "playerCardCount",
                "dat": counts
            }
        })

    def addEvents(self, dat):
        for x in self.players:
            if not x in self.events:
                self.events[x] = []
            self.events[x].append(dat)

        self.runEvents()

    def drawCard(self, playerId):
        if playerId != self.players[self.currentPlayer]:
            self.playerClasses[playerId].send_message(
                json.dumps({
                    "type": "message",
                    "dat": "Du bist zurzeit nicht am zug!"
                }))
            return
        if len(self.playerDeck[playerId]) < 25 or noCardLimit:
            choice = random.randint(0, len(self.availableCards) - 1)
            self.playerDeck[playerId].append(self.availableCards[choice])
            self.addEvent(
                playerId, {
                    "type": "action",
                    "dat": {
                        "type": "addCard",
                        "dat": self.availableCards[choice]
                    }
                })
            self.availableCards.pop(choice)
            self.easyEvents("playerCardCount")
        else:
            self.playerClasses[playerId].send_message(
                json.dumps({
                    "type": "message",
                    "dat": "Du hast schon zu viele Karten"
                }))

            # check if player has any card to lay down
            has = False
            maybeCard = None
            for card in self.playerDeck[playerId]:
                c = card.split("_")
                lc = self.lyingCards[-1].split("_")
                ok = True
                if c[0] == "":
                    pass
                else:
                    if c[0] != lc[0] and c[1] != lc[1]:
                        ok = False
                if ok:
                    has = True
                    maybeCard = card
            if not has:
                self.playerClasses[playerId].send_message(
                    json.dumps({
                        "type":
                        "message",
                        "dat":
                        "Da du keine Karten legen kannst wurde der nächste Spieler aufgerufen."
                    }))
                self.nextPlayer()
            else:
                pass

    def addEvent(self, player, dat):
        if not player in self.events:
            self.events[player] = []
        self.events[player].append(dat)

        self.runEvents()

    def runEvents(self):
        for x in self.players:
            for i in self.events[x]:
                self.playerClasses[x].send_message(json.dumps(i))
            self.events[x] = []

    def addCard(self, dat):
        self.lyingCards.append(dat)

        # remove old cards
        if len(self.lyingCards) > 10:
            self.addEvents({"type": "action", "dat": "removeLastLyingCard"})
            self.sendWatcher({"type": "action", "dat": "removeLastLyingCard"})
            self.sendTable({"type": "removeLastLyingCard"})

            card = self.lyingCards[0]
            if card.split("_")[1] in ["farbwechsel", "farbwechsel4+"]:
                card = "_" + card.split("_")[1]
            self.availableCards.append(card)
            self.lyingCards = self.lyingCards[1:]

    def gameEnd(self):
        threading.Thread(target=self.threadedEnd, daemon=True).start()

    def threadedEnd(self):
        if self.status == "gameEnd":
            return
        self.status = "gameEnd"
        sleep(1)
        self.messageAll("Spiel vorbei. Nur noch ein Spieler übrig!")
        self.messageAll("Ein neues Spiel wird gestarted.")
        sleep(3)
        self.status = "waiting_for_players"

        for x in self.watchers:
            # x.send_message(json.dumps(
            #    {"type": "message", "dat": "Neues Spiel!"}))
            x.send_message(json.dumps({"type": "action", "dat": "reconnect"}))

        for y in self.players:
            x = self.playerClasses[y]
            # x.send_message(json.dumps(
            #    {"type": "message", "dat": "Neues Spiel!"}))
            x.send_message(json.dumps({"type": "action", "dat": "reconnect"}))

        # for x in self.tables:
        #    x.send_message(json.dumps(
        #        {"type": "message", "dat": "Neues Spiel!"}))
        # for x in self.otherTables:
        #    x.send_message(json.dumps(
        #        {"type": "message", "dat": "Neues Spiel!"}))

        self.wonPlayers = []

    def sendTableDat(self, table):
        #table.send_message(json.dumps({"type": "", "dat": ""}))
        table.send_message(
            json.dumps({
                "type": "players",
                "dat": {
                    "id": self.players,
                    "name": self.playerNames
                }
            }))
        table.send_message(
            json.dumps({
                "type": "lyingCards",
                "dat": self.lyingCards
            }))

        counts = {}
        for x in self.playerDeck.keys():
            counts[x] = len(self.playerDeck[x])
        table.send_message(
            json.dumps({
                "type": "playerCardCount",
                "dat": counts
            }))
        table.send_message(
            json.dumps({
                "type": "currentPlayer",
                "dat": self.currentPlayer
            }))

        # won players
        for x in self.wonPlayers:
            table.send_message(json.dumps({
                "type": "playerFinished",
                "dat": x
            }))

    def sendWatcherDat(self, watcher):
        # players
        watcher.send_message(
            json.dumps({
                "type": "stats",
                "dat": {
                    "type": "players",
                    "dat": {
                        "id": self.players,
                        "name": self.playerNames
                    }
                }
            }))

        # card counts
        counts = {}
        for x in self.playerDeck.keys():
            counts[x] = len(self.playerDeck[x])
        watcher.send_message(
            json.dumps({
                "type": "stats",
                "dat": {
                    "type": "playerCardCount",
                    "dat": counts
                }
            }))
        watcher.send_message(
            json.dumps({
                "type": "stats",
                "dat": {
                    "type": "lyingCards",
                    "dat": self.lyingCards
                }
            }))
        watcher.send_message(
            json.dumps({
                "type": "stats",
                "dat": {
                    "type": "currentPlayer",
                    "dat": self.currentPlayer
                }
            }))

        # won players
        for x in self.wonPlayers:
            watcher.send_message(
                json.dumps({
                    "type": "action",
                    "dat": {
                        "type": "playerFinished",
                        "dat": x
                    }
                }))

master = gameMaster()

class Player:

    def __init__(self, client, idet) -> None:
        self.type = ""
        self.currentAction = ""
        self.client = client
        self.idet = idet
        self.i = -1

    def send_message(self, dat):
        try:
            if self.idet in self.client.clients:
                if self.client.clients[self.idet].connected:
                    self.client.clients[self.idet].send(dat)
            else:
                self.client.close_connection(self.idet)
        except ConnectionClosed:
            self.client.close_connection(self.idet)

    def handle(self, data):
        try:
            global master
            data = json.loads(data)
            # print(self.data)

            if data["type"] == "start_game":
                if data["dat"] == startCode:
                    master.start()
                else:
                    self.send_message(
                        json.dumps({
                            "type": "message",
                            "dat": "Error: Wrong start code"
                        }))
            elif data["type"] == "allCards":
                self.send_message(
                    json.dumps({
                        "type": "stats",
                        "dat": {
                            "type": "allCards",
                            "dat": deck
                        }
                    }))

            elif self.currentAction.startswith(
                    "specificSelect_") and data["type"] == "selectResponse":
                if self.currentAction == "specificSelect_winRichtungswechsel":
                    if data["dat"] in master.players:
                        p = master.playerDeck[self.playerId]
                        master.playerDeck[self.playerId] = master.playerDeck[
                            data["dat"]]
                        master.playerDeck[data["dat"]] = p

                        master.easyEvents("playerCardCount")
                        master.addEvent(
                            data["dat"], {
                                "type": "stats",
                                "dat": {
                                    "type": "deck",
                                    "dat": master.playerDeck[data["dat"]]
                                }
                            })
                        master.addEvent(
                            self.playerId, {
                                "type": "stats",
                                "dat": {
                                    "type": "deck",
                                    "dat": master.playerDeck[self.playerId]
                                }
                            })

                        self.send_message(
                            json.dumps({
                                "type": "message",
                                "dat": "Karten getauscht"
                            }))
                        master.addEvent(
                            data["dat"], {
                                "type":
                                "message",
                                "dat":
                                master.playerNames[self.playerId] +
                                " hat deine Karten getauscht"
                            })
                        master.nextPlayer()
                    else:
                        self.send_message(
                            json.dumps({
                                "type": "message",
                                "dat": "Dieser Spieler existiert nicht!"
                            }))
                        return
                elif self.currentAction == "specificSelect_96":
                    if data["dat"] == "_cancel":
                        self.currentAction = ""
                        master.nextPlayer()
                        self.send_message(
                            json.dumps({
                                "type": "action",
                                "dat": "closeSpecificSelect"
                            }))
                        return

                    # get all users that have the specified card and select a random user
                    availableUsers = []
                    for x in master.players:
                        if x != self.playerId:
                            for y in master.playerDeck[x]:
                                if y == data["dat"]:
                                    if x not in availableUsers:
                                        availableUsers.append(x)
                    if len(availableUsers) == 0:
                        # no cards available! send message and return
                        self.send_message(
                            json.dumps({
                                "type":
                                "message",
                                "dat":
                                "Es gibt keinen Spieler mit dieser Karte!"
                            }))
                        self.send_message(
                            json.dumps({
                                "type": "action",
                                "dat": {
                                    "type": "specificSelect",
                                    "dat": {
                                        "title":
                                        f"Welche karte möchtest du haben, die vorherige gibt es nicht. (noch {self.i} versuch(e))",
                                        "options": "_deck",
                                        "type": "card",
                                        "cancel": True
                                    }
                                }
                            }))
                        return
                    else:
                        # select random user
                        selectedUser = availableUsers[randint(
                            0,
                            len(availableUsers) - 1)]

                        # remove card from player and add to self player
                        master.playerDeck[selectedUser].remove(data["dat"])
                        if len(master.playerDeck[selectedUser]) == 0:
                            master.playerWin(selectedUser)
                        master.addEvent(
                            selectedUser, {
                                "type": "action",
                                "dat": "removeCard",
                                "dat2": data["dat"]
                            })

                        master.playerDeck[self.playerId].append(data["dat"])
                        master.addEvent(
                            self.playerId, {
                                "type": "action",
                                "dat": {
                                    "type": "addCard",
                                    "dat": data["dat"]
                                }
                            })

                        # tell all players to update the player card count
                        master.easyEvents("playerCardCount")

                        self.i -= 1
                        if self.i <= 0:
                            self.currentAction = ""
                            self.send_message(
                                json.dumps({
                                    "type": "action",
                                    "dat": "closeSpecificSelect"
                                }))
                            master.nextPlayer()
                        else:
                            self.send_message(
                                json.dumps({
                                    "type": "action",
                                    "dat": {
                                        "type": "specificSelect",
                                        "dat": {
                                            "title":
                                            f"Welche karte möchtest du haben? (noch {self.i} versuch(e))",
                                            "options": "_deck",
                                            "type": "card",
                                            "cancel": True
                                        }
                                    }
                                }))
                        return

                self.send_message(
                    json.dumps({
                        "type": "action",
                        "dat": "closeSpecificSelect"
                    }))
            elif self.currentAction == "get_name" and data[
                    "type"] == "get_name":
                if (data["dat"] != ""
                        and data["dat"] not in master.playerNames.values()):
                    self.playerName = data["dat"]
                    master.playerNames[self.playerId] = self.playerName
                    self.currentAction = ""
                    master.easyEvents("playerlist")
                    # self.send_message(json.dumps(
                    #    {"type": "message", "dat": "ok :)"}))
                    self.send_message(
                        json.dumps({
                            "type": "status",
                            "dat": "waiting_for_beginning"
                        }))
                else:
                    self.send_message(
                        json.dumps({
                            "type": "action",
                            "dat": "get_name"
                        }))
            elif data["type"] == "lay_card":
                d2 = None
                if "dat2" in data.keys():
                    d2 = data["dat2"]
                master.layCard(data["dat"], self.playerId, d2)
            elif data["type"] == "drawCard":
                master.drawCard(self.playerId)
            elif data["type"] == "select_color":
                master.selectColor(self.playerId, data["dat"])
                pass
            elif data["type"] == "getCardsStat":
                self.send_message(
                    json.dumps({
                        "type": "stats",
                        "dat": {
                            "type": "cardsStat",
                            "dat": master.playerDeck[self.playerId]
                        }
                    }))
            elif data["type"] == "resend_cards_deck":
                master.addEvent(
                    self.playerId, {
                        "type": "stats",
                        "dat": {
                            "type": "deck",
                            "dat": master.playerDeck[self.playerId]
                        }
                    })
            elif data["type"] == "withdraw2x":
                master.withdraw2x(self.playerId)
            elif data["type"] == "typeStatus":
                if ("dat3" not in data
                        or data["dat3"] != loginKey) and loginKey != "":
                    self.send_message('{"type":"wrongPass"}')
                    sleep(0.5)
                    self.handle_close()
                    self.client.close_connection(self.idet)
                    return
                if data["dat"] == "player":
                    secondTime = False
                    if "dat2" in data:
                        if data["dat2"] == "secondTime":
                            secondTime = True

                    self.type = "player"
                    if len(master.tables) != 0:
                        self.send_message(
                            json.dumps({
                                "type": "action",
                                "dat": "hasTable"
                            }))

                    self.currentAction = ""
                    r = 0  # random.randint(0, 9999999999999)
                    while r in master.players:
                        r = r + 1  # random.randint(0, 9999999999999)
                    self.playerId = r

                    self.send_message(
                        json.dumps({
                            "type": "stats",
                            "dat": {
                                "type": "yourId",
                                "dat": self.playerId
                            }
                        }))

                    if master.status == "waiting_for_players":
                        master.players.append(self.playerId)
                        master.playerClasses[self.playerId] = self

                        if not secondTime:
                            self.send_message(
                                json.dumps({
                                    "type": "message",
                                    "dat": "Mit UNU-Supreme Verbunden!"
                                }))
                            self.send_message(
                                json.dumps({
                                    "type":
                                    "message",
                                    "dat":
                                    "Spieler anzahl zurzeit: " +
                                    str(len(master.players))
                                }))

                        # select name
                        self.playerName = "[" + str(self.playerId) + "]"
                        self.send_message(
                            json.dumps({
                                "type": "status",
                                "dat": "waiting_for_name"
                            }))
                        self.send_message(
                            json.dumps({
                                "type": "action",
                                "dat": "get_name"
                            }))
                        self.currentAction = "get_name"
                        master.easyEvents("playerlist")
                    else:
                        self.send_message(
                            json.dumps({
                                "type": "message",
                                "dat": "Du bist als zuschauer verbunden"
                            }))
                        self.send_message(
                            json.dumps({
                                "type": "status",
                                "dat": "watcher"
                            }))
                        master.watchers.append(self)
                        master.sendWatcherDat(self)
                elif data["dat"] == "watcher":
                    self.type = "watcher"
                    if len(master.tables) != 0:
                        self.send_message(
                            json.dumps({
                                "type": "action",
                                "dat": "hasTable"
                            }))
                    self.send_message(
                        json.dumps({
                            "type": "message",
                            "dat": "Du bist als zuschauer verbunden"
                        }))
                    self.send_message(
                        json.dumps({
                            "type": "status",
                            "dat": "watcher"
                        }))
                    master.watchers.append(self)
                    master.sendWatcherDat(self)
                elif data["dat"] == "table":
                    self.type = "table"
                    self.send_message(
                        json.dumps({
                            "type": "message",
                            "dat": "Als Tisch verbunden"
                        }))
                    master.sendTableDat(self)
                    if (data["dat2"] == "normal"):
                        master.tables.append(self)
                        master.addEvents({"type": "action", "dat": "hasTable"})
                        master.sendWatcher({
                            "type": "action",
                            "dat": "hasTable"
                        })
                    else:
                        master.otherTables.append(self)
                elif data["dat"] == "console":
                    self.type = "console"
                    self.send_message("connected!")
            elif self.type == "console" and data["type"] == "eval":
                self.send_message(consoleEval(data["dat"]))
            else:
                self.send_message(
                    json.dumps({
                        "type":
                        "message",
                        "dat":
                        "action " + data["type"] +
                        " could not be used! (wanted action: " +
                        self.currentAction + ")"
                    }))
        except:
            traceback.print_exc()

    def connected(self):
        print(self.idet, 'connected')

    def handle_close(self, notReal=False):
        try:
            global master
            if self.type == "player":
                if self.playerId in master.players:
                    master.players.remove(self.playerId)

                if self.playerId in master.playerClasses.keys():
                    master.playerClasses.pop(self.playerId)

                if self.playerId in master.playerNames.keys():
                    master.playerNames.pop(self.playerId)

                if self.playerId in master.playerDeck.keys():
                    for x in master.playerDeck[self.playerId]:
                        master.availableCards.append(x)
                    master.playerDeck.pop(self.playerId)

                # check if currentPlayer id needs to change, because the player has disconnected
                if master.status == "playing" and master.currentPlayer >= len(
                        master.players):
                    print("toBig currentPlayer")
                    master.currentPlayer = len(master.players) - 1
                    if master.currentPlayer < 0:
                        master.currentPlayer = 0
                        master.gameEnd()
                        print("no Players anymore!")
                    else:
                        master.askPlayer()
                if master.status == "playing" and len(master.players) <= 1:
                    master.gameEnd()
                    return
            elif self.type == "table":
                if self in master.tables:
                    master.tables.remove(self)
                if self in master.otherTables:
                    master.otherTables.remove(self)
            elif self.type == "watcher":
                master.watchers.remove(self)
            master.easyEvents("disconnect")
            if not notReal:
                #print(self.idet, 'closed')
                pass
        except:
            if not notReal:
                print(self.idet, 'closed')
            traceback.print_exc()

def genPlayerId(inp):
    global master
    inp = str(inp)

    if (inp.startswith("{")):
        inp = master.players[int(inp[1:-1])]

    return int(inp)

def consoleEval(dat):
    global master
    command = dat.split(" ")
    try:
        if command[0] == "players":
            returnDat = []
            for player in master.players:
                if player in master.playerNames.keys():
                    name = master.playerNames[player]
                else:
                    name = "undefined"
                returnDat.append(name + ": " + str(player))
                pass
            return ",\n".join(returnDat)
        elif command[0] == "give":
            playerId = genPlayerId(command[2])
            if playerId not in master.playerDeck.keys():
                return "player doesn't exist"
            master.playerDeck[playerId].append(command[1])
            master.addEvent(playerId, {
                "type": "action",
                "dat": {
                    "type": "addCard",
                    "dat": command[1]
                }
            })
            master.easyEvents("playerCardCount")
            return "ok!"
        elif command[0] == "get":
            playerId = genPlayerId(command[1])
            if playerId in master.playerDeck:
                cards = ", ".join(master.playerDeck[playerId])
            else:
                cards = "player does not exist!"
            return cards
        elif command[0] == "setCurrentPlayer":
            playerId = genPlayerId(command[1])
            master.currentPlayer = playerId
            return "set to " + str(master.currentPlayer)
        elif command[0] == "getCurrentPlayer":
            return "CurrentPlayer: " + str(master.currentPlayer)
        elif command[0] == "win":
            playerId = genPlayerId(command[1])
            master.easyEvents("playerCardCount")
            master.addEvent(playerId, {
                "type": "stats",
                "dat": {
                    "type": "deck",
                    "dat": []
                }
            })

            master.playerWin(playerId)
            master.nextPlayer()

            return f"player {playerId} has now finished!"
        elif command[0] == "start":
            master.start()
            return "started"
        elif command[0] == "nextPlayer":
            master.nextPlayer()
            return "ok"
    except Exception as e:
        return traceback.format_exc()
        # return "error: " + str(e)
    return "command did not return! Does it realy exist?"

@app.route('/')
def projects():
    return render_template("index.html",
                           title='UNU Supreme',
                           serverHost=serverHost)

@app.route('/many')
def many():
    return render_template("many.html",
                           title='UNU Supreme',
                           serverHost=serverHost)

@app.route('/wsT')
def wsT():
    return render_template("websocketTester.html",
                           title='UNU Supreme',
                           serverHost=serverHost)

@app.route('/console')
def console():
    # serverHost: ${location.hostname}
    return render_template("console.html",
                           title='Debug console',
                           serverHost=serverHost)

@app.route("/table")
def desk():
    # serverHost: ${location.hostname}
    return render_template("table.html",
                           title="Table Stuff",
                           serverHost=serverHost)

def flaskServer():
    app.run("0.0.0.0", 80)

def run():
    threading.Thread(target=flaskServer, daemon=True).start()

@app.after_request
def add_header(r):
    """
    Add headers to both force latest IE rendering engine or Chrome Frame,
    and also to cache the rendered page for 10 minutes.
    """
    r.headers["Cache-Control"] = "max-age=604800"
    #r.headers["Pragma"] = "no-cache"
    r.headers["Expires"] = "9999"
    r.headers['Cache-Control'] = 'public, max-age=9999'
    return r

class Client():

    def __init__(self):
        self.clients = {}
        self.rec = {}

    def add_client(self, ws):
        idet = randint(0, 100000)
        self.clients[idet] = ws
        self.rec[idet] = Player(self, idet)
        #Timer(10, self.close_connection, (ws,)).start()
        Thread(target=self.check_disconnect, args=(idet, )).start()
        return idet

    def run(self, idet):
        data = self.clients[idet].receive()
        self.rec[idet].handle(data)

    def active_client(self, idet):
        if idet in self.clients.keys():
            return True
        #print("not active_client")
        return False

    def check_disconnect(self, idet):
        try:
            while self.clients[idet].connected:
                #print("Is connected")
                sleep(0.05)
            self.close_connection(idet)
        except Exception as e:
            #print("check disconnected errored")
            # print(e)
            pass

    def close_connection(self, idet):
        if idet in self.rec.keys():
            self.rec[idet].handle_close()
            self.rec.pop(idet)
        if idet in self.clients.keys():
            self.clients.pop(idet)

client = Client()

@sock.route('/sock')
def echo(ws):
    idet = client.add_client(ws)
    while client.active_client(idet):
        client.run(idet)
    ws.close(1006, "Invalid Message")
    return
    print("Client disconnected because of an error")

if __name__ == "__main__":
    run()
    while True:
        sleep(100)


#Import modules
import json, Bio, re, os, requests, pickle
from os.path import join, isdir
from collections import Counter
from copy import copy
from Bio import SeqIO, AlignIO
from Bio.Seq import Seq, MutableSeq
from Bio.SeqRecord import SeqRecord
from Bio.Align import MultipleSeqAlignment
from Bio.Data.CodonTable import TranslationError
from io import StringIO

#Define constants (DPA2/B2 is left out because they do not produce a well defined amino acid sequence, as they contain stop codons).
defaultLoci = ['A', 'B', 'C', 'DRA', 'DRB1', 'DRB345', 'DQA1', 'DQA2', 'DQB1', 'DPA1', 'DPB1', 'MICA', 'MICB']
defaultUrl = 'https://raw.githubusercontent.com/ANHIG/IMGTHLA/Latest/msf/'
defaultCodonStarts = {
    "A": -24,
    "B": -24,
    "C": -24,
    "DRA": -25,
    "DRB1": -29,
    "DRB3": -29,
    "DRB4": -29,
    "DRB5": -29,
    "DRB345": -29,
    "DQA1": -23,
    "DQA2": -23,
    "DQB1": -32,
    "DPA1": -31,
    "DPB1": -29,
    "MICA": -23,
    "MICB": -23
}
HLALoci = list(defaultCodonStarts.keys())

#Function to retrieve the IMGT/HLA latest database version from their github
latestDbVersion = None
def getLatestDbVersion():

    #Retrieve, the latest db version if it has not already been determined
    global latestDbVersion
    if latestDbVersion is None:
        completeUrl = defaultUrl.replace('msf/', 'Allele_status.txt')
        text = requests.get(completeUrl).text
        latestDbVersion = re.search(r'IPD-IMGT/HLA ([\d\.]+)', text[0:300]).group(1).replace('.', '')

    #Print and return the latest dbVersion
    print(f'Looking for latest IMGT/HLA database version number: {latestDbVersion}')
    return latestDbVersion

#Function to retrieve the alignment files from the github of IMGTHLA and save it locally when a outFolder is given.
#A list of loci can either be provided or the default is used and a database version can also be given (default is 'Latest').
#In any case a dictionary of file strings is returned
def downloadAlignments(outFolder = None, molecule = 'nt', loci = defaultLoci, databaseVersion = 'Latest'):
    alignments = {}
    databaseVersion = str(databaseVersion).replace('.', '')
    if databaseVersion != 'Latest' and getLatestDbVersion() == databaseVersion:
        databaseVersion = 'Latest'

    for locus in loci:

        #Recontruct the file name
        if molecule in ['nt', 'nuc', 'nucleotide']:
            inbetween = 'nuc'
        elif molecule in ['aa', 'prot', 'amino acid']:
            inbetween = 'prot'
        else:
            raise ValueError("The molecule argument must be 'nt', 'nuc' or 'nucleotide' for nucleotide alignments or 'aa', 'prot' or 'amino acid' for protein alignments.")
        fileName = f'{locus}_{inbetween}.msf'
        completeUrl = defaultUrl.replace('Latest', databaseVersion.replace('.','')) + fileName
        print(f'Retrieve alignment for locus {locus} from:\n{completeUrl}')

        #Save the file per locus
        text = requests.get(completeUrl).text
        if '404: Not Found' in text:
            print(f'Locus {locus} alignment not found.')
        else:
            if outFolder is not None:
                with open(os.path.join(outFolder, fileName), 'w') as f:
                    f.write(text)
            alignments[locus] = text

    return alignments

#Function to load an object of the class below from a pickle file.
#When a file location is given, the pickle is loaded directly
#If a folder name is given, a dbVersion must also be given in order to download the pickle of the same name
#If it is not available it is downloaded from the internet and saved to the input location
#When noNull is enabled a pickle object with no null alleles is searched for instead.
#The files should be/are named the following way:
# - Alignment with null alleles: <dbVersion>-all.pickle
# - Alignment without null alleles: <dbVersion>-nonull.pickle
def loadAlignments(input, dbVersion = 'Latest', noNull = False):
    if os.path.isfile(input):
        with open(input, 'rb') as f:
            return pickle.load(f)
    elif os.path.isdir(input) or input == '':

        #If the latest one should be returned, retrieve the latest db version number
        if dbVersion == 'Latest':
            dbVersion = getLatestDbVersion()

        #Check if an HLA object already exists as a .pickle.
        #If so, load and return it
        whichAlleles = 'nonull' if noNull else 'all'
        picklePath = os.path.join(input, f'{dbVersion}-{whichAlleles}.pickle')
        if os.path.isfile(picklePath):
            with open(picklePath, 'rb') as f:
                return pickle.load(f)
        #Otherwise create a new one to be saved as a file and into the HLAObjects dictionary
        else:
            HLAObject = HLA(databaseVersion=dbVersion, noNull = noNull)
            HLAObject.save(input)
            return HLAObject
    else:
        raise FileNotFoundError(f"'{input}' is neither a file or folder.")

#Function to calculate frequences of characters at every position in a BioPython multiple sequence alignment (not locusAlignment!).
#It returns an array of dictionaries for every position with:
# -'count': a counter object: https://pymotw.com/2/collections/counter.html
# -'alleles': a dictionary with codons mapped to allele names
def countAlignment(alignment, charLen = 1):

    #charLen must have a minimum of 1 and the total alignment length must be dividable by it
    alignmentLen = alignment.get_alignment_length()
    if charLen < 1:
        raise ValueError('charLen must have a value of at least 1.')
    if alignmentLen % charLen != 0:
        raise ValueError('The length of the alignment must be a multiple of charLen.')

    counts = []
    for charIndex in range(0, alignmentLen, charLen):
        count = Counter()
        alleles = {}
        for seqRec in alignment:
            chars = seqRec.seq[charIndex : charIndex + charLen]
            count.update({chars: 1})

            if chars not in alleles:
                alleles[chars] = []
            alleles[chars].append(seqRec.id)

        counts.append({'count': count, 'alleles': alleles})
    return counts

#Function to save Biopython's alignment objects
def saveAlignment(alignment, fileName, outputFolder, format):

    #Check if the output folder is indeed a folder
    if not isdir(outputFolder) and outputFolder != '':
        raise FileNotFoundError(f'{outputFolder} is not a valid directory.')

    #Save the alignment
    AlignIO.write(alignment, join(outputFolder, f'{fileName}.{format}'), format)

#Define class to represent all aligned HLA alleles, the used references and useful methods/operators
#When no alignment input is given, the alignment files per locus are retrieved from the internet, a databaseVersion can then be given to be used (the default is 'Latest').
#Otherwise it should be either a list of file names or a folder of files whose name starts with the locus name
#(and either followed by an underscore '_' or dot '.')
#Whenever noNull is True, any null alleles are left out of the alignment.
class HLA:
    def __init__(self, alignmentInput = None, format = 'msf', codonStarts = defaultCodonStarts, databaseVersion = 'Latest', noNull = False):
        self.codonStart = defaultCodonStarts
        if databaseVersion == 'Latest':
            self.databaseVersion = getLatestDbVersion()
        else:
            self.databaseVersion = str(databaseVersion).replace('.', '')
        self.noNull = noNull

        #Setup two datastructures: One for the alignments per locus
        #and one with the name pointing to the SeqRec object within the above structure
        self.alignments = {}
        self.IDIndex = {}

        #If no input is given, retrieve the alignments from the internet
        if alignmentInput == None:
            for locus, text in downloadAlignments(databaseVersion=databaseVersion).items():
                handle = StringIO(text)
                self.alignments[locus] = AlignIO.read(handle, format = 'msf')

        #If it is a string to a folder, load a list of all files in the folder first
        #and assign it to 'AlignmentInput'
        elif isinstance(alignmentInput, str):
            flist = []
            for f in os.listdir(alignmentInput):
                path = os.path.join(alignmentInput, f)
                flist.append(path)
                if os.path.isfile(path):
                    locus = re.search(r'^(\w{1,6}?)(_|\.)', f).group(1)
                    alignment = AlignIO.read(path, format = format)
                    self.alignments[locus] = alignment

        #If it is a list of paths, loop over all of them and load the alignments
        elif isinstance(alignmentInput, list):
            for f in alignmentInput:
                locus = re.search(r'^(\w{1,6}?)(_|\.)', os.path.basename(f)).group(1)
                alignment = AlignIO.read(f, format = format)
                self.alignments[locus] = alignment

        #Loop over every alignment while replacing it with an locusAlignment class which support codon indexing
        #while saving the locus and ids of every sequence to the IDIndex (except for null alleles if noNull is True)
        for locus, alignment in list(self.alignments.items()):

            #Do save null alleles
            if not self.noNull:

                #Loop over all sequences in the alignment to assign and index to their IDs.
                for i, rec in enumerate(alignment):
                    self.IDIndex[rec.id] = (locus, i)
                self.alignments[locus] = LocusAlignment(alignment, locus, codonStarts[locus])

            #Do not save null alleles
            else:

                #Loop over all sequences in the alignment to assign index their IDs and filter out any null alleles (if noNull is True)
                offset = 0
                filteredAlignment = []
                for i, rec in enumerate(alignment):
                    if not rec.id.endswith('N'):
                        self.IDIndex[rec.id] = (locus, i+offset)
                        filteredAlignment.append(rec)
                    else:
                        offset -= 1
                self.alignments[locus] = LocusAlignment(MultipleSeqAlignment(filteredAlignment), locus, codonStarts[locus])

            #When the locus is 'DRB345' additionaly create new locusAlignments for every locus and add the appropiate alleles
            if locus == 'DRB345':
                for i in range(3, 6):
                    self.alignments[f'DRB{i}'] = self.alignments[locus].copy(MultipleSeqAlignment([]))

                for rec in alignment:
                    subLocus = re.match(r'\w{4}', rec.id).group(0)
                    self.alignments[subLocus].alignment.append(rec)

    #Private method to add an allele (not supported)
    def __setitem__(self, key, rec):
        raise TypeError('You cannot assign new alleles to this object.')

    #Private method to check the type of the key
    def __check_key__(self, key):
        if isinstance(key, int):
            return 'HLAIndex'
        elif isinstance(key, str):
            if re.match(r'HLA\d{5}', key):
                return 'HLAID'
            elif re.match(r'[\w]{1,6}\*\d{2,4}:\d{2,4}:\d{2,4}:\d{2,4}[^\W\d_]?|[\w]{1,6}\*\d{2,4}:\d{2,4}:\d{2,4}[^\W\d_]?|[\w]{1,6}\*\d{2,4}:\d{2,4}[^\W\d_]?|[\w]{1,6}\*\d{2,4}[^\W\d_]?', key):
                return 'HLAName'
            elif re.search(r'^[\w]{1,6}$', key):
                return 'HLALocus'
            #Raise a ValueError when the string is not a valid key
            else:
                raise ValueError('The string is not in a supported format.')
        elif isinstance(key, slice) or isinstance(key, tuple) or isinstance(key, list):
            return 'HLASlice'
        elif key is None:
            return None
        #For any other key type, raise a TypeError
        else:
            raise TypeError('HLA alleles can only be accessed by strings in the appropiate format.')

    #Private slice method ([]) to return the allele based on either
    #the locus name (reference allele) or allele name (get first match when it does not fully match)
    #or a range of alleles either by comma separated names ([name1, name2]) or in between two of them ([name1:name2])
    #When index is enabled it will return the index instead for HLAName, but not the HLA locus.
    def __getitem__(self, key, returnIndex = False):
        #Return...
        keyType = self.__check_key__(key)
        if keyType == 'HLAIndex':                   #an allele with matching index (not supported)
            raise TypeError('HLA alleles can not be accessed by indices, only by key strings.')
        if keyType == 'HLAID':                      #an allele with matching ID (not supported)
            raise ValueError('HLAIDs cannot be used. Use the allele name/locus instead.')
        elif keyType == 'HLAName':                #an allele with matching name
            try:
                locus, index = self.IDIndex[key]
                return self.alignments[locus][index] if not returnIndex else (locus, index)
            #If no exact name match is present, search for other HLA alleles instead which are either of higher or at least lower resolution
            #and return those in a list or return just the indices
            except KeyError:
                locus = re.match('\w{1,6}', key).group(0)
                higher = []
                lower = []
                for i, rec in enumerate(self.align(locus).alignment):
                    #Search for higher res
                    if rec.id.startswith(key) and rec.id[len(key)] == ':':
                        if not returnIndex:
                            higher.append(rec)
                        else:
                            higher.append((locus, i))
                    #Search for lower res
                    elif key.startswith(rec.id) and key[len(rec.id)] == ':':
                        if not returnIndex:
                            lower.append(rec)
                        else:
                            lower.append((locus, i))

                #Preferably return the higher res, otherwise the lower res alleles.
                #If nothing was found, raise an error after all.
                if len(higher) > 0:
                    print(f'The allele name {key} did not return anything, so a list of higher resolution alleles is returned instead.')
                    return higher
                elif len(lower) > 0:
                    print(f'The allele name {key} did not return anything, so a list of lower resolution alleles is returned instead.')
                    return lower
                else:
                    raise KeyError(f'There is no HLA allele with the name \'{key}\' present, either of the same, higher or lower resolution.')
        elif keyType == 'HLALocus':                 #an reference allele with matching locus name
            try:
                if not returnIndex:
                    return self.alignments[key].ref
                else:
                    raise KeyError(f'You cannot retrieve the index for a locus. This is contained in <LocusAlignment>.ref.')
            except:
                raise KeyError(f'There is no {key} locus in the current allele reference list.')
        elif keyType == 'HLASlice':                 #multiple alleles in a HLASlice object when a slice is given
            return HLASlice(self, key)
        elif keyType is None:
            raise KeyError(f'You have to supply a allele name for a single key.')

    #Private method to return the number of HLA alleles present
    def __len__(self):
        return sum([len(al.alignment) for al in self.alignments.values()])

    #Shorthand method to return the aligment of a locus
    def align(self, locus):
        return self.alignments[locus]

    #Method to only return the allele names. When a locus is given, return the names of only that locus.
    #When returnDict is True, return a dictionary with loci as keys and lists of the names as values (giving a locus name will not do anything then).
    def names(self, optionalLocus = None, returnDict = False):
        if not returnDict:
            return [rec.id for locus, al in self.alignments.items() if locus not in ['DRB3', 'DRB4', 'DRB5'] for rec in al.alignment] if optionalLocus == None else [rec.id for rec in self.alignments[optionalLocus].alignment]
        else:
            alleleDict = {}
            for locus, al in self.alignments.items():
                for rec in al.alignment:
                    if locus in alleleDict:
                        alleleDict[locus].append(rec.id)
                    else:
                        alleleDict[locus] = [rec.id]
            return alleleDict

    #Method to save the current instance of the class as a pickle or alignments to the output folder
    #When specifying an alignment format, the alignments can be sliced by giving slice string as values
    #for loci in sliceDict, such as {'A': '1-10,15,20-25', 'B': ...}.
    #The type of slicing can also be specified by sliceType, which must either be 'gaps', 'no_gaps' or 'amino',
    #to preserve gaps introduced in the reference sequence, remove them or translate everything to amino acids.
    def save(self, outputFolder, format = 'pickle', sliceDict = {}, sliceType = 'gaps'):

        #Check if the output folder is indeed a folder
        if not isdir(outputFolder) and outputFolder != '':
            raise FileNotFoundError(f'{outputFolder} is not a valid directory.')

        #Save the current object as a pickle
        if format == 'pickle':
            whichAlleles = 'nonull' if self.noNull else 'all'
            outputFile = os.path.join(outputFolder, f'{self.databaseVersion}-{whichAlleles}.pickle')
            with open(outputFile, 'wb') as f:
                pickle.dump(self, f)
                print(f'Pickle of HLA object succesfully saved to \'{outputFile}\'.')

        #Save the current object as separate alignment files
        else:

            #Convert the slice string to slice objects
            for locus, sliceString in sliceDict.items():
                try:
                    sliceObject = [int(rng) if not '-' in rng else slice(*[int(i) for i in rng.split('-')]) for rng in sliceString.split(',')]
                    sliceDict[locus] = sliceObject
                except:
                    raise ValueError("The slice strings in sliceDict are in an incorrect format, it should be: {'A': '1-10,15,20-25', 'B': ...}")

            #When at least a single allele is present in a locus alignment, slice the alignment if necessary and save it
            for locus, locusAlignment in self.alignments.items():
                if locus in sliceDict:
                    if sliceType == 'gaps':
                        alignment = locusAlignment.slice[sliceDict[locus]]
                    elif sliceType == 'no_gaps':
                        alignment = locusAlignment.cod[sliceDict[locus]]
                    elif sliceType == 'amino':
                        alignment = locusAlignment.amino[sliceDict[locus]]
                    else:
                        raise ValueError("The sliceType is not 'gaps', 'no_gaps' or 'amino'.")
                else:
                    alignment = locusAlignment.alignment

                saveAlignment(alignment, locus, outputFolder , format)

#Class for wrapping around a locus alignment.
#Slice operators can be used as a shortcut of <locusAlignment>.alignment[].
#When doing <locusAlignment>.cod[] specific codons in an aligment will be returned.
class LocusAlignment:

    def __init__(self, alignment, locus, codonStart):
        self.alignment = alignment
        self.ref = alignment[0]
        self.locus = locus
        self.codonStart = codonStart
        self.codonIndex = {}

        #Save the positions of the nucleotides of every codon into the codonIndex
        pos = 0
        codonPos = codonStart
        while pos < len(self.ref.seq):
            codon = []
            while len(codon) < 3 and pos < len(self.ref.seq):
                if self.ref.seq[pos] != '-':
                    codon.append(pos)
                pos += 1
            if len(codon) == 3:
                self.codonIndex[codonPos] = codon
            elif len(codon) in [1,2]:
                raise Exception(f'The sequence for the reference allele for locus {locus} is not a multiple of tree.')
            codonPos += 1 if codonPos != -1 else 2

        #Create a codon (fast) and aminoacid (slow) slicer as a property
        self.slice = self.__AlignmentSlicer__(alignment, self.codonIndex)
        self.cod = self.__CodonSlicer__(alignment, self.codonIndex, False)
        self.amino = self.__CodonSlicer__(alignment, self.codonIndex, True)

    #Pass down the use of slice operators to the alignment
    def __getitem__(self, key):
        return self.alignment[key]

    #Return the number of alleles in the alignment
    def __len__(self):
        return len(self.alignment)

    #Method to return the range of the codons
    def range(self):
        if len(self.codonIndex) > 0:
            return list(self.codonIndex.keys())[0], list(self.codonIndex.keys())[-1]
        else:
            return None

    #Return a copy of the locus alignment with an updated alignment
    def copy(self, newAlignment):
        newLocusAlignment = copy(self)
        newLocusAlignment.alignment = newAlignment
        newLocusAlignment.slice = copy(self.slice)
        newLocusAlignment.slice.alignment = newAlignment
        newLocusAlignment.cod = copy(self.cod)
        newLocusAlignment.cod.alignment = newAlignment
        newLocusAlignment.amino = copy(self.amino)
        newLocusAlignment.amino.alignment = newAlignment
        return newLocusAlignment

    #Create a general parent class for the slicers
    class __Slicer__:
        def __init__(self, alignment, codonIndex):
            self.alignment = alignment
            self.ref = alignment[0]
            self.codonIndex = codonIndex

    #Create an internal class which slices based on codon positions but preserves gaps (fast)
    class __AlignmentSlicer__(__Slicer__):

        #Return the sliced alignment based on the codon positions
        def __getitem__(self, sliceObject):

            #When the sliceObject is an actual slice, return an alignment with only the codons (with gaps) in between these two values
            if isinstance(sliceObject, slice):
                start = sliceObject.start
                stop = sliceObject.stop
                if 0 in [start, stop]:
                    raise ValueError('No codon 0 exists: Codons are numbered starting from 1 or -1.')

                if len(self.codonIndex) > 0:

                    try:
                        startIndex = self.codonIndex[start][0] if start is not None else None
                    except:
                        startCodonPos = list(self.codonIndex.keys())[0]
                        stopCodonPos = list(self.codonIndex.keys())[-1]
                        raise KeyError(f'Codon {start} does not exist for this alignment. The codons range from {startCodonPos} to {stopCodonPos}')

                    try:
                        stopIndex = self.codonIndex[stop-1][2] + 1 if stop is not None else None
                    except:
                        startCodonPos = list(self.codonIndex.keys())[0]
                        stopCodonPos = list(self.codonIndex.keys())[-1]
                        raise KeyError(f'Codon {stop} does not exist for this alignment. The codons range from {startCodonPos} to {stopCodonPos}')

                else:
                    raise KeyError(f'There are no valid codons in this alignment.')

                return self.alignment[:, startIndex:stopIndex]

            #When it is a tuple or list, call the method recursively on each subSliceObject
            elif isinstance(sliceObject, tuple) or isinstance(sliceObject, list):
                for i, subSliceObject in enumerate(sliceObject):

                    subAlignment = self.__getitem__(subSliceObject)
                    if i == 0:
                        alignmentSum = subAlignment
                    else:
                        alignmentSum += subAlignment

                return alignmentSum

            #When it is an int it refers to a single codon, so return only this codon
            elif isinstance(sliceObject, int):
                codon = self.codonIndex[sliceObject]
                start = codon[0]
                stop = codon[2] + 1
                return self.alignment[:, start:stop]

    #Create an internal class for a codon slicer which can be translated or not (takes very long for long slices or lots of sequences)
    class __CodonSlicer__(__Slicer__):
        def __init__(self, alignment, codonIndex, translate = False):
            super().__init__(alignment, codonIndex)
            self.translate = translate

        #Return the the sliced codons (translated to aminoacids if self.translate is True) based on a slice object
        def __getitem__(self, sliceObject):

            #When the sliceObject is an actual slice, return the (translated) codons in between these two values
            if isinstance(sliceObject, slice):
                start = sliceObject.start
                stop = sliceObject.stop
                if 0 in [start, stop]:
                    raise ValueError('No codon 0 exists: Codons are numbered starting from 1 or -1.')

                if len(self.codonIndex) > 0:
                    try:

                        #Determine first and last codon positions
                        startCodonPos = list(self.codonIndex.keys())[0]
                        stopCodonPos = list(self.codonIndex.keys())[-1] + 1

                        #Assign these to start/stop when they are None
                        if start == None:
                            start = startCodonPos
                        if stop == None:
                            stop = stopCodonPos

                        #Retrieve (and translate) all the indices of all codons between start and stop
                        newAlignment = []
                        for allele in self.alignment:
                            sequence = []
                            for i in range(start, stop):
                                #Skip position 0
                                if i == 0:
                                    continue

                                #Try to translate and append an aminoacid/codon.
                                try:
                                    sequence.append(self.__get__codon__(i, allele, self.translate))
                                #When an aminoacid could not be translated, the sequence from that point onwards is just '-'
                                except TranslationError:
                                    sequence.extend(list((stop - i) * '-'))
                                    break

                            #Add the (translated) allele to the new alignment
                            newAlignment.append(SeqRecord(
                                id = allele.id,
                                seq = Seq(''.join(sequence)),
                                description=''
                                ))

                        return MultipleSeqAlignment(newAlignment)

                    except KeyError:
                        raise KeyError(f'Codon {i} does not exist for this alignment. The codons range from {startCodonPos} to {stopCodonPos}.')
                else:
                    raise KeyError(f'There are no valid codons in this alignment.')

            #When it is a tuple, call the method recursively on each subSliceObject
            elif isinstance(sliceObject, tuple) or isinstance(sliceObject, list):
                for i, subSliceObject in enumerate(sliceObject):

                    subAlignment = self.__getitem__(subSliceObject)
                    if i == 0:
                        alignmentSum = subAlignment
                    else:
                        alignmentSum += subAlignment

                return alignmentSum

            #When it is an int it refers to a single codon, so return only this translated codon
            elif isinstance(sliceObject, int):
                return self.__getitem__(slice(sliceObject, sliceObject+1))

        #Method to get a single codon (which can be translated)
        def __get__codon__(self, codonPos, allele, translate):
            #When a codon is not present raise a KeyError
            try:
                codon = self.codonIndex[codonPos]
            except:
                raise KeyError(f'Codon {codonPos} does not exist for this alignment.')

            #Try to construct the codon and translate it if enabled, if it fails raise an error
            codonStr = allele[codon[0]]  +  allele[codon[1]]  +  allele[codon[2]]
            if translate:
                try:
                    codonSeq = Seq(codonStr)
                    aminoacid = codonSeq.translate()
                except:
                    raise TranslationError(f'The codon {codonPos} could not be translated.')
                return str(aminoacid)
            else:
                return codonStr

#Class for slicing the main HLA class object into a smaller object containing a subset of HLA alleles
class HLASlice(HLA):
    def __init__(self, HLAObject, sliceObject):

        #Setup empty variables
        self.alignments = {}
        self.IDIndex = {}

        #Run a recursive method to add all of the HLA alleles based on the slice object
        self.__parse_slice__(HLAObject, sliceObject)

    #Private recursive method which splits tuples of slice objects and keys and adds any alleles based on the keys it comes across
    def __parse_slice__(self, HLAObject, sliceObject):

        #When the sliceObject is an actual slice, retrieve all the alleles in between these two values
        if isinstance(sliceObject, slice):

            #Check the type of the slice, it may only be names and no locus.
            #Optionally, only one of them can also be None to mean the first/last allele of the locus.
            start = sliceObject.start
            startType = self.__check_key__(start)
            stop = sliceObject.stop
            stopType = self.__check_key__(stop)
            if startType in ['HLAID', 'HLAIndex', 'HLALocus'] or stopType in ['HLAID', 'HLAIndex', 'HLALocus']:
                raise ValueError(f'HLA IDs, indices or locus names cannot be used for slicing. Use the allele name instead.')

            #Retrieve the indices. Choose the first/last allele of a locus if one of them is None
            #Additionaly when the index returned is a list, so choose the first / last item as index for the start / stop, respectively.
            if startType is not None and stopType is not None:
                startReturn = HLAObject.__getitem__(start, True)
                stopReturn = HLAObject.__getitem__(stop, True)
                startLocus, startIndex = startReturn[0] if isinstance(startReturn, list) else startReturn
                stopLocus, stopIndex = stopReturn[-1] if isinstance(stopReturn, list) else stopReturn
            elif startType is None and stopType is not None:
                stopReturn = HLAObject.__getitem__(stop, True)
                startReturn = None
                stopLocus, stopIndex = stopReturn[-1] if isinstance(stopReturn, list) else stopReturn
                startLocus, startIndex = (stopLocus, 0)
            elif startType is not None and stopType is None:
                startReturn = HLAObject.__getitem__(start, True)
                stopReturn = None
                startLocus, startIndex = startReturn[0] if isinstance(startReturn, list) else startReturn
                stopLocus, stopIndex = (startLocus, len(HLAObject.align(startLocus)) - 1)
            else:
                raise ValueError('When slicing, at least either a start or end allele name must be provided.')

            #when the start and stop index are not in the same locus, raise an error
            if startLocus != stopLocus:
                raise ValueError('When slicing alleles they must be present in the same locus.')

            #When the index of the start index is bigger than the stop index, switch them around
            if startIndex > stopIndex:
                temp = startIndex
                startIndex = stopIndex if not isinstance(stopReturn, list) else stopReturn[0]
                stopIndex = temp if not isinstance(startReturn, list) else startReturn[-1]

            #Retrieve and save all alleles between these two indices, but extend the current alignment if it is already saved
            for i in range(startIndex, stopIndex + 1):
                self.__add_allele__(startLocus, i, HLAObject)

        #When it is a tuple or list, call the method recursively on each subSliceObject
        elif isinstance(sliceObject, tuple) or isinstance(sliceObject, list):
            for subSliceObject in sliceObject:
                self.__parse_slice__(HLAObject, subSliceObject)

        #Otherwise it is a single key, so save it
        else:
            #Retrieve the locus and allele differently depending on whether it is a name or a locus (retrieve reference).
            keyType = self.__check_key__(sliceObject)

            if keyType == 'HLAName':
                #When only one allele is returned, add it to the alignment
                indices = HLAObject.__getitem__(sliceObject, True)
                if not isinstance(indices, list):
                    locus, index = indices
                    self.__add_allele__(locus, index, HLAObject)
                #When multiple alleles are returned, also add them all to the alignment
                else:
                    locus = indices[0][0]
                    for index in indices:
                        self.__add_allele__(locus, index[1], HLAObject)
            elif keyType == 'HLALocus':
                locus = sliceObject
                self.__add_alignment__(locus, HLAObject.align(locus).ref, HLAObject)
            else:
                raise ValueError('HLA IDs or indices cannot be used for access. Use the allele name instead.')

    #Method to add an allele from the old HLAObject
    def __add_allele__(self, locus, index, HLAObject):
        oldLocusAlignment = HLAObject.alignments[locus]
        allele = oldLocusAlignment.alignment[index]

        #Add the allele and create a new alignment from the old one if not already present
        self.__add_alignment__(locus, allele, HLAObject)

        #Update the IDIndex
        self.IDIndex[allele.id] = (locus, len(self.alignments[locus]) - 1)

        #When the locus is 'DRB345' additionaly add them to either the 'DRB3, -4 or -5' alignment
        if locus == 'DRB345':
            subLocus = allele.id[:4]
            self.__add_alignment__(subLocus, allele, HLAObject)

    #Method to only create a new alignment and add an allele to it without modifying the self.IDIndex from the old HLAObject
    def __add_alignment__(self, locus, allele, HLAObject):
        oldLocusAlignment = HLAObject.alignments[locus]
        if locus not in self.alignments:
            newLocusAlignment = oldLocusAlignment.copy(MultipleSeqAlignment([allele]))
            self.alignments[locus] = newLocusAlignment
        else:
            self.alignments[locus].alignment.append(allele)