# -*- coding: utf-8 -*-

"""
Copyright (C) 2008-2012 Wolfgang Rohdewald <wolfgang@rohdewald.de>

kajongg 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 sys
import os
import datetime
from util import logError, m18n, m18nc, logWarning, logDebug
from common import WINDS, LIGHTSOURCES, Options, Internal, Preferences, isAlive
import cgitb, tempfile, webbrowser
from twisted.internet.defer import succeed, fail

class MyHook(cgitb.Hook):
    """override the standard cgitb hook: invoke the browser"""
    def __init__(self):
        self.tmpFileName = tempfile.mkstemp(suffix='.html', prefix='bt_', text=True)[1]
        cgitb.Hook.__init__(self, file=open(self.tmpFileName, 'w'))

    def handle(self, info=None):
        """handling the exception: show backtrace in browser"""
        cgitb.Hook.handle(self, info)
        webbrowser.open(self.tmpFileName)

#sys.excepthook = MyHook()

NOTFOUND = []

try:
    from PyQt4.QtCore import Qt, QVariant, \
        QEvent, QMetaObject, PYQT_VERSION_STR, QString
    from PyQt4.QtGui import QPushButton, QMessageBox
    from PyQt4.QtGui import QWidget
    from PyQt4.QtGui import QGridLayout, QAction
    from PyQt4.QtGui import QComboBox, QSlider, QHBoxLayout, QLabel
    from PyQt4.QtGui import QVBoxLayout, QSpacerItem, QSizePolicy, QCheckBox
except ImportError as importError:
    NOTFOUND.append('Package python-qt4: PyQt4: %s' % importError)

try:
    from zope.interface import implements # pylint: disable=unused-import
except ImportError as importError:
    NOTFOUND.append('Package python-zope-interface missing: %s' % importError)

from kde import QuestionYesNo, KIcon, KAction, KApplication, KToggleFullScreenAction, \
    KXmlGuiWindow, KConfigDialog, KStandardAction

try:
    from query import Query, Transaction
    from board import WindLabel, FittingView, SelectorBoard, DiscardBoard, MJScene
    from handboard import ScoringHandBoard
    from playerlist import PlayerList
    from tileset import Tileset
    from background import Background
    from games import Games
    from statesaver import StateSaver
    from hand import Hand
    from tile import Tile
    from uitile import UITile
    from meld import Meld, CONCEALED
    from scoring import ExplainView, ScoringDialog, ScoreTable
    from tables import SelectRuleset
    from client import Client
    from humanclient import HumanClient
    from rulesetselector import RulesetSelector
    from tilesetselector import TilesetSelector
    from backgroundselector import BackgroundSelector
    from sound import Sound
    from uiwall import UIWall
    from animation import animate, afterCurrentAnimationDo, Animated
    from player import Player, Players
    from game import Game
    from chat import ChatWindow

except ImportError as importError:
    NOTFOUND.append('kajongg is not correctly installed: modules: %s' % importError)

if len(NOTFOUND):
    MSG = "\n".join(" * %s" % s for s in NOTFOUND)
    logError(MSG)
    os.popen("kdialog --sorry '%s'" % MSG)
    sys.exit(3)

class PlayConfigTab( QWidget):
    """Display Config tab"""
    def __init__(self, parent):
        super(PlayConfigTab, self).__init__(parent)
        self.setupUi()

    def setupUi(self):
        """layout the window"""
        self.setContentsMargins(0, 0, 0, 0)
        vlayout = QVBoxLayout(self)
        vlayout.setContentsMargins(0, 0, 0, 0)
        sliderLayout = QHBoxLayout()
        self.kcfg_showShadows = QCheckBox(m18n('Show tile shadows'), self)
        self.kcfg_showShadows.setObjectName('kcfg_showShadows')
        self.kcfg_rearrangeMelds = QCheckBox(m18n('Rearrange undisclosed tiles to melds'), self)
        self.kcfg_rearrangeMelds.setObjectName('kcfg_rearrangeMelds')
        self.kcfg_showOnlyPossibleActions = QCheckBox(m18n('Show only possible actions'))
        self.kcfg_showOnlyPossibleActions.setObjectName('kcfg_showOnlyPossibleActions')
        self.kcfg_propose = QCheckBox(m18n('Propose what to do'))
        self.kcfg_propose.setObjectName('kcfg_propose')
        self.kcfg_animationSpeed = QSlider(self)
        self.kcfg_animationSpeed.setObjectName('kcfg_animationSpeed')
        self.kcfg_animationSpeed.setOrientation(Qt.Horizontal)
        self.kcfg_animationSpeed.setSingleStep(1)
        lblSpeed = QLabel(m18n('Animation speed:'))
        lblSpeed.setBuddy(self.kcfg_animationSpeed)
        sliderLayout.addWidget(lblSpeed)
        sliderLayout.addWidget(self.kcfg_animationSpeed)
        self.kcfg_useSounds = QCheckBox(m18n('Use sounds if available'), self)
        self.kcfg_useSounds.setObjectName('kcfg_useSounds')
        self.kcfg_uploadVoice = QCheckBox(m18n('Let others hear my voice'), self)
        self.kcfg_uploadVoice.setObjectName('kcfg_uploadVoice')
        pol = QSizePolicy()
        pol.setHorizontalPolicy(QSizePolicy.Expanding)
        pol.setVerticalPolicy(QSizePolicy.Expanding)
        spacerItem = QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding)
        vlayout.addWidget(self.kcfg_showShadows)
        vlayout.addWidget(self.kcfg_rearrangeMelds)
        vlayout.addWidget(self.kcfg_showOnlyPossibleActions)
        vlayout.addWidget(self.kcfg_propose)
        vlayout.addWidget(self.kcfg_useSounds)
        vlayout.addWidget(self.kcfg_uploadVoice)
        vlayout.addLayout(sliderLayout)
        vlayout.addItem(spacerItem)
        self.setSizePolicy(pol)
        self.retranslateUi()

    def retranslateUi(self):
        """translate to current language"""
        pass

class ConfigDialog(KConfigDialog):
    """configuration dialog with several pages"""
    def __init__(self, parent, name):
        super(ConfigDialog, self).__init__(parent, QString(name), Preferences)
        self.pages = [
            self.addPage(PlayConfigTab(self),
                m18nc('kajongg','Play'), "arrow-right"),
            self.addPage(TilesetSelector(self),
                m18n("Tiles"), "games-config-tiles"),
            self.addPage(BackgroundSelector(self),
                m18n("Backgrounds"), "games-config-background")]
        StateSaver(self)

    def keyPressEvent(self, event):
        """The four tabs can be selected with CTRL-1 .. CTRL-4"""
        mod = event.modifiers()
        key = chr(event.key()%128)
        if Qt.ControlModifier | mod and key in '1234':
            self.setCurrentPage(self.pages[int(key)-1])
            return
        KConfigDialog.keyPressEvent(self, event)

class SwapDialog(QMessageBox):
    """ask the user if two players should change seats"""
    def __init__(self, swappers):
        QMessageBox.__init__(self)
        self.setWindowTitle(m18n("Swap Seats") + ' - Kajongg')
        self.setText(m18n("By the rules, %1 and %2 should now exchange their seats. ",
            swappers[0].name, swappers[1].name))
        self.yesAnswer = QPushButton(m18n("&Exchange"))
        self.addButton(self.yesAnswer, QMessageBox.YesRole)
        self.noAnswer = QPushButton(m18n("&Keep seat"))
        self.addButton(self.noAnswer, QMessageBox.NoRole)

class SelectPlayers(SelectRuleset):
    """a dialog for selecting four players. Used only for scoring game."""
    def __init__(self, game):
        SelectRuleset.__init__(self)
        self.game = game
        Players.load()
        self.setWindowTitle(m18n('Select four players') + ' - Kajongg')
        self.names = None
        self.nameWidgets = []
        for idx, wind in enumerate(WINDS):
            cbName = QComboBox()
            cbName.manualSelect = False
            # increase width, we want to see the full window title
            cbName.setMinimumWidth(350) # is this good for all platforms?
            cbName.addItems(Players.humanNames.values())
            self.grid.addWidget(cbName, idx+1, 1)
            self.nameWidgets.append(cbName)
            self.grid.addWidget(WindLabel(wind), idx+1, 0)
            cbName.currentIndexChanged.connect(self.slotValidate)

        query = Query("select p0,p1,p2,p3 from game where seed is null and game.id = (select max(id) from game)")
        if len(query.records):
            for pidx, playerId in enumerate(query.records[0]):
                try:
                    playerName = Players.humanNames[playerId]
                    cbName = self.nameWidgets[pidx]
                    playerIdx = cbName.findText(playerName)
                    if playerIdx >= 0:
                        cbName.setCurrentIndex(playerIdx)
                except KeyError:
                    logError('database is inconsistent: player with id %d is in game but not in player' \
                               % playerId)
        self.slotValidate()

    def showEvent(self, dummyEvent):
        """start with player 0"""
        self.nameWidgets[0].setFocus()

    def slotValidate(self):
        """try to find 4 different players and update status of the Ok button"""
        changedCombo = self.sender()
        if not isinstance(changedCombo, QComboBox):
            changedCombo = self.nameWidgets[0]
        changedCombo.manualSelect = True
        usedNames = set([unicode(x.currentText()) for x in self.nameWidgets if x.manualSelect])
        allNames = set(Players.humanNames.values())
        unusedNames = allNames - usedNames
        for combo in self.nameWidgets:
            combo.blockSignals(True)
        try:
            for combo in self.nameWidgets:
                if combo.manualSelect:
                    continue
                comboName = unusedNames.pop()
                combo.clear()
                combo.addItems([comboName])
                combo.addItems(sorted(allNames - usedNames - set([comboName])))
        finally:
            for combo in self.nameWidgets:
                combo.blockSignals(False)
        self.names = list(unicode(cbName.currentText()) for cbName in self.nameWidgets)
        assert len(set(self.names)) == 4

class ScoringPlayer(Player):
    """Player in a scoring game"""
    # pylint: disable=too-many-public-methods
    def __init__(self, game):
        self.handBoard = None # because Player.init calls clearHand()
        self.manualRuleBoxes = []
        Player.__init__(self, game)
        self.__front = self.game.wall[self.idx] # need front before setting handBoard
        self.handBoard = ScoringHandBoard(self)

    def hide(self):
        """clear visible data and hide"""
        self.clearHand()
        self.handBoard.hide()

    def clearHand(self):
        """clears attributes related to current hand"""
        Player.clearHand(self)
        if self.game and self.game.wall:
            # is None while __del__
            self.front = self.game.wall[self.idx]
        if self.handBoard and isAlive(self.handBoard):
            self.handBoard.setEnabled(True)
            self.handBoard.showMoveHelper()
        self.manualRuleBoxes = []

    @property
    def idx(self):
        """our index in the player list"""
        if not self in self.game.players:
            # we will be added next
            return len(self.game.players)
        return self.game.players.index(self)

    @property
    def front(self):
        """front"""
        return self.__front

    @front.setter
    def front(self, value):
        """also assign handBoard to front"""
        self.__front = value
        if value and self.handBoard:
            self.handBoard.setParentItem(value)

    @property
    def handTotal(self):
        """the hand total of this player"""
        if self.hasManualScore():
            spValue = Internal.field.scoringDialog.spValues[self.idx]
            return spValue.value()
        else:
            return self.hand.total()

    def handTotalForWall(self):
        """returns the total for the current hand"""
        return self.handTotal

    def hasManualScore(self):
        """True if no tiles are assigned to this player"""
        if Internal.field.scoringDialog:
            return Internal.field.scoringDialog.spValues[self.idx].isEnabled()
        return False

    def refreshManualRules(self, sender=None):
        """update status of manual rules"""
        assert Internal.field
        if not self.handBoard:
            # might happen at program exit
            return
        currentScore = self.hand.score
        hasManualScore = self.hasManualScore()
        for box in self.manualRuleBoxes:
            if box.rule in self.hand.computedRules:
                box.setVisible(True)
                box.setChecked(True)
                box.setEnabled(False)
            else:
                applicable = bool(self.hand.manualRuleMayApply(box.rule))
                if hasManualScore:
                    # only those rules which do not affect the score can be applied
                    applicable = applicable and box.rule.hasNonValueAction()
                else:
                    # if the action would only influence the score and the rule does not change the score,
                    # ignore the rule. If however the action does other things like penalties leave it applicable
                    if box != sender:
                        if applicable:
                            applicable = bool(box.rule.hasNonValueAction()) \
                                or (self.computeHand(singleRule=box.rule).score > currentScore)
                box.setApplicable(applicable)

    def __mjstring(self, singleRule, asWinner):
        """compile hand info into a string as needed by the scoring engine"""
        winds = self.wind.lower() + 'eswn'[self.game.roundsFinished % 4]
        if asWinner or self == self.game.winner:
            wonChar = 'M'
        else:
            wonChar = 'm'
        if self.lastTile and self.lastTile.istitle():
            lastSource = 'w'
        else:
            lastSource = 'd'
        declaration = ''
        rules = [x.rule for x in self.manualRuleBoxes if x.isChecked()]
        if singleRule:
            rules.append(singleRule)
        for rule in rules:
            options = rule.options
            if 'lastsource' in options:
                if lastSource != '1':
                    # this defines precedences for source of last tile
                    lastSource = options['lastsource']
            if 'declaration' in options:
                declaration = options['declaration']
        return ''.join([wonChar, winds, lastSource, declaration])

    def __lastString(self, asWinner):
        """compile hand info into a string as needed by the scoring engine"""
        if not asWinner or self != self.game.winner:
            return ''
        if not self.lastTile:
            return ''
        return 'L%s%s' % (self.lastTile, self.lastMeld)

    def computeHand(self, singleRule=None, asWinner=None): # pylint: disable=arguments-differ
        """returns a Hand object, using a cache"""
        if asWinner is None:
            asWinner = self == self.game.winner
        self.lastTile = Internal.field.computeLastTile()
        self.lastMeld = Internal.field.computeLastMeld()
        string = ' '.join([self.scoringString(), self.__mjstring(singleRule, asWinner), self.__lastString(asWinner)])
        return Hand.cached(self, string, computedRules=singleRule)

    def sortRulesByX(self, rules):
        """if this game has a GUI, sort rules by GUI order of the melds they are applied to"""
        withMelds = list(x for x in rules if x.meld)
        withoutMelds = list(x for x in rules if x not in withMelds)
        tuples = list(tuple([x, self.handBoard.findUIMeld(x.meld)]) for x in withMelds)
        tuples = sorted(tuples, key=lambda x: x[1][0].sortKey())
        return list(x[0] for x in tuples) + withoutMelds

    def addMeld(self, meld):
        """add meld to this hand in a scoring game"""
        meld = Meld(meld)  # convert UITile to Tile
        if len(meld) == 1 and meld[0].isBonus():
            self._bonusTiles.append(meld[0])
        elif meld.state == CONCEALED and not meld.isKong():
            self._concealedMelds.append(meld)
        else:
            self._exposedMelds.append(meld)
        self._hand = None

    def removeMeld(self, uiMeld):
        """remove a meld from this hand in a scoring game"""
        meld = Meld(uiMeld)
        if len(meld) == 1 and meld[0].isBonus():
            self._bonusTiles.remove(meld[0])
        else:
            popped = False
            for melds in [self._concealedMelds, self._exposedMelds]:
                for idx, myMeld in enumerate(melds):
                    if myMeld == meld:
                        melds.pop(idx)
                        popped = True
            if not popped:
                logDebug('%s: %s.removeMeld did not find %s' % (self.name, self.__class__.__name__, meld), showStack=3)
                logDebug('    concealed: %s' % self._concealedMelds)
                logDebug('      exposed: %s' % self._exposedMelds)
        self._hand = None

    def syncHandBoard(self, adding=None):
        """update display of handBoard. Set Focus to tileName."""
        self.handBoard.sync()

class ScoringGame(Game):
    """we play manually on a real table with real tiles and use
    kajongg only for scoring"""
    playerClass =  ScoringPlayer

    def __init__(self, names, ruleset, gameid=None, client=None, wantedGame=None):
        Game.__init__(self, names, ruleset, gameid=gameid, client=client, wantedGame=wantedGame)
        field = Internal.field
        field.selectorBoard.load(self)
        self.prepareHand()
        self.initHand()
        for player in self.players:
            player.clearHand()
        Internal.field.adjustView()
        Internal.field.updateGUI()
        self.wall.decorate()

    def close(self):
        """log off from the server and return a Deferred"""
        field = Internal.field
        if isAlive(field):
            field.setWindowTitle('Kajongg')
        if field:
            field.selectorBoard.tiles = []
            field.selectorBoard.allSelectorTiles = []
            if isAlive(field.centralScene):
                field.centralScene.removeTiles()
            for player in self.players:
                player.hide()
            if self.wall:
                self.wall.hide()
            field.game = None
            field.updateGUI()
            field.scoringDialog = None
        return Game.close(self)

    def prepareHand(self):
        """prepare a scoring game hand"""
        Game.prepareHand(self)
        if self.finished():
            self.close()
        else:
            selector = Internal.field.selectorBoard
            selector.refill()
            selector.hasFocus = True
            self.wall.build()

    @staticmethod
    def isScoringGame():
        """are we scoring a manual game?"""
        return True

    def saveStartTime(self):
        """write a new entry in the game table with the selected players"""
        Game.saveStartTime(self)
        # for PlayingGame, this one is already done in Connection.__updateServerInfoInDatabase
        known = Query('update server set lastruleset=? where url=?',
            list([self.ruleset.rulesetId, Query.localServerName]))
        if not known:
            Query('insert into server(url,lastruleset) values(?,?)',
                list([self.ruleset.rulesetId, Query.localServerName]))

    def _setGameId(self):
        """get a new id"""
        if not self.gameid:
            # a loaded game has gameid already set
            self.gameid = self._newGameId()

    def _mustExchangeSeats(self, pairs):
        """filter: which player pairs should really swap places?"""
        # pylint: disable=no-self-use
        return list(x for x in pairs if Internal.field.askSwap(x))

    def savePenalty(self, player, offense, amount):
        """save computed values to database, update score table and balance in status line"""
        scoretime = datetime.datetime.now().replace(microsecond=0).isoformat()
        with Transaction():
            Query("INSERT INTO SCORE "
                "(game,penalty,hand,data,manualrules,player,scoretime,"
                "won,prevailing,wind,points,payments, balance,rotated,notrotated) "
                "VALUES(%d,1,%d,?,?,%d,'%s',%d,'%s','%s',%d,%d,%d,%d,%d)" % \
                (self.gameid, self.handctr, player.nameid,
                    scoretime, int(player == self.winner),
                    WINDS[self.roundsFinished % 4], player.wind, 0,
                    amount, player.balance, self.rotated, self.notRotated),
                list([player.hand.string, offense.name]))
        if Internal.field:
            Internal.field.updateGUI()

class PlayField(KXmlGuiWindow):
    """the main window"""
    # pylint: disable=too-many-instance-attributes

    def __init__(self):
        # see http://lists.kde.org/?l=kde-games-devel&m=120071267328984&w=2
        Internal.field = self
        self.game = None
        self.__startingGame = False
        self.ignoreResizing = 1
        super(PlayField, self).__init__()
        self.background = None
        self.showShadows = None
        self._clientDialog = None

        self.playerWindow = None
        self.rulesetWindow = None
        self.scoreTable = None
        self.explainView = None
        self.scoringDialog = None
        self.confDialog = None
        self.setupUi()
        KStandardAction.preferences(self.showSettings, self.actionCollection())
        self.applySettings()
        self.setupGUI()
        self.retranslateUi()
        for action in self.toolBar().actions():
            if 'onfigure' in action.text():
                action.setPriority(QAction.LowPriority)
        if Options.host:
            self.playGame()

    @property
    def clientDialog(self):
        """wrapper: hide dialog when it is set to None"""
        return self._clientDialog

    @clientDialog.setter
    def clientDialog(self, value):
        """wrapper: hide dialog when it is set to None"""
        if isAlive(self._clientDialog) and not value:
            self._clientDialog.timer.stop()
            self._clientDialog.hide()
        self._clientDialog = value

    def sizeHint(self):
        """give the main window a sensible default size"""
        result = KXmlGuiWindow.sizeHint(self)
        result.setWidth(result.height() * 3 // 2) # we want space to the right for the buttons
        # the default is too small. Use at least 2/3 of screen height and 1/2 of screen width:
        available = KApplication.kApplication().desktop().availableGeometry()
        height = max(result.height(), available.height() * 2 // 3)
        width = max(result.width(), available.width() // 2)
        result.setHeight(height)
        result.setWidth(width)
        return result

    def resizeEvent(self, event):
        """Use this hook to determine if we want to ignore one more resize
        event happening for maximized / almost maximized windows.
        this misses a few cases where the window is almost maximized because at
        this point the window has no border yet: event.size, self.geometry() and
        self.frameGeometry are all the same. So we cannot check if the bordered
        window would fit into availableGeometry.
        """
        available = KApplication.kApplication().desktop().availableGeometry()
        if self.ignoreResizing == 1: # at startup
            if available.width() <= event.size().width() \
            or available.height() <= event.size().height():
                self.ignoreResizing += 1
        KXmlGuiWindow.resizeEvent(self, event)
        if self.clientDialog:
            self.clientDialog.placeInField()


    def showEvent(self, event):
        """force a resize which calculates the correct background image size"""
        self.centralView.resizeEvent(True)
        KXmlGuiWindow.showEvent(self, event)

    def handSelectorChanged(self, handBoard):
        """update all relevant dialogs"""
        if self.scoringDialog:
            self.scoringDialog.slotInputChanged()
        if self.game and not self.game.finished():
            self.game.wall.decoratePlayer(handBoard.player) # pylint:disable=no-member
        # first decorate walls - that will compute player.handBoard for explainView
        if self.explainView:
            self.explainView.refresh(self.game)

    def __kajonggAction(self, name, icon, slot=None, shortcut=None, actionData=None):
        """simplify defining actions"""
        res = KAction(self)
        res.setIcon(KIcon(icon))
        if slot:
            res.triggered.connect(slot)
        self.actionCollection().addAction(name, res)
        if shortcut:
            res.setShortcut( Qt.CTRL + shortcut)
            res.setShortcutContext(Qt.ApplicationShortcut)
        if PYQT_VERSION_STR != '4.5.2' or actionData is not None:
            res.setData(QVariant(actionData))
        return res

    def __kajonggToggleAction(self, name, icon, shortcut=None, actionData=None):
        """a checkable action"""
        res = self.__kajonggAction(name, icon, shortcut=shortcut, actionData=actionData)
        res.setCheckable(True)
        res.toggled.connect(self.__toggleWidget)
        return res

    def setupUi(self):
        """create all other widgets
        we could make the scene view the central widget but I did
        not figure out how to correctly draw the background with
        QGraphicsView/QGraphicsScene.
        QGraphicsView.drawBackground always wants a pixmap
        for a huge rect like 4000x3000 where my screen only has
        1920x1200"""
        # pylint: disable=too-many-statements
        self.setObjectName("MainWindow")
        centralWidget = QWidget()
        scene = MJScene()
        self.centralScene = scene
        self.centralView = FittingView()
        layout = QGridLayout(centralWidget)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.centralView)
        self.tileset = None # just for pylint
        self.background = None # just for pylint
        self.tilesetName = Preferences.tilesetName
        self.windTileset = Tileset(Preferences.windTilesetName)

        self.discardBoard = DiscardBoard()
        self.discardBoard.setVisible(False)
        scene.addItem(self.discardBoard)

        self.selectorBoard = SelectorBoard()
        self.selectorBoard.setVisible(False)
        scene.addItem(self.selectorBoard)

        self.setCentralWidget(centralWidget)
        self.centralView.setScene(scene)
        self.centralView.setFocusPolicy(Qt.StrongFocus)
        self.adjustView()
        self.actionScoreGame = self.__kajonggAction("scoreGame", "draw-freehand", self.scoreGame, Qt.Key_C)
        self.actionPlayGame = self.__kajonggAction("play", "arrow-right", self.playGame, Qt.Key_N)
        self.actionAbortGame = self.__kajonggAction("abort", "dialog-close", self.abortAction, Qt.Key_W)
        self.actionAbortGame.setEnabled(False)
        self.actionQuit = self.__kajonggAction("quit", "application-exit", self.close, Qt.Key_Q)
        self.actionPlayers = self.__kajonggAction("players", "im-user", self.slotPlayers)
        self.actionRulesets = self.__kajonggAction("rulesets", "games-kajongg-law", self.slotRulesets)
        self.actionChat = self.__kajonggToggleAction("chat", "call-start",
            shortcut=Qt.Key_H, actionData=ChatWindow)
        game = self.game
        self.actionChat.setEnabled(bool(game) and bool(game.client) and not game.client.hasLocalServer())
        self.actionChat.setChecked(bool(game) and bool(game.client) and bool(game.client.table.chatWindow))
        self.actionScoring = self.__kajonggToggleAction("scoring", "draw-freehand",
            shortcut=Qt.Key_S, actionData=ScoringDialog)
        self.actionScoring.setEnabled(False)
        self.actionAngle = self.__kajonggAction("angle", "object-rotate-left", self.changeAngle, Qt.Key_G)
        self.actionAngle.setEnabled(False)
        self.actionFullscreen = KToggleFullScreenAction(self.actionCollection())
        self.actionFullscreen.setShortcut(Qt.CTRL + Qt.Key_F)
        self.actionFullscreen.setShortcutContext(Qt.ApplicationShortcut)
        self.actionFullscreen.setWindow(self)
        self.actionCollection().addAction("fullscreen", self.actionFullscreen)
        self.actionFullscreen.toggled.connect(self.fullScreen)
        self.actionScoreTable = self.__kajonggToggleAction("scoreTable", "format-list-ordered",
            Qt.Key_T, actionData=ScoreTable)
        self.actionExplain = self.__kajonggToggleAction("explain", "applications-education",
            Qt.Key_E, actionData=ExplainView)
        self.actionAutoPlay = self.__kajonggAction("demoMode", "arrow-right-double", None, Qt.Key_D)
        self.actionAutoPlay.setCheckable(True)
        self.actionAutoPlay.toggled.connect(self.__toggleDemoMode)
        self.actionAutoPlay.setChecked(Internal.autoPlay)
        QMetaObject.connectSlotsByName(self)

    def showWall(self):
        """shows the wall according to the game rules (lenght may vary)"""
        UIWall(self.game)   # sets self.game.wall
        if self.discardBoard:
            # scale it such that it uses the place within the wall optimally.
            # we need to redo this because the wall length can vary between games.
            self.discardBoard.maximize()

    def fullScreen(self, toggle):
        """toggle between full screen and normal view"""
        self.actionFullscreen.setFullScreen(self, toggle)

    def abortAction(self):
        """abort current game"""
        def doNotQuit(dummy):
            """ignore failure to abort"""
        self.abort().addErrback(doNotQuit)

    def abort(self):
        """abort current game"""
        def gotAnswer(result, autoPlaying):
            """user answered"""
            if result:
                return self.abortGame()
            else:
                self.actionAutoPlay.setChecked(autoPlaying)
                return fail(Exception('no abort'))
        def gotError(result):
            """abortGame failed"""
            logDebug('abortGame error:%s/%s ' % (str(result), result.getErrorMessage()))
        if not self.game:
            self.startingGame = False
            return succeed(None)
        autoPlaying = self.actionAutoPlay.isChecked()
        self.actionAutoPlay.setChecked(False)
        if self.game.finished():
            return self.abortGame()
        else:
            return QuestionYesNo(m18n("Do you really want to abort this game?"), always=True).addCallback(
                gotAnswer, autoPlaying).addErrback(gotError)

    def abortGame(self):
        """if a game is active, abort it"""
        if self.game is None: # meanwhile somebody else might have aborted
            return succeed(None)
        game = self.game
        self.game = None
        return game.close()

    def closeEvent(self, event):
        """somebody wants us to close, maybe ALT-F4 or so"""
        event.ignore()
        def doNotQuit(dummy):
            """ignore failure to abort"""
        self.abort().addCallback(HumanClient.shutdownHumanClients).addCallbacks(Client.quitProgram, doNotQuit)

    def __moveTile(self, tile, wind, lowerHalf):
        """the user pressed a wind letter or X for center, wanting to move a tile there"""
        # this tells the receiving board that this is keyboard, not mouse navigation>
        # needed for useful placement of the popup menu
        assert self.game.isScoringGame()
        assert isinstance(tile, UITile), (tile, str(tile))
        currentBoard = tile.board
        if wind == 'X':
            receiver = self.selectorBoard
        else:
            receiver = self.game.players[wind].handBoard
        if receiver != currentBoard or bool(lowerHalf) != bool(tile.yoffset):
            movingLastMeld = tile.tile in self.computeLastMeld()
            if movingLastMeld:
                self.scoringDialog.clearLastTileCombo()
            receiver.dropTile(tile, lowerHalf)
            if movingLastMeld and receiver == currentBoard:
                self.scoringDialog.fillLastTileCombo()

    def __navigateScoringGame(self, event):
        """keyboard navigation in a scoring game"""
        mod = event.modifiers()
        key = event.key()
        wind = chr(key%128)
        moveCommands = m18nc('kajongg:keyboard commands for moving tiles to the players ' \
            'with wind ESWN or to the central tile selector (X)', 'ESWNX')
        uiTile = self.centralScene.focusItem()
        if wind in moveCommands:
            # translate i18n wind key to ESWN:
            wind = 'ESWNX'[moveCommands.index(wind)]
            self.__moveTile(uiTile, wind, bool(mod &Qt.ShiftModifier))
            return True
        if key == Qt.Key_Tab and self.game:
            tabItems = [self.selectorBoard]
            tabItems.extend(list(p.handBoard for p in self.game.players if p.handBoard.tiles))
            tabItems.append(tabItems[0])
            currentBoard = uiTile.board
            currIdx = 0
            while tabItems[currIdx] != currentBoard and currIdx < len(tabItems) -2:
                currIdx += 1
            tabItems[currIdx+1].hasFocus = True
            return True

    def keyPressEvent(self, event):
        """navigate in the selectorboard"""
        mod = event.modifiers()
        if mod in (Qt.NoModifier, Qt.ShiftModifier):
            if self.game and self.game.isScoringGame():
                if self.__navigateScoringGame(event):
                    return
            if self.clientDialog:
                self.clientDialog.keyPressEvent(event)
        KXmlGuiWindow.keyPressEvent(self, event)

    def retranslateUi(self):
        """retranslate"""
        self.actionScoreGame.setText(m18nc('@action:inmenu', "&Score Manual Game"))
        self.actionScoreGame.setIconText(m18nc('@action:intoolbar', 'Manual Game'))
        self.actionScoreGame.setHelpText(m18nc('kajongg @info:tooltip', '&Score a manual game.'))

        self.actionPlayGame.setText(m18nc('@action:intoolbar', "&Play"))
        self.actionPlayGame.setPriority(QAction.LowPriority)
        self.actionPlayGame.setHelpText(m18nc('kajongg @info:tooltip', 'Start a new game.'))

        self.actionAbortGame.setText(m18nc('@action:inmenu', "&Abort Game"))
        self.actionAbortGame.setPriority(QAction.LowPriority)
        self.actionAbortGame.setHelpText(m18nc('kajongg @info:tooltip', 'Abort the current game.'))

        self.actionQuit.setText(m18nc('@action:inmenu', "&Quit Kajongg"))
        self.actionQuit.setPriority(QAction.LowPriority)

        self.actionPlayers.setText(m18nc('@action:intoolbar', "&Players"))
        self.actionPlayers.setHelpText(m18nc('kajongg @info:tooltip', 'define your players.'))

        self.actionRulesets.setText(m18nc('@action:intoolbar', "&Rulesets"))
        self.actionRulesets.setHelpText(m18nc('kajongg @info:tooltip', 'customize rulesets.'))

        self.actionAngle.setText(m18nc('@action:inmenu', "&Change Visual Angle"))
        self.actionAngle.setIconText(m18nc('@action:intoolbar', "Angle"))
        self.actionAngle.setHelpText(m18nc('kajongg @info:tooltip', "Change the visual appearance of the tiles."))

        self.actionScoring.setText(m18nc('@action:inmenu', "&Show Scoring Editor"))
        self.actionScoring.setIconText(m18nc('@action:intoolbar', "&Scoring"))
        self.actionScoring.setHelpText(m18nc('kajongg @info:tooltip',
                "Show or hide the scoring editor for a manual game."))

        self.actionScoreTable.setText(m18nc('kajongg @action:inmenu', "&Score Table"))
        self.actionScoreTable.setIconText(m18nc('kajongg @action:intoolbar', "&Scores"))
        self.actionScoreTable.setHelpText(m18nc('kajongg @info:tooltip',
                "Show or hide the score table for the current game."))

        self.actionExplain.setText(m18nc('@action:inmenu', "&Explain Scores"))
        self.actionExplain.setIconText(m18nc('@action:intoolbar', "&Explain"))
        self.actionExplain.setHelpText(m18nc('kajongg @info:tooltip',
                'Explain the scoring for all players in the current game.'))

        self.actionAutoPlay.setText(m18nc('@action:inmenu', "&Demo Mode"))
        self.actionAutoPlay.setPriority(QAction.LowPriority)
        self.actionAutoPlay.setHelpText(m18nc('kajongg @info:tooltip',
                'Let the computer take over for you. Start a new local game if needed.'))

        self.actionChat.setText(m18n("C&hat"))
        self.actionChat.setHelpText(m18nc('kajongg @info:tooltip', 'Chat with the other players.'))

    def changeEvent(self, event):
        """when the applicationwide language changes, recreate GUI"""
        if event.type() == QEvent.LanguageChange:
            self.setupGUI()
            self.retranslateUi()

    def slotPlayers(self):
        """show the player list"""
        if not self.playerWindow:
            self.playerWindow = PlayerList(self)
        self.playerWindow.show()

    def slotRulesets(self):
        """show the player list"""
        if not self.rulesetWindow:
            self.rulesetWindow = RulesetSelector()
        self.rulesetWindow.show()

    def selectScoringGame(self):
        """show all games, select an existing game or create a new game"""
        Players.load()
        if len(Players.humanNames) < 4:
            logWarning(m18n('Please define four players in <interface>Settings|Players</interface>'))
            return False
        gameSelector = Games(self)
        if gameSelector.exec_():
            selected = gameSelector.selectedGame
            if selected is not None:
                ScoringGame.loadFromDB(selected)
            else:
                self.newGame()
            if self.game:
                self.game.throwDices()
        gameSelector.close()
        self.updateGUI()
        return bool(self.game)

    def scoreGame(self):
        """score a local game"""
        if self.selectScoringGame():
            self.actionScoring.setChecked(True)

    def playGame(self):
        """play a remote game: log into a server and show its tables"""
        self.startingGame = True
        HumanClient()

    def adjustView(self):
        """adjust the view such that exactly the wanted things are displayed
        without having to scroll"""
        if not Internal.scaleScene:
            return
        if self.game:
            with Animated(False):
                self.game.wall.decorate()
                if self.discardBoard:
                    self.discardBoard.maximize()
                if self.selectorBoard:
                    self.selectorBoard.maximize()
                for tile in self.game.wall.tiles:
                    if tile.board:
                        tile.board.placeTile(tile)
        view, scene = self.centralView, self.centralScene
        oldRect = view.sceneRect()
        view.setSceneRect(scene.itemsBoundingRect())
        newRect = view.sceneRect()
        if oldRect != newRect:
            view.fitInView(scene.itemsBoundingRect(), Qt.KeepAspectRatio)

    @property
    def startingGame(self):
        """are we trying to start a game?"""
        return self.__startingGame

    @startingGame.setter
    def startingGame(self, value):
        """are we trying to start a game?"""
        if value != self.__startingGame:
            self.__startingGame = value
            self.updateGUI()

    @property
    def tilesetName(self):
        """the name of the current tileset"""
        return self.tileset.desktopFileName

    @tilesetName.setter
    def tilesetName(self, name):
        """the name of the current tileset"""
        self.tileset = Tileset(name)

    @property
    def backgroundName(self):
        """setting this also actually changes the background"""
        return self.background.desktopFileName if self.background else ''

    @backgroundName.setter
    def backgroundName(self, name):
        """setter for backgroundName"""
        self.background = Background(name)
        self.background.setPalette(self.centralWidget())
        self.centralWidget().setAutoFillBackground(True)

    def applySettings(self):
        """apply preferences"""
        # pylint: disable=too-many-branches
        # too many branches
        self.actionAngle.setEnabled(bool(self.game) and Preferences.showShadows)
        animate() # drain the queue
        afterCurrentAnimationDo(self.__applySettings2)

    def __applySettings2(self, dummyResults):
        """now no animation is running"""
        with Animated(False):
            if self.tilesetName != Preferences.tilesetName:
                self.tilesetName = Preferences.tilesetName
                if self.game:
                    self.game.wall.tileset = self.tileset
                for item in self.centralScene.nonTiles():
                    try:
                        item.tileset = self.tileset
                    except AttributeError:
                        continue
                # change players last because we need the wall already to be repositioned
                self.adjustView() # the new tiles might be larger
            if self.game:
                for player in self.game.players:
                    if player.handBoard:
                        player.handBoard.rearrangeMelds = Preferences.rearrangeMelds
            if self.backgroundName != Preferences.backgroundName:
                self.backgroundName = Preferences.backgroundName
            if self.showShadows is None or self.showShadows != Preferences.showShadows:
                self.showShadows = Preferences.showShadows
                if self.game:
                    wall = self.game.wall
                    wall.showShadows = self.showShadows
                self.selectorBoard.showShadows = self.showShadows
                if self.discardBoard:
                    self.discardBoard.showShadows = self.showShadows
                for tile in self.centralScene.graphicsTileItems():
                    tile.setClippingFlags()
                self.adjustView()
        Sound.enabled = Preferences.useSounds
        self.centralScene.placeFocusRect()

    def showSettings(self):
        """show preferences dialog. If it already is visible, do nothing"""
        if KConfigDialog.showDialog("settings"):
            return
        # if an animation is running, Qt segfaults somewhere deep
        # in the SVG renderer rendering the wind tiles for the tile
        # preview
        afterCurrentAnimationDo(self.__showSettings2)

    def __showSettings2(self, dummyResult):
        """now that no animation is running, show settings dialog"""
        self.confDialog = ConfigDialog(self, "settings")
        self.confDialog.settingsChanged.connect(self.applySettings)
        self.confDialog.show()

    def newGame(self):
        """asks user for players and ruleset for a new game and returns that new game"""
        Players.load() # we want to make sure we have the current definitions
        selectDialog = SelectPlayers(self.game)
        if not selectDialog.exec_():
            return
        return ScoringGame(selectDialog.names, selectDialog.cbRuleset.current)

    def __toggleWidget(self, checked):
        """user has toggled widget visibility with an action"""
        action = self.sender()
        actionData = action.data().toPyObject()
        if checked:
            if isinstance(actionData, type):
                actionData = actionData(game=self.game)
                action.setData(QVariant(actionData))
                if isinstance(actionData, ScoringDialog):
                    self.scoringDialog = actionData
                    actionData.btnSave.clicked.connect(self.nextScoringHand)
                    actionData.scoringClosed.connect(self.__scoringClosed)
                elif isinstance(actionData, ExplainView):
                    self.explainView = actionData
                elif isinstance(actionData, ScoreTable):
                    self.scoreTable = actionData
            actionData.show()
            actionData.raise_()
        else:
            assert actionData
            actionData.hide()

    def __toggleDemoMode(self, checked):
        """switch on / off for autoPlay"""
        if self.game:
            self.centralScene.placeFocusRect() # show/hide it
            self.game.autoPlay = checked
            if checked and self.clientDialog:
                self.clientDialog.proposeAction() # an illegal action might have focus
                self.clientDialog.selectButton() # select default, abort timeout
        else:
            Internal.autoPlay = checked
            if checked:
                # TODO: use the last used ruleset. Right now it always takes the first of the list.
                self.playGame()

    def __scoringClosed(self):
        """the scoring window has been closed with ALT-F4 or similar"""
        self.actionScoring.setChecked(False)

    def nextScoringHand(self):
        """save hand to database, update score table and balance in status line, prepare next hand"""
        if self.game.winner:
            for player in self.game.players:
                player.usedDangerousFrom = None
                for ruleBox in player.manualRuleBoxes:
                    rule = ruleBox.rule
                    if rule.name == 'Dangerous Game' and ruleBox.isChecked():
                        self.game.winner.usedDangerousFrom = player
        self.game.saveHand()
        self.game.maybeRotateWinds()
        self.game.prepareHand()
        self.game.initHand()

    def prepareHand(self):
        """redecorate wall"""
        self.updateGUI()
        if self.game:
            self.game.wall.decorate()
        if self.scoringDialog:
            self.scoringDialog.clearLastTileCombo()

    def updateGUI(self):
        """update some actions, all auxiliary windows and the statusbar"""
        if not isAlive(self):
            return
        title = ''
        connections = list(x.connection for x in HumanClient.humanClients if x.connection)
        game = self.game
        if not game:
            title = ', '.join('{name}/{url}'.format(name=x.username, url=x.url) for x in connections)
            if title:
                self.setWindowTitle('%s - Kajongg' % title)
        for action in [self.actionScoreGame, self.actionPlayGame]:
            action.setEnabled(not bool(game))
        self.actionAbortGame.setEnabled(bool(game))
        self.actionAngle.setEnabled(bool(game) and self.showShadows)
        scoring = bool(game and game.isScoringGame())
        self.selectorBoard.setVisible(scoring)
        self.selectorBoard.setEnabled(scoring)
        self.discardBoard.setVisible(bool(game) and not scoring)
        self.actionScoring.setEnabled(scoring and not game.finished())
        self.actionAutoPlay.setEnabled(not self.startingGame and not scoring)
        self.actionChat.setEnabled(bool(game) and bool(game.client)
            and not game.client.hasLocalServer() and not self.startingGame)
            # chatting on tables before game started works with chat button per table
        self.actionChat.setChecked(self.actionChat.isEnabled() and bool(game.client.table.chatWindow))
        if self.actionScoring.isChecked():
            self.actionScoring.setChecked(scoring and not game.finished())
        for view in [self.explainView, self.scoreTable]:
            if view:
                view.refresh(game)
        self.__showBalance()

    def changeAngle(self):
        """change the lightSource"""
        if self.game:
            afterCurrentAnimationDo(self.__changeAngle2)

    def __changeAngle2(self, dummyResult):
        """now that no animation is running, really change"""
        if self.game: # might be finished meanwhile
            with Animated(False):
                wall = self.game.wall
                oldIdx = LIGHTSOURCES.index(wall.lightSource) # pylint:disable=no-member
                newLightSource = LIGHTSOURCES[(oldIdx + 1) % 4]
                wall.lightSource = newLightSource
                self.selectorBoard.lightSource = newLightSource
                self.discardBoard.lightSource = newLightSource
                self.adjustView()
                scoringDialog = self.actionScoring.data().toPyObject()
                if isinstance(scoringDialog, ScoringDialog):
                    scoringDialog.computeScores()
                self.centralScene.placeFocusRect()

    def __showBalance(self):
        """show the player balances in the status bar"""
        sBar = self.statusBar()
        if self.game:
            for idx, player in enumerate(self.game.players):
                sbMessage = player.localName + ': ' + str(player.balance)
                if sBar.hasItem(idx):
                    sBar.changeItem(sbMessage, idx)
                else:
                    sBar.insertItem(sbMessage, idx, 1)
                    sBar.setItemAlignment(idx, Qt.AlignLeft)
        else:
            for idx in range(5):
                if sBar.hasItem(idx):
                    sBar.removeItem(idx)

    def computeLastTile(self):
        """compile hand info into a string as needed by the scoring engine"""
        if self.scoringDialog:
            return self.scoringDialog.computeLastTile()

    def computeLastMeld(self):
        """compile hand info into a string as needed by the scoring engine"""
        if self.scoringDialog:
            cbLastMeld = self.scoringDialog.cbLastMeld
            idx = cbLastMeld.currentIndex()
            if idx >= 0:
                return Meld(str(cbLastMeld.itemData(idx).toString()))
        return Meld()

    @staticmethod
    def askSwap(swappers):
        """use this as a proxy such that module game does not have to import playfield.
        Game should also run on a server without KDE being installed"""
        return SwapDialog(swappers).exec_() == 0
        # I do not understand the logic of the exec return value. The yes button returns 0
        # and the no button returns 1. According to the C++ doc, the return value is an
        # opaque value that should not be used."""
