# loemu - A user friendly frontend for game emulators
# 
# Copyright (C) 2008 Bernat Pegueroles Forcadell
# 
# Author: Bernat Pegueroles Forcadell <bernat@pegueroles.cat>
# 
# 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 Street, Fifth Floor, Boston, MA  02110-1301, USA.

import os
import string
import re
import sys
import signal
from pickle import load,dump
from subprocess import Popen,PIPE,STDOUT
from ConfigParser import RawConfigParser
from time import sleep,time,strftime
from glob import glob

from tempfile import mkstemp
from cStringIO import StringIO

from xml.sax.handler import ContentHandler
from xml.sax import make_parser
from xml.sax.saxutils import escape

from libxml2 import parseFile
from libxslt import parseStylesheetDoc

from Config import config

# XML Gamelist handler
class XMLHandler(ContentHandler):
    def __init__(self,config, get_emulator):
        ContentHandler.__init__(self)
        self.record = False
        self.initialize=dict()
        for field in config.fields:
            self.initialize[field]=""
        self.gamelist=dict()
        self.identities=dict()
        for emulator in config.emulators():
            self.gamelist[emulator]=dict()
            self.identities[emulator]=config.emu(emulator).get("General","identity")
        self.size=0
        self.curr=0
        self.get_emulator=get_emulator

    def startElement(self, name, attrs):
        if name == "gamelist":
            if "size" in attrs.getNames():
                self.size=float(attrs.get("size"))
        if name == "game":
            game=self.initialize.copy()
            for value in attrs.getNames():
                game[value]=attrs.get(value)
            emulator=self.get_emulator(attrs)
            #attrs.getValue("emulator")
            self.gamelist[emulator][attrs.getValue(self.identities[emulator])]=game
            self.curr=self.curr+1
        
    def get_list(self):
        return self.gamelist

    # returns the part of the gamelist loaded [0,1]
    def get_fraction(self):
        if not self.size:
            return False
        return float(float(self.curr) / self.size)

    def get_size(self):
        return self.size

# Gamelist loader
class Loader:
    def __init__(self):
        self.xmlhandler=False
        self.size=0
        return

    def load_list(self):
        file = config.get("General","gamelist")
        if not os.access(file,os.R_OK):
             return _("Can not access to %s file") % file
        format = config.get("General","gamelist_format")
        if format == "xml":
            self.xmlhandler = XMLHandler(config,lambda x: x.getValue("emulator"))
            saxparser = make_parser()
            saxparser.setContentHandler(self.xmlhandler)
            saxparser.parse(open(file))
            self.size=self.xmlhandler.get_size()
            self.list=self.xmlhandler.get_list()
        elif format == "python":
            self.list = load(open(file))
        else:
            return _("Unknown gamelist format") + " ['xml','python']: " + format
        return None

    # returns the part of the gamelist loaded [0,1]
    def get_loader_fraction(self):
        if not self.xmlhandler:
            return False
        return self.xmlhandler.get_fraction()

# Gamelist Manager
class Manager(Loader):
    def __init__(self):
        Loader.__init__(self)
        self.size=0
        # dictionary of played file parsers
        self.played=dict()
        # dictionary of current patterns of categories that filter the gamelist
        self.pattern=dict()
        # dict off all games of all emulators
        self.list=dict()
        # current subset of gamelist
        self.match=list()
        # sort field
        self.sort = 'favorite'

    def load(self):
        err= self.load_list()
        if err:
            return err
        for emulator in config.emulators():
            self.played[emulator] = dict()
            if os.path.isdir(os.path.join(config.get_dir('home'),emulator.lower())):
                self.load_played_emulator(emulator)

    # load user emulator file of played games
    def load_played_emulator(self,emulator):
        for file in glob(os.path.join(config.get_dir('home'),emulator.lower(),"*.ini")):
            rcp=RawConfigParser()
            rcp.read(file)
            game=os.path.basename(file)[:-4]
            if game not in self.list[emulator]:
                for field in config.fields:
                    self.list[emulator][game][field]=""
            for option,value in rcp.items("General"):
                self.list[emulator][game][option]=value
            self.played[emulator][game]=rcp
        return None

    # return a dictionary of all the possible values of categories of the filters
    def get_filter_lists(self):
        lists=dict()
        self.filter_fraction=0
        states=config.get("General","states")
        filters=config.get("General","filters")
        for name in filters:
            lists[name]=list()
        for sublist in self.list.values():
            for game in sublist.values():
                self.filter_fraction=self.filter_fraction+1
                if game["state"] in states:
                    for name in filters:
                        if game[name] not in lists[name]:
                            lists[name].append(game[name])
        for name in filters:
            lists[name].sort()
            lists[name].insert(0,_("All"))
        return lists

    # returns the part of the filter list loaded [0,1]
    def get_filter_fraction(self):
        if not self.size:
            return False
        return float(float(self.filter_fraction)/self.size)

    # set the patterns of categories to execute a new search
    def set_selection(self,category,selection):
        self.pattern=dict()
        if _('All') not in selection:
            self.pattern[category]=("^"+string.join(selection,"$|^")+"$").replace('*','\*')
        self.pattern['emulator']=self.emu
        return

    # fix emulator category in pattern dictionary
    def set_emulator(self,emulator):
        self.emu="^"+emulator+"$"
        self.pattern['emulator']=self.emu
        return

    # filter the gamelist with the dictionary of patterns (or patterns passed)
    def filter(self,pattern=None,sort=None):
        if pattern:
            self.pattern=pattern
            self.pattern['emulator']=self.emu
        self.match=list()
        for sublist in self.list.values():
            self.match= self.match + sublist.values()
        if 'state' not in self.pattern and config.has_option('General','states'):
            states=config.get('General','states')
            self.pattern['state']=("^"+string.join(states,"$|^")+"$").replace('*','\*')
        for name,value in self.pattern.iteritems():
            if value != "" and value != "^"+_('All')+"$" :
                self.match=self.match_pattern(self.match,name,value)
        if sort:
            self.sort=sort
        if self.sort:
            self.match.sort(lambda a,b: a[self.sort] < b[self.sort] and 1 or -1)
        return self.match

    # filter the subset of gamelist 'match_list' 
    # with the pattern 'pattern' of category 'name'
    def match_pattern(self,match_list,name,pattern):
        rx=re.compile(pattern,re.I)
        match=list()
        for game in match_list:
            if rx.search(game[name]):
                match.append(game)
        return match

    # change favorite state
    def swap_favorite(self,index):
        game=self.match[index]
        if game["favorite"] == "":
            game["favorite"]="1"
        else:
            game["favorite"]=""
        
        self.write_game_prefs(game)
        return game["favorite"]=="1"

    # return game of current subset of gamelist
    def get(self,index):
        return self.match[index]

    # execute selected game
    def execute(self,game):
        emu=config.emu(game["emulator"])

        rx=re.compile("@\((?P<replace>[^)]+)\)")
        args=list() 
        for arg in emu.get("General","start").split(" "):
            if arg != '':
                mo = re.search(rx,arg)
                if mo:
                    args.append(re.sub(rx,game[mo.group('replace')],arg))
                else:
                    args.append(arg)
        t1 = time()
        p=Popen(args,close_fds=True,stderr=STDOUT,stdout=PIPE)
        sts = os.waitpid(p.pid, 0)
        t2 = time()

        sec = int(t2 - t1)
        if game["time"] != "":
            h = int(game["time"][0:2])
            m = int(game["time"][3:5])
            s = int(game["time"][6:8])
            sec = sec + h * 3600 + m * 60 + s

        h = abs(sec/3600)
        m = abs((sec % 3600) / 60)
        s = abs(sec % 60)
        game["time"]= "%02d:%02d:%02d" % (h,m,s)

        if game["exec"] == "":
            game["exec"]= str(1)
        else:
            game["exec"]= str( int(game["exec"]) + 1 )
        game["last"]= strftime("%Y-%m-%d %H:%M:%S")

        self.write_game_prefs(game)
        return sts

    def write_game_prefs(self,game):
        emulator=game["emulator"]
        emu=config.emu(emulator)

        ident= game[emu.get("General","identity")]
        if ident in self.played[emulator]:
            rcp=self.played[emulator][ident]
        else:
            if not os.path.isdir(os.path.join(config.get_dir('home'),emulator.lower())):
                os.makedirs(os.path.join(config.get_dir('home'),emulator.lower()))
            rcp=RawConfigParser()
            self.played[emulator][ident]=rcp
            rcp.add_section("General")

        rcp.set("General","time",game["time"])
        rcp.set("General","exec",game["exec"])
        rcp.set("General","last",game["last"])
        rcp.set("General","favorite",game["favorite"])
        fd=open(os.path.join(config.get_dir('home'),emulator.lower(),ident+".ini"),'w')
        rcp.write(fd)
        fd.close()
        return
        


# Gamelist Builder
class Builder(Loader):
    def __init__(self):
        Loader.__init__(self)
        if not os.access(os.path.dirname(config.get("General","gamelist")),os.W_OK):
            self.log("99",_("Can not write gamelist file: %s") % config.get("General","gamelist"))
            sys.exit(1)
            return
        self.out= sys.stdout
        # set signal handler to handle extern kill
        signal.signal(signal.SIGTERM,self.signal_handler)

    # build the gamelist of all emulators
    def build(self):
        self.wlist=list()
        for id in config.emulators():
            self.build_id(id)
            self.wlist = self.wlist + self.current.values()
        self.write()

    # build the gamelist with id 'id'
    def build_emulator(self,id):
        self.wlist=list()
        if os.access(config.get("General","gamelist"),os.R_OK):
            self.log("10",_("Loading gamelist ..."))
            self.log("11",config.get("General","gamelist"))
            self.log("00"," ")
            self.log("20","-1")
            err=self.load_list()
            if err:
                self.log("99",err)
            for sublist in self.list.values():
                self.wlist= self.wlist + sublist.values()
        self.id=id
        # remove the emulator to be rebuilded
        self.wlist=filter(lambda x: x['emulator'] != self.id, self.wlist)
        self.build_id(id)
        self.wlist = self.wlist + self.current.values()
        self.write()

    # build the subset of gamelist of emulator 'id'
    def build_id(self,id):
        self.current=dict()
        emu=config.emu(id)

        if not os.access(emu.get("General","binary"),os.X_OK):
            for path in os.environ['PATH'].split(":"):
                 if os.access(path + "/" + emu.get("General","binary"),os.X_OK):
                    break
            else:
                return

        for section in emu.get("General","sources"):
            type=emu.get(section,"type")
            source=emu.get(section,type)
            format = emu.get(section,"format")

            if type=="archive":
                src = None
                if os.access(source,os.R_OK):
                    src = open(source)
                self.log("10",_("Loading %s file ...") % format)
                self.log("11",source)
            elif type=="command":
                p=Popen(source.split(" "),close_fds=True,stdout=PIPE,stderr=PIPE)
                src = p.stdout
                self.log("10",_("Loading %s output ...") % format)
                self.log("11",source)
            else:
                self.log("99",_("Type of source unknown") + " ['archive','command']: " + type)
                sys.exit(1)
                return

            if format == "ini-sections":
                self.load_ini_sections_source(src,emu,section)
            elif format == "ini-options":
                self.load_ini_options_source(src,emu,section)
            elif format == "xml":
                self.load_xml_source(src,emu,section)
            elif format == "plain":
                self.load_plain_source(src,emu,section)
            else:
                self.log("99",_("Format of source unknown") + " ['xml','ini-sections','ini-options','plain']: " + format)
                sys.exit(1)
                return

        # transform the values returned to the standarized values of all emulators
        #  (example in state category: correct -> good, incorrect -> bad, ...)
        transform_values=dict()
        for field in config.fields:
            if field in emu.options('General'):
                transform_values[field]=emu.get('General',field)
        for game in self.current:
            self.current[game]["emulator"]=id
            for key,value in self.current[game].iteritems():
                if key in transform_values:
                    self.current[game][key]=transform_values[key].get(value,value)
        return

    def load_ini_sections_source(self,src,emu,section):
        if src == None:
            return
        self.log("20","-1")
        self.log("00",_("Initializing ..."))
        ilabel=emu.get("General","identity")
        sections=emu.get(section,"sections")

        parser= RawConfigParser()
        parser.readfp(src)
        for title,key in sections.iteritems():
            self.log("00",_("Parsing %s section ...") % title)
            if parser.has_section(title):
                for ident,value in parser.items(title):
                    if not self.current.has_key(ident):
                        self.current[ident]=dict()
                        for field in config.fields:
                            self.current[ident][field]=""
                        self.current[ident][ilabel]=ident
                    if self.current[ident][key]=="":
                        self.current[ident][key]=value
        return 

    def load_ini_options_source(self,src,emu,section):
        if src == None:
            return
        self.log("20","-1")
        self.log("00",_("Initializing ..."))
        ilabel=emu.get("General","identity")
        sections=emu.get(section,"sections")

        parser= RawConfigParser()
        parser.readfp(src)
        for ident in parser.sections():
            self.log("00",_("Parsing %s section ...") % ident)
            for title,value in parser.items(ident):
                if options.has_key(title):
                    if not self.current.has_key(ident):
                        self.current[ident]=dict()
                        for field in config.fields:
                            self.current[ident][field]=""
                        self.current[ident][ilabel]=ident
                    if self.current[ident][options[title]]=="":
                        self.current[ident][options[title]]=value
        return

    def load_xml_source(self,src,emu,section):
        if src == None:
            return
        self.log("00",_("Initializing ..."))
        self.log("20","0.125")
        xslfile=os.path.join(config.get_dir('config'),emu.get(section,"xsl"))
        if not os.access(xslfile,os.R_OK):
            return
        tempfile=mkstemp()
        os.write(tempfile[0],src.read())

        self.log("00",_("Parsing XSL file (%s)...") % xslfile)
        styledoc = parseFile(xslfile)
        style = parseStylesheetDoc(styledoc)
        self.log("00",_("Parsing xml output ..."))
        self.log("20","0.250")
        doc = parseFile(tempfile[1])
        self.log("00",_("Applying XSL (%s) ...") % xslfile)
        self.log("20","0.375")
        result = style.applyStylesheet(doc, None)

        self.log("00",_("Saving result ..."))
        self.log("20","0.500")
        str= style.saveResultToString(result)
        style.freeStylesheet()
        doc.freeDoc()
        result.freeDoc()

        self.log("00",_("Initializing result ..."))
        self.log("20","0.625")
        sio=StringIO(str)
        handler = XMLHandler(config,lambda x: emu.get("General","label"))
        saxparser = make_parser()
        saxparser.setContentHandler(handler)
        self.log("00",_("Parsing result ..."))
        self.log("20","0.750")
        saxparser.parse(sio)

        self.log("00",_("Finalizing ..."))
        self.log("20","0.875")
        ilabel=emu.get("General","identity")
        for sublist in handler.get_list().values():
            for game in sublist.values():
                ident=game[ilabel].strip()
                if not self.current.has_key(ident):
                    self.current[ident]=dict()
                    for field in config.fields:
                        self.current[ident][field]=""
                for key,value in game.iteritems():
                    if self.current[ident][key] == "":
                        self.current[ident][key]=value
        self.log("20","1.0")
        return True

    def load_plain_source(self,src,emu,section):
        if src == None:
            return
        self.log("20","0.0")
        self.log("00",_("Initializing ..."))
        ilabel=emu.get("General","identity")
        rx=re.compile(emu.get(section,"pattern"),re.I)
        current_game=0.0
        all_games=float(len(self.current)) or -1
        for line in src:
            match=rx.search(line)
            if match != None:
                current_game=current_game+1.0
                ident=match.group(ilabel)
                self.log("00",_("Parsing %s ...") % ident)
                self.log("20",str(float(current_game/all_games)))
                if not self.current.has_key(ident):
                    self.current[ident]=dict()
                    for field in config.fields:
                        self.current[ident][field]=""
                    self.current[ident][ilabel]=ident
                for key,value in match.groupdict().iteritems():
                    if key != ilabel and self.current[ident][key]=="":
                        self.current[ident][key]=value
        return

    def write(self):
        self.log("90",_("Writing ..."))
        self.log("11",config.get("General","gamelist"))
        self.log("00"," ")
        signal.signal(signal.SIGTERM,signal.SIG_IGN)
        format = config.get("General","gamelist_format")
        if  format == "xml":
            self.write_xml(config.get("General","gamelist"))
        elif format == "python":
            dump(self.wlist,open(config.get("General","gamelist"),"w"))
        else:
            self.log("99",_("Unknown gamelist format") + " ['xml','python']: " + format)
            sys.exit(1)

    def write_xml(self,file):
        self.log("20","-1")
        fields=config.generic_fields
        fd=open(file,'w')
        fd.write('<?xml version="1.0" ?>\n')
        fd.write('<gamelist version="1.1" size="%d">\n' % len(self.wlist))
        current_game=0.0
        all_games=float(len(self.wlist)) or -1
        for game in self.wlist:
            current_game=current_game+1.0
            self.log("20",str(float(current_game/all_games)))
            raw = '  <game'
            for field in fields:
                raw += ' %s="%s"' % (field, escape(game[field].encode('utf-8')))
            raw += ' />\n'
            fd.write(raw)
        fd.write('</gamelist>\n')
        fd.close()
        return

    def log(self,level,msg):
        self.out.write(level + " " + escape(msg) + "\n")
        self.out.flush()
        return

    def signal_handler(self,signum,frame):
        sys.exit(0)
        return
