## vim:ts=4:et:nowrap
##
##---------------------------------------------------------------------------##
##
## PySol -- a Python Solitaire game
##
## Copyright (C) 2003 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 2002 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 2001 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 2000 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 1999 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 1998 Markus Franz Xaver Johannes Oberhumer
## All Rights Reserved.
##
## 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; see the file COPYING.
## If not, write to the Free Software Foundation, Inc.,
## 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
##
## Markus F.X.J. Oberhumer
## <markus@oberhumer.com>
## http://www.oberhumer.com/pysol
##
##---------------------------------------------------------------------------##


# imports
import sys, os, re, string, time, types
import traceback

# PySol imports
from mfxtools import *
from mfxutil import destruct, dumpmem, Struct
from mfxutil import pickle, unpickle, Unpickler, UnpicklingError
from mfxutil import merge_dict, getusername, gethomedir, getprefdir, EnvError
from mfxutil import latin1_to_ascii
from util import DataLoader, Timer, bundle, cyclops
from util import PACKAGE, VERSION, VERSION_TUPLE, CARDSET
from util import IMAGE_EXTENSIONS
from resource import CSI, CardsetConfig, Cardset, CardsetManager
from resource import Tile, TileManager
from resource import Sample, SampleManager
from resource import Music, MusicManager
from images import Images, SubsampledImages
from random import LCRandom64
from game import Game
from gamedb import GI, GAME_DB, loadGame

# Toolkit imports
from pysoltk import tkname, tkversion, wm_withdraw, wm_set_icon, loadImage
from pysoltk import bind, unbind_destroy
from pysoltk import MfxDialog, MfxExceptionDialog
from pysoltk import TclError, MfxRoot, MfxCanvas, MfxScrolledCanvas
from pysoltk import PysolMenubar
from pysoltk import PysolProgressBar
from pysoltk import PysolToolbar
from pysoltk import PysolStatusbar
from pysoltk import SelectCardsetByTypeDialogWithPreview
from help import helpAbout



# really *dirty* hack...
class PysolScrolledCanvas(MfxScrolledCanvas, MfxCanvas):
    def createCanvas(self, kw):
        apply(MfxCanvas.__init__, (self, self.frame), kw)
        self.canvas = self
        self.canvas.grid(row=0, column=0, sticky="news")

    def setInitialSize(self, width, height):
        ##print "setInitialSize", width, height
        if self.canvas.winfo_ismapped():
            self.resize_pending = 1
        self.frame.config(width=width, height=height)
        self.canvas.config(width=width, height=height)
        self.canvas.config(scrollregion=(0, 0, width, height))

    def bindHbar(self, w=None):
        pass
    def bindVbar(self, w=None):
        pass


# /***********************************************************************
# // Options
# ************************************************************************/

class Options:
    def __init__(self):
        self.version_tuple = VERSION_TUPLE
        self.saved = 0
        # options menu:
        self.player = "unknown"
        self.confirm = 1
        self.update_player_stats = 1
        self.autofaceup = 1
        self.autodrop = 0
        self.autodeal = 1
        self.quickplay = 1
        self.undo = 1
        self.bookmarks = 1
        self.hint = 1
        self.highlight_piles = 1
        self.highlight_cards = 1
        self.highlight_samerank = 1
        self.tablecolor = "#008200"
        self.animations = 2                     # default to Timer based
        self.shadow = 1
        self.shade = 1
        self.hint_sleep = 1.5
        self.demo_sleep = 1.5
        self.demo_logo = 1
        self.demo_score = 0
        self.toolbar = 1
        self.toolbar_size = 0
        self.statusbar = 1
        self.sound = 1
        self.sound_mode = 1
        self.sound_sample_volume = 128
        self.sound_music_volume = 128
        # additional startup information
        self.recent_gameid = []
        self.last_gameid = 0        # last game played
        self.last_player = None     # last player
        self.last_save_dir = None   # last directory for load/save
        self.game_holded = 0
        self.wm_maximized = 0
        # defaults & constants
        self.setDefaults()
        self.setConstants()

    def setDefaults(self, top=None):
        sw, sh, sd = 0, 0, 8
        if top:
            sw, sh, sd = top.winfo_screenwidth(), top.winfo_screenheight(), top.winfo_screendepth()
        if sd > 8:
            self.tabletile_name = "Fade_Green.ppm"  # basename
        else:
            self.tabletile_name = None
        #
        c = "Oxymoron"
##        c = "Standard"
        if 0 < sw < 800 or 0 < sh < 600:
            c = "2000"
##        elif sw >= 1024 and sh >= 768 and sd > 8:
##            c = "Dondorf Whist A"
        self.cardset = {
            0:                  (c, ""),
            CSI.TYPE_FRENCH:    (c, ""),
            CSI.TYPE_HANAFUDA:  ("Kintengu", ""),
            ##CSI.TYPE_MAHJONGG:  ("KMahjongg", ""),
            CSI.TYPE_TAROCK:    ("Vienna 2K", ""),
            CSI.TYPE_HEXADECK:  ("Hex A Deck", ""),
            ##CSI.TYPE_MUGHAL_GANJIFA: ("Mughal Ganjifa", ""),
            CSI.TYPE_MUGHAL_GANJIFA: ("Dashavatara Ganjifa", ""),
            ##CSI.TYPE_NAVAGRAHA_GANJIFA: ("Navagraha Ganjifa", ""),
            CSI.TYPE_NAVAGRAHA_GANJIFA: ("Dashavatara Ganjifa", ""),
            CSI.TYPE_DASHAVATARA_GANJIFA: ("Dashavatara Ganjifa", ""),
            ##CSI.TYPE_TRUMP_ONLY: ("", ""),
        }

    # not changeable options
    def setConstants(self):
        self.win_animation = 1
        self.toolbar_relief = 1
        self.dragcursor = 1
        self.magnetic_mouse = 0             # doesn't work yet anyway
        self.magnetic_mouse_time = 2.0      # seconds
        self.raise_card_sleep = 1.0
        self.highlight_piles_sleep = 1.5
        self.highlight_piles_colors = (None, "#ffc000")
        self.highlight_cards_sleep = 1.5
        self.highlight_cards_colors = (None, "#ffc000", None, "#0000ff")
        self.highlight_samerank_sleep = 1.5
        self.highlight_samerank_colors = (None, "#ffc000", None, "#0000ff")
        self.hintarrow_color = "#303030"
        if PACKAGE == "PyJongg":
            self.highlight_samerank = 0
            self.shadow = 0
            self.shade = 0

    def copy(self):
        opt = Options()
        merge_dict(opt.__dict__, self.__dict__)
        opt.setConstants()
        return opt


# /***********************************************************************
# // Statistics
# ************************************************************************/

class Statistics:
    def __init__(self):
        self.version_tuple = VERSION_TUPLE
        self.saved = 0
        # a dictionary of dictionaries of tuples (won/lost),
        # indexed by player and gameid
        self.stats = {}
        self.demo_stats = {}
        # a dictionary of lists of tuples (key: player)
        self.prev_games = {}
        self.all_prev_games = {}
        self.session_games = {}
        # some simple balance scores (key: gameid)
        self.total_balance = {}     # a dictionary of integers
        self.session_balance = {}   # reset per session
        self.gameid_balance = 0     # reset when changing the gameid

    def new(self):
        return Statistics()

    #
    # player & demo statistics
    #

    def resetStats(self, player, gameid):
        self.__resetPrevGames(player, self.prev_games, gameid)
        self.__resetPrevGames(player, self.session_games, gameid)
        if not self.stats.has_key(player):
            return
        if gameid == 0:
            del self.stats[player]
        else:
            w0, l0 = self.getStats(player, 0)
            w1, l1 = self.getStats(player, gameid)
            self.stats[player][0] = (w0 - w1, l0 - l1)
            self.stats[player][gameid] = (0, 0)

    def __resetPrevGames(self, player, games, gameid):
        if not games.has_key(player):
            return
        if gameid == 0:
            del games[player]
        else:
            games[player] = filter(lambda a, b=gameid: a[0] != b, games[player])

    def getStats(self, player, gameid):
        d = self.stats.get(player)
        if d is None:
            # create an entry for this player
            d = self.stats[player] = {}
        return d.get(gameid, (0,0))

    def updateStats(self, player, game, status):
        won, lost = status > 0, status == 0
        w, l = self.getStats(player, 0)               # all games
        self.stats[player][0] = (w + won, l + lost)
        w, l = self.getStats(player, game.id)
        self.stats[player][game.id] = (w + won, l + lost)
        self.updateLog(player, game, status)

    def updateLog(self, player, game, status):
        log = (game.id, game.getGameNumber(format=0), status,
               game.gstats.start_time, game.gstats.total_elapsed_time,
               VERSION_TUPLE, game.getGameScore(), game.getGameScoreCasino(),
               game.GAME_VERSION)
        # full log
        if player is not None and status >= 0:
            if not self.prev_games.has_key(player):
                self.prev_games[player] = []
            self.prev_games[player].append(log)
            if not self.all_prev_games.has_key(player):
                self.all_prev_games[player] = []
            self.all_prev_games[player].append(log)
        # session log
        if not self.session_games.has_key(player):
            self.session_games[player] = []
        self.session_games[player].append(log)


# /***********************************************************************
# // Comments
# ************************************************************************/

class Comments:
    def __init__(self):
        self.version_tuple = VERSION_TUPLE
        self.saved = 0
        #
        self.comments = {}

    def new(self):
        return Comments()

    def setGameComment(self, gameid, text):
        player = None
        key = (1, gameid, player)
        self.comments[key] = str(text)

    def getGameComment(self, gameid):
        player = None
        key = (1, gameid, player)
        return self.comments.get(key, "")


# /***********************************************************************
# // Application
# // This is the glue between the toplevel window and a Game.
# // Also handles all global resources.
# ************************************************************************/

class Application:
    def __init__(self):
        self.starttimer = Timer("Application.__init__")
        self.gdb = GAME_DB
        self.opt = Options()
        self.startup_opt = self.opt.copy()
        self.stats = Statistics()
        self.comments = Comments()
        self.splashscreen = 1
        self.debug = 0
        # visual components
        self.top = None                 # the root toplevel window
        self.top_bg = None              # default background
        self.top_cursor = None          # default cursor
        self.menubar = None
        self.toolbar = None
        self.canvas = None
        self.statusbar = None
        #
        self.game = None
        self.dataloader = None
        self.audio = None
        self.images = None
        self.subsampled_images = None
        self.gimages = Struct(          # global images
            border = [],
            demo = [],                  # demo logos
            logos = [],
            redeal = [],
            shade = [],
            stats = [],
        )
        self.progress_bg = None
        self.progress_images = []
        self.cardset_manager = CardsetManager()
        self.cardset = None             # current cardset
        self.tabletile_manager = TileManager()
        self.tabletile_index = 0        # current table tile
        self.sample_manager = SampleManager()
        self.music_manager = MusicManager()
        self.music_playlist = []
        self.intro = Struct(
            progress = None,            # progress bar
        )
        # directory names
        home = os.path.normpath(gethomedir())
        config = os.path.normpath(getprefdir(PACKAGE, home))
        self.dn = Struct(
            home = home,
            config = config,
            plugins = os.path.join(config, "plugins"),
            savegames = os.path.join(config, "savegames"),
            maint = os.path.join(config, "maint"),          # debug
        )
        for k, v in self.dn.__dict__.items():
##            if os.name == "nt":
##                v = os.path.normcase(v)
            v = os.path.normpath(v)
            self.dn.__dict__[k] = v
        # file names
        self.fn = Struct(
            opt   = os.path.join(self.dn.config, "options.dat"),
            stats = os.path.join(self.dn.config, "statistics.dat"),
            holdgame = os.path.join(self.dn.config, "holdgame.dat"),
            comments = os.path.join(self.dn.config, "comments.dat"),
        )
        for k, v in self.dn.__dict__.items():
            if os.name == "nt":
                v = os.path.normcase(v)
            v = os.path.normpath(v)
            self.fn.__dict__[k] = v
        # random generators
        self.gamerandom = LCRandom64()
        self.miscrandom = LCRandom64()
        # player
        player = getusername()
        if not player:
            player = "unknown"
        player = player[:30]
        self.opt.player = player
        # misc
        self.nextgame = Struct(
            id = 0,                     # start this game
            random = None,              # use this random generator
            loadedgame = None,          # data for loaded game
            startdemo = 0,              # start demo ?
            cardset = None,             # use this cardset
            holdgame = 0,               # hold this game on exit ?
            bookmark = None,            # goto this bookmark (load new cardset)
        )
        self.commandline = Struct(
            loadgame = None,            # load a game ?
        )
        self.demo_counter = 0


    # the PySol mainloop
    def mainloop(self):
        # copy startup options
        self.startup_opt = self.opt.copy()
        # try to load statistics
        try: self.loadStatistics()
        except: pass
        # try to load comments
        try: self.loadComments()
        except: pass
        # startup information
        if self.getGameClass(self.opt.last_gameid):
            self.nextgame.id = self.opt.last_gameid
        # load a holded or saved game
        id = self.gdb.getGamesIdSortedByName()[0]
        tmpgame = self.constructGame(id)
        if self.opt.game_holded > 0 and not self.nextgame.loadedgame:
            game = None
            try:
                game = tmpgame._loadGame(self.fn.holdgame, self)
            except:
                game = None
            if game:
                if game.id == self.opt.game_holded and game.gstats.holded:
                    game.gstats.loaded = game.gstats.loaded - 1
                    game.gstats.holded = 0
                    self.nextgame.loadedgame = game
                else:
                    # not a holded game
                    game.destruct()
                    destruct(game)
            game = None
        if self.commandline.loadgame and not self.nextgame.loadedgame:
            try:
                self.nextgame.loadedgame = tmpgame._loadGame(self.commandline.loadgame, self)
                self.nextgame.loadedgame.gstats.holded = 0
            except:
                self.nextgame.loadedgame = None
        self.opt.game_holded = 0
        tmpgame.destruct()
        destruct(tmpgame)
        tmpgame = None
        # create the menubar
        self.menubar = PysolMenubar(self, self.top)
        # create the statusbar (before the toolbar)
        self.statusbar = PysolStatusbar(self.top)
        self.statusbar.show(self.opt.statusbar)
        # create the toolbar
        dir = self.getToolbarImagesDir(self.opt.toolbar_size)
        self.toolbar = PysolToolbar(self.top, dir=dir, size=self.opt.toolbar_size)
        self.toolbar.setRelief(self.opt.toolbar_relief)
        self.toolbar.show(self.opt.toolbar)
        if self.intro.progress: self.intro.progress.update(step=1)
        # create the canvas
        ##self.canvas = MfxCanvas(self.top)
        self.canvas = PysolScrolledCanvas(self.top)
        self.canvas.pack(fill="both", expand=1)
        self.setTile(self.tabletile_index, force=1)
        try:
            # this is the mainloop
            while 1:
                assert self.cardset is not None
                id, random = self.nextgame.id, self.nextgame.random
                self.nextgame.id, self.nextgame.random = 0, None
                self.runGame(id, random)
                if self.nextgame.holdgame:
                    assert self.nextgame.id <= 0
                    try:
                        self.game.gstats.holded = 1
                        self.game._saveGame(self.fn.holdgame)
                        self.opt.game_holded = self.game.id
                    except: pass
                self.freeGame()
                #
                if self.nextgame.id <= 0:
                    break
                # load new cardset
                if self.nextgame.cardset is not self.cardset:
                    self.loadCardset(self.nextgame.cardset, id=self.nextgame.id, update=7+256)
                else:
                    self.requestCompatibleCardsetType(self.nextgame.id)
        finally:
            # update options
            self.opt.last_gameid = id
            try: self.wm_save_state()
            except: pass
            # save options
            try: self.saveOptions()
            except: pass
            # save statistics
            try: self.saveStatistics()
            except: pass
            # save comments
            try: self.saveComments()
            except: pass
            # shut down audio
            try: self.audio.destroy()
            except: pass


    def runGame(self, id, random=None):
        self.top.connectApp(self)
        # create game instance
        g = self.getGameClass(id)
        if g is None:
            id = 2          # start Klondike as default game
            random = None
            g = self.getGameClass(id)
            if g is None:
                # start first available game
                id = self.gdb.getGamesIdSortedByName()[0]
                g = self.getGameClass(id)
        gi = self.getGameInfo(id)
        assert g and type(g) is types.ClassType and id > 0
        assert gi is not None and gi.id == id
        self.game = self.constructGame(id)
        self.gdb.setSelected(id)
        self.game.busy = 1
        # create stacks and layout
        self.game.create(self)
        # check screen/cardset size
        if not self.nextgame.startdemo and not self.nextgame.bookmark:
            if self.requestCompatibleCardsetSize() > 0:
                self.nextgame.id = id
                self.nextgame.random = random
                return
        # connect with game
        self.menubar.connectGame(self.game)
        self.toolbar.connectGame(self.game, self.menubar)
        self.game.updateStatus(player=self.opt.player)
        # update "Recent games" menubar entry
        while 1:
            try:
                self.opt.recent_gameid.remove(id)
            except ValueError:
                break
        self.opt.recent_gameid.insert(0, id)
        del self.opt.recent_gameid[15:]
        self.menubar.updateRecentGamesMenu(self.opt.recent_gameid)
        # delete intro progress bar
        if self.intro.progress:
            self.intro.progress.destroy()
            destruct(self.intro.progress)
            self.intro.progress = None
        # prepare game
        autoplay = 0
        if self.nextgame.loadedgame is not None:
            self.stats.gameid_balance = 0
            self.game.restoreGame(self.nextgame.loadedgame)
            destruct(self.nextgame.loadedgame)
        elif self.nextgame.bookmark is not None:
            self.game.restoreGameFromBookmark(self.nextgame.bookmark)
        else:
            self.stats.gameid_balance = 0
            self.game.newGame(random=random, autoplay=0)
            autoplay = 1
        self.nextgame.loadedgame = None
        self.nextgame.bookmark = None
        # splash screen
        if self.debug or bundle & 4:
            pass
        elif self.splashscreen > 0:
            status = helpAbout(self, timeout=20000, sound=0)
            if status == 2:                 # timeout - start a demo
                if autoplay:
                    self.nextgame.startdemo = 1
        self.splashscreen = 0
        # start demo/autoplay
        if self.nextgame.startdemo:
            self.nextgame.startdemo = 0
            self.game.startDemo()
            self.game.createDemoInfoText()
        elif autoplay:
            self.game.autoPlay()
            self.game.stats.player_moves = 0
        # enter the Tk mainloop
        self.game.busy = 0
        self.top.mainloop()


    # free game
    def freeGame(self):
        # disconnect from game
        self.toolbar.connectGame(None, None)
        self.menubar.connectGame(None)
        # clean up the canvas
        unbind_destroy(self.canvas)
        self.canvas.deleteAllItems()
        self.canvas.update_idletasks()
        # destruct the game
        if self.game:
            self.game.destruct()
            destruct(self.game)
        self.game = None
        self.top.connectApp(None)


    # debug
    def dumpMem(self, info=""):
        return
        if info: print "App.dumpMem: " + info
        print "freeGame:\n"; dumpmem(); print
        if 1 and cyclops: cyclops.clear()
        if 1 and cyclops: cyclops.register(self)
        if 1 and cyclops: cyclops.find_cycles()
        if 1 and cyclops: cyclops.show_stats()
        #if 1 and cyclops: cyclops.show_arcs()
        #if 1 and cyclops: cyclops.show_sccs()
        #if 1 and cyclops: cyclops.show_cycles()
        if 1 and cyclops: cyclops.clear()
        pass


    #
    # UI support
    #

    def wm_save_state(self):
        if self.top:
            s = self.top.wm_state()
            ##print "wm_save_state", s
            if s == "zoomed":
                self.opt.wm_maximized = 1
            elif s == "normal":
                self.opt.wm_maximized = 0

    def wm_withdraw(self):
        if self.intro.progress:
            self.intro.progress.destroy()
            destruct(self.intro.progress)
            self.intro.progress = None
        if self.top:
            wm_withdraw(self.top)
            self.top.busyUpdate()

    def loadImages1(self):
        dir = os.path.join("images", "logos")
        self.gimages.logos.append(loadImage(self.dataloader.findImage("joker07_40_774", dir)))
        self.gimages.logos.append(loadImage(self.dataloader.findImage("joker08_40_774", dir)))
        self.gimages.logos.append(self.dataloader.findImage("joker07_50_774", dir))
        self.gimages.logos.append(self.dataloader.findImage("joker08_50_774", dir))
        self.gimages.logos.append(self.dataloader.findImage("joker11_100_774", dir))
        self.gimages.logos.append(self.dataloader.findImage("joker10_100", dir))
        self.gimages.logos.append(self.dataloader.findImage("pysol_40", dir))
        dir = "images"
        ##for f in ("noredeal", "redeal",):
        for f in ("stopsign", "redeal",):
            self.gimages.redeal.append(self.dataloader.findImage(f, dir))

    def loadImages2(self):
        dir = os.path.join("images", "demo")
        for f in ("demo01", "demo02", "demo03", "demo04", "demo05",):
            self.gimages.demo.append(self.dataloader.findImage(f, dir))
        dir = os.path.join("images", "stats")
        for f in ("barchart",):
            self.gimages.stats.append(self.dataloader.findImage(f, dir))

    def loadImages3(self):
        pass

    def loadImages4(self):
        # load all remaining images
        for k, v in self.gimages.__dict__.items():
            if type(v) is types.ListType:
                for i in range(len(v)):
                    if type(v[i]) is types.StringType:
                        v[i] = loadImage(v[i])
                        if self.intro.progress:
                            self.intro.progress.update(step=1)
                self.gimages.__dict__[k] = tuple(v)

    def getToolbarImagesDir(self, size):
        dir = "small"
        if size: dir = "large"
        dir = os.path.join(self.dataloader.dir, "toolbar", dir)
        return dir

    def setTile(self, i, force=0):
        tile = self.tabletile_manager.get(i)
        if tile is None or tile.error:
            return 0
        ##print i, tile
        if i == 0:
            assert tile.color
            assert tile.filename is None
        else:
            assert tile.color is None
            assert tile.filename
            assert tile.basename
        if not force:
            if i == self.tabletile_index and tile.color == self.opt.tablecolor:
                return 0
        #
        if not self.canvas.setTile(tile.filename):
            tile.error = 1
            return 0
        #
        if i == 0:
            self.canvas.config(bg=tile.color)
            self.top.config(bg=tile.color)
            self.canvas.setTextColor(None)
            self.opt.tablecolor = tile.color
            self.opt.tabletile_name = None
        else:
            bg = self.top_bg
            self.canvas.config(bg=bg)
            self.top.config(bg=bg)
            self.canvas.setTextColor(tile.text_color)
            self.opt.tabletile_name = tile.basename
        self.tabletile_index = i
        self.tabletile_manager.setSelected(i)
        return 1


    #
    # cardset
    #

    def updateCardset(self, id=0, update=7):
        cs = self.images.cs
        self.cardset = cs
        self.nextgame.cardset = cs
        # update settings
        self.cardset_manager.setSelected(cs.index)
        # update options
        if update & 1:
            self.opt.cardset[0] = (cs.name, cs.backname)
        if update & 2:
            self.opt.cardset[cs.si.type] = (cs.name, cs.backname)
        gi = self.getGameInfo(id)
        if gi:
            if update & 256:
                try:
                    del self.opt.cardset[(1, gi.id)]
                except KeyError:
                    pass
            t = self.checkCompatibleCardsetType(gi, cs)
            if not t[1]:
                if update & 4:
                    self.opt.cardset[gi.category] = (cs.name, cs.backname)
                if update & 8:
                    self.opt.cardset[(1, gi.id)] = (cs.name, cs.backname)
        ##print self.opt.cardset

    def loadCardset(self, cs, id=0, update=7, progress=None):
        r = 0
        if cs is None or cs.error:
            return 0
        if cs is self.cardset:
            self.updateCardset(id, update=update)
            return 1
        if progress is None:
            self.wm_save_state()
            self.wm_withdraw()
            title = "Loading "+CARDSET+" "+ cs.name + "..."
            color = self.opt.tablecolor
            if self.tabletile_index > 0:
                color = "#008200"
            progress = PysolProgressBar(self, self.top, title=title, color=color, bg=self.progress_bg, images=self.progress_images)
        images = Images(self.dataloader, cs)
        try:
            if not images.load(app=self, progress=progress):
                raise Exception, "Invalid or damaged "+CARDSET
            simages = SubsampledImages(images)
            # update
            if self.images is not None:
                self.images.destruct()
                destruct(self.images)
            self.images = images
            self.subsampled_images = simages
            self.updateCardset(id, update=update)
            r = 1
        except (Exception, TclError, UnpicklingError), ex:
            cs.error = 1
            # restore settings
            self.nextgame.cardset = self.cardset
            if self.cardset:
                self.cardset_manager.setSelected(self.cardset.index)
            images.destruct()
            destruct(images)
            d = MfxExceptionDialog(self.top, ex, title=CARDSET+" load error",
                                   text="Error while loading "+CARDSET)
        self.intro.progress = progress
        if r and self.menubar is not None:
            self.menubar.updateBackgroundImagesMenu()
        return r

    def checkCompatibleCardsetType(self, gi, cs):
        assert gi is not None
        assert cs is not None
        gc = gi.category
        cs_type = cs.si.type
        t0, t1 = None, None
        if gc == GI.GC_FRENCH:
            t0 = "French"
            if cs_type not in (CSI.TYPE_FRENCH, CSI.TYPE_TAROCK,):
                t1 = t0
        elif gc == GI.GC_HANAFUDA:
            t0 = "Hanafuda"
            if cs_type not in (CSI.TYPE_HANAFUDA,):
                t1 = t0
        elif gc == GI.GC_TAROCK:
            t0 = "Tarock"
            if cs_type not in (CSI.TYPE_TAROCK,):
                t1 = t0
        elif gc == GI.GC_MAHJONGG:
            t0 = "Mahjongg"
            if cs_type not in (CSI.TYPE_MAHJONGG,):
                t1 = t0
        elif gc == GI.GC_HEXADECK:
            t0 = "Hex A Deck"
            if cs_type not in (CSI.TYPE_HEXADECK,):
                t1 = t0
        elif gc == GI.GC_MUGHAL_GANJIFA:
            t0 = "Mughal Ganjifa"
            if cs_type not in (CSI.TYPE_MUGHAL_GANJIFA, CSI.TYPE_NAVAGRAHA_GANJIFA, CSI.TYPE_DASHAVATARA_GANJIFA,):
                t1 = t0
        elif gc == GI.GC_NAVAGRAHA_GANJIFA:
            t0 = "Navagraha Ganjifa"
            if cs_type not in (CSI.TYPE_NAVAGRAHA_GANJIFA, CSI.TYPE_DASHAVATARA_GANJIFA,):
                t1 = t0
        elif gc == GI.GC_DASHAVATARA_GANJIFA:
            t0 = "Dashavatara Ganjifa"
            if cs_type not in (CSI.TYPE_DASHAVATARA_GANJIFA,):
                t1 = t0
        elif gc == GI.GC_TRUMP_ONLY:
            t0 = "Trump only"
            if cs_type not in (CSI.TYPE_TRUMP_ONLY,):
                t1 = t0
            elif len(cs.trumps) < gi.ncards:    # not enough cards
                t1 = t0
        else:
            # we should not come here
            t0 = t1 = "Unknown"
        return t0, t1

    def getCompatibleCardset(self, gi, cs):
        if gi is None:
            return cs, 1
        # try current
        if cs:
            t = self.checkCompatibleCardsetType(gi, cs)
            if not t[1]:
                return cs, 1
        # try by gameid / category
        for key, flag in (((1, gi.id), 8), (gi.category, 4)):
            c = self.opt.cardset.get(key)
            if not c or len(c) != 2:
                continue
            cs = self.cardset_manager.getByName(c[0])
            if not cs:
                continue
            t = self.checkCompatibleCardsetType(gi, cs)
            if not t[1]:
                cs.updateCardback(backname=c[1])
                return cs, flag
        # ask
        return None, 0

    def requestCompatibleCardsetType(self, id):
        gi = self.getGameInfo(id)
        #
        cs, cs_update_flag = self.getCompatibleCardset(gi, self.cardset)
        if cs is self.cardset:
            return 0
        self.wm_save_state()
        self.wm_withdraw()
        if cs is not None:
            self.loadCardset(cs, update=1)
            return 1
        #
        t = self.checkCompatibleCardsetType(gi, self.cardset)
        d = MfxDialog(self.top, title="Incompatible "+CARDSET, bitmap="warning",
                      text="The currently selected "+CARDSET+"\n" + self.cardset.name
                        + "\n\nis not compatible with the game\n"
                        + gi.name + "\n\nPlease select a " + t[0] + " type "+CARDSET+".",
                      strings=("OK",), default=0)
        cs = self.__selectCardsetDialog(t)
        if cs is None:
            return -1
        self.loadCardset(cs, id=id)
        return 1

    def requestCompatibleCardsetSize(self):
        # we don't need this check any longer now that we have a ScrolledCanvas
        return 0

    def __selectCardsetDialog(self, t):
        key = self.cardset.index
        d = SelectCardsetByTypeDialogWithPreview(self.top, title="Please select a " + t[0] + " type "+CARDSET,
                app=self, manager=self.cardset_manager, key=key,
                strings=(None, "OK", "Cancel"), default=1)
        if d.status != 0 or d.button != 1:
            return None
        cs = self.cardset_manager.get(d.key)
        if cs is None or d.key == key:
            return None
        return cs


    #
    # load & save options, statistics and comments
    #

    def loadOptions(self):
        self.opt.setDefaults(self.top)
        opt = unpickle(self.fn.opt)
        if opt:
            ##print "loaded:", opt.__dict__
            cardset = self.opt.cardset
            if hasattr(opt, "version_tuple") and hasattr(opt, "cardset"):
                merge_dict(cardset, opt.cardset)
            if hasattr(opt, "player"):
                self.opt.player = ustr(opt.player)   # opt.player can be unicode
            merge_dict(self.opt.__dict__, opt.__dict__)
            self.opt.cardset = cardset
        self.opt.setConstants()

    def loadStatistics(self):
        stats = unpickle(self.fn.stats)
        if stats:
            ##print "loaded:", stats.__dict__
            merge_dict(self.stats.__dict__, stats.__dict__)
        # start a new session
        self.stats.session_games = {}
        self.stats.session_balance = {}
        self.stats.gameid_balance = 0

    def loadComments(self):
        comments = unpickle(self.fn.comments)
        if comments:
            ##print "loaded:", comments.__dict__
            merge_dict(self.comments.__dict__, comments.__dict__)

    def __saveObject(self, obj, fn):
        obj.version_tuple = VERSION_TUPLE
        obj.saved = obj.saved + 1
        pickle(obj, fn, binmode=1)

    def saveOptions(self):
        self.__saveObject(self.opt, self.fn.opt)

    def saveStatistics(self):
        self.__saveObject(self.stats, self.fn.stats)

    def saveComments(self):
        self.__saveObject(self.comments, self.fn.comments)


    #
    # access games database
    #

    def constructGame(self, id):
        gi = self.gdb.get(id)
        if gi is None:
            raise Exception, "Unknown game (id %d)" % id
        return gi.gameclass(gi)

    def getGamesIdSortedById(self):
        return self.gdb.getGamesIdSortedById()

    def getGamesIdSortedByName(self):
        return self.gdb.getGamesIdSortedByName()

    def getGameInfo(self, id):
        return self.gdb.get(id)

    def getGameClass(self, id):
        gi = self.gdb.get(id)
        if gi is None: return None
        return gi.gameclass

    def getGameTitleName(self, id):
        gi = self.gdb.get(id)
        if gi is None: return None
        return gi.name

    def getGameMenuitemName(self, id):
        gi = self.gdb.get(id)
        if gi is None: return None
        return gi.short_name

    def getGameRulesFilename(self, id):
        gi = self.gdb.get(id)
        if gi is None: return None
        if gi.rules_filename is not None:
            return gi.rules_filename
        n = gi.name
        n = re.sub(r"[\[\(].*$", "", n)
        n = latin1_to_ascii(n)
        n = re.sub(r"[^\w]", "", n)
        n = string.lower(n) + ".html"
        gi.rules_filename = n    # cache the filename for next use
        return n

    def getGameSaveName(self, id):
        n = self.getGameTitleName(id)
        if not n: return None
        m = re.search(r"^(.*)([\[\(](\w+).*[\]\)])\s*$", n)
        if m:
            n = m.group(1) + "_" + string.lower(m.group(2))
        n = latin1_to_ascii(n)
        return re.sub(r"[^\w\-]", "", n)

    def getRandomGameId(self):
        return self.miscrandom.choice(self.gdb.getGamesIdSortedById())


    #
    # plugins
    #

    def loadPlugins(self, dir):
        if not dir or not os.path.isdir(dir) or bundle & 4:
            return
        names = os.listdir(dir)
        names = map(os.path.normcase, names)
        names.sort()
        for name in names:
            m = re.search(r"^(.+)\.py$", name)
            n = os.path.join(dir, name)
            if m and os.path.isfile(n):
                p = sys.path[:]
                try:
                    loadGame(m.group(1), n)
                except Exception, ex:
                    print "Error loading plugin " + n + ": " + str(ex)
                    sys.stdout.flush()
                except:
                    pass
                sys.path = p


    #
    # init cardsets
    #

    # read & parse a cardset config.txt file - see class Cardset in util.py
    def _readCardsetConfig(self, dir, filename):
        f = None
        try:
            f = open(filename, "r")
            lines = f.readlines()
        finally:
            if f: f.close()
        lines = map(string.strip, lines)
        if string.find(lines[0], "PySol") != 0:
            return None
        config = CardsetConfig()
        if not self._parseCardsetConfig(config, lines):
            return None
        if config.CARDD > self.top.winfo_screendepth():
            return None
        cs = Cardset()
        cs.dir = dir
        cs.update(config.__dict__)
        return cs

    def _parseCardsetConfig(self, cs, line):
        if len(line) < 6:
            return 0
        # line[0]: magic identifier, possible version information
        fields = filter(None, re.split(r";", line[0]))
        fields = map(string.strip, fields)
        if len(fields) >= 2:
            m = re.search(r"^(\d+)$", fields[1])
            if m: cs.version = int(m.group(1))
        if cs.version >= 3:
            if len(fields) < 5:
                return 0
            cs.ext = fields[2]
            m = re.search(r"^(\d+)$", fields[3])
            if not m: return 0
            cs.type = int(m.group(1))
            m = re.search(r"^(\d+)$", fields[4])
            if not m: return 0
            cs.ncards = int(m.group(1))
        if cs.version >= 4:
            if len(fields) < 6:
                return 0
            styles = string.splitfields(fields[5], ",")
            for s in styles:
                m = re.search(r"^\s*(\d+)\s*$", s)
                if not m: return 0
                s = int(m.group(1))
                if not s in cs.styles:
                    cs.styles.append(s)
        if cs.version >= 5:
            if len(fields) < 7:
                return 0
            m = re.search(r"^(\d+)$", fields[6])
            if not m: return 0
            cs.year = int(m.group(1))
        if len(cs.ext) < 2 or cs.ext[0] != ".":
            return 0
        # line[1]: identifier/name
        if not line[1]: return 0
        cs.ident = line[1]
        m = re.search(r"^(.*;)?([^;]+)$", cs.ident)
        if not m: return 0
        cs.name = string.strip(m.group(2))
        # line[2]: CARDW, CARDH, CARDD
        m = re.search(r"^(\d+)\s+(\d+)\s+(\d+)", line[2])
        if not m: return 0
        cs.CARDW, cs.CARDH, cs.CARDD = int(m.group(1)), int(m.group(2)), int(m.group(3))
        # line[3]: CARD_UP_YOFFSET, CARD_DOWN_YOFFSET, SHADOW_XOFFSET, SHADOW_YOFFSET
        m = re.search(r"^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)", line[3])
        if not m: return 0
        cs.CARD_UP_YOFFSET = int(m.group(1))
        ###cs.CARD_DOWN_YOFFSET = int(m.group(2))   # not used
        cs.SHADOW_XOFFSET = int(m.group(3))
        cs.SHADOW_YOFFSET = int(m.group(4))
        # line[4]: default background
        back = line[4]
        if not back: return 0
        # line[5]: all available backgrounds
        cs.backnames = re.split(r";", line[5])
        cs.backnames = map(string.strip, cs.backnames)
        cs.backnames = filter(None, cs.backnames)
        if back in cs.backnames:
            cs.backindex = cs.backnames.index(back)
        else:
            cs.backnames.insert(0, back)
            cs.backindex = 0
        ##if cs.type != 1: print cs.type, cs.name
        return 1

    def initCardsets(self):
        manager = self.cardset_manager
        # find all available cardsets
        dirs = manager.getSearchDirs(self, ("cardsets", ""), "PYSOL_CARDSETS")
        if self.debug:
            dirs = dirs + manager.getSearchDirs(self, "cardsets-*")
        try:
            dirs = dirs + manager.getRegistryDirs(self, ("PySol_Cardsets", "Cardsets"))
        except:
            pass
        ##print dirs
        found, t = [], {}
        for dir in dirs:
            dir = string.strip(dir)
            try:
                names = []
                if dir and os.path.isdir(dir) and not t.has_key(dir):
                    t[dir] = 1
                    names = os.listdir(dir)
                    names.sort()
                for name in names:
                    m = re.search(r"^cardset-", name, re.I)
                    if not m: continue
                    d = os.path.join(dir, name)
                    if not os.path.isdir(d): continue
                    f1 = os.path.join(d, "config.txt")
                    f2 = os.path.join(d, "COPYRIGHT")
                    if os.path.isfile(f1) and os.path.isfile(f2):
                        try:
                            cs = self._readCardsetConfig(d, f1)
                            if cs:
                                ##print cs.name, cs.__dict__
                                back = cs.backnames[cs.backindex]
                                f1 = os.path.join(d, back)
                                f2 = os.path.join(d, "shade" + cs.ext)
                                if os.path.isfile(f1) and os.path.isfile(f2):
                                    found.append(cs)
                        except:
                            pass
            except EnvError, ex:
                pass
        # register cardsets
        for obj in found:
            if not manager.getByName(obj.name):
                manager.register(obj)
                ##print obj.index, obj.name


    #
    # init tiles
    #

    def initTiles(self):
        manager = self.tabletile_manager
        # find all available tiles
        dirs = manager.getSearchDirs(self, "tiles-*", "PYSOL_TILES")
        try:
            dirs = dirs + manager.getRegistryDirs(self, "Tiles")
        except:
            pass
        ##print dirs
        s = "((\\" + string.join(IMAGE_EXTENSIONS, ")|(\\") + "))$"
        ext_re = re.compile(s, re.I)
        text_color_re = re.compile(r"^(.+)-([0-9A-Fa-f]{6})$")
        found, t = [], {}
        for dir in dirs:
            dir = string.strip(dir)
            try:
                names = []
                if dir and os.path.isdir(dir):
                    names = os.listdir(dir)
                    names.sort()
                for name in names:
                    if not name or not ext_re.search(name):
                        continue
                    f = os.path.join(dir, name)
                    if not os.path.isfile(f):
                        continue
                    tile = Tile()
                    tile.filename = f
                    n = ext_re.sub("", string.strip(name))
                    m = text_color_re.search(n)
                    if m:
                        n = m.group(1)
                        tile.text_color = "#" + string.lower(m.group(2))
                    n = re.sub("_", " ", n)
                    tile.name = n
                    key = string.lower(n)
                    if not t.has_key(key):
                        t[key] = 1
                        found.append((n, tile))
            except EnvError, ex:
                pass
        # register tiles
        found.sort()
        for f in found:
            obj = f[1]
            if not manager.getByName(obj.name):
                manager.register(obj)


    #
    # init samples / music
    #

    def initResource(self, manager, dirs, ext_re, Resource_Class):
        found, t = [], {}
        for dir in dirs:
            dir = string.strip(dir)
            if dir:
                dir = os.path.normpath(dir)
            try:
                names = []
                if dir and os.path.isdir(dir):
                    names = os.listdir(dir)
                    names = map(os.path.normcase, names)
                    names.sort()
                for name in names:
                    if not name or not ext_re.search(name):
                        continue
                    f = os.path.join(dir, name)
                    f = os.path.normpath(f)
                    if not os.path.isfile(f):
                        continue
                    obj = Resource_Class()
                    obj.filename = f
                    n = ext_re.sub("", string.strip(name))
                    obj.name = n
                    key = string.lower(n)
                    if not t.has_key(key):
                        t[key] = 1
                        found.append((n, obj))
            except EnvError, ex:
                pass
        # register songs
        found.sort()
        if manager:
            for f in found:
                obj = f[1]
                if not manager.getByName(obj.name):
                    manager.register(obj)
        return found


    def initSamples(self):
        manager = self.sample_manager
        # find all available samples
        dirs = manager.getSearchDirs(self, ("sound", os.path.join("sound", "extra")))
        ##print dirs
        ext_re = re.compile(r"\.((wav))$", re.I)
        self.initResource(manager, dirs, ext_re, Sample)


    def initMusic(self):
        manager = self.music_manager
        # find all available music songs
        dirs = manager.getSearchDirs(self, "music-*", "PYSOL_MUSIC")
        try:
            dirs = dirs + manager.getRegistryDirs(self, "Music")
        except:
            pass
        ##print dirs
        ext_re = re.compile(r"\.((it)|(mod)|(mp3)|(pym)|(s3m)|(xm))$", re.I)
        self.initResource(manager, dirs, ext_re, Music)

