"""
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
from typing import Union  # Used for typehints
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 interactions import SlashContext, ComponentContext  # Used for
# typehints
from interactions.utils.manage_components import (create_button,
create_actionrow)
from interactions.model import ButtonStyle

from gwendolyn_old.utils import encode_id
from .game_base import BoardGame

ROWCOUNT = 6
COLUMNCOUNT = 7

def _encode_board_string(board: list):
  string = [str(i) for row in board for i in row]

  while len(string) > 0 and string[0] == "0":
    string = string[1:]

  if string == "":
    string = "0"

  dec = 0
  for i, digit in enumerate(string[::-1]):
    dec += (3**i)*int(digit)

  return str(dec)

def _decode_board_string(board_string: str):
  dec = int(board_string)
  string = []
  while dec:
    string.append(str(dec % 3))
    dec = dec // 3

  while len(string) < ROWCOUNT * COLUMNCOUNT:
    string.append("0")

  string = string[::-1]

  board = [
    [int(x) for x in string[i*COLUMNCOUNT:i*COLUMNCOUNT+COLUMNCOUNT]]
    for i in range(ROWCOUNT)]

  return board


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

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

  def __init__(self, bot):
    """Initialize the class."""
    super().__init__(bot, "connectfour", DrawConnectFour)
    self.get_name = self.bot.database_funcs.get_name
    # pylint: disable=invalid-name
    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
    }
    # pylint: enable=invalid-name

  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)
    channel = str(ctx.channel_id)

    opponent, opponent_info = await self._test_opponent(ctx, opponent)
    if not opponent_info:
      return

    difficulty = opponent_info[0]
    difficulty_text = opponent_info[1]

    board = [[0 for _ in range(COLUMNCOUNT)] for _ in range(ROWCOUNT)]
    players = [ctx.author.id, opponent]
    random.shuffle(players)

    self.draw.draw_image(channel, board, players, [0, [0,0], ""])

    opponent_name = self.get_name(f"#{opponent}")
    turn_name = self.get_name(f"#{players[0]}")

    started_text = "Started game against {}{}.".format(
      opponent_name,
      difficulty_text
    )
    turn_text = f"It's {turn_name}'s turn"
    await ctx.send(f"{started_text} {turn_text}")
    self.bot.log("They started a game")

    # Sets the whole game in motion
    boards_path = "gwendolyn/resources/games/connect_four_boards/"
    file_path = f"{boards_path}board{ctx.channel_id}.png"
    image_message = await ctx.send(file=discord.File(file_path))

    board_string = _encode_board_string(board)
    if (players[0] == self.bot.user.id):
      await self._connect_four_ai(
        ctx, board_string, players, difficulty, image_message.id
      )
    else:
      buttons = []
      for i in range(7):
        custom_id = encode_id(
          [
            "connectfour",
            "place",
            str(players[0]),
            board_string,
            str(i),
            str(players[0]),
            str(players[1]),
            str(difficulty),
            str(image_message.id)
          ]
        )
        buttons.append(create_button(
          style=ButtonStyle.blue,
          label=str(i+1),
          custom_id=custom_id,
          disabled=(board[0][i] != 0)
        ))

      custom_id = encode_id(
        [
          "connectfour",
          "end",
          str(players[0]),
          str(players[1]),
          str(image_message.id)
        ]
      )
      buttons.append(create_button(
        style=ButtonStyle.red,
        label="Surrender",
        custom_id=custom_id
      ))

      action_rows = []
      for x in range(((len(buttons)-1)//4)+1):
        row_buttons = buttons[
          (x*4):(min(len(buttons),x*4+4))
        ]
        action_rows.append(create_actionrow(*row_buttons))

      await image_message.edit(
        components=action_rows
      )

  async def place_piece(self, ctx: ComponentContext, board_string: str,
              column: int, players: list[int], difficulty: int,
              placer: int, message_id: int):
    """
    Place a piece on the board.

    *Parameters*
      column: int
        The column the player is placing the piece in.
    """
    old_message = await ctx.channel.fetch_message(message_id)
    await old_message.delete()

    player_number = players.index(placer)+1
    user_name = self.get_name(f"#{placer}")
    placed_piece = False

    board = _decode_board_string(board_string)
    board = self._place_on_board(board, player_number, column)

    if board is None:
      send_message = "There isn't any room in that column"
      log_message = "There wasn't any room in the column"
    else:
      turn = player_number % 2

      self.bot.log("Checking for win")
      winner, win_direction, win_coordinates = self._is_won(board)

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

      gwendolyn_turn = (players[turn] == self.bot.user.id)

      placed_piece = True

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

    if placed_piece:
      channel = str(ctx.channel)
      self.draw.draw_image(
        channel,
        board,
        players,
        [winner, win_coordinates, win_direction]
      )


      boards_path = "gwendolyn/resources/games/connect_four_boards/"
      file_path = boards_path + f"board{channel}.png"
      image_message = await ctx.channel.send(file=discord.File(file_path))

      if game_won:
        self._end_game(winner, players, difficulty)
      else:
        board_string = _encode_board_string(board)
        if gwendolyn_turn:
          await self._connect_four_ai(
            ctx, board_string, players, difficulty, image_message.id
          )
        else:
          buttons = []
          for i in range(7):
            custom_id = encode_id(
              [
                "connectfour",
                "place",
                str(players[turn]),
                board_string,
                str(i),
                str(players[0]),
                str(players[1]),
                str(difficulty),
                str(image_message.id)
              ]
            )
            buttons.append(create_button(
              style=ButtonStyle.blue,
              label=str(i+1),
              custom_id=custom_id,
              disabled=(board[0][i] != 0)
            ))

          custom_id = encode_id(
            [
              "connectfour",
              "end",
              str(players[0]),
              str(players[1]),
              str(difficulty),
              str(image_message.id)
            ]
          )
          buttons.append(create_button(
            style=ButtonStyle.red,
            label="Surrender",
            custom_id=custom_id
          ))

          action_rows = []
          for x in range(((len(buttons)-1)//4)+1):
            row_buttons = buttons[
              (x*4):(min(len(buttons),x*4+4))
            ]
            action_rows.append(create_actionrow(*row_buttons))

          await image_message.edit(
            components=action_rows
          )


  async def surrender(self, ctx: ComponentContext, players: list,
            difficulty: int, message_id: int):
    """
    Surrender a connect four game.

    *Parameters*
    ------------
      ctx: SlashContext
        The context of the command.
    """

    loser_index = players.index(ctx.author.id)
    winner_index = (loser_index+1) % 2
    winner_id = players[winner_index]
    winner_name = self.get_name(f"#{winner_id}")

    send_message = f"{ctx.author.display_name} surrenders."
    send_message += f" This means {winner_name} is the winner."
    if winner_id != self.bot.user.id:
      difficulty = int(difficulty)
      reward = difficulty**2 + 5
      send_message += f" Adding {reward} to their account"

    await ctx.channel.send(send_message)

    self._end_game(winner_index+1, players, difficulty)
    old_image = await ctx.channel.fetch_message(message_id)
    await old_image.delete()


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

    This differs from place_piece() 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.
    """
    placement_x, placement_y = -1, column

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

    board[placement_x][placement_y] = player

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

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

  def _is_won(self, board: dict):
    won = 0
    win_direction = ""
    win_coordinates = [0, 0]

    for row in range(ROWCOUNT):
      for place in range(COLUMNCOUNT):
        if won == 0:
          piece_player = board[row][place]
          if piece_player != 0:
            # Checks horizontal
            if place <= COLUMNCOUNT-4:
              piece_one = board[row][place+1]
              piece_two = board[row][place+2]
              piece_three = board[row][place+3]

              pieces = [piece_one, piece_two, piece_three]
            else:
              pieces = [0]

            if all(x == piece_player for x in pieces):
              won = piece_player
              win_direction = "h"
              win_coordinates = [row, place]

            # Checks vertical
            if row <= ROWCOUNT-4:
              piece_one = board[row+1][place]
              piece_two = board[row+2][place]
              piece_three = board[row+3][place]

              pieces = [piece_one, piece_two, piece_three]
            else:
              pieces = [0]

            if all(x == piece_player for x in pieces):
              won = piece_player
              win_direction = "v"
              win_coordinates = [row, place]

            # Checks right diagonal
            good_row = (row <= ROWCOUNT-4)
            good_column = (place <= COLUMNCOUNT-4)
            if good_row and good_column:
              piece_one = board[row+1][place+1]
              piece_two = board[row+2][place+2]
              piece_three = board[row+3][place+3]

              pieces = [piece_one, piece_two, piece_three]
            else:
              pieces = [0]

            if all(x == piece_player for x in pieces):
              won = piece_player
              win_direction = "r"
              win_coordinates = [row, place]

            # Checks left diagonal
            if row <= ROWCOUNT-4 and place >= 3:
              piece_one = board[row+1][place-1]
              piece_two = board[row+2][place-2]
              piece_three = board[row+3][place-3]

              pieces = [piece_one, piece_two, piece_three]
            else:
              pieces = [0]

            if all(i == piece_player for i in pieces):
              won = piece_player
              win_direction = "l"
              win_coordinates = [row, place]

    return won, win_direction, win_coordinates

  async def _connect_four_ai(self,
                 ctx: Union[SlashContext, ComponentContext],
                 board_string: str, players: list,
                 difficulty: int, message_id: int):
    def out_of_range(possible_scores: list):
      allowed_range = max(possible_scores)*(1-0.1)
      more_than_one = len(possible_scores) != 1
      return (min(possible_scores) <= allowed_range) and more_than_one

    self.bot.log("Figuring out best move")

    board = _decode_board_string(board_string)
    player = players.index(self.bot.user.id)+1

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

    possible_scores = scores.copy()

    while out_of_range(possible_scores):
      possible_scores.remove(min(possible_scores))

    highest_score = random.choice(possible_scores)

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

    await self.place_piece(
      ctx,
      board_string,
      placement,
      players,
      difficulty,
      self.bot.user.id,
      message_id
    )

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

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

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

    # Checks right diagonal
    for row in range(ROWCOUNT-3):
      for place in range(COLUMNCOUNT-3):
        piece_one = board[row][place]
        piece_two = board[row+1][place+1]
        piece_three = board[row+2][place+2]
        piece_four = board[row+3][place+3]

        window = [piece_one, piece_two, piece_three, piece_four]
        score += self._evaluate_window(window, player, other_player)

      for place in range(3, COLUMNCOUNT):
        piece_one = board[row][place]
        piece_two = board[row+1][place-1]
        piece_three = board[row+2][place-2]
        piece_four = board[row+3][place-3]

        window = [piece_one, piece_two, piece_three, piece_four]
        score += self._evaluate_window(window, player, other_player)

    return score

  def _evaluate_window(self, window: list, player: int, other_player: 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(other_player) == 4:
      return self.AISCORES["enemy win"]
    else:
      return 0

  async def _minimax(self, board: dict, depth: int, player: int,
             original_player: int, alpha: int, beta: int,
             maximizing_player: 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.
      original_player: int
        The AI player.
      alpha, beta: int
        Used with alpha/beta pruning, in order to prune options
        that will never be picked.
      maximizing_player: 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._ai_calc_points(board, original_player)
    # 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 maximizing_player:
      value = -math.inf
      for column in range(0, COLUMNCOUNT):
        test_board = copy.deepcopy(board)
        test_board = self._place_on_board(test_board, player, column)
        if test_board is not None:
          minimax_parameters = [
            test_board,
            depth-1,
            (player % 2)+1,
            original_player,
            alpha,
            beta,
            False
          ]
          evaluation = await self._minimax(*minimax_parameters)
          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):
        test_board = copy.deepcopy(board)
        test_board = self._place_on_board(test_board, player, column)
        if test_board is not None:
          minimax_parameters = [
            test_board,
            depth-1,
            (player % 2)+1,
            original_player,
            alpha,
            beta,
            True
          ]
          evaluation = await self._minimax(*minimax_parameters)
          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*
  ---------
    draw_image(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 win_bar_alpha.
  """

  def __init__(self, bot, game):
    """Initialize the class."""
    self.bot = bot
    self.get_name = self.bot.database_funcs.get_name
    win_bar_alpha = 160
    font_path = "gwendolyn/resources/fonts/futura-bold.ttf"

    # pylint: disable=invalid-name
    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)
    self.WINBARCOLOR = (250, 250, 250, 255)
    self.PIECESIZE = 300
    # pylint: enable=invalid-name

    board_width = self.WIDTH-(2*(self.BORDER+self.GRIDBORDER))
    board_height = self.HEIGHT-(2*(self.BORDER+self.GRIDBORDER))
    board_size = [board_width, board_height]
    piece_width = board_size[0]//COLUMNCOUNT
    piece_height = board_size[1]//ROWCOUNT
    piece_start = (self.BORDER+self.GRIDBORDER)-(self.PIECESIZE//2)

    # pylint: disable=invalid-name
    self.FONT = ImageFont.truetype(font_path, self.TEXTSIZE)
    self.PIECEGRIDSIZE = [piece_width, piece_height]

    self.PIECESTARTX = piece_start+(self.PIECEGRIDSIZE[0]//2)
    self.PIECESTARTY = piece_start+(self.PIECEGRIDSIZE[1]//2)

    self.WINBARWHITE = (255, 255, 255, win_bar_alpha)
    # pylint: enable=invalid-name

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

    *Parameters*
    ------------
    """
    self.bot.log("Drawing connect four board")

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

    self._draw_board(drawer)

    self._draw_pieces(drawer, board)

    if win_info[0] != 0:
      self._draw_win(background, win_info[1], win_info[2])

    self._draw_footer(drawer, players)

    boards_path = "gwendolyn/resources/games/connect_four_boards/"
    background.save(boards_path + f"board{channel}.png")

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

    # - Corners -:
    corner_positions = [
      [
        (
          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
        )
      ]
    ]

    drawer.ellipse(corner_positions[0], **draw_parameters)
    drawer.ellipse(corner_positions[1], **draw_parameters)
    drawer.ellipse(corner_positions[2], **draw_parameters)
    drawer.ellipse(corner_positions[3], **draw_parameters)

    # - Rectangle -:
    rectangle_positions = [
      [
        (
          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))
        )
      ]
    ]
    drawer.rectangle(rectangle_positions[0], **draw_parameters)
    drawer.rectangle(rectangle_positions[1], **draw_parameters)

    # - Removing outline on the inside -:
    clean_up_positions = [
      [
        (
          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))
        )
      ]
    ]
    drawer.rectangle(clean_up_positions[0], fill=self.BOARDCOLOR)
    drawer.rectangle(clean_up_positions[1], fill=self.BOARDCOLOR)
    drawer.rectangle(clean_up_positions[2], fill=self.BOARDCOLOR)

  def _draw_pieces(self, drawer: ImageDraw, board: list):
    for piece_x, line in enumerate(board):
      for piece_y, piece in enumerate(line):

        if piece == 1:
          piece_color = self.PLAYER1COLOR
          outline_width = self.PIECEOUTLINESIZE
          outline_color = self.PIECEOUTLINECOLOR
        elif piece == 2:
          piece_color = self.PLAYER2COLOR
          outline_width = self.PIECEOUTLINESIZE
          outline_color = self.PIECEOUTLINECOLOR
        else:
          piece_color = self.BACKGROUNDCOLOR
          outline_width = self.EMPTYOUTLINESIZE
          outline_color = self.EMPTYOUTLINECOLOR

        start_x = self.PIECESTARTX + self.PIECEGRIDSIZE[0]*piece_y
        start_y = self.PIECESTARTY + self.PIECEGRIDSIZE[1]*piece_x

        position = [
          (start_x, start_y),
          (start_x+self.PIECESIZE, start_y+self.PIECESIZE)
        ]

        piece_parameters = {
          "fill": piece_color,
          "outline": outline_color,
          "width": outline_width
        }
        drawer.ellipse(position, **piece_parameters)

  def _draw_win(self, background: Image, win_coordinates: list,
          win_direction: str):
    """
    Draw the bar that shows the winning pieces.

    *Parameters*
    ------------
      background: Image
        The image of the board.
      game: dict
        The game data.
    """
    start = self.BORDER + self.GRIDBORDER
    start_x = start + self.PIECEGRIDSIZE[0]*win_coordinates[1]
    start_y = start + self.PIECEGRIDSIZE[1]*win_coordinates[0]
    diagonal_length = (
      (math.sqrt(
        (self.PIECEGRIDSIZE[0]*4-self.GRIDBORDER-self.BORDER)**2+
        (self.PIECEGRIDSIZE[1]*4-self.GRIDBORDER-self.BORDER)**2
      ))/
      self.PIECEGRIDSIZE[0]
    )
    size_ratio = self.PIECEGRIDSIZE[1]/self.PIECEGRIDSIZE[0]
    diagonal_angle = math.degrees(math.atan(size_ratio))

    if win_direction == "h":
      image_size = (self.PIECEGRIDSIZE[0]*4, self.PIECEGRIDSIZE[1])
      win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0))
      win_drawer = ImageDraw.Draw(win_bar)
      draw_position = [
        [
          (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])
        ]
      ]
      win_drawer.ellipse(draw_position[0], fill=self.WINBARWHITE)
      win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE)
      win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE)

    elif win_direction == "v":
      image_size = (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]*4)
      win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0))
      win_drawer = ImageDraw.Draw(win_bar)
      draw_position = [
        [
          (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))
        ]
      ]
      win_drawer.ellipse(draw_position[0], fill=self.WINBARWHITE)
      win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE)
      win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE)

    elif win_direction == "r":
      image_width = int(self.PIECEGRIDSIZE[0]*diagonal_length)
      image_size = (image_width, self.PIECEGRIDSIZE[1])
      win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0))
      win_drawer = ImageDraw.Draw(win_bar)
      draw_position = [
        [
          (
            0,
            0
          ), (
            self.PIECEGRIDSIZE[0],
            self.PIECEGRIDSIZE[1]
          )
        ], [
          (
            (self.PIECEGRIDSIZE[0]*(diagonal_length-1)),
            0
          ), (
            self.PIECEGRIDSIZE[0]*diagonal_length,
            self.PIECEGRIDSIZE[1]
          )
        ], [
          (
            int(self.PIECEGRIDSIZE[0]*0.5),
            0
          ), (
            int(self.PIECEGRIDSIZE[0]*(diagonal_length-0.5)),
            self.PIECEGRIDSIZE[1]
          )
        ]
      ]
      win_drawer.ellipse(draw_position[0], fill=self.WINBARWHITE)
      win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE)
      win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE)
      win_bar = win_bar.rotate(-diagonal_angle, expand=1)
      start_x -= 90
      start_y -= 100

    elif win_direction == "l":
      image_width = int(self.PIECEGRIDSIZE[0]*diagonal_length)
      image_size = (image_width, self.PIECEGRIDSIZE[1])
      win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0))
      win_drawer = ImageDraw.Draw(win_bar)
      draw_position = [
        [
          (0, 0),
          (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1])
        ], [
          (
            (self.PIECEGRIDSIZE[0]*(diagonal_length-1)),
            0
          ), (
            self.PIECEGRIDSIZE[0]*diagonal_length,
            self.PIECEGRIDSIZE[1]
          )
        ], [
          (
            int(self.PIECEGRIDSIZE[0]*0.5),
            0
          ), (
            int(self.PIECEGRIDSIZE[0]*(diagonal_length-0.5)),
            self.PIECEGRIDSIZE[1]
          )
        ]
      ]
      win_drawer.ellipse(draw_position[0], fill=self.WINBARWHITE)
      win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE)
      win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE)
      win_bar = win_bar.rotate(diagonal_angle, expand=1)
      start_x -= self.PIECEGRIDSIZE[0]*3 + 90
      start_y -= self.GRIDBORDER + 60

    mask = win_bar.copy()

    win_bar_image = Image.new("RGBA", mask.size, color=self.WINBARCOLOR)
    background.paste(win_bar_image, (start_x, start_y), mask)

  def _draw_footer(self, drawer: ImageDraw, players: list):
    if players[0] == "Gwendolyn":
      player1 = "Gwendolyn"
    else:
      player1 = self.get_name(f"#{players[0]}")

    if players[1] == "Gwendolyn":
      player2 = "Gwendolyn"
    else:
      player2 = self.get_name(f"#{players[1]}")

    circle_height = self.HEIGHT - self.BORDER
    circle_height += (self.BOTTOMBORDER+self.BORDER)//2 - self.TEXTSIZE//2
    text_width = self.FONT.getsize(player2)[0]
    circle_right_padding = (
      self.BORDER+self.TEXTSIZE+text_width+self.TEXTPADDING
    )
    circle_positions = [
      [
        (
          self.BORDER,
          circle_height
        ), (
          self.BORDER+self.TEXTSIZE,
          circle_height+self.TEXTSIZE
        )
      ], [
        (
          self.WIDTH-circle_right_padding,
          circle_height
        ), (
          self.WIDTH-self.BORDER-text_width-self.TEXTPADDING,
          circle_height+self.TEXTSIZE
        )
      ]
    ]
    circle_parameters = {
      "outline": self.BOARDOUTLINECOLOR,
      "width": 3
    }
    drawer.ellipse(
      circle_positions[0],
      fill=self.PLAYER1COLOR,
      **circle_parameters
    )
    drawer.ellipse(
      circle_positions[1],
      fill=self.PLAYER2COLOR,
      **circle_parameters
    )

    text_positions = [
      (self.BORDER+self.TEXTSIZE+self.TEXTPADDING, circle_height),
      (self.WIDTH-self.BORDER-text_width, circle_height)
    ]
    drawer.text(text_positions[0], player1, font=self.FONT, fill=(0, 0, 0))
    drawer.text(text_positions[1], player2, font=self.FONT, fill=(0, 0, 0))
