#!/usr/bin/python2.2
# -*- mode: python; coding: latin-1 -*-
#
# defakeatron
#
# $Id: defakeatron,v 1.1 2003/08/29 13:53:43 elho Exp $
#
# Copyright (C) 2003 Elmar Hoffmann
#
# defakeatron
#
# Requires Python 2.2 and PyXML available from
# http://pyxml.sourceforge.net/
#
#
# See http://www.elho.net/dev/guidscan/ for further information.
#
# Please report any bugs to guidscan@elho.net.
#
#
# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

import string

import sys
import os
import pty
import getopt
import re

from xml.sax import saxlib
from xml.sax import saxutils
from xml.sax import saxexts
from xml.sax import SAXParseException

class GUID:
    def __init__(self, game):
        self.guids = {}
        self.sguids = {}
        self.game = game

    def __len__(self):
        return len(self.guids)

    def __getitem__(self, key):
        return self.guids[key]

    def __contains__(self, item):
        return item in self.guids

    def updateName(self, guid, name):
        if name not in self.guids[guid]['names']:
            self.guids[guid]['names'].append(name)

    def update(self, new):
        for guid in new.keys():
            if guid in self.guids:
                for name in new[guid]['names']:
                    self.updateName(guid, name)
            else:
                self.guids[guid] = new[guid]
                self.sguids[guid[-8:]] = guid

    class _xmlDocumentHandler(saxlib.DocumentHandler):
        def __init__(self):
            self.game = ''
            self.guids = {}

            self._guid = ''
            self._names = []
            self._chars = u''

        def startElement(self, name, attrs):
            if name == 'guids':
                self.game = attrs['game'].encode('ascii')
            elif name == 'player':
                self._guid = attrs['id'].encode('ascii')
                self._names = []
            self._chars = u''

        def endElement(self, name):
            if name == 'player':
                self.guids[self._guid] = {
                    'names': self._names,
                    }
            elif name == 'name':
                s = ''
                for c in self._chars:
                    n = ord(c)
                    if n > 0x7f:
                        if n >= 0xe000 and n < 0xe020:
                            s += chr(n - 0xe000)
                        else:
                            raise UnicodeError
                    else:
                        s  += c
                self._names.append(s)

        def characters(self, data, start, length):
            self._chars += data[start:start+length]

    class ParseException(Exception):
        def __init__(self, value):
            self.value = value
        def __str__(self):
            return self.value

    def importXML(self, file, clear=False, validate=True):
        dochandler = self._xmlDocumentHandler()
        if validate:
            parser = saxexts.XMLValParserFactory.make_parser()
        else:
            parser = saxexts.XMLParserFactory.make_parser()
        parser.setDocumentHandler(dochandler)
        parser.setErrorHandler(saxutils.ErrorRaiser())
        try:
            parser.parseFile(file)
        except SAXParseException, err:
            raise self.ParseException(err.getMessage().encode())
        if dochandler.game != self.game:
            raise ValueError, "Wrong game type '%s', expected '%s'" \
                  % (dochandler.game, self.game)
        if clear:
            self.guids = {}
        self.update(dochandler.guids)

def q3StripColor(string):
    """strip Q3A color codes from given string
    returns string"""
    s = ''
    i = 0
    l = len(string)
    while i < l:
        if string[i] == '^':
            i += 1
            if i < l and string[i] == '^':
                s += '^'
        else:
            s += string[i]
        i += 1
    return s


def main():
    try:
        opts, args = getopt.getopt(sys.argv[1:], 'g:i:hV',
                                   [ 'game=', 'import=',
                                     'help', 'version'])
    except getopt.GetoptError, err:
        errorhelp("unrecognized option '%s'" % err)
        sys.exit(2)

    gametype = 'quake3'
    imports = []
    for (o, a) in opts:
        if o in ('-h', '--help'):
            usage()
            sys.exit()
        elif o in ('-V', '--version'):
            version()
            sys.exit()
        elif o in ('-g', '--game'):
            gametype = a
        elif o in ('-i', '--import'):
            imports.append(a)

    if not imports:
        errorhelp("no input files")
        sys.exit(2)

    guids = GUID(gametype)
    for db in imports:
        try:
            f = file(db)
        except IOError, err:
            error(err)
            sys.exit(1)
        else:
            try:
                guids.importXML(f)
            except (ValueError, guids.ParseException), err:
                error("Error importing '%s': %s" % (db, err))
                sys.exit(1)
    #
    Command = '/usr/local/bin/quake3'
    Args = [ Command, '+set vm_game 2', '+set vm_ui 2', '+set vm_cgame 2', '+set fs_game q3f2', '+set com_hunkmegs 96', '+exec autoexec.cfg' ]
    try:
        (pid, fd) = pty.fork()
    except OSError, err:
        error("Error calling fork(2): %s" % err)
        sys.exit(1)
    else:
        if pid == 0:
            # child
            try:
                os.execv(Command, Args)
            except OSError, err:
                error("Error calling execv(3): %s" % err)
            os._exit(127)
        else:
            # parent
            fin = os.fdopen(fd, "w")
            fout = os.fdopen(os.dup(fd), "r")

            qprint(fin, '^6defakeatron ^6$Revision: 1.1 $ ^6active')

            plist_re = re.compile('^(?P<slot>\d+) +(?P<sguid>[0-9a-z]+)\((?P<auth>[^\)]*)\) +(?P<status>[^ ]+) +(?P<authrate>\d+\.\d+) +(?P<recentss>\d+) +(?P<name>[^\r\n]*)')
            plist = False
            players = []
            while True:
                try:
                    line = fout.readline()
                except IOError:
                    break
                else:
                    sys.stdout.write(line)
                    if line.startswith('^5'):
                        line = line[2:]
                        if line.startswith('PunkBuster Client: ^5Player List: [Slot #] [GUID] [Status] [Auth Rate] [Recent SS] [Name]'):
                                plist = True
                                players = []
                        elif plist:
                            if line.startswith('End of Player List '):
                                plist = False
                                qprint(fin, "^6defakeatron: ^6Player ^6list:")
                                qprint(fin, "^6[Slot]  ^6[GUID]   ^6[Status] ^6[Name]  ^6[OtherNames]")
                                for player in players:
                                    qprint(fin, "%s %s^6/^6%s ^6%s ^6%s ^7%s^7%s" % (string.rjust('^6'+player['slot'], 4), string.rjust('^6'+player['sguid'], 10), player['auth'].ljust(6), player['status'].ljust(4), player['found'].ljust(1), player['name'], player['names']))
                            else:
                                m = plist_re.match(line)
                                if m:
                                    player = m.groupdict()
                                    curname = player['name']
                                    player['names'] = ''

                                    sguid = m.group('sguid')
                                    if sguid in guids.sguids:
                                        guid = guids.sguids[sguid]
                                        player['found'] = '*'
                                        for name in guids[guid]['names']:
                                            if name != curname:
                                                player['names'] += ' ' + name
                                    else:
                                        player['found'] = ''

                                    players.append(player)

            while True:
                wpid, status = os.waitpid(pid, 0)
                if os.WIFSTOPPED(status):
                    continue
                else:
                    break
            fin.close()
            fout.close()

def qprint(f, msg):
    print q3StripColor(msg)
    f.write('echo "%s"\n' % msg)


def usage():
    print "Usage: %s [options] file...\n" % os.path.basename(sys.argv[0])

    print "  -h, --help            print this help, then exit"
    print "  -V, --version         print version number, then exit"
    print
    print "  -i, --import=<file>   import XML GUID list from <file>"
    print "  -g, --game=<type>     set game to <type>, defaults to 'quake3'"
    print

def version():
    print "defakeatron $Revision: 1.1 $"
    print "Copyright 2003 Elmar Hoffmann"
    print "This is free software; see the GNU General Public Licence version 2 or"
    print "later for copying conditions.  There is NO warranty."

def error(msg):
    sys.stderr.write("%s: %s\n" % (os.path.basename(sys.argv[0]), msg))

def errorhelp(msg):
    progname = os.path.basename(sys.argv[0])
    sys.stderr.write("%s: %s\nTry '%s --help' for more information.\n"
                     % (progname, msg, progname))


if __name__ == '__main__':
    main()
