import utils
from typing import Union

Score = Union[int, float]
Move = tuple[tuple[int, int], tuple[int, int]]
Board = list[list[str]]

def evaluate(board: Board) -> Score:
    """
    Returns the score of the current position.

    Parameters
    ----------
    board: 2D list of lists. Contains characters "B", "W", and "_",
    representing black pawn, white pawn, and empty cell, respectively.

    Returns
    -------
    An evaluation (as a Score).
    """
    bcount = 0
    wcount = 0
    for r, row in enumerate(board):
        for tile in row:
            if tile == "B":
                if r == 5:
                    return utils.WIN
                bcount += 1
            elif tile == "W":
                if r == 0:
                    return -utils.WIN
                wcount += 1
    if wcount == 0:
        return utils.WIN
    if bcount == 0:
        return -utils.WIN
    return bcount - wcount

def generate_valid_moves(board: Board) -> list[Move]:
    """
    Generates a list containing all possible moves in a particular position for black.

    Parameters
    ----------
    board: 2D list of lists. Contains characters "B", "W", and "_",
    representing black pawn, white pawn, and empty cell, respectively.

    Returns
    -------
    A list of Moves.
    """
    res = []
    for r, row in enumerate(board):
        for c, tile in enumerate(row):
            if tile != "B":
                continue
            for d in (-1, 0, 1):
                dst = r + 1, c + d
                if utils.is_valid_move(board, (r, c), dst):
                    res.append(((r, c), dst))

    return res

def test_11():
    board1 = [
        ["_", "_", "B", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"]
    ]
    assert sorted(generate_valid_moves(board1)) == [((0, 2), (1, 1)), ((0, 2), (1, 2)), ((0, 2), (1, 3))], "board1 test output is incorrect"

    board2 = [
        ["_", "_", "_", "_", "B", "_"],
        ["_", "_", "_", "_", "_", "B"],
        ["_", "W", "_", "_", "_", "_"],
        ["_", "_", "W", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "B", "_", "_", "_"]
    ]
    assert sorted(generate_valid_moves(board2)) == [((0, 4), (1, 3)), ((0, 4), (1, 4)), ((1, 5), (2, 4)), ((1, 5), (2, 5))], "board2 test output is incorrect"

    board3 = [
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "B", "_", "_", "_"],
        ["_", "W", "W", "W", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"]
    ]
    assert sorted(generate_valid_moves(board3)) == [((1, 2), (2, 1)), ((1, 2), (2, 3))], "board3 test output is incorrect"

# test_11()

def minimax(
        board: Board, 
        depth: int, 
        max_depth: int, 
        is_black: bool
    ) -> tuple[Score, Move]:
    """
    Finds the best move for the input board state.
    Note that you are black.

    Parameters
    ----------
    board: 2D list of lists. Contains characters "B", "W", and "_",
    representing black pawn, white pawn, and empty cell, respectively.

    depth: int, the depth to search for the best move. When this is equal
    to `max_depth`, you should get the evaluation of the position using
    the provided heuristic function.

    max_depth: int, the maximum depth for cutoff.

    is_black: bool. True when finding the best move for black, False
    otherwise.

    Returns
    -------
    A tuple (evalutation, ((src_row, src_col), (dst_row, dst_col))):
    evaluation: the best score that black can achieve after this move.
    src_row, src_col: position of the pawn to move.
    dst_row, dst_col: position to move the pawn to.
    """
    if depth == max_depth or utils.is_game_over(board):
        return evaluate(board), None
    # max value
    if is_black:
        v = -utils.INF
        best_move = None
        for action in generate_valid_moves(board):
            next_board = utils.state_change(board, action[0], action[1], False)
            new_v, _ = minimax(next_board, depth + 1, max_depth, False)
            if new_v > v:
                v = new_v
                best_move = action
        return v, best_move
    # min value
    else:
        v = utils.INF
        best_move = None
        w_board = utils.invert_board(board, False)
        for action in generate_valid_moves(w_board):
            next_board = utils.state_change(w_board, action[0], action[1], False)
            new_v, _ = minimax(utils.invert_board(next_board, False), depth + 1, max_depth, True)
            if new_v < v:
                v = new_v
                best_move = action
        return v, best_move


def test_21():
    board1 = [
        list("______"),
        list("___B__"),
        list("____BB"),
        list("___WB_"),
        list("_B__WW"),
        list("_WW___"),
    ]
    score1, _ = minimax(board1, 0, 1, True)
    assert score1 == utils.WIN, "black should win in 1"

    board2 = [
        list("______"),
        list("___B__"),
        list("____BB"),
        list("_BW_B_"),
        list("____WW"),
        list("_WW___"),
    ]
    score2, _ = minimax(board2, 0, 3, True)
    assert score2 == utils.WIN, "black should win in 3"

    board3 = [
        list("______"),
        list("__B___"),
        list("_WWW__"),
        list("______"),
        list("______"),
        list("______"),
    ]
    score3, _ = minimax(board3, 0, 4, True)
    assert score3 == -utils.WIN, "white should win in 4"

# test_21()

def negamax(
        board: Board,
        depth: int, 
        max_depth: int
    ) -> tuple[Score, Move]:
    """
    Finds the best move for the input board state.
    Note that you are black.

    Parameters
    ----------
    board: 2D list of lists. Contains characters "B", "W", and "_",
    representing black pawn, white pawn, and empty cell, respectively.

    depth: int, the depth to search for the best move. When this is equal
    to `max_depth`, you should get the evaluation of the position using
    the provided heuristic function.

    max_depth: int, the maximum depth for cutoff.

    Notice that you no longer need the parameter `is_black`.

    Returns
    -------
    A tuple (evalutation, ((src_row, src_col), (dst_row, dst_col))):
    evaluation: the best score that black can achieve after this move.
    src_row, src_col: position of the pawn to move.
    dst_row, dst_col: position to move the pawn to.
    """
    if depth == max_depth or utils.is_game_over(board):
        return evaluate(board), None
    v = -utils.INF
    best_move = None
    for action in generate_valid_moves(board):
        # if current is black, then the next board is white. It is inverted after applying the black move
        next_board = utils.invert_board(utils.state_change(board, action[0], action[1], False), False)
        # since this move is white, the score needs to be negated (to get the min value of the white move)
        new_v, _ = negamax(next_board, depth + 1, max_depth)
        # if the negated score is greater than the current max value, update the max value and the best move
        if -new_v > v:
            v = -new_v
            best_move = action
    return v, best_move

def test_22():
    board1 = [
        list("______"),
        list("___B__"),
        list("____BB"),
        list("___WB_"),
        list("_B__WW"),
        list("_WW___"),
    ]
    score1, _ = negamax(board1, 0, 1)
    assert score1 == utils.WIN, "black should win in 1"

    board2 = [
        list("______"),
        list("___B__"),
        list("____BB"),
        list("_BW_B_"),
        list("____WW"),
        list("_WW___"),
    ]
    score2, _ = negamax(board2, 0, 3)
    assert score2 == utils.WIN, "black should win in 3"

    board3 = [
        list("______"),
        list("__B___"),
        list("_WWW__"),
        list("______"),
        list("______"),
        list("______"),
    ]
    score3, _ = negamax(board3, 0, 4)
    assert score3 == -utils.WIN, "white should win in 4"

# test_22()

def minimax_alpha_beta(
        board: Board,
        depth: int, 
        max_depth: int, 
        alpha: Score, 
        beta: Score, 
        is_black: bool
    ) -> tuple[Score, Move]:
    """
    Finds the best move for the input board state.
    Note that you are black.

    Parameters
    ----------
    board: 2D list of lists. Contains characters "B", "W", and "_",
    representing black pawn, white pawn, and empty cell, respectively.

    depth: int, the depth to search for the best move. When this is equal
    to `max_depth`, you should get the evaluation of the position using
    the provided heuristic function.

    max_depth: int, the maximum depth for cutoff.

    alpha: Score. The alpha value in a given state.

    beta: Score. The beta value in a given state.

    is_black: bool. True when finding the best move for black, False
    otherwise.

    Returns
    -------
    A tuple (evalutation, ((src_row, src_col), (dst_row, dst_col))):
    evaluation: the best score that black can achieve after this move.
    src_row, src_col: position of the pawn to move.
    dst_row, dst_col: position to move the pawn to.
    """
    # base case
    if depth == max_depth or utils.is_game_over(board):
        return evaluate(board), None
    
    # max value
    if is_black:
        v = -utils.INF
        best_move = None
        for action in generate_valid_moves(board):
            next_board = utils.state_change(board, action[0], action[1], False)
            new_v, _ = minimax_alpha_beta(next_board, depth + 1, max_depth, alpha, beta, False)
            if new_v > v:
                v = new_v
                best_move = action
            if  v >= beta:
                break
            alpha = max(alpha, v)
        return v, best_move
    # min value
    else:
        v = utils.INF
        best_move = None
        w_board = utils.invert_board(board, False)
        for action in generate_valid_moves(w_board):
            next_board = utils.invert_board(utils.state_change(w_board, action[0], action[1], False), False)
            new_v, _ = minimax_alpha_beta(next_board, depth + 1, max_depth, alpha, beta, True)
            if new_v < v:
                v = new_v
                best_move = action
            if v <= alpha:
                break
            beta = min(beta, v)
        return v, best_move

def test_31():
    board1 = [
        list("______"),
        list("__BB__"),
        list("____BB"),
        list("WBW_B_"),
        list("____WW"),
        list("_WW___"),
    ]
    score1, _ = minimax_alpha_beta(board1, 0, 3, -utils.INF, utils.INF, True)
    assert score1 == utils.WIN, "black should win in 3"


    board2 = [
        list("____B_"),
        list("___B__"),
        list("__B___"),
        list("_WWW__"),
        list("____W_"),
        list("______"),
    ]
    score2, _ = minimax_alpha_beta(board2, 0, 5, -utils.INF, utils.INF, True)
    assert score2 == utils.WIN, "black should win in 5"

    board3 = [
        list("____B_"),
        list("__BB__"),
        list("______"),
        list("_WWW__"),
        list("____W_"),
        list("______"),
    ]
    score3, m = minimax_alpha_beta(board3, 0, 6, -utils.INF, utils.INF, True)
    print(m)
    assert score3 == -utils.WIN, "white should win in 6"

# print("custom")
# print(minimax_alpha_beta([['_', '_', '_', '_', 'B', '_'], ['_', '_', 'B', 'B', '_', '_'], ['_', '_', '_', '_', '_', '_'], ['_', 'W', 'W', 'W', '_', '_'], ['_', '_', '_', '_', 'W', '_'], ['_', '_', '_', '_', '_', '_']], 
#                        0, 6, -utils.INF, utils.INF, True))
# print("custom end")

def negamax_alpha_beta(
        board: Board, 
        depth: int, 
        max_depth: int, 
        alpha: Score, 
        beta: Score
    ) -> tuple[Score, Move]:
    """
    Finds the best move for the input board state.
    Note that you are black.

    Parameters
    ----------
    board: 2D list of lists. Contains characters "B", "W", and "_",
    representing black pawn, white pawn, and empty cell, respectively.

    depth: int, the depth to search for the best move. When this is equal
    to `max_depth`, you should get the evaluation of the position using
    the provided heuristic function.

    max_depth: int, the maximum depth for cutoff.

    alpha: Score. The alpha value in a given state.

    beta: Score. The beta value in a given state.

    Notice that you no longer need the parameter `is_black`.

    Returns
    -------
    A tuple (evalutation, ((src_row, src_col), (dst_row, dst_col))):
    evaluation: the best score that black can achieve after this move.
    src_row, src_col: position of the pawn to move.
    dst_row, dst_col: position to move the pawn to.
    """
    if depth == max_depth or utils.is_game_over(board):
        return evaluate(board), None
    best_move = None
    for action in generate_valid_moves(board):
        # if current is black, then the next board is white. It is inverted after applying the black move
        next_board = utils.invert_board(utils.state_change(board, action[0], action[1], False), False)
        # since this move is white, the score needs to be negated (to get the min value of the white move)
        new_v, _ = negamax_alpha_beta(next_board, depth + 1, max_depth, -beta, -alpha)
        # if the negated score is greater than the current max value, update the max value and the best move
        if -new_v > alpha:
            alpha = -new_v
            best_move = action
        if alpha >= beta:
            break
    return alpha, best_move

def test_32():
    board1 = [
        list("______"),
        list("__BB__"),
        list("____BB"),
        list("WBW_B_"),
        list("____WW"),
        list("_WW___"),
    ]
    score1, _ = negamax_alpha_beta(board1, 0, 3, -utils.INF, utils.INF)
    assert score1 == utils.WIN, "black should win in 3"

    board2 = [
        list("____B_"),
        list("___B__"),
        list("__B___"),
        list("_WWW__"),
        list("____W_"),
        list("______"),
    ]
    score2, _ = negamax_alpha_beta(board2, 0, 5, -utils.INF, utils.INF)
    assert score2 == utils.WIN, "black should win in 5"

    board3 = [
        list("____B_"),
        list("__BB__"),
        list("______"),
        list("_WWW__"),
        list("____W_"),
        list("______"),
    ]
    score3, _ = negamax_alpha_beta(board3, 0, 6, -utils.INF, utils.INF)
    assert score3 == -utils.WIN, "white should win in 6"

# test_32()

# Uncomment and implement the function.
# Note: this will override the provided `evaluate` function.

def evaluate(board: Board) -> Score:
    score = 0
    bcount = 0
    wcount = 0
    for r, row in enumerate(board):
        for tile in row:
            if tile == "B":
                bcount += 1
                if r == 0:
                    score += 10
                if r == 1:
                    score += 10
                if r == 2:
                    score += 20
                if r == 3:
                    score += 40
                if r == 4:
                    score += 50
                if r == 5:
                    return utils.WIN
            elif tile == "W":
                wcount += 1
                if r == 0:
                    return -utils.WIN
                if r == 1:
                    score -= 50
                if r == 2:
                    score -= 40
                if r == 3:
                    score -= 20
                if r == 4:
                    score -= 10
                if r == 5:
                    score -= 10
    if wcount == 0:
        return utils.WIN
    if bcount == 0:
        return -utils.WIN
    return score

def test_41():
    board1 = [
        ["_", "_", "_", "B", "_", "_"],
        ["_", "_", "_", "W", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "B", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"]
    ]
    assert evaluate(board1) == 0

    board2 = [
        ["_", "_", "_", "B", "W", "_"],
        ["_", "_", "_", "W", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"]
    ]
    assert evaluate(board2) == -utils.WIN

    board3 = [
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "B", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"],
        ["_", "_", "_", "_", "_", "_"]
    ]
    assert evaluate(board3) == utils.WIN

test_41()

class PlayerAI:

    def make_move(self, board: Board) -> Move:
        """
        This is the function that will be called from main.py
        You should combine the functions in the previous tasks
        to implement this function.

        Parameters
        ----------
        self: object instance itself, passed in automatically by Python.
        
        board: 2D list-of-lists. Contains characters "B", "W", and "_",
        representing black pawn, white pawn, and empty cell, respectively.
        
        Returns
        -------
        A tuple of tuples containing coordinates (row_index, col_index).
        The first tuple contains the source position of the black pawn
        to be moved, the second tuple contains the destination position.
        """
        # TODO: Replace starter code with your AI
        ################
        # Starter code #
        ################
        for r in range(len(board)):
            for c in range(len(board[r])):
                # check if B can move forward directly
                if board[r][c] == "B" and board[r + 1][c] == "_":
                    src = r, c
                    dst = r + 1, c
                    return src, dst # valid move
        return (0, 0), (0, 0) # invalid move

class PlayerNaive:
    """
    A naive agent that will always return the first available valid move.
    """
    def make_move(self, board: Board) -> Move:
        return utils.generate_rand_move(board)

##########################
# Game playing framework #
##########################
if __name__ == "__main__":
    assert utils.test_move([
        ["B", "B", "B", "B", "B", "B"], 
        ["_", "B", "B", "B", "B", "B"], 
        ["_", "_", "_", "_", "_", "_"], 
        ["_", "B", "_", "_", "_", "_"], 
        ["_", "W", "W", "W", "W", "W"], 
        ["W", "W", "W", "W", "W", "W"]
    ], PlayerAI())

    assert utils.test_move([
        ["_", "B", "B", "B", "B", "B"], 
        ["_", "B", "B", "B", "B", "B"], 
        ["_", "_", "_", "_", "_", "_"], 
        ["_", "B", "_", "_", "_", "_"], 
        ["W", "W", "W", "W", "W", "W"], 
        ["_", "_", "W", "W", "W", "W"]
    ], PlayerAI())

    assert utils.test_move([
        ["_", "_", "B", "B", "B", "B"], 
        ["_", "B", "B", "B", "B", "B"], 
        ["_", "_", "_", "_", "_", "_"], 
        ["_", "B", "W", "_", "_", "_"], 
        ["_", "W", "W", "W", "W", "W"], 
        ["_", "_", "_", "W", "W", "W"]
    ], PlayerAI())

    # generates initial board
    # board = utils.generate_init_state()
    # res = utils.play(PlayerAI(), PlayerNaive(), board)
    # # Black wins means your agent wins.
    # print(res)
