import asyncio
import itertools
import random
import logging

import chess.pgn
import chess.engine
import chess
from chess import WHITE, BLACK


class Arena:
    MATE_SCORE = 32767

    def __init__(self, enginea, engineb, limit, max_len, win_adj_count, win_adj_score):
        self.enginea = enginea
        self.engineb = engineb
        self.limit = limit
        self.max_len = max_len
        self.win_adj_count = win_adj_count
        self.win_adj_score = win_adj_score

    def adjudicate(self, score_hist):
        if len(score_hist) > self.max_len:
            return '1/2-1/2'
        # Note win_adj_count is in moves, not plies
        count_max = self.win_adj_count * 2
        if count_max > len(score_hist):
            return None
        # Test if white has been winning. Notice score_hist is from whites pov.
        if all(v >= self.win_adj_score for v in score_hist[-count_max:]):
            return '1-0'
        # Test if black has been winning
        if all(v <= -self.win_adj_score for v in score_hist[-count_max:]):
            return '0-1'
        return None

    async def play_game(self, init_node, game_id, white, black):
        """ Yields (play, error) tuples. Also updates the game with headers and moves. """
        try:
            game = init_node.root()
            node = init_node
            score_hist = []
            for ply in itertools.count(int(node.board().turn == BLACK)):
                board = node.board()

                adj_result = self.adjudicate(score_hist)
                if adj_result is not None:
                    game.headers.update({
                        'Result': adj_result,
                        'Termination': 'adjudication'
                    })
                    return

                if board.is_game_over(claim_draw=True):
                    result = board.result(claim_draw=True)
                    game.headers["Result"] = result
                    return

                # Try to actually make a move
                play = await (white, black)[ply % 2].play(
                    board, self.limit, game=game_id,
                    info=chess.engine.INFO_BASIC | chess.engine.INFO_SCORE)
                yield play, None

                if play.resigned:
                    game.headers.update({'Result': ('0-1', '1-0')[ply % 2]})
                    node.comment += f'; {("White","Black")[ply%2]} resgined'
                    return

                node = node.add_variation(play.move, comment=
                        f'{play.info.get("score",0)}/{play.info.get("depth",0)}'
                        f' {play.info.get("time",0)}s')

                # Adjudicate game by score, save score in wpov
                try:
                    score_hist.append(play.info['score'].white().score(
                        mate_score=max(self.win_adj_score, Arena.MATE_SCORE)))
                except KeyError:
                    logging.debug('Engine didn\'t return a score for adjudication. Assuming 0.')
                    score_hist.append(0)

        except (asyncio.CancelledError, KeyboardInterrupt) as e:
            print('play_game Cancelled')
            # We should get CancelledError when the user pressed Ctrl+C
            game.headers.update({'Result': '*', 'Termination': 'unterminated'})
            node.comment += '; Game was cancelled'
            await asyncio.wait([white.quit(), black.quit()])
            yield None, e
        except chess.engine.EngineError as e:
            game.headers.update(
                {'Result': ('0-1', '1-0')[ply % 2], 'Termination': 'error'})
            node.comment += f'; {("White","Black")[ply%2]} terminated: {e}'
            yield None, e

    async def configure(self, args):
        # We configure enginea, engineb is our unchanged opponent.
        # Maybe this should be refactored.
        self.enginea.id['args'] = args
        self.engineb.id['args'] = {}
        try:
            await self.enginea.configure(args)
        except chess.engine.EngineError as e:
            print(f'Unable to start game {e}')
            return [], 0

    async def run_games(self, book, game_id=0, games_played=2):
        score = 0
        games = []
        assert games_played % 2 == 0
        for r in range(games_played//2):
            init_board = random.choice(book)
            for color in [WHITE, BLACK]:
                white, black = (self.enginea, self.engineb) if color == WHITE \
                    else (self.engineb, self.enginea)
                game_round = games_played * game_id + color + 2*r
                game = chess.pgn.Game({
                    'Event': 'Tune.py',
                    'White': white.id['name'],
                    'WhiteArgs': repr(white.id['args']),
                    'Black': black.id['name'],
                    'BlackArgs': repr(black.id['args']),
                    'Round': game_round
                })
                games.append(game)
                # Add book moves
                game.setup(init_board.root())
                node = game
                for move in init_board.move_stack:
                    node = node.add_variation(move, comment='book')
                # Run engines
                async for _play, er in self.play_game(node, game_round, white, black):
                    # If an error occoured, return as much as we got
                    if er is not None:
                        return games, score, er
                result = game.headers["Result"]
                if result == '1-0' and color == WHITE or result == '0-1' and color == BLACK:
                    score += 1
                if result == '1-0' and color == BLACK or result == '0-1' and color == WHITE:
                    score -= 1
        return games, score/games_played, None