import re
from math import ceil
import time
import operator

from gi.repository import GLib, GObject

from pychess.System.Log import log
from pychess.ic import GAME_TYPES, BLKCMD_ALLOBSERVERS

titles = r"(?:\([A-Z*]+\))*"
names = "([A-Za-z]+)" + titles
titlesC = re.compile(titles)
namesC = re.compile(names)
ratings = r"\(\s*([0-9\ \-\+]{1,4}[P E]?|UNR)\)"

CHANNEL_SHOUT = "shout"
CHANNEL_CSHOUT = "cshout"


class ChatManager(GObject.GObject):

    __gsignals__ = {
        'channelMessage': (GObject.SignalFlags.RUN_FIRST, None,
                           (str, bool, bool, str, str)),
        'kibitzMessage': (GObject.SignalFlags.RUN_FIRST, None,
                          (str, int, str)),
        'whisperMessage': (GObject.SignalFlags.RUN_FIRST, None,
                           (str, int, str)),
        'privateMessage': (GObject.SignalFlags.RUN_FIRST, None,
                           (str, str, bool, str)),
        'bughouseMessage': (GObject.SignalFlags.RUN_FIRST, None, (str, str)),
        'announcement': (GObject.SignalFlags.RUN_FIRST, None, (str, )),
        'arrivalNotification': (GObject.SignalFlags.RUN_FIRST, None,
                                (object, )),
        'departedNotification': (GObject.SignalFlags.RUN_FIRST, None,
                                 (object, )),
        'channelAdd': (GObject.SignalFlags.RUN_FIRST, None, (str, )),
        'channelRemove': (GObject.SignalFlags.RUN_FIRST, None, (str, )),
        'channelJoinError': (GObject.SignalFlags.RUN_FIRST, None, (str, str)),
        'channelsListed': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
        'channelLog': (GObject.SignalFlags.RUN_FIRST, None,
                       (str, int, str, str)),
        'toldChannel': (GObject.SignalFlags.RUN_FIRST, None, (str, int)),
        'receivedChannels': (GObject.SignalFlags.RUN_FIRST, None,
                             (str, object)),
        'receivedNames': (GObject.SignalFlags.RUN_FIRST, None, (str, object)),
        'observers_received': (GObject.SignalFlags.RUN_FIRST, None,
                               (str, str)),
    }

    def __init__(self, connection):
        GObject.GObject.__init__(self)
        self.connection = connection

        self.connection.expect_line(
            self.onPrivateMessage,
            r"%s(\*)?(?:\[\d+\])? (?:tells you|says): (.*)" % names)
        self.connection.expect_line(self.onAnnouncement,
                                    r"\s*\*\*ANNOUNCEMENT\*\* (.*)")
        self.connection.expect_line(self.onChannelMessage,
                                    r"%s(\*)?\((\d+)\): (.*)" % names)
        self.connection.expect_line(self.onShoutMessage,
                                    r"%s(\*)? (c-)?shouts: (.*)" % names)
        self.connection.expect_line(self.onShoutMessage,
                                    r"--> %s(\*)?:? (.*)" % names)
        self.connection.expect_line(self.onKibitzMessage,
                                    r"%s%s\[(\d+)\] kibitzes: (.*)" %
                                    (names, ratings))
        self.connection.expect_line(self.onWhisperMessage,
                                    r"%s%s\[(\d+)\] whispers: (.*)" %
                                    (names, ratings))

        self.connection.expect_line(self.onArrivalNotification,
                                    r"Notification: %s has arrived\." % names)
        self.connection.expect_line(self.onDepartedNotification,
                                    r"Notification: %s has departed\." % names)

        self.connection.expect_fromto(
            self.onChannelList, "channels only for their designated topics.",
            "SPECIAL NOTE")

        self.connection.expect_line(
            lambda m: GLib.idle_add(self.emit, 'channelAdd', m.groups()[0]),
            r"\[(\d+)\] added to your channel list.")
        self.connection.expect_line(
            lambda m: GLib.idle_add(self.emit, 'channelRemove', m.groups()[0]),
            r"\[(\d+)\] removed to your channel list.")

        self.connection.expect_line(
            lambda m: GLib.idle_add(self.emit, 'channelJoinError', *m.groups()),
            r"Only (.+?) may join channel (\d+)\.")

        self.connection.expect_line(self.getNoChannelPlayers,
                                    r"Channel (\d+) is empty\.")
        self.connection.expect_fromto(
            self.getChannelPlayers, r"Channel (\d+)(?: \"(\w+)\")?: (.+)",
            r"(\d+) player(?: is|s are) in channel \d+\.")

        self.connection.expect_fromto(self.gotPlayerChannels,
                                      "%s is in the following channels:" %
                                      names, r"(?!(?:\d+\s+)+)")

        # self.connection.expect_line (self.toldChannel,
        #        '\(told (\d+) players in channel (\d+) ".+"\)')
        # (told Chronatog)

        # Only chess advisers and admins may join channel 63.
        # Only (.+?) may sey send tells to channel (\d+).
        # Only admins may send tells to channel 0.
        # Only registered users may send tells to channels other than 4, 7 and 53.

        self.currentLogChannel = None
        self.connection.expect_line(
            self.onChannelLogStart,
            r":Channel (\d+|shout|c-shout) log for the last \d+ minutes:$")
        self.connection.expect_line(
            self.onChannelLogLine,
            r":\[(\d+):(\d+):(\d+)\] (?:(?:--> )?%s(?: shouts)?)\S* (.+)$" %
            names)
        self.connection.expect_line(self.onChannelLogBreak,
                                    ":Use \"tell chLog Next\" to print more.$")

        # TODO handling of this case is nessesary for console:
        # fics% tell 1 hi
        # You are not in channel 1, auto-adding you if possible.

        # Setting 'Lang' is a workaround for
        # http://code.google.com/p/pychess/issues/detail?id=376
        # and can be removed when conversion to FICS block mode is done
        self.connection.client.run_command("set Lang English")

        self.connection.client.run_command("set height 240")

        self.connection.client.run_command("inchannel %s" %
                                           self.connection.username)
        self.connection.client.run_command("help channel_list")
        self.channels = {}

        # Observing 112 [DrStupp vs. hajaK]: pgg (1 user)
        self.connection.expect_line(
            self.get_allob_list,
            r'(?:Observing|Examining)\s+(\d+).+: (.+) \(')

        self.connection.expect_line(self.on_allob_no,
                                    r"No one is observing game (\d+).")

    def get_allob_list(self, match):
        """ Description: Processes the returning pattern matched of the FICS allob command
                         extracts out the gameno and a list of observers before emmiting them for collection
                         by the observers view
            match: (re.reg-ex) is the complied matching pattern for processing
        """

        obs_dic = {}
        gameno = match.group(1)
        observers = match.group(2)
        oblist = observers.split()
        for player in oblist:
            if '(U)' not in player:  # deals with unregistered players
                try:
                    if '(' in player:  # deals with admin and multi titled players
                        player, rest = player.split('(', 1)
                    ficsplayer = self.connection.players.get(player)
                    obs_dic[player] = ficsplayer.getRatingByGameType(
                        GAME_TYPES['standard'])
                except KeyError:
                    obs_dic[player] = 0
                    # print("player %s is not in self.connection.players" % player)
            else:
                obs_dic[player] = 0
        obs_sorted = sorted(obs_dic.items(),
                            key=operator.itemgetter(1),
                            reverse=True)
        obs_str = ""
        for toople in obs_sorted:
            player, rating = toople
            if rating == 0:
                obs_str += "%s " % player  # Don't print ratings for guest accounts
            else:
                obs_str += "%s(%s) " % (player, rating)
        self.emit('observers_received', gameno, obs_str)

    get_allob_list.BLKCMD = BLKCMD_ALLOBSERVERS

    def on_allob_no(self, match):
        gameno = match.group(1)
        self.emit('observers_received', gameno, "")

    on_allob_no.BLKCMD = BLKCMD_ALLOBSERVERS

    def getChannels(self):
        return self.channels

    def getJoinedChannels(self):
        channels = self.connection.lvm.getList("channel")
        return channels

    channelListItem = re.compile(r"((?:\d+,?)+)\s*(.*)")

    def onChannelList(self, matchlist):
        self.channels = [(CHANNEL_SHOUT, _("Shout")),
                         (CHANNEL_CSHOUT, _("Chess Shout"))]
        numbers = set(range(256))  # TODO: Use limits->Server->Channels
        for line in matchlist[1:-1]:
            match = self.channelListItem.match(line)
            if not match:
                continue
            ids, desc = match.groups()
            for id in ids.split(","):
                numbers.remove(int(id))
                self.channels.append((id, desc))
        for i in numbers:
            self.channels.append((str(i), _("Unofficial channel %d" % i)))

        GLib.idle_add(self.emit, 'channelsListed', self.channels)

    def getNoChannelPlayers(self, match):
        channel = match.groups()[0]
        GLib.idle_add(self.emit, 'receivedNames', channel, [])

    def getChannelPlayers(self, matchlist):
        channel, name, people = matchlist[0].groups()
        people += " " + " ".join(matchlist[1:-1])
        people = namesC.findall(titlesC.sub("", people))
        GLib.idle_add(self.emit, 'receivedNames', channel, people)

    def gotPlayerChannels(self, matchlist):
        list = []
        for line in matchlist[1:-1]:
            list += line.split()

    def onPrivateMessage(self, match):
        name, isadmin, text = match.groups()
        text = self.entityDecode(text)
        GLib.idle_add(self.emit, "privateMessage", name, "title", isadmin,
                      text)

    def onAnnouncement(self, match):
        text = match.groups()[0]
        text = self.entityDecode(text)
        GLib.idle_add(self.emit, "announcement", text)

    def onChannelMessage(self, match):
        name, isadmin, channel, text = match.groups()
        text = self.entityDecode(text)
        isme = name.lower() == self.connection.username.lower()
        GLib.idle_add(self.emit, "channelMessage", name, isadmin, isme,
                      channel, text)

    def onShoutMessage(self, match):
        if len(match.groups()) == 4:
            name, isadmin, type, text = match.groups()
        elif len(match.groups()) == 3:
            name, isadmin, text = match.groups()
            type = ""
        text = self.entityDecode(text)
        isme = name.lower() == self.connection.username.lower()
        # c-shout should be used ONLY for chess-related messages, such as
        # questions about chess or announcing being open for certain kinds of
        # chess matches. Use "shout" for non-chess messages.

        # t-shout is used to invite to tournaments
        if type == "c-":
            GLib.idle_add(self.emit, "channelMessage", name, isadmin, isme,
                          CHANNEL_CSHOUT, text)
        else:
            GLib.idle_add(self.emit, "channelMessage", name, isadmin, isme,
                          CHANNEL_SHOUT, text)

    def onKibitzMessage(self, match):
        name, rating, gameno, text = match.groups()
        text = self.entityDecode(text)
        GLib.idle_add(self.emit, "kibitzMessage", name, int(gameno), text)

    def onWhisperMessage(self, match):
        name, rating, gameno, text = match.groups()
        text = self.entityDecode(text)
        GLib.idle_add(self.emit, "whisperMessage", name, int(gameno), text)

    def onArrivalNotification(self, match):
        name = match.groups()[0]
        try:
            player = self.connection.players.get(name)
        except KeyError:
            return
        if player.name not in self.connection.notify_users:
            self.connection.notify_users.append(player.name)
        GLib.idle_add(self.emit, "arrivalNotification", player)

    def onDepartedNotification(self, match):
        name = match.groups()[0]
        try:
            player = self.connection.players.get(name)
        except KeyError:
            return
        GLib.idle_add(self.emit, "departedNotification", player)

    def toldChannel(self, match):
        amount, channel = match.groups()
        GLib.idle_add(self.emit, "toldChannel", channel, int(amount))

    def onChannelLogStart(self, match):
        channel, = match.groups()
        self.currentLogChannel = channel

    def onChannelLogLine(self, match):
        if not self.currentLogChannel:
            log.warning("Received log line before channel was set")
            return
        hour, minutes, secs, handle, text = match.groups()
        conv_time = self.convTime(int(hour), int(minutes), int(secs))
        text = self.entityDecode(text)
        GLib.idle_add(self.emit, "channelLog", self.currentLogChannel, conv_time,
                      handle, text)

    def onChannelLogBreak(self, match):
        self.connection.client.run_command("xtell chlog Next")

    def convTime(self, h, m, s):
        # Convert to timestamp
        t1, t2, t3, t4, t5, t6, t7, t8, t9 = time.localtime()
        tstamp = time.mktime((t1, t2, t3, h, m, s, 0, 0, 0))
        # Difference to now in hours
        dif = (tstamp - time.time()) / 60. / 60.
        # As we know there is maximum 30 minutes in difference, we can guess when the
        # message was sent, without knowing the sending time zone
        return tstamp - ceil(dif) * 60 * 60

    entityExpr = re.compile("&#x([a-f0-9]+);")

    def entityDecode(self, text):
        return self.entityExpr.sub(lambda m: chr(int(m.groups()[0], 16)),
                                   text)

    def entityEncode(self, text):
        buf = []
        for char in text:
            if not 32 <= ord(char) <= 127:
                char = "&#" + hex(ord(char))[1:] + ";"
            buf.append(char)
        return "".join(buf)

    def getChannelLog(self, channel, minutes=30):
        """ Channel can be channel_id, shout or c-shout """
        assert 1 <= minutes <= 120
        # Using the chlog bot
        self.connection.client.run_command("xtell chlog show %s -t %d" %
                                           (channel, minutes))

    def getPlayersChannels(self, player):
        self.connection.client.run_command("inchannel %s" % player)

    def getPeopleInChannel(self, channel):
        if channel in (CHANNEL_SHOUT, CHANNEL_CSHOUT):
            people = self.connection.players.get_online_playernames()
            GLib.idle_add(self.emit, 'receivedNames', channel, people)
        self.connection.client.run_command("inchannel %s" % channel)

    def joinChannel(self, channel):
        self.connection.client.run_command("+channel %s" % channel)

    def removeChannel(self, channel):
        self.connection.client.run_command("-channel %s" % channel)

    def mayTellChannel(self, channel):
        if self.connection.isRegistred() or channel in ("4", "7", "53"):
            return True
        return False

    def tellPlayer(self, player, message):
        message = self.entityEncode(message)
        self.connection.client.run_command("tell %s %s" % (player, message))

    def tellChannel(self, channel, message):
        message = self.entityEncode(message)
        if channel == CHANNEL_SHOUT:
            self.connection.client.run_command("shout %s" % message)
        elif channel == CHANNEL_CSHOUT:
            self.connection.client.run_command("cshout %s" % message)
        else:
            self.connection.client.run_command("tell %s %s" %
                                               (channel, message))

    def tellAll(self, message):
        message = self.entityEncode(message)
        self.connection.client.run_command("shout %s" % message)

    def tellGame(self, gameno, message):
        message = self.entityEncode(message)
        self.connection.client.run_command("xkibitz %s %s" % (gameno, message))

    def tellOpponent(self, message):
        message = self.entityEncode(message)
        self.connection.client.run_command("say %s" % message)

    def tellBughousePartner(self, message):
        message = self.stripChars(message)
        self.connection.client.run_command("ptell %s" % message)

    def tellUser(self, player, message):
        for line in message.strip().split("\n"):
            self.tellPlayer(player, line)

    def whisper(self, message):
        message = self.entityEncode(message)
        self.connection.client.run_command("whisper %s" % message)
