"""
Contains the classes that deal with playing and drawing connect four.

*Classes*
---------
    ConnectFour
    DrawConnectFour
"""
import random  # Used to shuffle players and by the ai to pick from
# similar options
import copy  # Used to deepcopy boards
import math  # Used for math.inf
import discord  # Used for typehints, discord.file and to check whether
# the opponent in ConnectFour.start is a discord.User

from PIL import Image, ImageDraw, ImageFont  # Used by DrawConnectFour()
from discord_slash.context import SlashContext  # Used for typehints
from typing import Union  # Used for typehints

ROWCOUNT = 6
COLUMNCOUNT = 7


class ConnectFour():
    """
    Deals with connect four commands and logic.

    *Methods*
    ---------
        start(ctx: SlashContext, opponent: Union[int, Discord.User])
        placePiece(ctx: Union[SlashContext, discord.Message],
                   user: int, column: int)
        surrender(ctx: SlashContext)
    """

    def __init__(self, bot):
        """Initialize the class."""
        self.bot = bot
        self.draw = DrawConnectFour(bot)
        self.AISCORES = {
            "middle": 3,
            "two in a row": 10,
            "three in a row": 50,
            "enemy two in a row": -35,
            "enemy three in a row": -200,
            "enemy win": -10000,
            "win": 10000,
            "avoid losing": 100
        }
        self.REACTIONS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣"]

    async def start(self, ctx: SlashContext,
                    opponent: Union[int, discord.User]):
        """
        Start a game of connect four.

        *Parameters*
        ------------
            ctx: SlashContext
                The context of the slash command.
            opponent: Union[int, discord.User]
                The requested opponent. If it's an int, the opponent is
                Gwendolyn with a difficulty equal to the value of the
                int. The difficulty is equal to how many levels the AI
                searches when minimaxing.
        """
        await self.bot.defer(ctx)
        user = f"#{ctx.author.id}"
        channel = str(ctx.channel_id)
        game = self.bot.database["connect 4 games"].find_one({"_id": channel})

        startedGame = False
        canStart = True

        if game is not None:
            sendMessage = self.bot.long_strings["Connect 4 going on"]
            log_message = "There was already a game going on"
            canStart = False
        elif type(opponent) == int:
            # Opponent is Gwendolyn
            if opponent in range(1, 6):
                difficulty = int(opponent)
                diffText = f" with difficulty {difficulty}"
                opponent = f"#{self.bot.user.id}"
            else:
                sendMessage = "Difficulty doesn't exist"
                log_message = "They challenged a difficulty that doesn't exist"
                canStart = False
        elif type(opponent) == discord.User:
            if opponent.bot:
                # User has challenged a bot
                if opponent == self.bot.user:
                    # It was Gwendolyn
                    difficulty = 3
                    diffText = f" with difficulty {difficulty}"
                    opponent = f"#{self.bot.user.id}"
                else:
                    sendMessage = "You can't challenge a bot!"
                    log_message = "They tried to challenge a bot"
                    canStart = False
            else:
                # Opponent is another player
                if ctx.author != opponent:
                    opponent = f"#{opponent.id}"
                    difficulty = 5
                    diffText = ""
                else:
                    sendMessage = "You can't play against yourself"
                    log_message = "They tried to play against themself"
                    canStart = False

        if canStart:
            x, y = COLUMNCOUNT, ROWCOUNT
            board = [[0 for _ in range(x)] for _ in range(y)]
            players = [user, opponent]
            random.shuffle(players)

            newGame = {
                "_id": channel,
                "board": board,
                "winner": 0,
                "win direction": "",
                "win coordinates": [0, 0],
                "players": players,
                "turn": 0,
                "difficulty": difficulty
            }

            self.bot.database["connect 4 games"].insert_one(newGame)

            self.draw.drawImage(channel)

            gwendoTurn = (players[0] == f"#{self.bot.user.id}")
            startedGame = True

            opponentName = self.bot.database_funcs.get_name(opponent)
            turnName = self.bot.database_funcs.get_name(players[0])

            startedText = f"Started game against {opponentName}{diffText}."
            turnText = f"It's {turnName}'s turn"
            sendMessage = f"{startedText} {turnText}"
            log_message = "They started a game"

        self.bot.log(log_message)
        await ctx.send(sendMessage)

        # Sets the whole game in motion
        if startedGame:
            boardsPath = "gwendolyn/resources/games/connect4Boards/"
            file_path = f"{boardsPath}board{ctx.channel_id}.png"
            old_image = await ctx.channel.send(file=discord.File(file_path))

            old_imagesPath = "gwendolyn/resources/games/old_images/"
            old_imagePath = f"{old_imagesPath}connect_four{ctx.channel_id}"
            with open(old_imagePath, "w") as f:
                f.write(str(old_image.id))

            if gwendoTurn:
                await self._connect_fourAI(ctx)
            else:
                for reaction in self.REACTIONS:
                    await old_image.add_reaction(reaction)

    async def placePiece(self, ctx: Union[SlashContext, discord.Message],
                         user: int, column: int):
        """
        Place a piece on the board.

        *Parameters*
        ------------
            ctx: Union[SlashContext, discord.Message]
                The context of the command/reaction. ctx can only be
                SlashContext if the piece is placed by the bot
                immediately after the player starts the game with a
                command. Otherwise, ctx will be the message the player
                reacted to when placing a piece.
            user: int
                The player-number of the player placing the piece.
            column: int
                The column the player is placing the piece in.
        """
        channel = str(ctx.channel.id)
        connect4Games = self.bot.database["connect 4 games"]
        game = connect4Games.find_one({"_id": channel})
        playerNumber = game["players"].index(user)+1
        user_name = self.bot.database_funcs.get_name(user)
        placedPiece = False

        if game is None:
            sendMessage = "There's no game in this channel"
            log_message = "There was no game in the channel"
        else:
            board = game["board"]
            board = self._placeOnBoard(board, playerNumber, column)

            if board is None:
                sendMessage = "There isn't any room in that column"
                log_message = "There wasn't any room in the column"
            else:
                updater = {"$set": {"board": board}}
                connect4Games.update_one({"_id": channel}, updater)
                turn = (game["turn"]+1) % 2
                updater = {"$set": {"turn": turn}}
                connect4Games.update_one({"_id": channel}, updater)

                self.bot.log("Checking for win")
                won, winDirection, winCoordinates = self._isWon(board)

                if won != 0:
                    gameWon = True
                    updater = {"$set": {"winner": won}}
                    connect4Games.update_one({"_id": channel}, updater)
                    updater = {"$set": {"win direction": winDirection}}
                    connect4Games.update_one({"_id": channel}, updater)
                    updater = {"$set": {"win coordinates": winCoordinates}}
                    connect4Games.update_one({"_id": channel}, updater)

                    sendMessage = "{} placed a piece in column {} and won. "
                    sendMessage = sendMessage.format(user_name, column+1)
                    log_message = f"{user_name} won"
                    winAmount = int(game["difficulty"])**2+5
                    if game["players"][won-1] != f"#{self.bot.user.id}":
                        sendMessage += "Adding {} GwendoBucks to their account"
                        sendMessage = sendMessage.format(winAmount)
                elif 0 not in board[0]:
                    gameWon = True
                    sendMessage = "It's a draw!"
                    log_message = "The game ended in a draw"
                else:
                    gameWon = False
                    otherUserId = game["players"][turn]
                    otherUserName = self.bot.database_funcs.get_name(otherUserId)
                    sendMessage = self.bot.long_strings["Connect 4 placed"]
                    formatParams = [user_name, column+1, otherUserName]
                    sendMessage = sendMessage.format(*formatParams)
                    log_message = "They placed the piece"

                gwendoTurn = (game["players"][turn] == f"#{self.bot.user.id}")

                placedPiece = True

        await ctx.channel.send(sendMessage)
        self.bot.log(log_message)

        if placedPiece:
            self.draw.drawImage(channel)

            old_imagePath = f"gwendolyn/resources/games/old_images/connect_four{channel}"
            with open(old_imagePath, "r") as f:
                old_image = await ctx.channel.fetch_message(int(f.read()))

            if old_image is not None:
                await old_image.delete()
            else:
                self.bot.log("The old image was already deleted")

            file_path = f"gwendolyn/resources/games/connect4Boards/board{channel}.png"
            old_image = await ctx.channel.send(file=discord.File(file_path))

            if gameWon:
                self._endGame(channel)
            else:
                with open(old_imagePath, "w") as f:
                    f.write(str(old_image.id))
                if gwendoTurn:
                    await self._connect_fourAI(ctx)
                else:
                    for reaction in self.REACTIONS:
                        await old_image.add_reaction(reaction)

    async def surrender(self, ctx: SlashContext):
        """
        Surrender a connect four game.

        *Parameters*
        ------------
            ctx: SlashContext
                The context of the command.
        """
        await self.bot.defer(ctx)
        channel = str(ctx.channel_id)
        game = self.bot.database["connect 4 games"].find_one({"_id": channel})

        if f"#{ctx.author.id}" in game["players"]:
            loserIndex = game["players"].index(f"#{ctx.author.id}")
            winnerIndex = (loserIndex+1) % 2
            winnerID = game["players"][winnerIndex]
            winnerName = self.bot.database_funcs.get_name(winnerID)

            sendMessage = f"{ctx.author.display_name} surrenders."
            sendMessage += f" This means {winnerName} is the winner."
            if winnerID != f"#{self.bot.user.id}":
                difficulty = int(game["difficulty"])
                reward = difficulty**2 + 5
                sendMessage += f" Adding {reward} to their account"

            await ctx.send(sendMessage)
            old_imagePath = f"gwendolyn/resources/games/old_images/connect_four{channel}"
            with open(old_imagePath, "r") as f:
                old_image = await ctx.channel.fetch_message(int(f.read()))

            if old_image is not None:
                await old_image.delete()
            else:
                self.bot.log("The old image was already deleted")

            self._endGame(channel)
        else:
            await ctx.send("You can't surrender when you're not a player")

    def _placeOnBoard(self, board: dict, player: int, column: int):
        """
        Place a piece in a given board.

        This differs from placePiece() by not having anything to do
        with any actual game. It just places the piece in a game dict.

        *Parameters*
        ------------
            board: dict
                The board to place the piece in.
            player: int
                The player who places the piece.
            column: int
                The column the piece is placed in.

        *Returns*
        ---------
            board: dict
                The board with the placed piece.
        """
        placementX, placementY = -1, column

        for x, line in list(enumerate(board))[::-1]:
            if line[column] == 0:
                placementX = x
                break

        board[placementX][placementY] = player

        if placementX == -1:
            return None
        else:
            return board

    def _endGame(self, channel: str):
        game = self.bot.database["connect 4 games"].find_one({"_id": channel})

        winner = game["winner"]
        if winner != 0:
            if game["players"][winner-1] != f"#{self.bot.user.id}":
                difficulty = int(game["difficulty"])
                reward = difficulty**2 + 5
                self.bot.money.addMoney(game["players"][winner-1], reward)

        self.bot.database_funcs.delete_game("connect 4 games", channel)

    def _isWon(self, board: dict):
        won = 0
        winDirection = ""
        winCoordinates = [0, 0]

        for row in range(ROWCOUNT):
            for place in range(COLUMNCOUNT):
                if won == 0:
                    piecePlayer = board[row][place]
                    if piecePlayer != 0:
                        # Checks horizontal
                        if place <= COLUMNCOUNT-4:
                            pieceOne = board[row][place+1]
                            pieceTwo = board[row][place+2]
                            pieceThree = board[row][place+3]

                            pieces = [pieceOne, pieceTwo, pieceThree]
                        else:
                            pieces = [0]

                        if all(x == piecePlayer for x in pieces):
                            won = piecePlayer
                            winDirection = "h"
                            winCoordinates = [row, place]

                        # Checks vertical
                        if row <= ROWCOUNT-4:
                            pieceOne = board[row+1][place]
                            pieceTwo = board[row+2][place]
                            pieceThree = board[row+3][place]

                            pieces = [pieceOne, pieceTwo, pieceThree]
                        else:
                            pieces = [0]

                        if all(x == piecePlayer for x in pieces):
                            won = piecePlayer
                            winDirection = "v"
                            winCoordinates = [row, place]

                        # Checks right diagonal
                        goodRow = (row <= ROWCOUNT-4)
                        goodColumn = (place <= COLUMNCOUNT-4)
                        if goodRow and goodColumn:
                            pieceOne = board[row+1][place+1]
                            pieceTwo = board[row+2][place+2]
                            pieceThree = board[row+3][place+3]

                            pieces = [pieceOne, pieceTwo, pieceThree]
                        else:
                            pieces = [0]

                        if all(x == piecePlayer for x in pieces):
                            won = piecePlayer
                            winDirection = "r"
                            winCoordinates = [row, place]

                        # Checks left diagonal
                        if row <= ROWCOUNT-4 and place >= 3:
                            pieceOne = board[row+1][place-1]
                            pieceTwo = board[row+2][place-2]
                            pieceThree = board[row+3][place-3]

                            pieces = [pieceOne, pieceTwo, pieceThree]
                        else:
                            pieces = [0]

                        if all(x == piecePlayer for x in pieces):
                            won = piecePlayer
                            winDirection = "l"
                            winCoordinates = [row, place]

        return won, winDirection, winCoordinates

    async def _connect_fourAI(self, ctx: SlashContext):
        def outOfRange(possibleScores: list):
            allowedRange = max(possibleScores)*(1-0.1)
            moreThanOne = len(possibleScores) != 1
            return (min(possibleScores) <= allowedRange) and moreThanOne

        channel = str(ctx.channel.id)

        self.bot.log("Figuring out best move")
        game = self.bot.database["connect 4 games"].find_one({"_id": channel})

        board = game["board"]
        player = game["players"].index(f"#{self.bot.user.id}")+1
        difficulty = game["difficulty"]

        scores = [-math.inf for _ in range(COLUMNCOUNT)]
        for column in range(COLUMNCOUNT):
            testBoard = copy.deepcopy(board)
            testBoard = self._placeOnBoard(testBoard, player, column)
            if testBoard is not None:
                _minimaxParams = [
                    testBoard,
                    difficulty,
                    player % 2+1,
                    player,
                    -math.inf,
                    math.inf,
                    False
                ]
                scores[column] = await self._minimax(*_minimaxParams)
            self.bot.log(f"Best score for column {column} is {scores[column]}")

        possibleScores = scores.copy()

        while outOfRange(possibleScores):
            possibleScores.remove(min(possibleScores))

        highestScore = random.choice(possibleScores)

        bestColumns = [i for i, x in enumerate(scores) if x == highestScore]
        placement = random.choice(bestColumns)

        await self.placePiece(ctx, f"#{self.bot.user.id}", placement)

    def _AICalcPoints(self, board: dict, player: int):
        score = 0
        otherPlayer = player % 2+1

        # Adds points for middle placement
        # Checks horizontal
        for row in range(ROWCOUNT):
            if board[row][3] == player:
                score += self.AISCORES["middle"]
            rowArray = [int(i) for i in list(board[row])]
            for place in range(COLUMNCOUNT-3):
                window = rowArray[place:place+4]
                score += self._evaluateWindow(window, player, otherPlayer)

        # Checks Vertical
        for column in range(COLUMNCOUNT):
            columnArray = [int(i[column]) for i in list(board)]
            for place in range(ROWCOUNT-3):
                window = columnArray[place:place+4]
                score += self._evaluateWindow(window, player, otherPlayer)

        # Checks right diagonal
        for row in range(ROWCOUNT-3):
            for place in range(COLUMNCOUNT-3):
                pieceOne = board[row][place]
                pieceTwo = board[row+1][place+1]
                pieceThree = board[row+2][place+2]
                pieceFour = board[row+3][place+3]

                window = [pieceOne, pieceTwo, pieceThree, pieceFour]
                score += self._evaluateWindow(window, player, otherPlayer)

            for place in range(3, COLUMNCOUNT):
                pieceOne = board[row][place]
                pieceTwo = board[row+1][place-1]
                pieceThree = board[row+2][place-2]
                pieceFour = board[row+3][place-3]

                window = [pieceOne, pieceTwo, pieceThree, pieceFour]
                score += self._evaluateWindow(window, player, otherPlayer)

        return score

    def _evaluateWindow(self, window: list, player: int, otherPlayer: int):
        if window.count(player) == 4:
            return self.AISCORES["win"]
        elif window.count(player) == 3 and window.count(0) == 1:
            return self.AISCORES["three in a row"]
        elif window.count(player) == 2 and window.count(0) == 2:
            return self.AISCORES["two in a row"]
        elif window.count(otherPlayer) == 4:
            return self.AISCORES["enemy win"]
        else:
            return 0

    async def _minimax(self, board: dict, depth: int, player: int,
                       originalPlayer: int, alpha: int, beta: int,
                       maximizingPlayer: bool):
        """
        Evaluate the minimax value of a board.

        *Parameters*
        ------------
            board: dict
                The board to evaluate.
            depth: int
                How many more levels to check. If 0, the current value
                is returned.
            player: int
                The player who can place a piece.
            originalPlayer: int
                The AI player.
            alpha, beta: int
                Used with alpha/beta pruning, in order to prune options
                that will never be picked.
            maximizingPlayer: bool
                Whether the current player is the one trying to
                maximize their score.

        *Returns*
        ---------
            value: int
                The minimax value of the board.
        """
        terminal = 0 not in board[0]
        points = self._AICalcPoints(board, originalPlayer)
        # The depth is how many moves ahead the computer checks. This
        # value is the difficulty.
        if depth == 0 or terminal or (points > 5000 or points < -6000):
            return points
        if maximizingPlayer:
            value = -math.inf
            for column in range(0, COLUMNCOUNT):
                testBoard = copy.deepcopy(board)
                testBoard = self._placeOnBoard(testBoard, player, column)
                if testBoard is not None:
                    _minimaxParams = [
                        testBoard,
                        depth-1,
                        (player % 2)+1,
                        originalPlayer,
                        alpha,
                        beta,
                        False
                    ]
                    evaluation = await self._minimax(*_minimaxParams)
                    if evaluation < -9000:
                        evaluation += self.AISCORES["avoid losing"]
                    value = max(value, evaluation)
                    alpha = max(alpha, evaluation)
                    if beta <= alpha:
                        break
            return value
        else:
            value = math.inf
            for column in range(0, COLUMNCOUNT):
                testBoard = copy.deepcopy(board)
                testBoard = self._placeOnBoard(testBoard, player, column)
                if testBoard is not None:
                    _minimaxParams = [
                        testBoard,
                        depth-1,
                        (player % 2)+1,
                        originalPlayer,
                        alpha,
                        beta,
                        True
                    ]
                    evaluation = await self._minimax(*_minimaxParams)
                    if evaluation < -9000:
                        evaluation += self.AISCORES["avoid losing"]
                    value = min(value, evaluation)
                    beta = min(beta, evaluation)
                    if beta <= alpha:
                        break
            return value


class DrawConnectFour():
    """
    Functions for drawing the connect four board.

    *Methods*
    ---------
        drawImage(channel: str)

    *Attributes*
    ------------
        BORDER: int
            The border around the game board.
        GRIDBORDER: int
            The border between the edge of the board and the piece
            furthest towards the edge.
        CORNERSIZE: int
            The size of the corner circles.
        BOARDOUTLINESIZE: int
            How big the outline of the board is.
        BOARDOUTLINECOLOR: RGBA tuple
            The color of the outline of the board.
        PIECEOUTLINESIZE: int
            How big the outline of each piece is.
        PIECEOUTLINECOLOR: RGB tuple
            The color of the outline of each piece.
        EMPTYOUTLINESIZE: int
            How big the outline of an empty place on the board is.
        EMPTYOUTLINECOLOR: RGB tuple
            The color of the outline of an empty place on the board.
        BOTTOMBORDER: int
            The border at the bottom where the names are.
        TEXTSIZE: int
            The size of the text.
        TEXTPADDING: int
            How much padding to add to the the text.
        WIDTH, HEIGHT: int
            The size of the image, minus the bottom border
        BACKGROUNDCOLOR: RGBA tuple
            The color of the background.
        PLAYER1COLOR, PLAYER2COLOR: RGB tuple
            The color of each player.
        BOARDCOLOR: RGB tuple
            The color of the board
        WINBARCOLOR: RGBA tuple
            The color of the bar placed over the winning pieces.
        PIECESIZE: int
            The size of each piece.
        FONT: FreeTypeFont
            The font to use to write text.
        PIECEGRIDSIZE: list
            The size of each grid cell on the board.
        PIECESTARTX, PIECESTARTY: int
            Where the top left piece should be placed.
        WINBARWHITE: RGBA tuple
            White, but with the alpha set to winbarAlpha.
    """

    def __init__(self, bot):
        """Initialize the class."""
        self.bot = bot

        self.BORDER = 40
        self.GRIDBORDER = 40
        self.CORNERSIZE = 300
        self.BOARDOUTLINESIZE = 10
        self.BOARDOUTLINECOLOR = (0, 0, 0)

        self.PIECEOUTLINESIZE = 10
        self.PIECEOUTLINECOLOR = (244, 244, 248)
        self.EMPTYOUTLINESIZE = 0
        self.EMPTYOUTLINECOLOR = (0, 0, 0)

        self.BOTTOMBORDER = 110
        self.TEXTSIZE = 100
        self.TEXTPADDING = 20
        self.WIDTH, self.HEIGHT = 2800, 2400
        self.BACKGROUNDCOLOR = (230, 230, 234, 255)
        self.PLAYER1COLOR = (254, 74, 73)
        self.PLAYER2COLOR = (254, 215, 102)
        self.BOARDCOLOR = (42, 183, 202)
        winbarAlpha = 160
        self.WINBARCOLOR = (250, 250, 250, 255)
        self.PIECESIZE = 300
        fontPath = "gwendolyn/resources/fonts/futura-bold.ttf"

        self.FONT = ImageFont.truetype(fontPath, self.TEXTSIZE)

        boardWidth = self.WIDTH-(2*(self.BORDER+self.GRIDBORDER))
        boardHeight = self.HEIGHT-(2*(self.BORDER+self.GRIDBORDER))
        boardSize = [boardWidth, boardHeight]

        pieceWidth = boardSize[0]//COLUMNCOUNT
        pieceHeight = boardSize[1]//ROWCOUNT
        self.PIECEGRIDSIZE = [pieceWidth, pieceHeight]

        pieceStart = (self.BORDER+self.GRIDBORDER)-(self.PIECESIZE//2)
        self.PIECESTARTX = pieceStart+(self.PIECEGRIDSIZE[0]//2)
        self.PIECESTARTY = pieceStart+(self.PIECEGRIDSIZE[1]//2)

        self.WINBARWHITE = (255, 255, 255, winbarAlpha)

    # Draws the whole thing
    def drawImage(self, channel: str):
        """
        Draw an image of the connect four board.

        *Parameters*
        ------------
            channel: str
                The id of the channel the game is in.
        """
        self.bot.log("Drawing connect four board")
        game = self.bot.database["connect 4 games"].find_one({"_id": channel})

        board = game["board"]

        imageSize = (self.WIDTH, self.HEIGHT+self.BOTTOMBORDER)
        background = Image.new("RGB", imageSize, self.BACKGROUNDCOLOR)
        d = ImageDraw.Draw(background, "RGBA")

        self._drawBoard(d)

        self._drawPieces(d, board)

        if game["winner"] != 0:
            self._drawWin(background, game)

        self._drawFooter(d, game)

        background.save(f"gwendolyn/resources/games/connect4Boards/board{channel}.png")

    def _drawBoard(self, d: ImageDraw):
        # This whole part was the easiest way to make a rectangle with
        # rounded corners and an outline
        drawParams = {
            "fill": self.BOARDCOLOR,
            "outline": self.BOARDOUTLINECOLOR,
            "width": self.BOARDOUTLINESIZE
        }

        # - Corners -:
        cornerPositions = [
            [
                (
                    self.BORDER,
                    self.BORDER
                ), (
                    self.BORDER+self.CORNERSIZE,
                    self.BORDER+self.CORNERSIZE
                )
            ], [
                (
                    self.WIDTH-(self.BORDER+self.CORNERSIZE),
                    self.HEIGHT-(self.BORDER+self.CORNERSIZE)
                ), (
                    self.WIDTH-self.BORDER,
                    self.HEIGHT-self.BORDER
                )
            ], [
                (
                    self.BORDER,
                    self.HEIGHT-(self.BORDER+self.CORNERSIZE)
                ), (
                    self.BORDER+self.CORNERSIZE,
                    self.HEIGHT-self.BORDER
                )
            ], [
                (
                    self.WIDTH-(self.BORDER+self.CORNERSIZE),
                    self.BORDER
                ), (
                    self.WIDTH-self.BORDER,
                    self.BORDER+self.CORNERSIZE
                )
            ]
        ]

        d.ellipse(cornerPositions[0], **drawParams)
        d.ellipse(cornerPositions[1], **drawParams)
        d.ellipse(cornerPositions[2], **drawParams)
        d.ellipse(cornerPositions[3], **drawParams)

        # - Rectangle -:
        rectanglePositions = [
            [
                (
                    self.BORDER+(self.CORNERSIZE//2),
                    self.BORDER
                ), (
                    self.WIDTH-(self.BORDER+(self.CORNERSIZE//2)),
                    self.HEIGHT-self.BORDER
                )
            ], [
                (
                    self.BORDER,
                    self.BORDER+(self.CORNERSIZE//2)
                ), (
                    self.WIDTH-self.BORDER,
                    self.HEIGHT-(self.BORDER+(self.CORNERSIZE//2))
                )
            ]
        ]
        d.rectangle(rectanglePositions[0], **drawParams)
        d.rectangle(rectanglePositions[1], **drawParams)

        # - Removing outline on the inside -:
        cleanUpPositions = [
            [
                (
                    self.BORDER+(self.CORNERSIZE//2),
                    self.BORDER+(self.CORNERSIZE//2)
                ), (
                    self.WIDTH-(self.BORDER+(self.CORNERSIZE//2)),
                    self.HEIGHT-(self.BORDER+(self.CORNERSIZE//2))
                )
            ], [
                (
                    self.BORDER+(self.CORNERSIZE/2),
                    self.BORDER+self.BOARDOUTLINESIZE
                ), (
                    self.WIDTH-(self.BORDER+(self.CORNERSIZE//2)),
                    self.HEIGHT-(self.BORDER+self.BOARDOUTLINESIZE)
                )
            ], [
                (
                    self.BORDER+self.BOARDOUTLINESIZE,
                    self.BORDER+(self.CORNERSIZE//2)
                ), (
                    self.WIDTH-(self.BORDER+self.BOARDOUTLINESIZE),
                    self.HEIGHT-(self.BORDER+(self.CORNERSIZE//2))
                )
            ]
        ]
        d.rectangle(cleanUpPositions[0], fill=self.BOARDCOLOR)
        d.rectangle(cleanUpPositions[1], fill=self.BOARDCOLOR)
        d.rectangle(cleanUpPositions[2], fill=self.BOARDCOLOR)

    def _drawPieces(self, d: ImageDraw, board: list):
        for x, line in enumerate(board):
            for y, piece in enumerate(line):

                if piece == 1:
                    pieceColor = self.PLAYER1COLOR
                    outlineWidth = self.PIECEOUTLINESIZE
                    outlineColor = self.PIECEOUTLINECOLOR
                elif piece == 2:
                    pieceColor = self.PLAYER2COLOR
                    outlineWidth = self.PIECEOUTLINESIZE
                    outlineColor = self.PIECEOUTLINECOLOR
                else:
                    pieceColor = self.BACKGROUNDCOLOR
                    outlineWidth = self.EMPTYOUTLINESIZE
                    outlineColor = self.EMPTYOUTLINECOLOR

                startX = self.PIECESTARTX + self.PIECEGRIDSIZE[0]*y
                startY = self.PIECESTARTY + self.PIECEGRIDSIZE[1]*x

                position = [
                    (startX, startY),
                    (startX+self.PIECESIZE, startY+self.PIECESIZE)
                ]

                pieceParams = {
                    "fill": pieceColor,
                    "outline": outlineColor,
                    "width": outlineWidth
                }
                d.ellipse(position, **pieceParams)

    def _drawWin(self, background: Image, game: dict):
        """
        Draw the bar that shows the winning pieces.

        *Parameters*
        ------------
            background: Image
                The image of the board.
            game: dict
                The game data.
        """
        coordinates = game["win coordinates"]
        start = self.BORDER + self.GRIDBORDER
        startX = start + self.PIECEGRIDSIZE[0]*coordinates[1]
        startY = start + self.PIECEGRIDSIZE[1]*coordinates[0]
        a = (self.PIECEGRIDSIZE[0]*4-self.GRIDBORDER-self.BORDER)**2
        b = (self.PIECEGRIDSIZE[1]*4-self.GRIDBORDER-self.BORDER)**2
        diagonalLength = (math.sqrt(a+b))/self.PIECEGRIDSIZE[0]
        sizeRatio = self.PIECEGRIDSIZE[1]/self.PIECEGRIDSIZE[0]
        diagonalAngle = math.degrees(math.atan(sizeRatio))

        if game["win direction"] == "h":
            imageSize = (self.PIECEGRIDSIZE[0]*4, self.PIECEGRIDSIZE[1])
            winBar = Image.new("RGBA", imageSize, (0, 0, 0, 0))
            winD = ImageDraw.Draw(winBar)
            drawPositions = [
                [
                    (0, 0),
                    (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1])
                ], [
                    ((self.PIECEGRIDSIZE[0]*3), 0),
                    (self.PIECEGRIDSIZE[0]*4, self.PIECEGRIDSIZE[1])
                ], [
                    (int(self.PIECEGRIDSIZE[0]*0.5), 0),
                    (int(self.PIECEGRIDSIZE[0]*3.5), self.PIECEGRIDSIZE[1])
                ]
            ]
            winD.ellipse(drawPositions[0], fill=self.WINBARWHITE)
            winD.ellipse(drawPositions[1], fill=self.WINBARWHITE)
            winD.rectangle(drawPositions[2], fill=self.WINBARWHITE)

        elif game["win direction"] == "v":
            imageSize = (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]*4)
            winBar = Image.new("RGBA", imageSize, (0, 0, 0, 0))
            winD = ImageDraw.Draw(winBar)
            drawPositions = [
                [
                    (0, 0),
                    (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1])
                ], [
                    (0, (self.PIECEGRIDSIZE[1]*3)),
                    (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]*4)
                ], [
                    (0, (int(self.PIECEGRIDSIZE[1]*0.5))),
                    (self.PIECEGRIDSIZE[0], int(self.PIECEGRIDSIZE[1]*3.5))
                ]
            ]
            winD.ellipse(drawPositions[0], fill=self.WINBARWHITE)
            winD.ellipse(drawPositions[1], fill=self.WINBARWHITE)
            winD.rectangle(drawPositions[2], fill=self.WINBARWHITE)

        elif game["win direction"] == "r":
            imageWidth = int(self.PIECEGRIDSIZE[0]*diagonalLength)
            imageSize = (imageWidth, self.PIECEGRIDSIZE[1])
            winBar = Image.new("RGBA", imageSize, (0, 0, 0, 0))
            winD = ImageDraw.Draw(winBar)
            drawPositions = [
                [
                    (
                        0,
                        0
                    ), (
                        self.PIECEGRIDSIZE[0],
                        self.PIECEGRIDSIZE[1]
                    )
                ], [
                    (
                        (self.PIECEGRIDSIZE[0]*(diagonalLength-1)),
                        0
                    ), (
                        self.PIECEGRIDSIZE[0]*diagonalLength,
                        self.PIECEGRIDSIZE[1]
                    )
                ], [
                    (
                        int(self.PIECEGRIDSIZE[0]*0.5),
                        0
                    ), (
                        int(self.PIECEGRIDSIZE[0]*(diagonalLength-0.5)),
                        self.PIECEGRIDSIZE[1]
                    )
                ]
            ]
            winD.ellipse(drawPositions[0], fill=self.WINBARWHITE)
            winD.ellipse(drawPositions[1], fill=self.WINBARWHITE)
            winD.rectangle(drawPositions[2], fill=self.WINBARWHITE)
            winBar = winBar.rotate(-diagonalAngle, expand=1)
            startX -= 90
            startY -= 100

        elif game["win direction"] == "l":
            imageWidth = int(self.PIECEGRIDSIZE[0]*diagonalLength)
            imageSize = (imageWidth, self.PIECEGRIDSIZE[1])
            winBar = Image.new("RGBA", imageSize, (0, 0, 0, 0))
            winD = ImageDraw.Draw(winBar)
            drawPositions = [
                [
                    (0, 0),
                    (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1])
                ], [
                    (
                        (self.PIECEGRIDSIZE[0]*(diagonalLength-1)),
                        0
                    ), (
                        self.PIECEGRIDSIZE[0]*diagonalLength,
                        self.PIECEGRIDSIZE[1]
                    )
                ], [
                    (
                        int(self.PIECEGRIDSIZE[0]*0.5),
                        0
                    ), (
                        int(self.PIECEGRIDSIZE[0]*(diagonalLength-0.5)),
                        self.PIECEGRIDSIZE[1]
                    )
                ]
            ]
            winD.ellipse(drawPositions[0], fill=self.WINBARWHITE)
            winD.ellipse(drawPositions[1], fill=self.WINBARWHITE)
            winD.rectangle(drawPositions[2], fill=self.WINBARWHITE)
            winBar = winBar.rotate(diagonalAngle, expand=1)
            startX -= self.PIECEGRIDSIZE[0]*3 + 90
            startY -= self.GRIDBORDER + 60

        mask = winBar.copy()

        winBarImage = Image.new("RGBA", mask.size, color=self.WINBARCOLOR)
        background.paste(winBarImage, (startX, startY), mask)

    def _drawFooter(self, d: ImageDraw, game: dict):
        if game["players"][0] == "Gwendolyn":
            player1 = "Gwendolyn"
        else:
            player1 = self.bot.database_funcs.get_name(game["players"][0])

        if game["players"][1] == "Gwendolyn":
            player2 = "Gwendolyn"
        else:
            player2 = self.bot.database_funcs.get_name(game["players"][1])

        exampleHeight = self.HEIGHT - self.BORDER
        exampleHeight += (self.BOTTOMBORDER+self.BORDER)//2 - self.TEXTSIZE//2
        textWidth = self.FONT.getsize(player2)[0]
        exampleRPadding = self.BORDER+self.TEXTSIZE+textWidth+self.TEXTPADDING
        circlePositions = [
            [
                (
                    self.BORDER,
                    exampleHeight
                ), (
                    self.BORDER+self.TEXTSIZE,
                    exampleHeight+self.TEXTSIZE
                )
            ], [
                (
                    self.WIDTH-exampleRPadding,
                    exampleHeight
                ), (
                    self.WIDTH-self.BORDER-textWidth-self.TEXTPADDING,
                    exampleHeight+self.TEXTSIZE
                )
            ]
        ]
        circleParams = {
            "outline": self.BOARDOUTLINECOLOR,
            "width": 3
        }
        d.ellipse(circlePositions[0], fill=self.PLAYER1COLOR, **circleParams)
        d.ellipse(circlePositions[1], fill=self.PLAYER2COLOR, **circleParams)

        textPositions = [
            (self.BORDER+self.TEXTSIZE+self.TEXTPADDING, exampleHeight),
            (self.WIDTH-self.BORDER-textWidth, exampleHeight)
        ]
        d.text(textPositions[0], player1, font=self.FONT, fill=(0, 0, 0))
        d.text(textPositions[1], player2, font=self.FONT, fill=(0, 0, 0))
