import pandas as pd
import numpy as np
import logging
import pygame
from time import sleep
import pygameMenu
import copy
from tictacAI.minimax import MiniMax

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(message)s',
                    handlers=[logging.FileHandler("log.log"),
                              logging.StreamHandler()])
logger = logging.getLogger(__name__)
logger.setLevel(level="INFO")


def create_n_dimensional_array(num_rows: int, num_cols: int = None):
    if num_cols < 1 or num_rows < 1:
        logger.exception("num_cols and num_rows must be > 1")
        raise Exception
    if num_cols is None:
        num_cols = num_rows

    array = []
    for i in range(0, num_rows):
        to_append = []
        for i in range(0, num_cols):
            to_append.append(0)
        array.append(to_append)

    array = np.array(array)
    return array

class TicTacToe:
    def __init__(self, turn=0, dimensionality=3, ai='minimax', ai_player=1):
        self.logger = logger.getChild(self.__class__.__name__)
        self.dimensionality = dimensionality
        self.num_rows = dimensionality
        self.num_cols = dimensionality
        self.board = create_n_dimensional_array(self.num_rows, self.num_cols)

        self.player_options = [-1, 1]
        self.player = self.player_options[turn % 2]
        self.ai_player = ai_player
        if ai == 'minimax':
            self.ai = MiniMax(self, maximum=ai_player)

    def set_cell_to_player(self, row, col, board, player):
        """
        set the value of a cell on the board based on the player and the row and column
        :param row: the row of the desired cell
        :param col: the column of the desired cell
        :param board: the board to set cell on
        :param player: which player it is
        :return: the board with the cell edited.
        """
        new_board = copy.deepcopy(board)
        new_board[row][col] = player
        return new_board

    def undo_move(self, board, row, col):
        """
        set a cell to 0, undoing a move
        :param board: the board to edit
        :param row: the row of the desired cell
        :param col: the column of the desired cell
        :return: the board with the cell edited.
        """
        new_board = copy.deepcopy(board)
        new_board[row][col] = 0
        return new_board

    def check_game_over(self, board, dimensionality):
        """
        check of the game is over
        :param board:
        :return: bool: True if game is over, False otherwise
        """
        if np.count_nonzero(board) == (dimensionality * dimensionality):
            return True
        else:
            return False

    def check_legal_move(self, row, col, board):
        """
        given a row, column and board, check if taking a move on the row and column is legal
        :param row: the row of the desired cell
        :param col: the column of the desired cell
        :param board: the board to check the move on
        :return: bool: True if move is legal, False otherwise
        """
        if board[row][col] == 0:
            return True
        else:
            return False

    def check_win(self, board):
        """
        check who the winner is of a completed board
        :param board: the board to check the win on
        :return: -1 if -1 player wins, 1 if 1 player wins, 0 if draw, None if game is continuing
        """
        check_cols = np.sum(board, axis=0)
        check_rows = np.sum(board, axis=1)
        check_diags = np.trace(board)
        check_anti_diags = np.trace(np.fliplr(board))

        if (0 - self.dimensionality) in check_rows or (0 - self.dimensionality) in check_rows or (
                0 - self.dimensionality) == check_diags or (0 - self.dimensionality) == check_anti_diags:
            # -1 wins
            return -1
        elif self.dimensionality in check_rows or self.dimensionality in check_cols or self.dimensionality == check_diags or self.dimensionality == check_anti_diags:
            # 1 wins
            return 1
        elif np.count_nonzero(board) == (self.dimensionality * self.dimensionality):
            # draw
            return 0
        else:
            return None

    def _take_turn(self, row, col, board):
        """
        make a given move based on the row, column, and turn. checks for wins, game ends if there is a win
        :param row: row of desired cell
        :param col: column of desired cell
        :param board: board game is being played on
        :return: -1, 0, 1, or None based on player -1 win, draw, player 1 win, or game not completed
        """

        # if the move is legal:
        if self.check_legal_move(row, col, board):

            # set the cell to the correct value
            new_board = self.set_cell_to_player(row, col, board=board, player=self.player)

            # check if there is a winner
            win = self.check_win(new_board)
            self.board = new_board

            # if the game is over, return the win value
            if win is not None:
                return win

            # otherwise, continue the game by changing the player and returning None
            self.player = self.get_next_player(self.player)
            return win

        # if the move is illegal:
        else:
            self.logger.info("Illegal move!")

    def get_depth(self, board):
        """
        return the number of zeroes on the board: the number of possible moves left in the game
        :param board: board to check
        :return: number of zeroes on board
        """
        return np.count_nonzero(board == 0)

    def get_player(self, turn):
        """
        get the current player based on the turn
        :param turn: the turn of the game
        :return:
        """
        return self.player_options[turn % 2]

    def get_legal_moves(self, board):
        """
        get a list of legal moves on a given board
        :param board: the board to get the moves from
        :return: list of legal moves
        """
        legal_moves = []
        for row in range(self.dimensionality):
            for col in range(self.dimensionality):
                if self.check_legal_move(row=row, col=col, board=board):
                    legal_moves.append((row, col))
        return legal_moves

    def get_next_player(self, player):
        """
        get the next player based on the current player
        :param player: the current player
        :return: the next player
        """
        return -player

# below is lots of messy Pygame code simply to get the graphics working, will clean up eventually
class PygameTicTacToe(TicTacToe):
    def __init__(self, turn=0, dimensionality=3):
        super().__init__(turn, dimensionality)
        pygame.init()
        pygame.font.init()
        self.font = pygame.font.SysFont('Arial', 30)
        self.width = 800
        self.height = 800
        self.middle_tuple = (self.width / 2, self.height / 2)
        self.row_height = self.height / self.num_rows
        self.col_width = self.width / self.num_cols
        self.draw_width = self.col_width * 0.9
        self.draw_height = self.row_height * 0.9
        self.surface = pygame.display.set_mode((self.width, self.height))
        self.game_state = 0
        self.win_text = None
        self.main()

    def _pygame_set_up_board(self):
        # create the lines on the surface to represent the board
        for row in range(1, self.num_rows):
            pygame.draw.line(self.surface, (0, 0, 0), (0, row * self.row_height), (self.width, row * self.row_height))
        for col in range(1, self.num_cols):
            pygame.draw.line(self.surface, (0, 0, 0), (col * self.col_width, 0), (col * self.col_width, self.height))
        pass

    def _pygame_take_turn(self, event):
        # place an X or an O in the appropriate section on the surface
        if self.player == self.ai_player:
            best_move = self.ai.get_move(self.board, self.get_depth(self.board), self.ai_player)
            row, col = best_move[0], best_move[1]
            win = self._take_turn(row, col, self.board)
        else:
            mouse_x = event.pos[0]
            mouse_y = event.pos[1]
            mouse_row = None
            mouse_col = None
            for row in range(0, self.num_rows):
                if mouse_y > (row * self.row_height) and mouse_y < ((row + 1) * self.row_height):
                    mouse_row = row
            for col in range(0, self.num_cols):
                if mouse_x > (col * self.col_width) and mouse_x < ((col + 1) * self.col_width):
                    mouse_col = col
            win = self._take_turn(mouse_row, mouse_col, self.board)
        if win is not None:
            self.game_state = 1
            if win == 1:
                self.win_text = self.font.render("O wins!", False, (0, 0, 0))
            elif win == -1:
                self.win_text = self.font.render("X wins!", False, (0, 0, 0))
            else:
                self.win_text = self.font.render("Nobody wins :(", False, (0, 0, 0))
            self.win_text_rect = self.win_text.get_rect(center=self.middle_tuple)

    def _draw_x_or_o_on_mouse(self):
        mouse_pos = pygame.mouse.get_pos()
        mouse_x = mouse_pos[0]
        mouse_y = mouse_pos[1]

        if self.player == -1:
            self._draw_x((mouse_x, mouse_y))
        else:
            self._draw_y((mouse_x, mouse_y))

    def _draw_x(self, position):
        x_pos = position[0]
        y_pos = position[1]

        top_left = (x_pos - (self.draw_width / 2), y_pos - (self.draw_height / 2))
        top_right = (x_pos + (self.draw_width / 2), y_pos - (self.draw_height / 2))
        bot_left = (x_pos - (self.draw_width / 2), y_pos + (self.draw_height / 2))
        bot_right = (x_pos + (self.draw_width / 2), y_pos + (self.draw_height / 2))

        # draw x at event pos with size based on self.width and self.height, num_rows and num_columns
        pygame.draw.line(self.surface, (0, 0, 0), top_left, bot_right)
        pygame.draw.line(self.surface, (0, 0, 0), bot_left, top_right)

    def _draw_y(self, position):
        x_pos = position[0]
        y_pos = position[1]

        top_left = (x_pos - (self.draw_width / 2), y_pos - (self.draw_height / 2))
        # draw o at event pos with size based on self.width and self.height
        rectangle = pygame.Rect((top_left), (self.draw_width, self.draw_height))
        pygame.draw.ellipse(self.surface, (0, 0, 0), rectangle, 1)

    def _draw_current_filled_positions(self):
        row_num = 0
        for row in self.board:
            col_num = 0
            for col in row:
                # find centre of cell then draw col
                middle_x = ((col_num + 1) * self.col_width) - (self.col_width / 2)
                middle_y = ((row_num + 1) * self.row_height) - (self.row_height / 2)
                if col == -1:
                    self._draw_x((middle_x, middle_y))
                if col == 1:
                    self._draw_y((middle_x, middle_y))
                col_num += 1
            row_num += 1
        pass

    def _draw_board(self):
        self.surface.fill((255, 255, 255))
        self._pygame_set_up_board()
        self._draw_x_or_o_on_mouse()
        self._draw_current_filled_positions()
        if self.game_state == 1:
            self.surface.blit(self.win_text, self.win_text_rect)

    def main(self):
        # main game loop
        while True:
            # draw all the elements on the pygame surface
            self._draw_board()

            # monitor the pygame events
            for event in pygame.event.get():
                # self.logger.info(event)

                # quit the game if the player clicks the x
                if event.type == pygame.QUIT:
                    pygame.quit()
                    quit()

                # place take a players turn when the mouse button is pressed
                if event.type == pygame.MOUSEBUTTONDOWN:
                    if self.game_state == 0:
                        self._pygame_take_turn(event)

                if self.player == self.ai_player:
                    if self.game_state == 0:
                        self._pygame_take_turn(event)

            pygame.display.update()


if __name__ == "__main__":
    # get the pygame print statement out of the way...
    sleep(1)

    # play a game of 3x3 tictactoe starting at turn 0
    ttt = PygameTicTacToe(turn=0, dimensionality=3)

import pandas as pd
import numpy as np
import logging
import pygame
from time import sleep
import pygameMenu
import copy
from tictacAI.minimax import MiniMax

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(message)s',
                    handlers=[logging.FileHandler("log.log"),
                              logging.StreamHandler()])
logger = logging.getLogger(__name__)
logger.setLevel(level="INFO")


def create_n_dimensional_array(num_rows: int, num_cols: int = None):
    if num_cols < 1 or num_rows < 1:
        logger.exception("num_cols and num_rows must be > 1")
        raise Exception
    if num_cols is None:
        num_cols = num_rows

    array = []
    for i in range(0, num_rows):
        to_append = []
        for i in range(0, num_cols):
            to_append.append(0)
        array.append(to_append)

    array = np.array(array)
    return array

class TicTacToe:
    def __init__(self, turn=0, dimensionality=3, ai='minimax', ai_player=1):
        self.logger = logger.getChild(self.__class__.__name__)
        self.dimensionality = dimensionality
        self.num_rows = dimensionality
        self.num_cols = dimensionality
        self.board = create_n_dimensional_array(self.num_rows, self.num_cols)

        self.player_options = [-1, 1]
        self.player = self.player_options[turn % 2]
        self.ai_player = ai_player
        if ai == 'minimax':
            self.ai = MiniMax(self, maximum=ai_player)

    def set_cell_to_player(self, row, col, board, player):
        """
        set the value of a cell on the board based on the player and the row and column
        :param row: the row of the desired cell
        :param col: the column of the desired cell
        :param board: the board to set cell on
        :param player: which player it is
        :return: the board with the cell edited.
        """
        new_board = copy.deepcopy(board)
        new_board[row][col] = player
        return new_board

    def undo_move(self, board, row, col):
        """
        set a cell to 0, undoing a move
        :param board: the board to edit
        :param row: the row of the desired cell
        :param col: the column of the desired cell
        :return: the board with the cell edited.
        """
        new_board = copy.deepcopy(board)
        new_board[row][col] = 0
        return new_board

    def check_game_over(self, board, dimensionality):
        """
        check of the game is over
        :param board:
        :return: bool: True if game is over, False otherwise
        """
        if np.count_nonzero(board) == (dimensionality * dimensionality):
            return True
        else:
            return False

    def check_legal_move(self, row, col, board):
        """
        given a row, column and board, check if taking a move on the row and column is legal
        :param row: the row of the desired cell
        :param col: the column of the desired cell
        :param board: the board to check the move on
        :return: bool: True if move is legal, False otherwise
        """
        if board[row][col] == 0:
            return True
        else:
            return False

    def check_win(self, board):
        """
        check who the winner is of a completed board
        :param board: the board to check the win on
        :return: -1 if -1 player wins, 1 if 1 player wins, 0 if draw, None if game is continuing
        """
        check_cols = np.sum(board, axis=0)
        check_rows = np.sum(board, axis=1)
        check_diags = np.trace(board)
        check_anti_diags = np.trace(np.fliplr(board))

        if (0 - self.dimensionality) in check_rows or (0 - self.dimensionality) in check_rows or (
                0 - self.dimensionality) == check_diags or (0 - self.dimensionality) == check_anti_diags:
            # -1 wins
            return -1
        elif self.dimensionality in check_rows or self.dimensionality in check_cols or self.dimensionality == check_diags or self.dimensionality == check_anti_diags:
            # 1 wins
            return 1
        elif np.count_nonzero(board) == (self.dimensionality * self.dimensionality):
            # draw
            return 0
        else:
            return None

    def _take_turn(self, row, col, board):
        """
        make a given move based on the row, column, and turn. checks for wins, game ends if there is a win
        :param row: row of desired cell
        :param col: column of desired cell
        :param board: board game is being played on
        :return: -1, 0, 1, or None based on player -1 win, draw, player 1 win, or game not completed
        """

        # if the move is legal:
        if self.check_legal_move(row, col, board):

            # set the cell to the correct value
            new_board = self.set_cell_to_player(row, col, board=board, player=self.player)

            # check if there is a winner
            win = self.check_win(new_board)
            self.board = new_board

            # if the game is over, return the win value
            if win is not None:
                return win

            # otherwise, continue the game by changing the player and returning None
            self.player = self.get_next_player(self.player)
            return win

        # if the move is illegal:
        else:
            self.logger.info("Illegal move!")

    def get_depth(self, board):
        """
        return the number of zeroes on the board: the number of possible moves left in the game
        :param board: board to check
        :return: number of zeroes on board
        """
        return np.count_nonzero(board == 0)

    def get_player(self, turn):
        """
        get the current player based on the turn
        :param turn: the turn of the game
        :return:
        """
        return self.player_options[turn % 2]

    def get_legal_moves(self, board):
        """
        get a list of legal moves on a given board
        :param board: the board to get the moves from
        :return: list of legal moves
        """
        legal_moves = []
        for row in range(self.dimensionality):
            for col in range(self.dimensionality):
                if self.check_legal_move(row=row, col=col, board=board):
                    legal_moves.append((row, col))
        return legal_moves

    def get_next_player(self, player):
        """
        get the next player based on the current player
        :param player: the current player
        :return: the next player
        """
        return -player

# below is lots of messy Pygame code simply to get the graphics working, will clean up eventually
class PygameTicTacToe(TicTacToe):
    def __init__(self, turn=0, dimensionality=3):
        super().__init__(turn, dimensionality)
        pygame.init()
        pygame.font.init()
        self.font = pygame.font.SysFont('Arial', 30)
        self.width = 800
        self.height = 800
        self.middle_tuple = (self.width / 2, self.height / 2)
        self.row_height = self.height / self.num_rows
        self.col_width = self.width / self.num_cols
        self.draw_width = self.col_width * 0.9
        self.draw_height = self.row_height * 0.9
        self.surface = pygame.display.set_mode((self.width, self.height))
        self.game_state = 0
        self.win_text = None
        self.main()

    def _pygame_set_up_board(self):
        # create the lines on the surface to represent the board
        for row in range(1, self.num_rows):
            pygame.draw.line(self.surface, (0, 0, 0), (0, row * self.row_height), (self.width, row * self.row_height))
        for col in range(1, self.num_cols):
            pygame.draw.line(self.surface, (0, 0, 0), (col * self.col_width, 0), (col * self.col_width, self.height))
        pass

    def _pygame_take_turn(self, event):
        # place an X or an O in the appropriate section on the surface
        if self.player == self.ai_player:
            best_move = self.ai.get_move(self.board, self.get_depth(self.board), self.ai_player)
            row, col = best_move[0], best_move[1]
            win = self._take_turn(row, col, self.board)
        else:
            mouse_x = event.pos[0]
            mouse_y = event.pos[1]
            mouse_row = None
            mouse_col = None
            for row in range(0, self.num_rows):
                if mouse_y > (row * self.row_height) and mouse_y < ((row + 1) * self.row_height):
                    mouse_row = row
            for col in range(0, self.num_cols):
                if mouse_x > (col * self.col_width) and mouse_x < ((col + 1) * self.col_width):
                    mouse_col = col
            win = self._take_turn(mouse_row, mouse_col, self.board)
        if win is not None:
            self.game_state = 1
            if win == 1:
                self.win_text = self.font.render("O wins!", False, (0, 0, 0))
            elif win == -1:
                self.win_text = self.font.render("X wins!", False, (0, 0, 0))
            else:
                self.win_text = self.font.render("Nobody wins :(", False, (0, 0, 0))
            self.win_text_rect = self.win_text.get_rect(center=self.middle_tuple)

    def _draw_x_or_o_on_mouse(self):
        mouse_pos = pygame.mouse.get_pos()
        mouse_x = mouse_pos[0]
        mouse_y = mouse_pos[1]

        if self.player == -1:
            self._draw_x((mouse_x, mouse_y))
        else:
            self._draw_y((mouse_x, mouse_y))

    def _draw_x(self, position):
        x_pos = position[0]
        y_pos = position[1]

        top_left = (x_pos - (self.draw_width / 2), y_pos - (self.draw_height / 2))
        top_right = (x_pos + (self.draw_width / 2), y_pos - (self.draw_height / 2))
        bot_left = (x_pos - (self.draw_width / 2), y_pos + (self.draw_height / 2))
        bot_right = (x_pos + (self.draw_width / 2), y_pos + (self.draw_height / 2))

        # draw x at event pos with size based on self.width and self.height, num_rows and num_columns
        pygame.draw.line(self.surface, (0, 0, 0), top_left, bot_right)
        pygame.draw.line(self.surface, (0, 0, 0), bot_left, top_right)

    def _draw_y(self, position):
        x_pos = position[0]
        y_pos = position[1]

        top_left = (x_pos - (self.draw_width / 2), y_pos - (self.draw_height / 2))
        # draw o at event pos with size based on self.width and self.height
        rectangle = pygame.Rect((top_left), (self.draw_width, self.draw_height))
        pygame.draw.ellipse(self.surface, (0, 0, 0), rectangle, 1)

    def _draw_current_filled_positions(self):
        row_num = 0
        for row in self.board:
            col_num = 0
            for col in row:
                # find centre of cell then draw col
                middle_x = ((col_num + 1) * self.col_width) - (self.col_width / 2)
                middle_y = ((row_num + 1) * self.row_height) - (self.row_height / 2)
                if col == -1:
                    self._draw_x((middle_x, middle_y))
                if col == 1:
                    self._draw_y((middle_x, middle_y))
                col_num += 1
            row_num += 1
        pass

    def _draw_board(self):
        self.surface.fill((255, 255, 255))
        self._pygame_set_up_board()
        self._draw_x_or_o_on_mouse()
        self._draw_current_filled_positions()
        if self.game_state == 1:
            self.surface.blit(self.win_text, self.win_text_rect)

    def main(self):
        # main game loop
        while True:
            # draw all the elements on the pygame surface
            self._draw_board()

            # monitor the pygame events
            for event in pygame.event.get():
                # self.logger.info(event)

                # quit the game if the player clicks the x
                if event.type == pygame.QUIT:
                    pygame.quit()
                    quit()

                # place take a players turn when the mouse button is pressed
                if event.type == pygame.MOUSEBUTTONDOWN:
                    if self.game_state == 0:
                        self._pygame_take_turn(event)

                if self.player == self.ai_player:
                    if self.game_state == 0:
                        self._pygame_take_turn(event)

            pygame.display.update()


if __name__ == "__main__":
    # get the pygame print statement out of the way...
    sleep(1)

    # play a game of 3x3 tictactoe starting at turn 0
    ttt = PygameTicTacToe(turn=0, dimensionality=3)

set the value of a cell on the board based on the player and the row and column         :param row: the row of the desired cell         :param col: the column of the desired cell         :param board: the board to set cell on         :param player: which player it is         :return: the board with the cell edited. set a cell to 0, undoing a move         :param board: the board to edit         :param row: the row of the desired cell         :param col: the column of the desired cell         :return: the board with the cell edited. check of the game is over         :param board:         :return: bool: True if game is over, False otherwise given a row, column and board, check if taking a move on the row and column is legal         :param row: the row of the desired cell         :param col: the column of the desired cell         :param board: the board to check the move on         :return: bool: True if move is legal, False otherwise check who the winner is of a completed board         :param board: the board to check the win on         :return: -1 if -1 player wins, 1 if 1 player wins, 0 if draw, None if game is continuing # -1 wins # 1 wins # draw make a given move based on the row, column, and turn. checks for wins, game ends if there is a win         :param row: row of desired cell         :param col: column of desired cell         :param board: board game is being played on         :return: -1, 0, 1, or None based on player -1 win, draw, player 1 win, or game not completed # if the move is legal: # set the cell to the correct value # check if there is a winner # if the game is over, return the win value # otherwise, continue the game by changing the player and returning None # if the move is illegal: return the number of zeroes on the board: the number of possible moves left in the game         :param board: board to check         :return: number of zeroes on board get the current player based on the turn         :param turn: the turn of the game         :return: get a list of legal moves on a given board         :param board: the board to get the moves from         :return: list of legal moves get the next player based on the current player         :param player: the current player         :return: the next player # below is lots of messy Pygame code simply to get the graphics working, will clean up eventually # create the lines on the surface to represent the board # place an X or an O in the appropriate section on the surface # draw x at event pos with size based on self.width and self.height, num_rows and num_columns # draw o at event pos with size based on self.width and self.height # find centre of cell then draw col # main game loop # draw all the elements on the pygame surface # monitor the pygame events # self.logger.info(event) # quit the game if the player clicks the x # place take a players turn when the mouse button is pressed # get the pygame print statement out of the way... # play a game of 3x3 tictactoe starting at turn 0