from random import randint, choice
import time
import math
from typing import Callable
import chess
from heuristics import h_shannon


class PlayerInterface:
    board: chess.Board
    color: chess.Color

    # Returns your player name , as to be displayed during the game
    def getPlayerName(self) -> str:
        return "Not Defined"

    # Returns your move. The move must be a valid string of coordinates ("A1",
    # "D5", ...) on the grid or the special "PASS" move. A couple of two integers,
    # which are the coordinates of where you want to put your piece on the board.
    # Coordinates are the coordinates given by the Goban. py method legal_moves ().
    def getPlayerMove(self) -> chess.Move:
        return chess.Move.null()

    # Inform you that the oponent has played this move. You must play it with no
    # search (just update your local variables to take it into account)
    def playOpponentMove(self, move):
        self.board.push(move)

    # Starts a new game, and give you your color. As defined in Goban.py : color=1
    # for BLACK , and color=2 for WHITE
    def newGame(self, color):
        self.board = chess.Board()
        self.color = color

    # You can get a feedback on the winner
    # This function gives you the color of the winner
    def endGame(self, color):
        pass


class RandomPlayer(PlayerInterface):
    # Returns your player name , as to be displayed during the game
    def getPlayerName(self) -> str:
        return "Random Player"

    def playOpponentMove(self, move):
        self.board.push(move)

    def getPlayerMove(self):
        """
        Renvoie un mouvement au hasard sur la liste des mouvements possibles. Pour avoir un choix au hasard, il faut
        construire explicitement tous les mouvements. Or, generate_legal_moves() nous donne un itérateur.
        """
        move = choice([m for m in self.board.generate_legal_moves()])
        self.board.push(move)
        return move


def minmax(
    board: chess.Board,
    heuristic: Callable[[chess.Board, chess.Color], float],
    color: chess.Color,
    depth: int = 3,
) -> chess.Move:
    def aux(
        board: chess.Board,
        heuristic: Callable[[chess.Board, chess.Color], float],
        color: chess.Color,
        depth: int = 3,
    ) -> tuple[float, chess.Move]:
        wantMax = board.turn == color

        if depth == 0 or board.is_game_over():
            return heuristic(board, color), board.peek()

        if wantMax:
            acc = -math.inf, chess.Move.null()
            for move in board.generate_legal_moves():
                board.push(move)
                acc = max(
                    acc,
                    (
                        aux(board, heuristic=heuristic, color=color, depth=depth - 1)[
                            0
                        ],
                        move,
                    ),
                    key=lambda t: t[0],
                )
                board.pop()

        else:  # minimizing player
            acc = math.inf, chess.Move.null()
            for move in board.generate_legal_moves():
                board.push(move)
                acc = min(
                    acc,
                    (
                        aux(board, heuristic=heuristic, color=color, depth=depth - 1)[
                            0
                        ],
                        move,
                    ),
                    key=lambda t: t[0],
                )
                board.pop()

        return acc

    _, move = aux(board, heuristic, color, depth)
    return move


class MinMaxPlayer(PlayerInterface):
    depth = 3

    # Returns your player name , as to be displayed during the game
    def getPlayerName(self) -> str:
        return "MinMax Player"

    def getPlayerMove(self):
        move = minmax(
            self.board, heuristic=h_shannon, color=self.color, depth=self.depth
        )
        self.board.push(move)
        return move


# Returns heuristic, move
def _alphabeta(
    board: chess.Board,
    heuristic: Callable[[chess.Board, chess.Color], float],
    color: chess.Color,
    alpha=-math.inf,
    beta=math.inf,
    depth: int = 3,
) -> tuple[float, chess.Move]:
    wantMax = board.turn == color
    if depth == 0 or board.is_game_over():
        return heuristic(board, color), board.peek()

    if wantMax:
        acc = -math.inf, chess.Move.null()
        for move in board.generate_legal_moves():
            board.push(move)
            value = (
                _alphabeta(
                    board,
                    alpha=alpha,
                    beta=beta,
                    heuristic=heuristic,
                    color=color,
                    depth=depth - 1,
                )[0],
                move,
            )
            acc = max(
                acc,
                value,
                key=lambda t: t[0],
            )
            board.pop()

            if acc[0] >= beta:
                break  # beta cutoff
            alpha = max(alpha, value[0])

    else:
        acc = math.inf, chess.Move.null()
        for move in board.generate_legal_moves():
            board.push(move)
            value = (
                _alphabeta(
                    board,
                    alpha=alpha,
                    beta=beta,
                    heuristic=heuristic,
                    color=color,
                    depth=depth - 1,
                )[0],
                move,
            )
            acc = min(
                acc,
                value,
                key=lambda t: t[0],
            )
            board.pop()

            if acc[0] <= alpha:
                break  # alpha cutoff
            beta = min(beta, value[0])

    return acc


def alphabeta(
    board: chess.Board,
    heuristic: Callable[[chess.Board, chess.Color], float],
    color: chess.Color,
    depth: int = 3,
) -> chess.Move:

    _, move = _alphabeta(board, heuristic=heuristic, color=color, depth=depth)
    return move


class AlphaBetaPlayer(PlayerInterface):
    depth = 5

    # Returns your player name , as to be displayed during the game
    def getPlayerName(self) -> str:
        return "AlphaBeta Player"

    def getPlayerMove(self):
        move = alphabeta(
            self.board, heuristic=h_shannon, color=self.color, depth=self.depth
        )
        self.board.push(move)
        return move


def IDDFS(
    board: chess.Board, heuristic, color: chess.Color, duration: float, maxdepth=42
) -> chess.Move:
    st = time.time()
    depth = 1
    move = chess.Move.null()
    while time.time() - st < duration and depth < maxdepth:
        move = _alphabeta(board, heuristic, color, alpha=-10, beta=10, depth=depth)[1]

        depth += 1
    return move


class IDDFSPlayer(PlayerInterface):
    # Returns your player name , as to be displayed during the game
    def getPlayerName(self) -> str:
        return "AlphaBeta Player"

    def getPlayerMove(self):
        move = IDDFS(self.board, h_shannon, self.color, 1)
        self.board.push(move)
        return move
