#!/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

from six.moves import range


def factorial(x):
    if x <= 1:
        return 1
    a = 1
    for i in range(x):
        a *= (i+1)
    return a


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

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()
        return

    def getHelp(self):
        return ''


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

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 __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")
        # 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()

    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


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

class AbstractMahjonggGame(Game):
    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

    #
    # 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:
            dx = l.XOFFSET
            dy = -l.YOFFSET
            d_x = cs.SHADOW_XOFFSET
            d_y = cs.SHADOW_YOFFSET
            if self.preview:
                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 = 80

        # 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
            #
            above = tuple([_f for _f in above.keys() if _f])
            below = tuple([_f for _f in below.keys() if _f])
            left = tuple([_f for _f in left.keys() if _f])
            right = tuple([_f for _f in right.keys() if _f])
            # up = tuple(filter(None, up.keys()))
            # bottom = tuple(filter(None, bottom.keys()))

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

        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
            #
            i = factorial(len(free_stacks))//2//factorial(len(free_stacks)-2)
            old_pairs = []
            for j in range(i):
                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 = []
            i = factorial(len(suitable_stacks))//2 \
                // factorial(len(suitable_stacks)-2)
            for j in range(i):
                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

        # 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
        self.texts.info.config(text=t)

    #
    # 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
        else:
            # 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
