#
# BigBrotherBot(B3) (www.bigbrotherbot.net)
# Copyright (C) 2005 Michael "ThorN" Thornton
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

#   CHANGELOG
#   18/10/2011 - 1.0.3 - Bravo17
#    Check slot number go up in order in getplayerlist to weed out data errors
#   22/12/2012 - 1.1 - Courgette
#    fix regex for parsing PB_SV_PList results for cases where a player has no power
#   23/12/2012 - 1.2 - Courgette
#    getPlayerList now only returns info cid, pbid, guid and ip for each connected player. PB is infamously known for
#    missing a character randomly in its PB_SV_PList response, we should succeed if that missing char is in an unimportant
#    part of the response.
#   25/12/2012 - 1.2.1 - Courgette
#    improve reliability of the regular expression for parsing PB_SV_PList response
#   2013-01-20 - 1.2.3 - Courgette
#    improve punkbuster event parsing by accepting whatever punkbuster prefix from the pb responses
#
__author__  = 'ThorN'
__version__ = '1.2.3'

import re

#--------------------------------------------------------------------------------------------------
class PunkBuster(object):
    console = None

#    : Player List: [Slot #] [GUID] [Address] [Status] [Power] [Auth Rate] [Recent SS] [O/S] [Name]
#    : 4  27b26543216546163546513465135135(-) 111.11.1.11:28960 OK   1 3.0 0 (W) "ShyRat"
#    : 5 387852749658574858598854913cdf11(-) 222.222.222.222:28960 OK   1 10.0 0 (W) "shatgun"
#    : 6 9732d328485274156125252141252ba1(-) 33.133.3.133:-28960 OK   1 5.0 0 (W) "FATTYBMBLATY"
    regPlayer = re.compile(r"""
        ^.*?                                        # a new line start with junk (ungreedy mode)
        (.*?):?\s*                                  # end of PB response prefix (potentially missing one char)
          (?P<slot>[1-9][0-9]??)                    # slot number between 1 and 99 (ungreedy mode)
        (?:\s+|)                                    # blank character(s) or nothing
          (?P<pbid>[a-f0-9]{30,32})                 # PB id (at least 30 char long, max 32)
        [^a-f0-9].*?                                # anything but a pbid char and eventually some junk (ungreedy mode)
          (?P<ip>(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])) # a valid IP number
        .+?                                         # junk
        (?:\)\s*"|\)\s*|\s+")                       # detect start of name
          (?P<name>.*?)"?                           # name
        \s*$                                        # end of line
        """, re.IGNORECASE|re.VERBOSE)


    def __init__(self, console):
        self.console = console

    def send(self, command):
        return self.console.write(command)

    def badName(self, grace, filter):
        """
        PB_SV_BadName [grace_seconds] [text_filter]
        Adds a bad name to the list of bad names for the server to disallow in player names
        """
        return self.send('PB_SV_BadName "%s" "%s"' % (grace, filter))


    def badNameDel(self, slot):
        """
        PB_SV_BadNameDel [slot #]
        Deletes a bad name from the list of bad names 
        """
        return self.send('PB_SV_BadNameDel "%s"' % slot)

    def ban(self, client, reason='', private=''):
        """
        PB_SV_Ban [name or slot #] [displayed_reason] | [optional_private_reason]
        Removes a player from the game and permanently bans that player from the server based
        on the player's guid (based on the cdkey); the ban is logged and also written to the
        pbbans.dat file in the pb folder 
        """

        if client.cid and client.connected:
            return self.send('PB_SV_Ban "%s" "%s" "%s"' % (int(client.cid) + 1, reason, private))
        else:
            return self.banGUID(client, reason)

    def banGUID(self, client, reason=''):
        """
        PB_SV_BanGuid [guid] [player_name] [IP_Address] [reason]
        Adds a guid directly to PB's permanent ban list; if the player_name or IP_Address
        are not known, we recommend using "???" 
        """

        if not client.pbid:
            return False

        return self.send('PB_SV_BanGuid "%s" "%s" "%s" "%s"' % (client.pbid, client.name, client.ip, reason))

    def kick(self, client, minutes=1, reason='', private=''):
        """
        PB_SV_Kick [name or slot #] [minutes] [displayed_reason] | [optional_private_reason]
        Removes a player from the game and won't let the player rejoin until specified [minutes] 
        has passed or until the server is restarted, whichever comes first - kicks are not written
        to the pbbans.dat file but they are logged and will show up in the output from the pb_sv_banlist command 
        """

        if not client.cid or not client.connected:
            return False

        return self.send('PB_SV_Kick "%s" "%s" "%s" "%s"' % (int(client.cid) + 1, minutes, reason, private))

    def getSs(self, client):
        """
        Sends a request to all applicable connected players asking for a screen shot to be captured and sent to the PB Server; to specify a player name or substring (as opposed to slot #), surround the text with double-quote marks 
        """
        
        if not client.cid or not client.connected:
            return False

        return self.send('PB_SV_GetSs "%s"' % (int(client.cid) + 1))

    def pList(self):
        """
        PB_SV_PList
        Displays a list of connected players and their current status 
        """
        return self.send('PB_SV_PList')

    def unBan(self, slot):
        """
        PB_SV_UnBan [slot #]
        Unbans a player from the ban list stored in memory; use pb_sv_updbanfile to update the
        permanent ban file after using this command 
        """
        return self.send('PB_SV_UnBan "%s"' % slot)

    def unBanGUID(self, client):
        """
        PB_SV_UnBanGuid [guid]
        Unbans a guid from the ban list stored in memory; use pb_sv_updbanfile to update the
        permanent ban file after using this command 
        """

        if not client.pbid:
            return False

        result = self.send('PB_SV_UnBanGuid "%s"' % client.pbid)
        if result:
            self.send('pb_sv_updbanfile')
            return result

        return False

    def getPlayerList(self):
        """
        Extract cid, pbid, ip for all connected players.
        :return: a dict having slot numbers (minus 1) as keys and an other dict as values. This later dict has keys :
        cid, pbid, guid and ip
        """
        data = self.pList()
        if not data:
            return {}

        players = {}
        lastslot = 0
        for line in data.split('\n'):
            m = re.match(self.regPlayer, line)
            if m:
                d = m.groupdict()
                if int(m.group('slot')) > lastslot:
                    d['guid'] = d['pbid']
                    lastslot = int(m.group('slot'))
                    players[str(lastslot - 1)] = d
                    
                else:
                    self.console.debug('Duplicate or Incorrect PB slot number - client ignored %s lastslot %s' % (m.group('slot'), lastslot))
            elif 'Player List:' not in line:
                self.console.verbose2("PB player info cannot be extracted of %r" % line)
        return players

    def __setattr__(self, key, value):
        self.__dict__[key] = value

        if key != 'console':
            self.send('PB_SV_%s %s' % (key.title(), value))

    def __getattr__(self, key):
        try:        
            return self.__dict__[key]
        except:
            return self.send('PB_SV_%s' % key.title())
