import copy
import random

from typing import List, Any, Tuple
from zulip_bots.game_handler import GameAdapter, BadMoveException

# -------------------------------------

State = List[List[str]]


class TicTacToeModel:
    smarter = True
    # If smarter is True, the computer will do some extra thinking - it'll be harder for the user.

    triplets = [[(0, 0), (0, 1), (0, 2)],  # Row 1
                [(1, 0), (1, 1), (1, 2)],  # Row 2
                [(2, 0), (2, 1), (2, 2)],  # Row 3
                [(0, 0), (1, 0), (2, 0)],  # Column 1
                [(0, 1), (1, 1), (2, 1)],  # Column 2
                [(0, 2), (1, 2), (2, 2)],  # Column 3
                [(0, 0), (1, 1), (2, 2)],  # Diagonal 1
                [(0, 2), (1, 1), (2, 0)]   # Diagonal 2
                ]

    initial_board = [[0, 0, 0],
                     [0, 0, 0],
                     [0, 0, 0]]

    def __init__(self, board: Any = None) -> None:
        if board is not None:
            self.current_board = board
        else:
            self.current_board = copy.deepcopy(self.initial_board)

    def get_value(self, board: Any, position: Tuple[int, int]) -> int:
        return board[position[0]][position[1]]

    def determine_game_over(self, players: List[str]) -> str:
        if self.contains_winning_move(self.current_board):
            return 'current turn'
        if self.board_is_full(self.current_board):
            return 'draw'
        return ''

    def board_is_full(self, board: Any) -> bool:
        ''' Determines if the board is full or not. '''
        for row in board:
            for element in row:
                if element == 0:
                    return False
        return True

    # Used for current board & trial computer board
    def contains_winning_move(self, board: Any) -> bool:
        ''' Returns true if all coordinates in a triplet have the same value in them (x or o) and no coordinates
        in the triplet are blank. '''
        for triplet in self.triplets:
            if (
                self.get_value(board, triplet[0])
                == self.get_value(board, triplet[1])
                == self.get_value(board, triplet[2])
                != 0
            ):
                return True
        return False

    def get_locations_of_char(self, board: Any, char: int) -> List[List[int]]:
        ''' Gets the locations of the board that have char in them. '''
        locations = []
        for row in range(3):
            for col in range(3):
                if board[row][col] == char:
                    locations.append([row, col])
        return locations

    def two_blanks(self, triplet: List[Tuple[int, int]], board: Any) -> List[Tuple[int, int]]:
        ''' Determines which rows/columns/diagonals have two blank spaces and an 2 already in them. It's more advantageous
        for the computer to move there. This is used when the computer makes its move. '''

        o_found = False
        for position in triplet:
            if self.get_value(board, position) == 2:
                o_found = True
                break

        blanks_list = []
        if o_found:
            for position in triplet:
                if self.get_value(board, position) == 0:
                    blanks_list.append(position)

            if len(blanks_list) == 2:
                return blanks_list
        return []

    def computer_move(self, board: Any, player_number: Any) -> Any:
        ''' The computer's logic for making its move. '''
        my_board = copy.deepcopy(
            board)  # First the board is copied; used later on
        blank_locations = self.get_locations_of_char(my_board, 0)
        # Gets the locations that already have x's
        x_locations = self.get_locations_of_char(board, 1)
        # List of the coordinates of the corners of the board
        corner_locations = [[0, 0], [0, 2], [2, 0], [2, 2]]
        # List of the coordinates of the edge spaces of the board
        edge_locations = [[1, 0], [0, 1], [1, 2], [2, 1]]

        # If no empty spaces are left, the computer can't move anyway, so it just returns the board.
        if blank_locations == []:
            return board

        # This is special logic only used on the first move.
        if len(x_locations) == 1:
            # If the user played first in the corner or edge,
            # the computer should move in the center.
            if x_locations[0] in corner_locations or x_locations[0] in edge_locations:
                board[1][1] = 2
            # If user played first in the center, the computer should move in the corner. It doesn't matter which corner.
            else:
                location = random.choice(corner_locations)
                row = location[0]
                col = location[1]
                board[row][col] = 2
            return board

        # This logic is used on all other moves.
        # First I'll check if the computer can win in the next move. If so, that's where the computer will play.
        # The check is done by replacing the blank locations with o's and seeing if the computer would win in each case.
        for row, col in blank_locations:
            my_board[row][col] = 2
            if self.contains_winning_move(my_board):
                board[row][col] = 2
                return board
            else:
                my_board[row][col] = 0  # Revert if not winning

        # If the computer can't immediately win, it wants to make sure the user can't win in their next move, so it
        # checks to see if the user needs to be blocked.
        # The check is done by replacing the blank locations with x's and seeing if the user would win in each case.
        for row, col in blank_locations:
            my_board[row][col] = 1
            if self.contains_winning_move(my_board):
                board[row][col] = 2
                return board
            else:
                my_board[row][col] = 0  # Revert if not winning

        # Assuming nobody will win in their next move, now I'll find the best place for the computer to win.
        for row, col in blank_locations:
            if (
                1 not in my_board[row]
                and my_board[0][col] != 1
                and my_board[1][col] != 1
                and my_board[2][col] != 1
            ):
                board[row][col] = 2
                return board

        # If no move has been made, choose a random blank location. If smarter is True, the computer will choose a
        # random blank location from a set of better locations to play. These locations are determined by seeing if
        # there are two blanks and an 2 in each row, column, and diagonal (done in two_blanks).
        # If smarter is False, all blank locations can be chosen.
        if self.smarter:
            blanks = []  # type: Any
            for triplet in self.triplets:
                result = self.two_blanks(triplet, board)
                if result:
                    blanks = blanks + result
            blank_set = set(blanks)
            blank_list = list(blank_set)
            if blank_list == []:
                location = random.choice(blank_locations)
            else:
                location = random.choice(blank_list)
            row = location[0]
            col = location[1]
            board[row][col] = 2
            return board

        else:
            location = random.choice(blank_locations)
            row = location[0]
            col = location[1]
            board[row][col] = 2
            return board

    def is_valid_move(self, move: str) -> bool:
        ''' Checks the validity of the coordinate input passed in to make sure it's not out-of-bounds (ex. 5, 5) '''
        try:
            split_move = move.split(",")
            row = split_move[0].strip()
            col = split_move[1].strip()
            valid = False
            if row in ("1", "2", "3") and col in ("1", "2", "3"):
                valid = True
        except IndexError:
            valid = False
        return valid

    def make_move(self, move: str, player_number: int, computer_move: bool = False) -> Any:
        if computer_move:
            return self.computer_move(self.current_board, player_number + 1)
        move_coords_str = coords_from_command(move)
        if not self.is_valid_move(move_coords_str):
            raise BadMoveException('Make sure your move is from 0-9')
        board = self.current_board
        move_coords = move_coords_str.split(',')
        # Subtraction must be done to convert to the right indices,
        # since computers start numbering at 0.
        row = (int(move_coords[1])) - 1
        column = (int(move_coords[0])) - 1
        if board[row][column] != 0:
            raise BadMoveException('Make sure your space hasn\'t already been filled.')
        board[row][column] = player_number + 1
        return board


class TicTacToeMessageHandler:
    tokens = [':x:', ':o:']

    def parse_row(self, row: Tuple[int, int], row_num: int) -> str:
        ''' Takes the row passed in as a list and returns it as a string. '''
        row_chars = []
        num_symbols = [':one:', ':two:', ':three:', ':four:', ':five:', ':six:', ':seven:', ':eight:', ':nine:']
        for i, e in enumerate(row):
            if e == 0:
                row_chars.append(num_symbols[row_num * 3 + i])
            else:
                row_chars.append(self.get_player_color(e - 1))
        row_string = ' '.join(row_chars)
        return row_string + '\n\n'

    def parse_board(self, board: Any) -> str:
        ''' Takes the board as a nested list and returns a nice version for the user. '''
        return "".join([self.parse_row(r, r_num) for r_num, r in enumerate(board)])

    def get_player_color(self, turn: int) -> str:
        return self.tokens[turn]

    def alert_move_message(self, original_player: str, move_info: str) -> str:
        move_info = move_info.replace('move ', '')
        return '{} put a token at {}'.format(original_player, move_info)

    def game_start_message(self) -> str:
        return ("Welcome to tic-tac-toe!"
                "To make a move, type @-mention `move <number>` or `<number>`")


class ticTacToeHandler(GameAdapter):
    '''
    You can play tic-tac-toe! Make sure your message starts with
    "@mention-bot".
    '''
    META = {
        'name': 'TicTacToe',
        'description': 'Lets you play Tic-tac-toe against a computer.',
    }

    def usage(self) -> str:
        return '''
            You can play tic-tac-toe now! Make sure your
            message starts with @mention-bot.
            '''

    def __init__(self) -> None:
        game_name = 'Tic Tac Toe'
        bot_name = 'tictactoe'
        move_help_message = '* To move during a game, type\n`move <number>` or `<number>`'
        move_regex = r'(move (\d)$)|((\d)$)'
        model = TicTacToeModel
        gameMessageHandler = TicTacToeMessageHandler
        rules = '''Try to get three in horizontal or vertical or diagonal row to win the game.'''
        super().__init__(
            game_name,
            bot_name,
            move_help_message,
            move_regex,
            model,
            gameMessageHandler,
            rules,
            supports_computer=True
        )


def coords_from_command(cmd: str) -> str:
    # This function translates the input command into a TicTacToeGame move.
    # It should return two indices, each one of (1,2,3), separated by a comma, eg. "3,2"
    ''' As there are various ways to input a coordinate (with/without parentheses, with/without spaces, etc.) the
    input is stripped to just the numbers before being used in the program. '''
    cmd_num = int(cmd.replace('move ', '')) - 1
    cmd = '{},{}'.format((cmd_num % 3) + 1, (cmd_num // 3) + 1)
    return cmd


handler_class = ticTacToeHandler
