import numpy as np
import os
import copy

# A game of Othello (Reversi)
# Players take turns placing tokens on an 8x8 board.
# When you place a token, all opponent tokens "sandwiched"
# between your new token and an existing token of yours
# (in any direction) are flipped to your color.
# The player with the most tokens when neither player
# can move wins.

# Game parameters
n = 8       # board size (n x n), standard Othello uses 8

n_players = 2

# Symbols for players
P1 = "X"
P2 = "O"
PSYM = [" ", P1, P2]

# Special move representing a forced pass (no valid moves available)
PASS_MOVE = None

# All 8 directions to check for flips
DIRECTIONS = [(-1,-1), (-1, 0), (-1, 1),
              ( 0,-1),          ( 0, 1),
              ( 1,-1), ( 1, 0), ( 1, 1)]


def new_game():
    """
    Creates the standard Othello starting position.
    The four central squares are pre-filled.

    The state is a list [grid, player] where player is the
    current player. This allows state-only functions to always
    know whose turn it is.

    Returns: 
        list: [grid, player] as the initial game state
    """
    grid = np.zeros([n, n], dtype=int)
    mid = n // 2
    grid[mid-1, mid-1] = 1
    grid[mid-1, mid  ] = 2
    grid[mid,   mid-1] = 2
    grid[mid,   mid  ] = 1
    return [grid, 1]   # player 1 starts


def next_player(player):
    """
    Number of the player whose turn comes after the given player.
    
    Note: Player numbering starts from 1 and ends at n_players.
    
    Args:
        player (int): the player to check
    
    Returns: 
        int: the number of the next player
    """
    return player % n_players + 1


def previous_player(player):
    """
    Number of the player whose turn comes before the given player.
    
    Note: Player numbering starts from 1 and ends at n_players.
    
    Args:
        player (int): the player to check
    
    Returns: 
        int: the number of the previous player
    """
    return player % n_players + 1


def _get_flips(row, col, player, grid):
    """
    For a proposed move at (row, col) by player, find all opponent
    tokens that would be flipped.

    Args:
        row (int): row index of the proposed move
        col (int): column index of the proposed move
        player (int): the player making the move
        grid (array): the play area

    Returns: 
        list: (row, col) positions that would be flipped
    """
    opponent = next_player(player)
    flips = []

    for dr, dc in DIRECTIONS:
        r, c = row + dr, col + dc
        candidates = []

        # Walk in this direction while we see opponent tokens
        while 0 <= r < n and 0 <= c < n and grid[r, c] == opponent:
            candidates.append((r, c))
            r += dr
            c += dc

        # If we stopped on our own token, the sandwiched pieces flip
        if candidates and 0 <= r < n and 0 <= c < n and grid[r, c] == player:
            flips.extend(candidates)

    return flips


def _is_valid_move(row, col, player, grid):
    """
    Checks if placing a token at (row, col) is a valid move for player.
    A move is valid if the square is empty and flips at least one opponent.

    Args:
        row (int): row index of the proposed move
        col (int): column index of the proposed move
        player (int): the player making the move
        grid (array): the play area

    Returns: 
        boolean: True if the move is valid, False otherwise
    """
    if grid[row, col] != 0:
        return False
    return len(_get_flips(row, col, player, grid)) > 0


def _all_plays(player, grid):
    """
    Lists all valid moves for player as (row, col) tuples.

    Args:
        player (int): the player making the move
        grid (array): the play area

    Returns: 
        list: valid (row, col) moves
    """
    moves = []
    for i in range(n):
        for j in range(n):
            if _is_valid_move(i, j, player, grid):
                moves.append((i, j))
    return moves


def all_reasonable_plays(state, player):
    """
    Lists all valid moves for the current player.
    In Othello all valid moves are reasonable.

    If the current player has no valid moves, returns [PASS_MOVE]
    so the game loop can still make a move (which does nothing).

    Args:
        state (list): [grid, player]
        
    Returns: 
        list: (row, col) moves, or [PASS_MOVE] if none exist
    """
    grid, pl = state
    moves = _all_plays(player, grid)
    if not moves:
        return [PASS_MOVE]
    return moves


def can_continue(state):
    """
    Checks if the game can continue.

    The game ends when BOTH players have no valid moves.
    A single player having no moves does not end the game
    since they can pass their turn.

    Args:
        state (list): [grid, player]
        
    Returns: 
        boolean: True if at least one player can still move, False otherwise
    """
    grid, player = state
    if _all_plays(player, grid):
        return True
    if _all_plays(next_player(player), grid):
        return True
    return False


def check_winner(state):
    """
    Determines the winner when the game is over.
    Returns 0 if the game is still in progress.

    The game is over when neither player has valid moves.
    The player with the most tokens then wins.

    Args:
        state (list): [grid, player]
        
    Returns: 
        int: the winning player number, or 0 for a draw or unfinished game
    """
    grid, player = state

    # If the game can still continue, there is no winner yet
    if can_continue(state):
        return 0

    count1 = np.sum(grid == 1)
    count2 = np.sum(grid == 2)

    if count1 > count2:
        return 1
    elif count2 > count1:
        return 2
    else:
        return 0


def make_move(play, player, state):
    """
    Let a player place a token and flip the appropriate opponent tokens.
    Modifies state in place.

    Also updates state[1] to the next player so that state-only
    functions always know whose turn it is.

    If play is PASS_MOVE, the board is unchanged and the turn passes.

    Args:
        play (array): (row, col), or PASS_MOVE
        player (int): number of the player who makes the move
        state (list): [grid, player]
    """
    grid = state[0]

    if play is not PASS_MOVE:
        row, col = play
        flips = _get_flips(row, col, player, grid)
        grid[row, col] = player
        for r, c in flips:
            grid[r, c] = player

    # Update the stored current player to the next one
    state[1] = next_player(player)


def copy_game(state):
    """
    Returns a deep copy of the game state.

    Args:
        state (list): [grid, player]
        
    Returns: 
        list: a copy of state
    """
    grid, player = state
    return [copy.deepcopy(grid), player]


def draw(state):
    """
    Draw the board with text graphics, showing token counts
    and valid moves for the current player.

    Args:
        state (list): [grid, player]
    """
    grid, player = state

    os.system('cls' if os.name == 'nt' else 'clear')

    count1 = np.sum(grid == 1)
    count2 = np.sum(grid == 2)

    # Header with column indices
    topheader = "    "
    vline     = "    "
    for j in range(n):
        topheader += " " + str(j) + "  "
        vline     += "---+"

    print(topheader)
    print()

    for i in range(n):
        line = "  " + str(i) + " "
        for j in range(n):
            if grid[i, j] == 0:
                line += "   |"
            else:
                line += " " + PSYM[grid[i, j]] + " |"
        print(line[:-1])
        if i < n - 1:
            print(vline[:-1])

    print()
    print(f"  {P1}: {count1}   {P2}: {count2}")
    print()

    valid = _all_plays(player, grid)
    if valid:
        print("  Valid moves for " + PSYM[player] + ": "
              + ", ".join(str(m) for m in valid))
    else:
        print("  No valid moves for " + PSYM[player] + " - turn will be skipped!")
    print()


def ask_for_move(player, state):
    """
    Asks a human player for a move.
    If the player has no valid moves, automatically returns PASS_MOVE.

    Args:
        player (int): the player whose turn it is
        state (list): [grid, player]
    
    Returns: 
        int, int: (row, col), or PASS_MOVE
    """
    grid = state[0]
    draw(state)

    valid = _all_plays(player, grid)
    if not valid:
        input(f"  {PSYM[player]} has no valid moves. Press Enter to pass...")
        return PASS_MOVE

    print("You are " + PSYM[player])

    while True:
        try:
            row = int(input("  Enter row: "))
            col = int(input("  Enter col: "))
        except:
            print("  Please enter integers (or -9 to quit)")
            continue

        if row == -9 or col == -9:
            quit()

        if (row, col) in valid:
            return (row, col)
        else:
            print(f"  ({row}, {col}) is not a valid move. Try again.")


def declare_winner(winner):
    """
    Announces the winner or a draw.

    Args:
        winner (int): winning player number, or 0 for a draw
    """
    if winner == 0:
        print("The game ended in a draw!")
    else:
        print(PSYM[winner] + " won!")