import asyncio
from collections import defaultdict

from pychess.Players.Player import Player, PlayerIsDead, TurnInterrupt
from pychess.Utils.Move import parseSAN, toAN
from pychess.Utils.lutils.lmove import ParsingError
from pychess.Utils.Offer import Offer
from pychess.Utils.const import REMOTE, UNFINISHED_STATES, CHAT_ACTION, CASTLE_KK, \
    FISCHERRANDOMCHESS, CASTLE_SAN, TAKEBACK_OFFER
from pychess.System.Log import log


class ICPlayer(Player):
    __type__ = REMOTE

    def __init__(self,
                 gamemodel,
                 ichandle,
                 gameno,
                 color,
                 name,
                 icrating=None):
        Player.__init__(self)
        self.offers = {}
        self.setName(name)
        self.ichandle = ichandle
        self.icrating = icrating
        self.color = color
        self.gameno = gameno
        self.gamemodel = gamemodel
        self.turn_interrupt = False

        # If some times later FICS creates another game with same wplayer,bplayer,gameno
        # this will change to False and boardUpdate messages will be ignored
        self.current = True

        self.connection = connection = self.gamemodel.connection

        self.connections = connections = defaultdict(list)
        connections[connection.bm].append(connection.bm.connect_after(
            "playGameCreated", self.__playGameCreated))
        connections[connection.bm].append(connection.bm.connect_after(
            "obsGameCreated", self.__obsGameCreated))
        connections[connection.om].append(connection.om.connect(
            "onOfferAdd", self.__onOfferAdd))
        connections[connection.om].append(connection.om.connect(
            "onOfferRemove", self.__onOfferRemove))
        connections[connection.om].append(connection.om.connect(
            "onOfferDeclined", self.__onOfferDeclined))
        connections[connection.cm].append(connection.cm.connect(
            "privateMessage", self.__onPrivateMessage))

        self.cid = self.gamemodel.connect_after("game_terminated", self.on_game_terminated)

    def on_game_terminated(self, model):
        self.gamemodel.disconnect(self.cid)

    def getICHandle(self):
        return self.name

    @property
    def time(self):
        return self.gamemodel.timemodel.getPlayerTime(self.color)

    # Handle signals from the connection

    def __playGameCreated(self, bm, ficsgame):
        if self.gamemodel.ficsplayers[0] == ficsgame.wplayer and \
            self.gamemodel.ficsplayers[1] == ficsgame.bplayer and \
                self.gameno == ficsgame.gameno:
            log.debug("ICPlayer.__playGameCreated: gameno reappeared: gameno=%s white=%s black=%s" % (
                ficsgame.gameno, ficsgame.wplayer.name, ficsgame.bplayer.name))
            self.current = False

    def __obsGameCreated(self, bm, ficsgame):
        if self.gamemodel.ficsplayers[0] == ficsgame.wplayer and \
            self.gamemodel.ficsplayers[1] == ficsgame.bplayer and \
                self.gameno == ficsgame.gameno:
            log.debug("ICPlayer.__obsGameCreated: gameno reappeared: gameno=%s white=%s black=%s" % (
                ficsgame.gameno, ficsgame.wplayer.name, ficsgame.bplayer.name))
            self.current = False

    def __onOfferAdd(self, om, offer):
        if self.gamemodel.status in UNFINISHED_STATES and not self.gamemodel.isObservationGame(
        ):
            log.debug("ICPlayer.__onOfferAdd: emitting offer: self.gameno=%s self.name=%s %s" % (
                self.gameno, self.name, offer))
            self.offers[offer.index] = offer
            self.emit("offer", offer)

    def __onOfferDeclined(self, om, offer):
        for offer_ in list(self.gamemodel.offers.keys()):
            if offer.type == offer_.type:
                offer.param = offer_.param
        log.debug("ICPlayer.__onOfferDeclined: emitting decline for %s" %
                  offer)
        self.emit("decline", offer)

    def __onOfferRemove(self, om, offer):
        if offer.index in self.offers:
            log.debug("ICPlayer.__onOfferRemove: emitting withdraw: \
                      self.gameno=%s self.name=%s %s" % (self.gameno, self.name, offer))
            self.emit("withdraw", self.offers[offer.index])
            del self.offers[offer.index]

    def __onPrivateMessage(self, cm, name, title, isadmin, text):
        if name == self.ichandle:
            self.emit("offer", Offer(CHAT_ACTION, param=text))

    def __disconnect(self):
        if self.connections is None:
            return
        for obj in self.connections:
            for handler_id in self.connections[obj]:
                if obj.handler_is_connected(handler_id):
                    obj.disconnect(handler_id)
        self.connections = None

    def end(self, status, reason):
        self.__disconnect()
        self.gamemodel.ficsgame.queue.put_nowait("del")

    def kill(self, reason):
        self.__disconnect()
        self.gamemodel.ficsgame.queue.put_nowait("del")

    # Send the player move updates

    @asyncio.coroutine
    def makeMove(self, board1, move, board2):
        log.debug("ICPlayer.makemove: id(self)=%d self=%s move=%s board1=%s board2=%s" % (
            id(self), self, move, board1, board2))
        if board2 and not self.gamemodel.isObservationGame():
            # TODO: Will this work if we just always use CASTLE_SAN?
            castle_notation = CASTLE_KK
            if board2.variant == FISCHERRANDOMCHESS:
                castle_notation = CASTLE_SAN
            self.connection.bm.sendMove(toAN(board2, move, castleNotation=castle_notation))
        item = yield from self.gamemodel.ficsgame.queue.get()
        try:
            if item == "del":
                raise PlayerIsDead

            gameno, ply, curcol, lastmove, fen, wname, bname, wms, bms = item
            self.gamemodel.onBoardUpdate(gameno, ply, curcol, lastmove, fen, wname, bname, wms, bms)

            if curcol == self.color and ply == self.gamemodel.ply:
                item = yield from self.gamemodel.ficsgame.queue.get()

                if item == "del":
                    raise PlayerIsDead

                gameno, ply, curcol, lastmove, fen, wname, bname, wms, bms = item
                self.gamemodel.onBoardUpdate(gameno, ply, curcol, lastmove, fen, wname, bname, wms, bms)

            if self.turn_interrupt:
                self.turn_interrupt = False
                raise TurnInterrupt

            if ply < board1.ply:
                # This should only happen in an observed game
                board1 = self.gamemodel.getBoardAtPly(max(ply - 1, 0))
            log.debug("ICPlayer.makemove: id(self)=%d self=%s from queue got: ply=%d sanmove=%s" % (
                id(self), self, ply, lastmove))

            try:
                move = parseSAN(board1, lastmove)
                log.debug("ICPlayer.makemove: id(self)=%d self=%s parsed move=%s" % (
                    id(self), self, move))
            except ParsingError:
                raise
            return move
        finally:
            log.debug("ICPlayer.makemove: id(self)=%d self=%s returning move=%s" % (id(self), self, move))

    # Interacting with the player

    def pause(self):
        pass

    def resume(self):
        pass

    def setBoard(self, fen):
        # setBoard will currently only be called for ServerPlayer when starting
        # to observe some game. In this case FICS already knows how the board
        # should look, and we don't need to set anything
        pass

    def playerUndoMoves(self, movecount, gamemodel):
        log.debug("ICPlayer.playerUndoMoves: id(self)=%d self=%s, undoing movecount=%d" % (
            id(self), self, movecount))
        # If current player has changed so that it is no longer us to move,
        # We raise TurnInterrupt in order to let GameModel continue the game
        if movecount % 2 == 1 and gamemodel.curplayer != self:
            self.turn_interrupt = True

    def resetPosition(self):
        """ Used in observed examined games f.e. when LectureBot starts another example"""
        self.turn_interrupt = True

    def putMessage(self, text):
        self.connection.cm.tellPlayer(self.ichandle, text)

    # Offer handling

    def offerRematch(self):
        if self.gamemodel.timed:
            minimum = int(self.gamemodel.timemodel.intervals[0][0]) / 60
            inc = self.gamemodel.timemodel.gain
        else:
            minimum = 0
            inc = 0
        self.connection.om.challenge(self.ichandle,
                                     self.gamemodel.ficsgame.game_type, minimum,
                                     inc, self.gamemodel.ficsgame.rated)

    def offer(self, offer):
        log.debug("ICPlayer.offer: self=%s %s" % (repr(self), offer))
        if offer.type == TAKEBACK_OFFER:
            # only 1 outstanding takeback offer allowed on FICS, so remove any of ours
            for index in list(self.offers.keys()):
                if self.offers[index].type == TAKEBACK_OFFER:
                    log.debug("ICPlayer.offer: del self.offers[%s] %s" %
                              (index, offer))
                    del self.offers[index]
        self.connection.om.offer(offer, self.gamemodel.ply)

    def offerDeclined(self, offer):
        log.debug("ICPlayer.offerDeclined: sending decline for %s" % offer)
        self.connection.om.decline(offer)

    def offerWithdrawn(self, offer):
        pass

    def offerError(self, offer, error):
        pass

    def observe(self):
        self.connection.client.run_command("observe %s" % self.ichandle)
