#!/usr/bin/env python
# -*- mode: python; coding: utf-8; -*-
# ---------------------------------------------------------------------------
#
# Copyright (C) 1998-2003 Markus Franz Xaver Johannes Oberhumer
# Copyright (C) 2003 Mt. Hood Playing Card Co.
# Copyright (C) 2005-2009 Skomoroh
#
# 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 3 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, see <http://www.gnu.org/licenses/>.
#
# ---------------------------------------------------------------------------

import re
import time

from pysollib.game import Game
from pysollib.gamedb import GI, GameInfo, registerGame
from pysollib.hint import AbstractHint
from pysollib.layout import Layout
from pysollib.mfxutil import Image, Struct, kwdefault
from pysollib.mygettext import _
from pysollib.mygettext import ungettext
from pysollib.pysoltk import ANCHOR_NW, EVENT_HANDLED, bind
from pysollib.pysoltk import MfxCanvasImage, MfxCanvasText
from pysollib.pysoltk import MfxMessageDialog
from pysollib.settings import DEBUG, TOOLKIT
from pysollib.stack import \
        InitialDealTalonStack, \
        OpenStack
from pysollib.util import ANY_SUIT, NO_RANK


def _count_pairs(in_n):
    """Returns the number of unordered pairs of n elements: (n choose 2)"""
    return in_n * (in_n - 1) // 2


# ************************************************************************
# *
# ************************************************************************

class Mahjongg_Hint(AbstractHint):
    # FIXME: no intelligence whatsoever is implemented here
    def computeHints(self):
        game = self.game
        # get free stacks
        stacks = []
        for r in game.s.rows:
            if r.cards and not r.basicIsBlocked():
                stacks.append(r)
        # find matching tiles
        i = 0
        for r in stacks:
            for t in stacks[i+1:]:
                if game.cardsMatch(r.cards[0], t.cards[0]):
                    # simple scoring...
                    # score = 10000 + r.id + t.id
                    rb = r.blockmap
                    tb = t.blockmap
                    score = \
                        10000 + \
                        1000 * (len(rb.below) + len(tb.below)) + \
                        len(rb.all_left) + len(rb.all_right) + \
                        len(tb.all_left) + len(tb.all_right)
                    self.addHint(score, 1, r, t)
            i += 1


# ************************************************************************
# *
# ************************************************************************

# class Mahjongg_Foundation(AbstractFoundationStack):
class Mahjongg_Foundation(OpenStack):

    def __init__(self, x, y, game, suit=ANY_SUIT, **cap):
        kwdefault(cap, max_move=0, max_accept=0, max_cards=game.NCARDS)
        OpenStack.__init__(self, x, y, game, **cap)

    def acceptsCards(self, from_stack, cards):
        # We do not accept any cards - pairs will get
        # delivered by _dropPairMove() below.
        return 0

    def basicIsBlocked(self):
        return 1

    # def initBindings(self):
    #    pass

    def _position(self, card):
        # AbstractFoundationStack._position(self, card)
        OpenStack._position(self, card)

        fnds = self.game.s.foundations

        cols = (3, 2, 1, 0)
        for i in cols:
            for j in range(9):
                n = i*9+j
                if fnds[n].cards:
                    fnds[n].group.tkraise()

    def getHelp(self):
        return ''

    def canSelect(self):
        return False


# ************************************************************************
# *
# ************************************************************************

class Mahjongg_RowStack(OpenStack):
    def __init__(self, x, y, game, **cap):
        kwdefault(cap, max_move=1, max_accept=1, max_cards=2,
                  base_rank=NO_RANK)
        OpenStack.__init__(self, x, y, game, **cap)

    def basicIsBlocked(self):
        # any of above blocks
        for stack in self.blockmap.above:
            if stack.cards:
                return 1
        # any of left blocks - but we can try right as well
        for stack in self.blockmap.left:
            if stack.cards:
                break
        else:
            return 0
        # any of right blocks
        for stack in self.blockmap.right:
            if stack.cards:
                return 1
        return 0

    def acceptsCards(self, from_stack, cards):
        if not OpenStack.acceptsCards(self, from_stack, cards):
            return 0
        return self.game.cardsMatch(self.cards[0], cards[-1])

    def canFlipCard(self):
        return 0

    def canDropCards(self, stacks):
        return (None, 0)

    def moveMove(self, ncards, to_stack, frames=-1, shadow=-1):
        self._dropPairMove(ncards, to_stack, frames=-1, shadow=shadow)

    def _dropPairMove(self, n, other_stack, frames=-1, shadow=-1):
        # print 'drop:', self.id, other_stack.id
        assert n == 1 and self.acceptsCards(
            other_stack, [other_stack.cards[-1]])
        if not self.game.demo:
            self.game.playSample("droppair", priority=200)
        old_state = self.game.enterState(self.game.S_FILL)
        c = self.cards[0]
        if c.suit == 3:
            if c.rank >= 8:
                i = 35
            elif c.rank >= 4:
                i = 34
            else:
                i = 30+c.rank
        elif c.rank == 9:
            i = 27+c.suit
        else:
            i = c.suit*9+c.rank
        f = self.game.s.foundations[i]
        self.game.moveMove(n, self, f, frames=frames, shadow=shadow)
        self.game.moveMove(n, other_stack, f, frames=frames, shadow=shadow)
        self.game.leaveState(old_state)
        self.fillStack()
        other_stack.fillStack()

    #
    # Mahjongg special overrides
    #

    # Mahjongg special: we must preserve the relative stacking order
    # to keep our pseudo 3D look.
    def _position(self, card):
        OpenStack._position(self, card)
        #
        if TOOLKIT == 'tk':
            rows = [s for s in self.game.s.rows[:self.id] if s.cards]
            if rows:
                self.group.tkraise(rows[-1].group)
                return
            rows = [s for s in self.game.s.rows[self.id+1:] if s.cards]
            if rows:
                self.group.lower(rows[0].group)
                return
        elif TOOLKIT == 'kivy':
            rows = [s for s in self.game.s.rows[:self.id] if s.cards]
            if rows:
                self.group.tkraise(rows[-1].group)
                return
            rows = [s for s in self.game.s.rows[self.id+1:] if s.cards]
            if rows:
                self.group.lower(rows[0].group)
                return
        elif TOOLKIT == 'gtk':
            # FIXME (this is very slow)
            for s in self.game.s.rows[self.id+1:]:
                s.group.tkraise()

    def _calcMouseBind(self, binding_format):
        return self.game.app.opt.calcCustomMouseButtonsBinding(binding_format)

    # In Mahjongg games type there are a lot of stacks, so we optimize
    # and don't create bindings that are not used anyway.
    def initBindings(self):
        group = self.group
        # FIXME: dirty hack to access the Stack's private methods
        # bind(group, "<1>", self._Stack__clickEventHandler)
        # bind(group, "<3>", self._Stack__controlclickEventHandler)
        # bind(group, "<Control-1>", self._Stack__controlclickEventHandler)
        #
        bind(
            group,
            self._calcMouseBind("<{mouse_button1}>"),
            self.__clickEventHandler
        )
        bind(
            group,
            self._calcMouseBind("<{mouse_button3}>"),
            self.__controlclickEventHandler
        )
        bind(
            group,
            self._calcMouseBind("<Control-{mouse_button1}>"),
            self.__controlclickEventHandler
        )
        # bind(group, "<Enter>", self._Stack__enterEventHandler)
        # bind(group, "<Leave>", self._Stack__leaveEventHandler)

    def keyboardAction(self, card, event, type=1):
        self.keyboard_movement = True
        self.keyboard_card = card
        if type == 1:
            self.__clickEventHandler(event)
        else:
            self.__rightclickEventHandler(event)
        self.keyboard_card = None

    def __defaultClickEventHandler(self, event, handler):
        self.game.event_handled = True  # for Game.undoHandler
        if self.game.demo:
            self.game.stopDemo(event)
        if self.game.busy:
            return EVENT_HANDLED
        handler(event)
        return EVENT_HANDLED

    def __clickEventHandler(self, event):
        # print 'click:', self.id
        return self.__defaultClickEventHandler(event, self.clickHandler)

    def __controlclickEventHandler(self, event):
        return self.__defaultClickEventHandler(event, self.controlclickHandler)

    def clickHandler(self, event):
        game = self.game
        drag = game.drag
        # checks
        if not self.cards:
            return 1
        card = self.cards[-1]
        from_stack = drag.stack
        if from_stack is self:
            # remove selection
            self.game.playSample("nomove")
            self._stopDrag()
            return 1
        if self.basicIsBlocked():
            # remove selection
            # self.game.playSample("nomove")
            return 1
        # possible move
        if from_stack:
            if self.acceptsCards(from_stack, from_stack.cards):
                self._stopDrag()
                # this code actually moves the tiles
                from_stack.playMoveMove(1, self, frames=0, sound=True)
                if TOOLKIT == 'kivy':
                    if drag.shade_img:
                        # drag.shade_img.dtag(drag.shade_stack.group)
                        drag.shade_img.delete()
                        # game.canvas.delete(drag.shade_img)
                        drag.shade_img = None
                return 1
        drag.stack = self
        self.game.playSample("startdrag")
        self.game.app.speech.speak(_("Selected"))
        # create the shade image (see stack.py, _updateShade)
        if drag.shade_img:
            # drag.shade_img.dtag(drag.shade_stack.group)
            drag.shade_img.delete()
            # game.canvas.delete(drag.shade_img)
            drag.shade_img = None
        img = game.app.images.getHighlightedCard(
            card.deck, card.suit, card.rank)
        if img is None:
            return 1
        img = MfxCanvasImage(game.canvas, self.x, self.y, image=img,
                             anchor=ANCHOR_NW, group=self.group)
        drag.shade_img = img
        # raise/lower the shade image to the correct stacking order
        img.tkraise(card.item)
        drag.shade_stack = self
        return 1

    def cancelDrag(self, event=None):
        if event is None:
            self._stopDrag()
            self.game.app.speech.speak(_("Unselected"))

    def _findCard(self, event):
        # we need to override this because the shade may be hiding
        # the tile (from Tk's stacking view)
        return len(self.cards) - 1

    def getBottomImage(self):
        return None

    def canSelect(self):
        return len(self.cards) > 0


# ************************************************************************
# *
# ************************************************************************

def _nontrivial_keys_to_tuple(in_dict):
    return tuple(_f for _f in in_dict if _f)


class AbstractMahjonggGame(Game):
    RANKS = ("1", "2", "3", "4", "5", "6", "7", "8", "9", _("Dragon"))
    SUITS = (_("Strings"), _("Sticks"), _("Coins"))
    DRAGONS = (_("Green"), _("White"), _("Red"))
    TRUMPS = (_("North Wind"), _("South Wind"), _("East Wind"),
              _("West Wind"), _("Winter"), _("Fall"), _("Spring"),
              _("Summer"), _("Bamboo"), _("Orchid"), _("Plum"),
              _("Chrysanthemum"))

    Hint_Class = Mahjongg_Hint
    RowStack_Class = Mahjongg_RowStack

    GAME_VERSION = 3

    NCARDS = 144

    def getTiles(self):
        # decode tile positions
        L = self.L

        assert L[0] == "0"
        assert (len(L) - 1) % 3 == 0

        tiles = []
        max_tl, max_tx, max_ty = -1, -1, -1
        t = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
        for i in range(1, len(L), 3):
            n = t.find(L[i])
            level, height = n // 7, n % 7 + 1
            tx = t.find(L[i+1])
            ty = t.find(L[i+2])
            assert n >= 0 and tx >= 0 and ty >= 0
            max_tl = max(level + height - 1, max_tl)
            max_tx = max(tx, max_tx)
            max_ty = max(ty, max_ty)
            for tl in range(level, level + height):
                tiles.append((tl, tx, ty))
        assert len(tiles) == self.NCARDS
        # tiles.sort()
        # tiles = tuple(tiles)
        return tiles, max_tl, max_tx, max_ty

    def parseCard(self, card):
        if not card.face_up:
            return _("Face-down")
        if card.suit > 2:
            return self.TRUMPS[card.rank]
        if card.rank > 8:
            return self.DRAGONS[card.suit] + " " + self.RANKS[card.rank]
        suit = self.SUITS[card.suit]
        rank = self.RANKS[card.rank]
        return rank + " - " + suit

    def getStackSpeech(self, stack, cardindex):
        if stack not in self.s.rows:
            return Game.getStackSpeech(self, stack, cardindex)
        if len(stack.cards) == 0:
            return self.parseEmptyStack(stack)
        if hasattr(stack, 'blockmap') and len(stack.blockmap.above) > 0:
            mainCard = _("Covered Tile")
        else:
            mainCard = self.parseCard(stack.cards[cardindex])
        coverCards = ()
        blockedByCovered = False
        if (not hasattr(stack, 'blockmap') or
                not len(stack.blockmap.left) > 0 or
                not len(stack.blockmap.right) > 0):
            return mainCard
        for r in stack.blockmap.above:
            if r.cards:
                if len(r.blockmap.above) > 0:
                    blockedByCovered = True
                else:
                    coverCards += (r,)
        if len(coverCards) > 0 or blockedByCovered:
            mainCard += " - " + _("Covered by")
            if blockedByCovered:
                mainCard += " - " + _("Covered Tiles")
            for c in coverCards:
                mainCard += " - " + self.parseCard(c.cards[0])
        coverCards = ()
        blockedByCovered = False
        for r in stack.blockmap.left:
            if r.cards:
                if len(r.blockmap.above) > 0:
                    blockedByCovered = True
                else:
                    coverCards += (r,)
        if len(coverCards) > 0 or blockedByCovered:
            mainCard += " - " + _("Blocked on left by")
            if blockedByCovered:
                mainCard += " - " + _("Covered Tiles")
            for c in coverCards:
                mainCard += " - " + self.parseCard(c.cards[0])
        coverCards = ()
        blockedByCovered = False
        for r in stack.blockmap.right:
            if r.cards:
                if len(r.blockmap.above) > 0:
                    blockedByCovered = True
                else:
                    coverCards += (r,)
        if len(coverCards) > 0 or blockedByCovered:
            mainCard += " - " + _("Blocked on right by")
            if blockedByCovered:
                mainCard += " - " + _("Covered Tiles")
            for c in coverCards:
                mainCard += " - " + self.parseCard(c.cards[0])
        return mainCard

    def keyboardSelectLayer(self, direction):
        oldstack = self.keyboard_selected_stack
        stack = oldstack
        if direction == 1:
            layercheck = oldstack.blockmap.above
        else:
            layercheck = oldstack.blockmap.below
        for checkstack in layercheck:
            if checkstack.canSelect():
                stack = checkstack
        if oldstack != stack:
            self.keyboard_selected_stack = stack
            self.keyboard_select_count = 1
            self._updateKeyboardSelector()
            if len(stack.cards) > 0:
                self.app.speech.speak(self.getStackSpeech(stack, -1))
            else:
                self.app.speech.speak(self.getStackSpeech(stack, 0))
        else:
            self.playSample("edge", priority=200)

    def _getKeyboardSelectStack(self, direction):
        if self.keyboard_selected_stack is None:
            for s in self.allstacks:
                if s.canSelect():
                    self.keyboard_selected_stack = s
                    break
            return

        currentstack = None
        currentstacky = None
        currentstackx = None
        cw, ch = self.app.images.getSize()
        cs = self.app.images.cs

        scaleh = ch / cs.CARDH
        scalew = cw / cs.CARDW

        offsetw = cs.SHADOW_XOFFSET * scalew
        offseth = cs.SHADOW_YOFFSET * scaleh

        for stack in self.allstacks:
            if (stack in self.s.internals or
                    stack == self.keyboard_selected_stack or
                    not stack.canSelect()):
                continue
            cs = self.app.images.cs
            cw2, ch2 = cw, ch

            if (hasattr(self.keyboard_selected_stack, 'blockmap')
                    and self.keyboard_selected_stack.blockmap.level
                    != stack.blockmap.level):
                continue

            if cs.version == 6 or cs.mahjongg3d:
                cw2 -= offsetw
                ch2 -= offseth
            cw2 -= 2
            ch2 -= 2

            # The x and y are multiplied, to create some distance and prevent
            # the stacks from overlapping.  There is probably a better
            # solution.
            multiplier = 1 + max(scalew, scaleh)
            sy = stack.y * multiplier
            sx = stack.x * multiplier
            ky = self.keyboard_selected_stack.y * multiplier
            kx = self.keyboard_selected_stack.x * multiplier

            if direction == 0:  # up
                if ((sy >= ky) or
                        (sx < kx - cw2 or
                         sx > kx + cw2) or
                        (currentstack is not None and
                         sy < currentstacky)):
                    continue
            elif direction == 1:  # down
                if ((sy <= ky) or
                        (sx < kx - cw2 or
                         sx > kx + cw2) or
                        (currentstack is not None and
                         sy > currentstacky)):
                    continue
            elif direction == 2:  # left
                if ((sx >= kx) or
                        (sy < ky - ch2 or
                         sy > ky + ch2) or
                        (currentstack is not None and
                         sx < currentstackx)):
                    continue
            elif direction == 3:  # right
                if ((sx <= kx) or
                        (sy < ky - ch2 or
                         sy > ky + ch2) or
                        (currentstack is not None and
                         sx > currentstackx)):
                    continue
            currentstack = stack
            currentstackx = sx
            currentstacky = sy
        if currentstack is not None:
            self.keyboard_selected_stack = currentstack

    def keyboardSelectNextType(self, dir=1):
        oldstack = self.keyboard_selected_stack
        rows = self.s.rows

        try:
            index = rows.index(oldstack)
        except ValueError:
            if dir > 0:
                index = -1
            else:
                index = 0

        for n in range(1, len(rows) + 1):
            try_index = (index + (dir * n)) % len(rows)
            if not rows[try_index].basicIsBlocked():
                self.keyboard_selected_stack = rows[try_index]
                break

        stack = self.keyboard_selected_stack

        self._updateKeyboardSelector()
        if len(stack.cards) > 0:
            self.app.speech.speak(self.getStackSpeech(stack, -1))
        else:
            self.app.speech.speak(self.getStackSpeech(stack, 0))

    #
    # game layout
    #

    def createGame(self):
        tiles, max_tl, max_tx, max_ty = self.getTiles()

        # start layout
        l, s = Layout(self), self.s
        show_removed = self.app.opt.mahjongg_show_removed

        # dx, dy = 2, -2
        # dx, dy = 3, -3
        cs = self.app.images.cs
        if cs.version == 6 or cs.mahjongg3d:
            dx = l.XOFFSET
            dy = -l.YOFFSET
            d_x = cs.SHADOW_XOFFSET
            d_y = cs.SHADOW_YOFFSET
            if self.preview and (not self.app.opt.auto_scale or
                                 not self.app.opt.preview_scale):
                size_cap, r = 100, 2
                if l.CW // r > size_cap or l.CH // r > size_cap:
                    r = max(l.CW, l.CH) // size_cap

                # Fixme
                dx, dy, d_x, d_y = dx // r, dy // r, d_x // r, d_y // r

            self._delta_x, self._delta_y = dx, -dy
        else:
            dx = 3
            dy = -3
            d_x = 0
            d_y = 0
            self._delta_x, self._delta_y = 0, 0
        # print dx, dy, d_x, d_y, cs.version

        font = self.app.getFont("canvas_default")

        # width of self.texts.info
        # ti_width = Font(self.canvas, font).measure(_('Remaining'))
        ti_width = max(80, l.CW)

        # set window size
        dxx, dyy = abs(dx) * (max_tl+1), abs(dy) * (max_tl+1)
        # foundations dxx dyy
        if self.NCARDS > 144:
            fdxx = abs(dx)*8
            fdyy = abs(dy)*8
        else:
            fdxx = abs(dx)*4
            fdyy = abs(dy)*4
        cardw, cardh = l.CW - d_x, l.CH - d_y
        if show_removed:
            left_margin = l.XM + 4*cardw+fdxx+d_x + l.XM
        else:
            left_margin = l.XM
        tableau_width = (max_tx+2)*cardw//2+dxx+d_x
        right_margin = l.XM+ti_width+l.XM
        w = left_margin + tableau_width + right_margin
        h = l.YM + dyy + (max_ty + 2) * cardh // 2 + d_y + l.YM
        if show_removed:
            h = max(h, l.YM+fdyy+cardh*9+d_y+l.YM)
        self.setSize(w, h)

        # set game extras
        self.check_dist = l.CW*l.CW + l.CH*l.CH     # see _getClosestStack()

        # sort tiles (for 3D)
        tiles.sort(key=lambda x: (x[0], x[2]-x[1]))

        # create a row stack for each tile and compute the tilemap
        tilemap = {}
        x0 = left_margin
        y0 = l.YM + dyy
        for level, tx, ty in tiles:
            # print level, tx, ty
            x = x0 + (tx * cardw) // 2 + level * dx
            y = y0 + (ty * cardh) // 2 + level * dy
            stack = self.RowStack_Class(x, y, self)
            # stack.G = (level, tx, ty)
            stack.CARD_XOFFSET = dx
            stack.CARD_YOFFSET = dy
            s.rows.append(stack)
            # tilemap - each tile covers 4 positions
            tilemap[(level, tx, ty)] = stack
            tilemap[(level, tx+1, ty)] = stack
            tilemap[(level, tx, ty+1)] = stack
            tilemap[(level, tx+1, ty+1)] = stack

        # compute blockmap
        for stack in s.rows:
            level, tx, ty = tiles[stack.id]
            above, below, left, right = {}, {}, {}, {}
            # above blockers
            for tl in range(level+1, level+2):
                above[tilemap.get((tl, tx, ty))] = 1
                above[tilemap.get((tl, tx+1, ty))] = 1
                above[tilemap.get((tl, tx, ty+1))] = 1
                above[tilemap.get((tl, tx+1, ty+1))] = 1
            #
            for tl in range(level):
                below[tilemap.get((tl, tx, ty))] = 1
                below[tilemap.get((tl, tx+1, ty))] = 1
                below[tilemap.get((tl, tx, ty+1))] = 1
                below[tilemap.get((tl, tx+1, ty+1))] = 1
            # left blockers
            left[tilemap.get((level, tx-1, ty))] = 1
            left[tilemap.get((level, tx-1, ty+1))] = 1
            # right blockers
            right[tilemap.get((level, tx+2, ty))] = 1
            right[tilemap.get((level, tx+2, ty+1))] = 1
            # up blockers
            # up[tilemap.get((level, tx, ty-1))] = 1
            # up[tilemap.get((level, tx+1, ty-1))] = 1
            # bottom blockers
            # bottom[tilemap.get((level, tx, ty+2))] = 1
            # bottom[tilemap.get((level, tx+1, ty+2))] = 1
            # sanity check - assert that there are no overlapping tiles
            assert tilemap.get((level, tx, ty)) is stack
            assert tilemap.get((level, tx+1, ty)) is stack
            assert tilemap.get((level, tx, ty+1)) is stack
            assert tilemap.get((level, tx+1, ty+1)) is stack
            #
            # up = tuple(filter(None, up.keys()))
            # bottom = tuple(filter(None, bottom.keys()))

            # assemble
            stack.blockmap = Struct(
                above=_nontrivial_keys_to_tuple(above),
                below=_nontrivial_keys_to_tuple(below),
                left=_nontrivial_keys_to_tuple(left),
                right=_nontrivial_keys_to_tuple(right),
                # up=up,
                # bottom=bottom,
                all_left=None,
                all_right=None,
                level=level
            )

        def get_all_left(s):
            if s.blockmap.all_left is None:
                s.blockmap.all_left = {}
            for t in s.blockmap.left:
                if t.blockmap.all_left is None:
                    get_all_left(t)
                s.blockmap.all_left.update(t.blockmap.all_left)
                s.blockmap.all_left[t] = 1

        def get_all_right(s):
            if s.blockmap.all_right is None:
                s.blockmap.all_right = {}
            for t in s.blockmap.right:
                if t.blockmap.all_right is None:
                    get_all_right(t)
                s.blockmap.all_right.update(t.blockmap.all_right)
                s.blockmap.all_right[t] = 1

        for r in s.rows:
            get_all_left(r)
            get_all_right(r)
        for r in s.rows:
            r.blockmap.all_left = tuple(r.blockmap.all_left.keys())
            r.blockmap.all_right = tuple(r.blockmap.all_right.keys())

        # create other stacks
        for i in range(4):
            for j in range(9):
                if show_removed:
                    x = l.XM+i*cardw
                    y = l.YM+fdyy+j*cardh
                else:
                    if TOOLKIT == 'tk':
                        x = -l.XS-self.canvas.xmargin
                        y = l.YM+dyy
                    elif TOOLKIT == 'kivy':
                        x = -1000
                        y = l.YM+dyy
                    elif TOOLKIT == 'gtk':
                        # FIXME
                        x = self.width - l.XS
                        y = self.height - l.YS
                stack = Mahjongg_Foundation(x, y, self)
                if show_removed:
                    stack.CARD_XOFFSET = dx
                    stack.CARD_YOFFSET = dy
                s.foundations.append(stack)

        self.texts.info = MfxCanvasText(self.canvas,
                                        self.width - l.XM - ti_width,
                                        l.YM + dyy,
                                        anchor="nw", font=font)
        # the Talon is invisble
        s.talon = InitialDealTalonStack(-l.XS-self.canvas.xmargin,
                                        self.height-dyy, self)

        # Define stack groups
        l.defaultStackGroups()

    #
    # game overrides
    #

    def _shuffleHook(self, cards):
        if self.app.opt.mahjongg_create_solvable == 0:
            return cards
        # try to create a solvable game
        if self.app.opt.mahjongg_create_solvable == 1:
            # easy
            return self._shuffleHook1(cards[:])
        # hard
        new_cards = self._shuffleHook2(self.s.rows, cards)
        if new_cards is None:
            return cards
        return new_cards

    def _shuffleHook1(self, cards):
        # old version; it generate a very easy layouts
        old_cards = cards[:]
        rows = self.s.rows

        def is_blocked(s, new_cards):
            # any of above blocks
            for stack in s.blockmap.above:
                if new_cards[stack.id] is None:
                    return True
            # any of left blocks - but we can try right as well
            for stack in s.blockmap.left:
                if new_cards[stack.id] is None:
                    break
            else:
                return False
            # any of right blocks
            for stack in s.blockmap.right:
                if new_cards[stack.id] is None:
                    return True
            return False

        def create_solvable(cards, new_cards):
            if not cards:
                return new_cards
            # select two matching cards
            c1 = cards[0]
            del cards[0]
            c2 = None
            for i in range(len(cards)):
                if self.cardsMatch(c1, cards[i]):
                    c2 = cards[i]
                    del cards[i]
                    break
            #
            free_stacks = []            # none-blocked stacks
            for r in rows:
                if new_cards[r.id] is None and not is_blocked(r, new_cards):
                    free_stacks.append(r)
            if len(free_stacks) < 2:
                return None             # try another way
            #
            old_pairs = []
            for j in range(_count_pairs(len(free_stacks))):
                nc = new_cards[:]
                while True:
                    # create uniq pair
                    r1 = self.random.randrange(0, len(free_stacks))
                    r2 = self.random.randrange(0, len(free_stacks)-1)
                    if r2 >= r1:
                        r2 += 1
                    if (r1, r2) not in old_pairs and (r2, r1) not in old_pairs:
                        old_pairs.append((r1, r2))
                        break
                # add two selected cards to new_cards
                s1 = free_stacks[r1]
                s2 = free_stacks[r2]
                nc[s1.id] = c1
                nc[s2.id] = c2
                # check if this layout is solvable (backtracking)
                nc = create_solvable(cards[:], nc)
                if nc:
                    return nc
            return None                 # try another way

        new_cards = create_solvable(cards, [None]*len(cards))
        if new_cards:
            new_cards.reverse()
            return new_cards
        print('oops! can\'t create a solvable game')
        return old_cards

    def _shuffleHook2(self, rows, cards):

        start_time = time.time()
        iters = [0]
        # limitations
        max_time = 5.0                  # seconds
        max_iters = 2*len(cards)

        def is_suitable(stack, cards):
            for s in stack.blockmap.below:
                if cards[s.id] == 1:
                    continue
                # check if below stacks are non-empty
                if cards[s.id] is None:
                    return False

            for s in stack.blockmap.left:
                if cards[s.id] == 1:
                    continue
                if cards[s.id] is None:
                    for t in s.blockmap.all_left:
                        if cards[t.id] == 1:
                            continue
                        if cards[t.id] is not None:
                            # we have empty stack between two non-empty
                            return False

            for s in stack.blockmap.right:
                if cards[s.id] == 1:
                    continue
                if cards[s.id] is None:
                    for t in s.blockmap.all_right:
                        if cards[t.id] == 1:
                            continue
                        if cards[t.id] is not None:
                            # we have empty stack between two non-empty
                            return False
            return True

        def create_solvable(cards, new_cards):
            iters[0] += 1
            if iters[0] > max_iters:
                return None
            if time.time() - start_time > max_time:
                return None
            if not cards:
                return new_cards

            nc = new_cards[:]

            # select two matching cards
            c1 = cards[0]
            del cards[0]
            c2 = None
            for i in range(len(cards)):
                if self.cardsMatch(c1, cards[i]):
                    c2 = cards[i]
                    del cards[i]
                    break

            # find suitable stacks
            #  suitable_stacks = []
            #  for r in rows:
            #      if nc[r.id] is None and is_suitable(r, nc):
            #          suitable_stacks.append(r)
            suitable_stacks = [r for r in rows
                               if nc[r.id] is None and is_suitable(r, nc)]

            old_pairs = []
            for j in range(_count_pairs(len(suitable_stacks))):
                if iters[0] > max_iters:
                    return None
                if time.time() - start_time > max_time:
                    return None

                # select two suitable stacks
                while True:
                    # create a uniq pair
                    r1 = self.random.randrange(0, len(suitable_stacks))
                    r2 = self.random.randrange(0, len(suitable_stacks))
                    if r1 == r2:
                        continue
                    if (r1, r2) not in old_pairs and (r2, r1) not in old_pairs:
                        old_pairs.append((r1, r2))
                        break
                s1 = suitable_stacks[r1]
                s2 = suitable_stacks[r2]
                # check if s1 don't block s2
                nc[s1.id] = c1
                if not is_suitable(s2, nc):
                    nc[s1.id] = None
                    continue
                nc[s2.id] = c2
                # check if this layout is solvable (backtracking)
                ret = create_solvable(cards[:], nc)
                if ret:
                    ret = [x for x in ret if x != 1]
                    return ret
                nc[s1.id] = nc[s2.id] = None  # try another way

            return None

        new_cards = [None]*len(self.s.rows)  # None - empty stack, 1 - non-used
        drows = dict.fromkeys(rows)     # optimization
        for r in self.s.rows:
            if r not in drows:
                new_cards[r.id] = 1
        del drows

        while True:
            ret = create_solvable(cards[:], new_cards)
            if DEBUG:
                print('create_solvable time:', time.time() - start_time)
            if ret:
                ret.reverse()
                return ret
            if time.time() - start_time > max_time or \
                    iters[0] <= max_iters:
                print('oops! can\'t create a solvable game')
                return None
            iters = [0]
        print('oops! can\'t create a solvable game')
        return None

    def _mahjonggShuffle(self):
        talon = self.s.talon
        rows = []
        cards = []

        for r in self.s.rows:
            if r.cards:
                rows.append(r)
                cards.append(r.cards[0])
        if not rows:
            return

        if self.app.opt.mahjongg_create_solvable == 0:
            self.playSample('turnwaste')
            old_state = self.enterState(self.S_FILL)
            self.saveSeedMove()
            for r in rows:
                self.moveMove(1, r, talon, frames=0)
            self.shuffleStackMove(talon)
            for r in rows:
                self.moveMove(1, talon, r, frames=0)
            self.leaveState(old_state)
            self.finishMove()
            return

        self.playSample('turnwaste')
        old_state = self.enterState(self.S_FILL)
        self.saveSeedMove()

        new_cards = self._shuffleHook2(rows, cards)
        if new_cards is None:
            if TOOLKIT != 'kivy':
                MfxMessageDialog(self.top, title=_('Warning'),
                                 text=_('''\
Sorry, I can\'t find
a solvable configuration.'''),
                                 bitmap='warning')

            self.leaveState(old_state)
            # self.finishMove()
            # hack
            am = self.moves.current[0]
            am.undo(self)               # restore random
            self.moves.current = []
            return

        self.stats.shuffle_moves += 1
        # move new_cards to talon
        for c in new_cards:
            for r in rows:
                if r.cards and r.cards[0] is c:
                    self.moveMove(1, r, talon, frames=0)
                    break
        # deal
        for r in rows:
            self.moveMove(1, talon, r, frames=0)

        self.leaveState(old_state)
        self.finishMove()

    def canShuffle(self):
        return True

    def startGame(self):
        assert len(self.s.talon.cards) == self.NCARDS
        # self.s.talon.dealRow(rows = self.s.rows, frames = 0)
        n = 12
        self.s.talon.dealRow(rows=self.s.rows[:self.NCARDS-n], frames=0)
        self.startDealSample()
        self.s.talon.dealRow(rows=self.s.rows[self.NCARDS-n:])
        assert len(self.s.talon.cards) == 0

    def isGameWon(self):
        return sum(len(f.cards) for f in self.s.foundations) == self.NCARDS

    def shallHighlightMatch(self, stack1, card1, stack2, card2):
        if stack1.basicIsBlocked() or stack2.basicIsBlocked():
            return 0
        return self.cardsMatch(card1, card2)

    def getAutoStacks(self, event=None):
        return ((), (), ())

    def updateText(self):
        if self.preview > 1 or self.texts.info is None:
            return
        t = self.getText()
        self.texts.info.config(text=t)

    def getText(self):
        # find matching tiles
        stacks = []
        for r in self.s.rows:
            if r.cards and not r.basicIsBlocked():
                stacks.append(r)
        f, i = 0, 0
        for r in stacks:
            n = 0
            for t in stacks[i+1:]:
                if self.cardsMatch(r.cards[0], t.cards[0]):
                    n += 1
            # if n == 3: n = 1
            # elif n == 2: n = 0
            n = n % 2
            f += n
            i += 1

        if f == 0:
            f = _('No Free\nMatching\nPairs')
        else:
            f = ungettext('%d Free\nMatching\nPair',
                          '%d Free\nMatching\nPairs',
                          f) % f
        t = sum(len(ii.cards) for ii in self.s.foundations)
        r1 = ungettext('%d\nTile\nRemoved\n\n',
                       '%d\nTiles\nRemoved\n\n',
                       t) % t
        r2 = ungettext('%d\nTile\nRemaining\n\n',
                       '%d\nTiles\nRemaining\n\n',
                       self.NCARDS - t) % (self.NCARDS - t)

        t = r1 + r2 + f
        return t

    def parseStackInfo(self, stack):
        if not hasattr(stack, 'blockmap'):
            return ""
        return _("Layer: %d") % (stack.blockmap.level + 1)

    def parseGameInfo(self):
        return self.getText()

    #
    # Mahjongg special overrides
    #

    def getHighlightPilesStacks(self):
        # Mahjongg special: highlight all moveable tiles
        return ((self.s.rows, 1),)

    def _highlightCards(self, info, sleep=1.5, delta=(1, 1, 1, 1)):
        if not Image:
            delta = (-self._delta_x, 0, 0, -self._delta_y)
            return Game._highlightCards(self, info, sleep=sleep, delta=delta)

        if not info:
            return 0
        if self.pause:
            return 0
        self.stopWinAnimation()
        items = []
        for s, c1, c2, color in info:
            assert c1 is c2
            assert c1 in s.cards
            x, y = s.x, s.y
            img = self.app.images.getHighlightedCard(
                c1.deck, c1.suit, c1.rank, 'black')
            if img is None:
                continue
            img = MfxCanvasImage(self.canvas, x, y, image=img,
                                 anchor=ANCHOR_NW, group=s.group)
            if self.drag.stack and s is self.drag.stack:
                img.tkraise(self.drag.shade_img)
            else:
                img.tkraise(c1.item)
            items.append(img)
        if not items:
            return 0
        self.canvas.update_idletasks()
        if sleep:
            self.sleep(sleep)
            items.reverse()
            for r in items:
                r.delete()
            self.canvas.update_idletasks()
            return EVENT_HANDLED
        # remove items later (find_card_dialog)
        return items

    def getCardFaceImage(self, deck, suit, rank):
        if suit == 3:
            cs = self.app.cardset
            if len(cs.ranks) >= 12 and len(cs.suits) >= 4:
                # make Mahjongg type games playable with other cardsets
                if rank >= 8:       # flower
                    suit = 1
                    rank = len(cs.ranks) - 2
                elif rank >= 4:     # season
                    rank = max(10, len(cs.ranks) - 3)
                else:               # wind
                    suit = rank
                    rank = len(cs.ranks) - 1
        return self.app.images.getFace(deck, suit, rank)

    def getCardBackImage(self, deck, suit, rank):
        # We avoid screen updates caused by flipping cards - all
        # cards are face up anyway. The Talon should be invisible
        # or else the top tile of the Talon will be visible during
        # game start.
        return self.getCardFaceImage(deck, suit, rank)

    def _createCard(self, id, deck, suit, rank, x, y):
        # if deck >= 1 and suit == 3 and rank >= 4:
        if deck % 4 and suit == 3 and rank >= 4:
            return None
        return Game._createCard(self, id, deck, suit, rank, x, y)

    def _getClosestStack(self, cx, cy, stacks, dragstack):
        closest, cdist = None, 999999999
        # Since we only compare distances,
        # we don't bother to take the square root.
        for stack in stacks:
            dist = (stack.x - cx)**2 + (stack.y - cy)**2
            if dist < cdist:
                # Mahjongg special: if the stack is very close, do
                # not consider blocked stacks
                if dist > self.check_dist or not stack.basicIsBlocked():
                    closest, cdist = stack, dist
        return closest

    #
    # Mahjongg extras
    #

    def cardsMatch(self, card1, card2):
        if card1.suit != card2.suit:
            return 0
        if card1.suit == 3:
            if card1.rank >= 8:
                return card2.rank >= 8
            if card1.rank >= 4:
                return 7 >= card2.rank >= 4
        return card1.rank == card2.rank


#  mahjongg util
def comp_cardset(ncards):
    # calc decks, ranks & trumps
    assert ncards % 4 == 0
    assert 0 < ncards <= 288  # ???
    decks = 1
    cards = ncards//4
    if ncards > 144:
        assert ncards % 8 == 0
        decks = 2
        cards = cards//2
    ranks, trumps = divmod(cards, 3)
    if ranks > 10:
        trumps += (ranks-10)*3
        ranks = 10
    if trumps > 4:
        trumps = 4+(trumps-4)*4
    assert 0 <= ranks <= 10 and 0 <= trumps <= 12
    return decks, ranks, trumps

# ************************************************************************
# * register a Mahjongg type game
# ************************************************************************


def r(id, short_name, name=None, ncards=144, layout=None):
    assert layout
    if not name:
        name = "Mahjongg " + short_name
    classname = re.sub('\\W', '', name)
    # create class
    gameclass = type(classname, (AbstractMahjonggGame,), {})
    gameclass.L = layout
    gameclass.NCARDS = ncards
    decks, ranks, trumps = comp_cardset(ncards)
    gi = GameInfo(id, gameclass, name,
                  GI.GT_MAHJONGG, 4*decks, 0,  # GI.SL_MOSTLY_SKILL,
                  category=GI.GC_MAHJONGG, short_name=short_name,
                  suits=list(range(3)), ranks=list(range(ranks)),
                  trumps=list(range(trumps)),
                  si={"decks": decks, "ncards": ncards})
    gi.ncards = ncards
    gi.rules_filename = "mahjongg.html"
    registerGame(gi)
    return gi
