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

from pysollib.mfxutil import Image, ImageTk, USE_PIL
from pysollib.mfxutil import Struct, SubclassResponsibility, kwdefault
from pysollib.mygettext import _
from pysollib.pysoltk import ANCHOR_NW, ANCHOR_SE
from pysollib.pysoltk import CURSOR_DOWN_ARROW, CURSOR_DRAG
from pysollib.pysoltk import EVENT_HANDLED, EVENT_PROPAGATE
from pysollib.pysoltk import MfxCanvasGroup, MfxCanvasImage
from pysollib.pysoltk import MfxCanvasRectangle, MfxCanvasText
from pysollib.pysoltk import after_cancel, after_idle
from pysollib.pysoltk import bind, unbind_destroy
from pysollib.pysoltk import get_text_width
from pysollib.pysoltk import markImage
from pysollib.settings import DEBUG
from pysollib.settings import TOOLKIT
from pysollib.util import ACE, KING
from pysollib.util import ANY_RANK, ANY_SUIT, NO_RANK

# ************************************************************************
# * Let's start with some test methods for cards.
# * Empty card-lists return false.
# ************************************************************************


# check that all cards are face-up
def cardsFaceUp(cards):
    if not cards:
        return False
    for c in cards:
        if not c.face_up:
            return False
    return True


# check that all cards are face-down
def cardsFaceDown(cards):
    if not cards:
        return False
    for c in cards:
        if c.face_up:
            return False
    return True


# check that cards are face-up and build down by rank
def isRankSequence(cards, mod=8192, dir=-1):
    if not cardsFaceUp(cards):
        return False
    c1 = cards[0]
    for c2 in cards[1:]:
        if (c1.rank + dir) % mod != c2.rank:
            return False
        c1 = c2
    return True


# check that cards are face-up and build down by alternate color
def isAlternateColorSequence(cards, mod=8192, dir=-1):
    if not cardsFaceUp(cards):
        return False
    c1 = cards[0]
    for c2 in cards[1:]:
        if (c1.rank + dir) % mod != c2.rank or c1.color == c2.color:
            return False
        c1 = c2
    return True


# check that cards are face-up and build down by same color
def isSameColorSequence(cards, mod=8192, dir=-1):
    if not cardsFaceUp(cards):
        return False
    c1 = cards[0]
    for c2 in cards[1:]:
        if (c1.rank + dir) % mod != c2.rank or c1.color != c2.color:
            return False
        c1 = c2
    return True


# check that cards are face-up and build down by same suit
def isSameSuitSequence(cards, mod=8192, dir=-1):
    if not cardsFaceUp(cards):
        return False
    c1 = cards[0]
    for c2 in cards[1:]:
        if (c1.rank + dir) % mod != c2.rank or c1.suit != c2.suit:
            return False
        c1 = c2
    return True


# check that cards are face-up and build down by any suit but own
def isAnySuitButOwnSequence(cards, mod=8192, dir=-1):
    if not cardsFaceUp(cards):
        return False
    c1 = cards[0]
    for c2 in cards[1:]:
        if (c1.rank + dir) % mod != c2.rank or c1.suit == c2.suit:
            return False
        c1 = c2
    return True


def getNumberOfFreeStacks(stacks):
    return len([s for s in stacks if not s.cards])


# collect the top cards of several stacks into a pile
def getPileFromStacks(stacks, reverse=False):
    cards = []
    for s in stacks:
        if not s.cards or not s.cards[-1].face_up:
            return None
        cards.append(s.cards[-1])
    return (reversed(cards) if reverse else cards)


class Stack:
    # A generic stack of cards.
    #
    # This is used as a base class for all other stacks (e.g. the talon,
    # the foundations and the row stacks).
    #
    # The default event handlers turn the top card of the stack with
    # its face up on a (single or double) click, and also support
    # moving a subpile around.

    # constants
    MIN_VISIBLE_XOFFSET = 3
    MIN_VISIBLE_YOFFSET = 3
    SHRINK_FACTOR = 2.

    def __init__(self, x, y, game, cap={}):
        # Arguments are the stack's nominal x and y position (the top
        # left corner of the first card placed in the stack), and the
        # game object (which is used to get the canvas; subclasses use
        # the game object to find other stacks).

        #
        # link back to game
        #
        id = len(game.allstacks)
        game.allstacks.append(self)
        x = int(round(x))
        y = int(round(y))
        mapkey = (x, y)
        # assert not game.stackmap.has_key(mapkey) ## can happen in PyJonngg
        game.stackmap[mapkey] = id

        #
        # setup our pseudo MVC scheme
        #
        model, view = self, self

        #
        # model
        #
        model.id = id
        model.game = game
        model.cards = []
        #
        model.is_filled = False

        # capabilites - the game logic
        model.cap = Struct(
            suit=-1,          # required suit for this stack (-1 is ANY_SUIT)
            color=-1,         # required color for this stack (-1 is ANY_COLOR)
            rank=-1,          # required rank for this stack (-1 is ANY_RANK)
            base_suit=-1,     # base suit for this stack (-1 is ANY_SUIT)
            base_color=-1,    # base color for this stack (-1 is ANY_COLOR)
            base_rank=-1,     # base rank for this stack (-1 is ANY_RANK)
            dir=0,            # direction - stack builds up/down
            mod=8192,         # modulo for wrap around (typically 13 or 8192)
            max_move=0,       # can move at most # cards at a time
            max_accept=0,     # can accept at most # cards at a time
            max_cards=999999,  # total number of cards may not exceed this
            # not commonly used:
            min_move=1,       # must move at least # cards at a time
            min_accept=1,     # must accept at least # cards at a time
            # total number of cards this stack at least requires
            min_cards=0,
        )
        model.cap.update(cap)
        assert isinstance(model.cap.suit, int)
        assert isinstance(model.cap.color, int)
        assert isinstance(model.cap.rank, int)
        assert isinstance(model.cap.base_suit, int)
        assert isinstance(model.cap.base_color, int)
        assert isinstance(model.cap.base_rank, int)
        #
        # view
        #
        self.init_coord = (x, y)
        view.x = x
        view.y = y
        view.canvas = game.canvas
        view.CARD_XOFFSET = 0
        view.CARD_YOFFSET = 0
        view.INIT_CARD_OFFSETS = (0, 0)
        view.INIT_CARD_YOFFSET = 0      # for reallocateCards
        view.group = MfxCanvasGroup(view.canvas)

        if (TOOLKIT == 'kivy'):
            if hasattr(view.group, 'stack'):
                view.group.stack = self

        view.shrink_face_down = 1
        # image items
        view.images = Struct(
            bottom=None,              # canvas item
            redeal=None,              # canvas item
            redeal_img=None,          # the corresponding PhotoImage
            shade_img=None,
        )
        # other canvas items
        view.items = Struct(
            bottom=None,              # dummy canvas item
            shade_item=None,
        )
        # text items
        view.texts = Struct(
            ncards=None,              # canvas item
            # by default only used by Talon:
            rounds=None,              # canvas item
            redeal=None,              # canvas item
            redeal_str=None,          # the corresponding string
            # for use by derived stacks:
            misc=None,                # canvas item
        )
        view.top_bottom = None          # the highest of all bottom items
        cardw, cardh = game.app.images.CARDW, game.app.images.CARDH
        dx, dy = cardw+view.canvas.xmargin, cardh+view.canvas.ymargin
        view.is_visible = view.x >= -dx and view.y >= -dy
        view.is_open = -1
        view.can_hide_cards = -1
        view.max_shadow_cards = -1
        view.current_cursor = ''
        view.cursor_changed = False

    def destruct(self):
        # help breaking circular references
        unbind_destroy(self.group)

    def prepareStack(self):
        self.prepareView()
        if self.is_visible:
            self.initBindings()

    # bindings {view widgets bind to controller}
    def initBindings(self):
        group = self.group
        bind(group, "<1>", self.__clickEventHandler)
        # bind(group, "<B1-Motion>", self.__motionEventHandler)
        bind(group, "<Motion>", self.__motionEventHandler)
        bind(group, "<ButtonRelease-1>", self.__releaseEventHandler)
        bind(group, "<Control-1>", self.__controlclickEventHandler)
        bind(group, "<Shift-1>", self.__shiftclickEventHandler)
        bind(group, "<Double-1>", self.__doubleclickEventHandler)
        bind(group, "<3>", self.__rightclickEventHandler)
        bind(group, "<2>", self.__middleclickEventHandler)
        bind(group, "<Control-3>", self.__middleclickEventHandler)
        # bind(group, "<Control-2>", self.__controlmiddleclickEventHandler)
        # bind(group, "<Shift-3>", self.__shiftrightclickEventHandler)
        # bind(group, "<Double-2>", "")
        bind(group, "<Enter>", self.__enterEventHandler)
        bind(group, "<Leave>", self.__leaveEventHandler)

    def prepareView(self):
        # assertView(self)
        if (self.CARD_XOFFSET == 0 and self.CARD_YOFFSET == 0):
            assert self.cap.max_move <= 1
        # prepare some variables
        ox, oy = self.CARD_XOFFSET, self.CARD_YOFFSET
        if isinstance(ox, (int, float)):
            self.CARD_XOFFSET = (ox,)
        else:
            self.CARD_XOFFSET = tuple([int(round(x)) for x in ox])
        if isinstance(oy, (int, float)):
            self.CARD_YOFFSET = (oy,)
        else:
            self.CARD_YOFFSET = tuple([int(round(y)) for y in oy])

        # preserve offsets
        # for resize()
        self.INIT_CARD_OFFSETS = (self.CARD_XOFFSET, self.CARD_YOFFSET)
        self.INIT_CARD_YOFFSET = self.CARD_YOFFSET  # for reallocateCards

        if self.can_hide_cards < 0:
            self.can_hide_cards = self.is_visible
            if self.cap.max_cards < 3:
                self.can_hide_cards = 0
            elif [_f for _f in self.CARD_XOFFSET if _f]:
                self.can_hide_cards = 0
            elif [_f for _f in self.CARD_YOFFSET if _f]:
                self.can_hide_cards = 0
            elif self.canvas.preview:
                self.can_hide_cards = 0
        if self.is_open < 0:
            self.is_open = False
            if (self.is_visible and
                (abs(self.CARD_XOFFSET[0]) >= self.MIN_VISIBLE_XOFFSET or
                 abs(self.CARD_YOFFSET[0]) >= self.MIN_VISIBLE_YOFFSET)):
                self.is_open = True
        if self.max_shadow_cards < 0:
            self.max_shadow_cards = 999999
            # if abs(self.CARD_YOFFSET[0])
            #     != self.game.app.images.CARD_YOFFSET:
            #           # don't display a shadow if the YOFFSET of the stack
            #           # and the images don't match
            #      self.max_shadow_cards = 1
        if (self.game.app.opt.shrink_face_down and
                isinstance(ox, (int, float)) and
                isinstance(oy, (int, float))):
            # no shrink if xoffset/yoffset too small
            f = self.SHRINK_FACTOR
            if ((ox == 0 and oy >= self.game.app.images.CARD_YOFFSET//f) or
                    (oy == 0 and
                     ox >= self.game.app.images.CARD_XOFFSET//f)):
                self.shrink_face_down = f
        # bottom image
        if self.is_visible:
            self.prepareBottom()

    # stack bottom image
    def prepareBottom(self):
        assert self.is_visible and self.images.bottom is None
        img = self.getBottomImage()
        if img is not None:
            self.images.bottom = MfxCanvasImage(self.canvas, self.x, self.y,
                                                image=img, anchor=ANCHOR_NW,
                                                group=self.group)
            self.top_bottom = self.images.bottom

    # invisible stack bottom
    # We need this if we want to get any events for an empty stack (which
    # is needed by the quickPlayHandler in some games like Montana)
    def prepareInvisibleBottom(self):
        assert self.is_visible and self.items.bottom is None
        images = self.game.app.images
        self.items.bottom = MfxCanvasRectangle(self.canvas, self.x, self.y,
                                               self.x + images.CARDW,
                                               self.y + images.CARDH,
                                               fill="", outline="", width=0,
                                               group=self.group)
        self.top_bottom = self.items.bottom

    # sanity checks
    def assertStack(self):
        assert self.cap.min_move > 0
        assert self.cap.min_accept > 0
        assert not hasattr(self, "suit")

    #
    # Core access methods {model -> view}
    #

    # Add a card add the top of a stack. Also update display. {model -> view}
    def addCard(self, card, unhide=1, update=1):
        model, view = self, self
        model.cards.append(card)
        card.tkraise(unhide=unhide)
        if view.can_hide_cards and len(model.cards) >= 3:
            # we only need to display the 2 top cards
            model.cards[-3].hide(self)
        card.item.addtag(view.group)
        view._position(card)
        if update:
            view.updateText()
        self.closeStack()
        return card

    def insertCard(self, card, position, unhide=1, update=1):
        model, view = self, self
        model.cards.insert(position, card)
        for c in model.cards[position:]:
            c.tkraise(unhide=unhide)
        if (view.can_hide_cards and len(model.cards) >= 3 and
                len(model.cards)-position <= 2):
            # we only need to display the 2 top cards
            model.cards[-3].hide(self)
        card.item.addtag(view.group)
        for c in model.cards[position:]:
            view._position(c)
        if update:
            view.updateText()
        self.closeStack()
        return card

    # Remove a card from the stack. Also update display. {model -> view}
    def removeCard(self, card=None, unhide=1, update=1, update_positions=0):
        model, view = self, self
        assert len(model.cards) > 0
        if card is None:
            card = model.cards[-1]
            # optimized a little bit (compare with the else below)
            card.item.dtag(view.group)
            if unhide and self.can_hide_cards:
                card.unhide()
                if len(self.cards) >= 3:
                    model.cards[-3].unhide()
            del model.cards[-1]
        else:
            card.item.dtag(view.group)
            if unhide and view.can_hide_cards:
                # Note: the 2 top cards ([-1] and [-2]) are already unhidden.
                card.unhide()
                if len(model.cards) >= 3:
                    if card is model.cards[-1] or model is self.cards[-2]:
                        # Make sure that 2 top cards will be un-hidden.
                        model.cards[-3].unhide()
            card_index = model.cards.index(card)
            model.cards.remove(card)
            if update_positions:
                for c in model.cards[card_index:]:
                    view._position(c)

        if update:
            view.updateText()
        self.unshadeStack()
        self.is_filled = False
        return card

    # Get the top card {model}
    def getCard(self):
        if self.cards:
            return self.cards[-1]
        return None

    # get the largest moveable pile {model} - uses canMoveCards()
    def getPile(self):
        if self.cap.max_move > 0:
            cards = self.cards[-self.cap.max_move:]
            while len(cards) >= self.cap.min_move:
                if self.canMoveCards(cards):
                    return cards
                del cards[0]
        return None

    # Position the card on the canvas {view}
    def _position(self, card):
        x, y = self.getPositionFor(card)
        card.moveTo(x, y)

    # find card
    def _findCard(self, event):
        model, view = self, self
        if event is not None and model.cards:
            # ask the canvas
            return view.canvas.findCard(self, event)
        return -1

    # find card
    def _findCardXY(self, x, y, cards=None):
        model = self
        if cards is None:
            cards = model.cards
        images = self.game.app.images
        cw, ch = images.getSize()
        index = -1
        for i in range(len(cards)):
            c = cards[i]
            r = (c.x, c.y, c.x + cw, c.y + ch)
            if r[0] <= x < r[2] and r[1] <= y < r[3]:
                index = i
        return index

    # generic model update (can be used for undo/redo - see move.py)
    def updateModel(self, undo, flags):
        pass

    # copy model data - see Hint.AClonedStack
    def copyModel(self, clone):
        clone.id = self.id
        clone.game = self.game
        clone.cap = self.cap

    def getRankDir(self, cards=None):
        if cards is None:
            cards = self.cards[-2:]
        if len(cards) < 2:
            return 0
        dir = (cards[-1].rank - cards[-2].rank) % self.cap.mod
        if dir > self.cap.mod // 2:
            return dir - self.cap.mod
        return dir

    #
    # Basic capabilities {model}
    # Used by various subclasses.
    #

    def basicIsBlocked(self):
        # Check if the stack is blocked (e.g. Pyramid or Mahjongg)
        return False

    def basicAcceptsCards(self, from_stack, cards):
        # Check that the limits are ok and that the cards are face up
        if from_stack is self or self.basicIsBlocked():
            return False
        cap = self.cap
        mylen = len(cards)
        if mylen < cap.min_accept or mylen > cap.max_accept:
            return False
        mylen += len(self.cards)
        # note: we don't check cap.min_cards here
        if mylen > cap.max_cards:
            return False

        def _check(c, suit, color, rank):
            return ((suit >= 0 and c.suit != suit) or
                    (color >= 0 and c.color != color) or
                    (rank >= 0 and c.rank != rank))
        for c in cards:
            if not c.face_up or _check(c, cap.suit, cap.color, cap.rank):
                return False
        if self.cards:
            # top card of our stack must be face up
            return self.cards[-1].face_up
        # check required base
        return not _check(cards[0], cap.base_suit, cap.base_color,
                          cap.base_rank)

    def basicCanMoveCards(self, cards):
        # Check that the limits are ok and the cards are face up
        if self.basicIsBlocked():
            return False
        cap = self.cap
        mylen = len(cards)
        if mylen < cap.min_move or mylen > cap.max_move:
            return False
        mylen = len(self.cards) - mylen
        # note: we don't check cap.max_cards here
        if mylen < cap.min_cards:
            return False
        return cardsFaceUp(cards)

    #
    # Capabilities - important for game logic {model}
    #

    def acceptsCards(self, from_stack, cards):
        # Do we accept receiving `cards' from `from_stack' ?
        return False

    def canMoveCards(self, cards):
        # Can we move these cards when assuming they are our top-cards ?
        return False

    def canFlipCard(self):
        # Can we flip our top card ?
        return False

    def canDropCards(self, stacks):
        # Can we drop the top cards onto one of the foundation stacks ?
        return (None, 0)    # return the stack and the number of cards

    #
    # State {model}
    #

    def resetGame(self):
        # Called when starting a new game.
        self.CARD_YOFFSET = self.INIT_CARD_YOFFSET
        self.items.shade_item = None
        self.images.shade_img = None
        # self.items.bottom = None
        # self.images.bottom = None

    def __repr__(self):
        # Return a string for debug print statements.
        return "%s(%d)" % (self.__class__.__name__, self.id)

    #
    # Atomic move actions {model -> view}
    #

    def flipMove(self, animation=False):
        # Flip the top card.
        if animation:
            self.game.singleFlipMove(self)
        else:
            self.game.flipMove(self)

    def moveMove(self, ncards, to_stack, frames=-1, shadow=-1):
        # Move the top n cards.
        self.game.moveMove(
            ncards, self, to_stack, frames=frames, shadow=shadow)
        self.fillStack()

    def fillStack(self):
        self.game.fillStack(self)

    def closeStack(self):
        pass

    #
    # Playing move actions. Better not override.
    #

    def playFlipMove(self, sound=True, animation=False):
        if sound:
            self.game.playSample("flip", 5)
        self.flipMove(animation=animation)
        if not self.game.checkForWin():
            self.game.autoPlay()
        self.game.finishMove()

    def playMoveMove(self, ncards, to_stack, frames=-1, shadow=-1, sound=True):
        if sound:
            if to_stack in self.game.s.foundations:
                self.game.playSample("drop", priority=30)
            else:
                self.game.playSample("move", priority=10)
        self.moveMove(ncards, to_stack, frames=frames, shadow=shadow)
        if not self.game.checkForWin():
            # let the player put cards back from the foundations
            if self not in self.game.s.foundations:
                self.game.autoPlay()
        self.game.finishMove()

    #
    # Appearance {view}
    #

    def _getBlankBottomImage(self):
        return self.game.app.images.getBlankBottom()

    def _getReserveBottomImage(self):
        return self.game.app.images.getReserveBottom()

    def _getSuitBottomImage(self):
        return self.game.app.images.getSuitBottom(self.cap.base_suit)

    def _getNoneBottomImage(self):
        return None

    def _getTalonBottomImage(self):
        return self.game.app.images.getTalonBottom()

    def _getBraidBottomImage(self):
        return self.game.app.images.getBraidBottom()

    def _getLetterImage(self):
        return self.game.app.images.getLetter(self.cap.base_rank)

    getBottomImage = _getBlankBottomImage

    def getPositionFor(self, card):
        model, view = self, self
        x, y = view.x, view.y
        if view.can_hide_cards:
            return x, y
        ix, iy, lx, ly = 0, 0, len(view.CARD_XOFFSET), len(view.CARD_YOFFSET)
        d = self.shrink_face_down
        for c in model.cards:
            if c is card:
                break
            if c.face_up:
                x += self.CARD_XOFFSET[ix]
                y += self.CARD_YOFFSET[iy]
            else:
                x += self.CARD_XOFFSET[ix]//d
                y += self.CARD_YOFFSET[iy]//d
            ix = (ix + 1) % lx
            iy = (iy + 1) % ly
        return int(x), int(y)

    def getPositionForNextCard(self):
        model, view = self, self
        x, y = view.x, view.y
        if view.can_hide_cards:
            return x, y
        if not self.cards:
            return x, y
        ix, iy, lx, ly = 0, 0, len(view.CARD_XOFFSET), len(view.CARD_YOFFSET)
        d = self.shrink_face_down
        for c in model.cards:
            if c.face_up:
                x += self.CARD_XOFFSET[ix]
                y += self.CARD_YOFFSET[iy]
            else:
                x += self.CARD_XOFFSET[ix]//d
                y += self.CARD_YOFFSET[iy]//d
            ix = (ix + 1) % lx
            iy = (iy + 1) % ly
        return int(x), int(y)

    def getOffsetFor(self, card):
        model, view = self, self
        if view.can_hide_cards:
            return 0, 0
        lx, ly = len(view.CARD_XOFFSET), len(view.CARD_YOFFSET)
        i = list(model.cards).index(card)
        return view.CARD_XOFFSET[i % lx], view.CARD_YOFFSET[i % ly]

    # Fully update the view of a stack - updates
    # hiding, card positions and stacking order.
    # Avoid calling this as it is rather slow.
    def refreshView(self):
        model, view = self, self
        cards = model.cards
        if not view.is_visible or len(cards) < 2:
            return
        if view.can_hide_cards:
            # hide all lower cards
            for c in cards[:-2]:
                # print "refresh hide", c, c.hide_stack
                c.hide(self)
            # unhide the 2 top cards
            for c in cards[-2:]:
                # print "refresh unhide 1", c, c.hide_stack
                c.unhide()
                # print "refresh unhide 1", c, c.hide_stack
        # update the card postions and stacking order
        item = cards[0].item
        x, y = view.x, view.y
        ix, iy, lx, ly = 0, 0, len(view.CARD_XOFFSET), len(view.CARD_YOFFSET)
        for c in cards[1:]:
            c.item.tkraise(item)
            item = c.item
            if not view.can_hide_cards:
                d = self.shrink_face_down
                if c.face_up:
                    x += self.CARD_XOFFSET[ix]
                    y += self.CARD_YOFFSET[iy]
                else:
                    x += int(self.CARD_XOFFSET[ix]/d)
                    y += int(self.CARD_YOFFSET[iy]/d)
                ix = (ix + 1) % lx
                iy = (iy + 1) % ly
                c.moveTo(x, y)

    def updateText(self):
        if self.game.preview > 1 or self.texts.ncards is None:
            return
        t = ""
        format = "%d"
        if self.texts.ncards.text_format is not None:
            format = self.texts.ncards.text_format
            if format == "%D":
                format = ""
                if self.cards:
                    format = "%d"
        if format:
            t = format % len(self.cards)
            # if 0:
            #     visible = 0
            #     for c in self.cards:
            #         if c.isHidden():
            #             assert c.hide_stack is not None
            #         else:
            #             visible = visible + 1
            #             assert c.hide_stack is None
            #     t  = t + " (%d)" % visible
        self.texts.ncards.config(text=t)

    def updatePositions(self):
        # compact the stack when a cards goes off screen
        if self.reallocateCards():
            for c in self.cards:
                self._position(c)

    def reallocateCards(self):
        # change CARD_YOFFSET if a cards is off-screen
        # returned False if CARD_YOFFSET is not changed, otherwise True
        if not self.game.app.opt.compact_stacks:
            return False
        if TOOLKIT != 'tk':
            return False
        if self.CARD_XOFFSET != (0,):
            return False
        if len(self.CARD_YOFFSET) != 1:
            return False
        if self.CARD_YOFFSET[0] <= 0:
            return False
        if len(self.cards) <= 1:
            return False
        if not self.canvas.winfo_ismapped():
            return False
        yoffset = self.CARD_YOFFSET[0]
        # 1/2 of a card is visible
        cardh = self.game.app.images.getSize()[0] // 2
        num_face_up = len([c for c in self.cards if c.face_up])
        num_face_down = len(self.cards) - num_face_up
        stack_height = int(self.y +
                           num_face_down * yoffset // self.shrink_face_down +
                           num_face_up * yoffset +
                           cardh)
        visible_height = self.canvas.winfo_height()
        if USE_PIL and self.game.app.opt.auto_scale:
            # use visible_height only
            game_height = 0
        else:
            game_height = self.game.height + 2*self.canvas.ymargin
        height = max(visible_height, game_height)
        # print 'reallocateCards:', stack_height, height, \
        # visible_height, game_height
        if stack_height > height:
            # compact stack
            n = num_face_down // self.shrink_face_down + num_face_up
            dy = float(height - self.y - cardh) / n
            if dy < yoffset:
                # print 'compact:', dy
                self.CARD_YOFFSET = (dy,)
            return True
        elif stack_height < height:
            # expande stack
            if self.CARD_YOFFSET == self.INIT_CARD_YOFFSET:
                return False
            n = num_face_down // self.shrink_face_down + num_face_up
            dy = float(height - self.y - cardh) / n
            dy = min(dy, self.INIT_CARD_YOFFSET[0])
            # print 'expande:', dy
            self.CARD_YOFFSET = (dy,)
            return True
        return False

    def resize(self, xf, yf):
        # resize and move stack
        # xf, yf - a multiplicative factor (from the original values)
        # print 'Stack.resize:', self, self.is_visible, xf, yf
        x0, y0 = self.init_coord
        x, y = int(round(x0*xf)), int(round(y0*yf))
        self.x, self.y = x, y
        # offsets
        xoffset = tuple(int(round(i*xf)) for i in self.INIT_CARD_OFFSETS[0])
        yoffset = tuple(int(round(i*yf)) for i in self.INIT_CARD_OFFSETS[1])
        self.CARD_XOFFSET = xoffset
        self.CARD_YOFFSET = yoffset
        self.INIT_CARD_YOFFSET = yoffset
        # print '* resize offset:', self.INIT_CARD_XOFFSET,
        # move cards
        for c in self.cards:
            cx, cy = self.getPositionFor(c)
            c.moveTo(cx, cy)
        # ---
        if not self.is_visible:
            return
        # bottom and shade
        if self.images.bottom:
            img = self.getBottomImage()
            self.images.bottom['image'] = img
            self.images.bottom.moveTo(x, y)
        if self.items.bottom:
            c = self.items.bottom.coords()
            c = ((int(round(c[0]*xf)), int(round(c[1]*yf))),
                 (int(round(c[2]*xf)), int(round(c[3]*yf))))
            self.items.bottom.coords(c)
        if self.items.shade_item:
            c = self.cards[-1]
            img = self.game.app.images.getHighlightedCard(
                c.deck, c.suit, c.rank)
            if img:
                self.items.shade_item['image'] = img
            self.items.shade_item.moveTo(x, y)

        # move the items
        def move(item):
            ix, iy = item.init_coord
            x = int(round(ix*xf))
            y = int(round(iy*yf))
            item.moveTo(x, y)
        # images
        if self.images.redeal:
            move(self.images.redeal)
        # texts
        if self.texts.ncards:
            move(self.texts.ncards)
        if self.texts.rounds:
            move(self.texts.rounds)
        if self.texts.redeal:
            move(self.texts.redeal)
        if self.texts.misc:
            move(self.texts.misc)

    def basicShallHighlightSameRank(self, card):
        # by default all open stacks are available for highlighting
        assert card in self.cards
        if not self.is_visible or not card.face_up:
            return False
        if card is self.cards[-1]:
            return True
        if not self.is_open:
            return False
        # dx, dy = self.getOffsetFor(card)
        # if ((dx == 0 and dy <= self.MIN_VISIBLE_XOFFSET) or
        #     (dx <= self.MIN_VISIBLE_YOFFSET and dy == 0)):
        #     return False
        return True

    def basicShallHighlightMatch(self, card):
        # by default all open stacks are available for highlighting
        return self.basicShallHighlightSameRank(card)

    def highlightSameRank(self, event):
        i = self._findCard(event)
        if i < 0:
            return 0
        card = self.cards[i]
        if not self.basicShallHighlightSameRank(card):
            return 0
        col_1 = self.game.app.opt.colors['samerank_1']
        col_2 = self.game.app.opt.colors['samerank_2']
        info = [(self, card, card, col_1)]
        for s in self.game.allstacks:
            for c in s.cards:
                if c is card:
                    continue
                # check the rank
                if c.rank != card.rank:
                    continue
                # ask the target stack
                if s.basicShallHighlightSameRank(c):
                    info.append((s, c, c, col_2))
        self.game.stats.highlight_samerank += 1
        return self.game._highlightCards(
            info, self.game.app.opt.timeouts['highlight_samerank'])

    def highlightMatchingCards(self, event):
        i = self._findCard(event)
        if i < 0:
            return 0
        card = self.cards[i]
        if not self.basicShallHighlightMatch(card):
            return 0
        col_1 = self.game.app.opt.colors['cards_1']
        col_2 = self.game.app.opt.colors['cards_2']
        c1 = c2 = card
        info = []
        found = 0
        for s in self.game.allstacks:
            # continue if both stacks are foundations
            if (self in self.game.s.foundations and
                    s in self.game.s.foundations):
                continue
            # for all cards
            for c in s.cards:
                if c is card:
                    continue
                # ask the target stack
                if not s.basicShallHighlightMatch(c):
                    continue
                # ask the game
                if self.game.shallHighlightMatch(self, card, s, c):
                    found = 1
                    if s is self:
                        # enlarge rectangle for neighbours
                        j = self.cards.index(c)
                        if i - 1 == j:
                            c1 = c
                            continue
                        if i + 1 == j:
                            c2 = c
                            continue
                    info.append((s, c, c, col_1))
        if found:
            if info:
                self.game.stats.highlight_cards += 1
            info.append((self, c1, c2, col_2))
            return self.game._highlightCards(
                info, self.game.app.opt.timeouts['highlight_cards'])
        if not self.basicIsBlocked():
            self.game.highlightNotMatching()
        return 0

    #
    # Subclass overridable handlers {contoller -> model -> view}
    #

    def clickHandler(self, event):
        return 0

    def middleclickHandler(self, event):
        # default action: show the card if it is overlapped by other cards
        if not self.is_open:
            return 0
        i = self._findCard(event)
        positions = len(self.cards) - i - 1
        if i < 0 or positions <= 0 or not self.cards[i].face_up:
            return 0
        # print self.cards[i]
        self.cards[i].item.tkraise()
        self.canvas.update_idletasks()
        self.game.sleep(self.game.app.opt.timeouts['raise_card'])
        if TOOLKIT == 'tk':
            self.cards[i].item.lower(self.cards[i+1].item)
        elif TOOLKIT == 'gtk':
            for c in self.cards[i+1:]:
                c.tkraise()
        self.canvas.update_idletasks()
        return 1

    def controlmiddleclickHandler(self, event):
        # cheating: show face-down card
        if not self.is_open:
            return 0
        i = self._findCard(event)
        positions = len(self.cards) - i - 1
        if i < 0 or positions < 0:
            return 0
        # print self.cards[i]
        face_up = self.cards[i].face_up
        if not face_up:
            self.cards[i].showFace()
        self.cards[i].item.tkraise()
        self.canvas.update_idletasks()
        self.game.sleep(self.game.app.opt.timeouts['raise_card'])
        if not face_up:
            self.cards[i].showBack()
        if TOOLKIT == 'tk':
            if positions > 0:
                self.cards[i].item.lower(self.cards[i+1].item)
        elif TOOLKIT == 'gtk':
            for c in self.cards[i+1:]:
                c.tkraise()
        self.canvas.update_idletasks()
        return 1

    def rightclickHandler(self, event):
        return 0

    def doubleclickHandler(self, event):
        return self.clickHandler(event)

    def controlclickHandler(self, event):
        return 0

    def shiftclickHandler(self, event):
        # default action: highlight all cards of the same rank
        if self.game.app.opt.highlight_samerank:
            return self.highlightSameRank(event)
        return 0

    def shiftrightclickHandler(self, event):
        return 0

    def releaseHandler(self, event, drag, sound=True):
        # default action: move cards back to their origin position
        if drag.cards:
            if sound:
                self.game.playSample("nomove")
            if self.game.app.opt.mouse_type == 'point-n-click':
                drag.stack.moveCardsBackHandler(event, drag)
            else:
                self.moveCardsBackHandler(event, drag)

    def moveCardsBackHandler(self, event, drag):
        if self.game.app.opt.animations:
            if drag.cards:
                c = drag.cards[0]
                x0, y0 = drag.stack.getPositionFor(c)
                x1, y1 = c.x, c.y
                dx, dy = abs(x0-x1), abs(y0-y1)
                w, h = self.game.app.images.getSize()
                if dx > 2*w or dy > 2*h:
                    self.game.animatedMoveTo(drag.stack, drag.stack,
                                             drag.cards, x0, y0, frames=-1)
                elif dx > w or dy > h:
                    self.game.animatedMoveTo(drag.stack, drag.stack,
                                             drag.cards, x0, y0, frames=4)
        for card in drag.cards:
            self._position(card)
        if self.is_filled and self.items.shade_item:
            self.items.shade_item.show()
            self.items.shade_item.tkraise()

    #
    # Event handlers {controller}
    #

    def __defaultClickEventHandler(self, event, handler,
                                   start_drag=0, cancel_drag=1):
        self.game.event_handled = True  # for Game.undoHandler
        if self.game.demo:
            self.game.stopDemo(event)
            return EVENT_HANDLED
        self.game.interruptSleep()
        if self.game.busy:
            return EVENT_HANDLED
        if self.game.drag.stack and cancel_drag:
            # in case we lost an event
            self.game.drag.stack.cancelDrag(event)
        if start_drag:
            # this handler may start a drag operation
            r = handler(event)
            if r <= 0:
                sound = r == 0
                self.startDrag(event, sound=sound)
        else:
            handler(event)
        return EVENT_HANDLED

    if (TOOLKIT == 'kivy'):
        def _motionEventHandler(self, event):
            return self.__motionEventHandler(event)

    def __clickEventHandler(self, event):
        if self.game.app.opt.mouse_type == 'drag-n-drop':
            cancel_drag = 1
            start_drag = 1
            handler = self.clickHandler
        else:  # sticky-mouse or point-n-click
            cancel_drag = 0
            start_drag = not self.game.drag.stack
            if start_drag:
                handler = self.clickHandler
            else:
                handler = self.finishDrag
        return self.__defaultClickEventHandler(
            event, handler, start_drag, cancel_drag)

    def __doubleclickEventHandler(self, event):
        return self.__defaultClickEventHandler(event, self.doubleclickHandler)

    def __middleclickEventHandler(self, event):
        return self.__defaultClickEventHandler(event, self.middleclickHandler)

    def __controlmiddleclickEventHandler(self, event):
        return self.__defaultClickEventHandler(
            event, self.controlmiddleclickHandler)

    def __rightclickEventHandler(self, event):
        return self.__defaultClickEventHandler(event, self.rightclickHandler)

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

    def __shiftclickEventHandler(self, event):
        return self.__defaultClickEventHandler(event, self.shiftclickHandler)

    def __shiftrightclickEventHandler(self, event):
        return self.__defaultClickEventHandler(
            event, self.shiftrightclickHandler)

    def __motionEventHandler(self, event):
        if not self.game.drag.stack or self is not self.game.drag.stack:
            return EVENT_PROPAGATE
        if self.game.demo:
            self.game.stopDemo(event)
        if self.game.busy:
            return EVENT_HANDLED
        if self.game.app.opt.mouse_type == 'point-n-click':
            return EVENT_HANDLED
        self.keepDrag(event)
        #  if self.game.app.opt.mouse_type == 'drag-n-drop' \
        #           and TOOLKIT == 'tk':
        #      # use a timer to update the drag
        #      # this allows us to skip redraws on slow machines
        #      drag = self.game.drag
        #      if drag.timer is None:
        #          drag.timer = after_idle(self.canvas, self.keepDragTimer)
        #      drag.event = event
        #  else:
        #      # update now
        #      self.keepDrag(event)
        return EVENT_HANDLED

    def __releaseEventHandler(self, event):
        if self.game.demo:
            self.game.stopDemo(event)
        self.game.interruptSleep()
        if self.game.busy:
            return EVENT_HANDLED
        if self.game.app.opt.mouse_type == 'drag-n-drop':

            if TOOLKIT == 'kivy':
                drag = self.game.drag
                if drag and drag.stack:
                    drag.stack.keepDrag(event)
                    drag.stack.finishDrag(event)
                return EVENT_HANDLED

            self.keepDrag(event)
            self.finishDrag(event)
        return EVENT_HANDLED

    def __enterEventHandler(self, event):
        if self.game.drag.stack:
            if self.game.app.opt.mouse_type == 'point-n-click':
                if self.acceptsCards(self.game.drag.stack,
                                     self.game.drag.cards):
                    self.canvas.config(cursor=CURSOR_DOWN_ARROW)
                    self.current_cursor = CURSOR_DOWN_ARROW
                    self.cursor_changed = True
        else:
            help = self.getHelp()  # +' '+self.getBaseCard(),
            if DEBUG:
                help = repr(self)
            after_idle(self.canvas, self.game.showHelp,
                       'help', help,
                       'info', self.getNumCards())
        return EVENT_HANDLED

    def __leaveEventHandler(self, event):
        if not self.game.drag.stack:
            after_idle(self.canvas, self.game.showHelp)
        if self.game.app.opt.mouse_type == 'drag-n-drop':
            return EVENT_HANDLED
        if self.cursor_changed:
            self.canvas.config(cursor='')
            self.current_cursor = ''
            self.cursor_changed = False
        drag_stack = self.game.drag.stack
        if self is drag_stack:
            x, y = event.x, event.y
            w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
            if x < 0 or y < 0 or x >= w or y >= h:
                # cancel drag if mouse leave canvas
                drag_stack.cancelDrag(event)
                after_idle(self.canvas, self.game.showHelp)
                return EVENT_HANDLED
            else:
                # continue drag
                return self.__motionEventHandler(event)
        else:
            return EVENT_PROPAGATE

    #
    # Drag internals {controller -> model -> view}
    #

    def getDragCards(self, index):
        return self.cards[index:]

    # begin a drag operation
    def startDrag(self, event, sound=True):
        # print event.x, event.y
        assert self.game.drag.stack is None
        # import pdb
        # pdb.set_trace()
        i = self._findCard(event)
        if i < 0 or not self.canMoveCards(self.cards[i:]):
            return
        if self.is_filled and self.items.shade_item:
            self.items.shade_item.hide()
        x_offset, y_offset = self.cards[i].x, self.cards[i].y
        if sound:
            self.game.playSample("startdrag")
        self.lastx = event.x
        self.lasty = event.y
        game = self.game
        drag = game.drag
        drag.start_x = event.x
        drag.start_y = event.y
        drag.stack = self
        drag.noshade_stacks = [self]
        drag.cards = self.getDragCards(i)
        drag.index = i
        if self.game.app.opt.mouse_type == 'point-n-click':
            self._markCards(drag)
            return
        # if TOOLKIT == 'gtk':
        #     drag.stack.group.tkraise()
        images = game.app.images
        drag.shadows = self.createShadows(drag.cards)
        # sx, sy = 0, 0
        sx, sy = -images.SHADOW_XOFFSET, -images.SHADOW_YOFFSET
        dx, dy = 0, 0
        cw, ch = images.getSize()
        if game.app.opt.mouse_type == 'sticky-mouse':
            # return cards under mouse
            dx = event.x - (x_offset+cw+sx) - game.canvas.xmargin
            dy = event.y - (y_offset+ch+sy) - game.canvas.ymargin
            if dx < 0:
                dx = 0
            if dy < 0:
                dy = 0
        for s in drag.shadows:
            if dx > 0 or dy > 0:
                s.move(dx, dy)
            if TOOLKIT == 'gtk':
                s.addtag(drag.stack.group)
            s.tkraise()
        for card in drag.cards:
            card.tkraise()
            card.moveBy(sx+dx, sy+dy)
        if game.app.opt.dragcursor:
            game.canvas.config(cursor=CURSOR_DRAG)

    # continue a drag operation
    def keepDrag(self, event):
        drag = self.game.drag
        if not drag.cards:
            return
        assert self is drag.stack
        dx = event.x - self.lastx
        dy = event.y - self.lasty
        if dx or dy:
            self.lastx = event.x
            self.lasty = event.y
            if self.game.app.opt.shade:
                self._updateShade()
            for s in drag.shadows:
                s.move(dx, dy)
            for card in drag.cards:
                card.moveBy(dx, dy)
        drag.event = None

    def keepDragTimer(self):
        drag = self.game.drag
        after_cancel(drag.timer)
        drag.timer = None
        if drag.event:
            self.keepDrag(drag.event)
            self.canvas.update_idletasks()

    # create shadows, return a tuple of MfxCanvasImages
    def createShadows(self, cards, dx=0, dy=0):
        if not self.game.app.opt.shadow or self.canvas.preview > 1:
            return ()
        mylen = len(cards)
        if mylen == 0 or mylen > self.max_shadow_cards:
            return ()
        images = self.game.app.images
        cx, cy = cards[0].x, cards[0].y
        ddx, ddy = cx-cards[-1].x, cy-cards[-1].y
        cw, ch = images.getSize()
        if USE_PIL:
            c0 = cards[-1]
            if self.CARD_XOFFSET[0] < 0:
                c0 = cards[0]
            if self.CARD_YOFFSET[0] < 0:
                c0 = cards[0]
            img = images.getShadowPIL(self, cards)
            cx, cy = c0.x + cw + dx, c0.y + ch + dy
            s = MfxCanvasImage(self.canvas, cx, cy,
                               image=img, anchor=ANCHOR_SE)
            s.lower(c0.item)
            return (s,)

        if ddx == 0:  # vertical
            for c in cards[1:]:
                if c.x != cx or abs(c.y - cy) != images.CARD_YOFFSET:
                    return ()
                cy = c.y
            img0, img1 = images.getShadow(0), images.getShadow(mylen)
            c0 = cards[-1]
            if self.CARD_YOFFSET[0] < 0:
                c0 = cards[0]
        elif ddy == 0:  # horizontal
            for c in cards[1:]:
                if c.y != cy or abs(c.x - cx) != images.CARD_XOFFSET:
                    return ()
                cx = c.x
            img0, img1 = images.getShadow(-mylen), images.getShadow(1)
            c0 = cards[-1]
            if self.CARD_XOFFSET[0] < 0:
                c0 = cards[0]
        else:
            return ()
        if img0 and img1:
            cx, cy = c0.x + cw + dx, c0.y + ch + dy

            if TOOLKIT == 'kivy':
                height0 = img0.getHeight()
            else:
                height0 = img0.height()

            s1 = MfxCanvasImage(self.game.canvas, cx, cy - height0,
                                image=img1, anchor=ANCHOR_SE)
            s2 = MfxCanvasImage(self.canvas, cx, cy,
                                image=img0, anchor=ANCHOR_SE)
            if TOOLKIT == 'tk':
                s1.lower(c0.item)
                s2.lower(c0.item)
            #  elif TOOLKIT == 'gtk':
            #      positions = 2           ## FIXME
            #      s1.lower(positions)
            #      s2.lower(positions)
            return (s1, s2)
        return ()

    # handle shade within a drag operation
    def _deleteShade(self):
        if self.game.drag.shade_img:
            self.game.drag.shade_img.delete()
        self.game.drag.shade_img = None
        self.game.drag.shade_stack = None

    def _updateShade(self):
        # optimized for speed - we use lots of local variables
        game = self.game
        images = game.app.images
        CW, CH = images.CARDW, images.CARDH
        drag = game.drag
        # stacks = game.allstacks
        c = drag.cards[0]
        stacks = (game.getClosestStack(c, drag.stack), )
        r1_0, r1_1, r1_2, r1_3 = c.x, c.y, c.x + CW, c.y + CH
        sstack, sdiff, sx, sy = None, 999999999, 0, 0
        for s in stacks:
            if s is None or s in drag.noshade_stacks:
                continue
            if s.cards:
                c = s.cards[-1]
                r2 = (c.x, c.y, c.x + CW, c.y + CH)
            else:
                r2 = (s.x, s.y, s.x + CW, s.y + CH)
            if (r1_2 <= r2[0] or r1_3 <= r2[1] or
                    r2[2] <= r1_0 or r2[3] <= r1_1):
                # rectangles do not intersect
                continue
            if s in drag.canshade_stacks:
                pass
            elif s.acceptsCards(drag.stack, drag.cards):
                drag.canshade_stacks.append(s)
            else:
                drag.noshade_stacks.append(s)
                continue
            diff = (r1_0 - r2[0])**2 + (r1_1 - r2[1])**2
            if diff < sdiff:
                sstack, sdiff, sx, sy = s, diff, r2[0], r2[1]
        if sstack is drag.shade_stack:
            return
        if sstack is None:
            self._deleteShade()
            return
        if drag.shade_img:
            self._deleteShade()
        # create the shade image
        drag.shade_stack = sstack
        if sstack.cards:
            card = sstack.cards[-1]
            if card.face_up:
                img = images.getHighlightedCard(
                    card.deck, card.suit, card.rank)
            else:
                img = images.getHighlightedBack()
        else:
            img = images.getShade()
        if not img:
            return
        img = MfxCanvasImage(game.canvas, sx, sy, image=img, anchor=ANCHOR_NW)
        drag.shade_img = img
        # raise/lower the shade image to the correct stacking order
        if TOOLKIT == 'tk':
            if drag.shadows:
                img.lower(drag.shadows[0])
            else:
                img.lower(drag.cards[0].item)
        elif TOOLKIT == 'gtk':
            img.tkraise()
            drag.stack.group.tkraise()

    # for closeStack
    def _shadeStack(self):
        if not self.game.app.opt.shade_filled_stacks:
            return
        #  if (self.CARD_XOFFSET != (0,) or
        #      self.CARD_YOFFSET != (0,)):
        #      return
        card = self.cards[-1]
        img = self.game.app.images.getHighlightedCard(
            card.deck, card.suit, card.rank)
        if img is None:
            return
        # self.canvas.update_idletasks()
        if TOOLKIT == 'kivy':
            self.game.top.waitAnimation()
        item = MfxCanvasImage(self.canvas, card.x, card.y,
                              image=img, anchor=ANCHOR_NW, group=self.group)
        # item.tkraise()
        self.items.shade_item = item

    def unshadeStack(self):
        if self.items.shade_item:
            self.items.shade_item.delete()
            self.items.shade_item = None

    def _markCards(self, drag):
        cards = drag.cards
        drag.stack.group.tkraise()
        #
        x0, y0 = self.getPositionFor(cards[0])
        x1, y1 = self.getPositionFor(cards[-1])
        x0, x1 = min(x1, x0), max(x1, x0)
        y0, y1 = min(y1, y0), max(y1, y0)
        cw, ch = self.game.app.images.getSize()
        x1 += cw
        y1 += ch
        xx0, yy0 = x0, y0
        w, h = x1-x0, y1-y0
        #
        if TOOLKIT == 'gtk' or not Image:
            color = self.game.app.opt.colors['cards_1']
            r = MfxCanvasRectangle(self.canvas, xx0, yy0, xx0+w, yy0+h,
                                   fill="", outline=color, width=4,
                                   group=self.group)
            drag.shadows.append(r)
            #  mylen = MfxCanvasLine(self.canvas, xx0, yy0, xx0+w, yy0+h,
            #                    fill=color, width=4)
            #  drag.shadows.append(mylen)
            #  mylen = MfxCanvasLine(self.canvas, xx0, yy0+h, xx0+w, yy0,
            #                    fill=color, width=4)
            #  drag.shadows.append(mylen)
            return
        #
        shade = Image.new('RGBA', (w, h))
        for c in cards:
            x, y = self.getPositionFor(c)
            x, y = x-xx0, y-yy0
            im = c._active_image._pil_image
            shade.paste(im, (x, y), im)
        #
        shade = markImage(shade)
        tkshade = ImageTk.PhotoImage(shade)
        im = MfxCanvasImage(self.canvas, xx0, yy0,
                            image=tkshade, anchor=ANCHOR_NW,
                            group=self.group)
        drag.shadows.append(im)

    def _stopDrag(self):
        drag = self.game.drag
        after_cancel(drag.timer)
        drag.timer = None
        self._deleteShade()
        drag.canshade_stacks = []
        drag.noshade_stacks = []
        for s in drag.shadows:
            s.delete()
        drag.shadows = []
        drag.stack = None
        drag.cards = []

    # finish a drag operation
    def finishDrag(self, event=None):
        if self.game.app.opt.dragcursor:
            self.canvas.config(cursor='')
        drag = self.game.drag.copy()
        if self.game.app.opt.mouse_type == 'point-n-click':
            drag.stack._stopDrag()
        else:
            self._stopDrag()
        if drag.cards:
            if self.game.app.opt.mouse_type == 'point-n-click':
                self.releaseHandler(event, drag)
            else:
                assert drag.stack is self
                self.releaseHandler(event, drag)

    # cancel a drag operation
    def cancelDrag(self, event=None):
        if self.game.app.opt.dragcursor:
            self.canvas.config(cursor='')
        drag = self.game.drag.copy()
        if self.game.app.opt.mouse_type == 'point-n-click':
            drag.stack._stopDrag()
        else:
            self._stopDrag()
        if drag.cards:
            assert drag.stack is self
            self.moveCardsBackHandler(event, drag)

    def getHelp(self):
        return str(self)  # debug

    def getBaseCard(self):
        return ''

    def _getBaseCard(self, rank=None):
        # FIXME: no-french games
        if self.cap.max_accept == 0:
            return ''
        if rank is None:
            br = self.cap.base_rank
        else:
            br = rank
        s = _('Base card - %s.')
        if br == NO_RANK:
            s = _('Empty row cannot be filled.')
        elif br == -1:
            s = s % _('any card')
        elif br == 10:
            s = s % _('Jack')
        elif br == 11:
            s = s % _('Queen')
        elif br == 12:
            s = s % _('King')
        elif br == 0:
            s = s % _('Ace')
        else:
            s = s % str(br+1)
        return s

    def getNumCards(self):
        from pysollib.mygettext import ungettext
        n = len(self.cards)
        if n == 0:
            return _('No cards')
        else:
            return ungettext('%d card', '%d cards', n) % n


# ************************************************************************
# * Abstract interface that supports a concept of dealing.
# ************************************************************************

class DealRow_StackMethods:
    # Deal a card to each of the RowStacks. Return number of cards dealt.
    def dealRow(self, rows=None, flip=1, reverse=0, frames=-1, sound=False):
        if rows is None:
            rows = self.game.s.rows
        if sound and frames and self.game.app.opt.animations:
            self.game.startDealSample()
        n = self.dealToStacks(rows, flip, reverse, frames)
        if sound:
            self.game.stopSamples()
        return n

    # Same, but no error if not enough cards are available.
    def dealRowAvail(self, rows=None, flip=1,
                     reverse=0, frames=-1, sound=False):
        if rows is None:
            rows = self.game.s.rows
        if sound and frames and self.game.app.opt.animations:
            self.game.startDealSample()
        if len(self.cards) < len(rows):
            rows = rows[:len(self.cards)]
        n = self.dealToStacks(rows, flip, reverse, frames)
        if sound:
            self.game.stopSamples()
        return n

    def dealToStacks(self, stacks, flip=1, reverse=0, frames=-1):
        if not self.cards or not stacks:
            return 0
        assert len(self.cards) >= len(stacks)
        old_state = self.game.enterState(self.game.S_DEAL)
        if reverse:
            stacks = list(stacks)
            stacks.reverse()
        for r in stacks:
            assert not self.getCard().face_up
            assert r is not self
            if flip:
                self.game.flipMove(self)
            self.game.moveMove(1, self, r, frames=frames)
        self.game.leaveState(old_state)
        if TOOLKIT == 'kivy':
            self.game.top.waitAnimation()
        return len(stacks)

    # all Aces go to the Foundations
    def dealToStacksOrFoundations(self, stacks, flip=1,
                                  reverse=0, frames=-1, rank=-1):
        if rank < 0:
            rank = self.game.s.foundations[0].cap.base_rank
        if not self.cards or not stacks:
            return 0
        old_state = self.game.enterState(self.game.S_DEAL)
        if reverse:
            stacks = list(stacks)
            stacks.reverse()
        n = 0
        for r in stacks:
            assert r is not self
            while self.cards:
                n += 1
                if flip:
                    self.game.flipMove(self)
                if flip and self.cards[-1].rank == rank:
                    for s in self.game.s.foundations:
                        assert s is not self
                        if s.acceptsCards(self, self.cards[-1:]):
                            self.game.moveMove(1, self, s, frames=frames)
                            break
                else:
                    self.game.moveMove(1, self, r, frames=frames)
                    break
        self.game.leaveState(old_state)
        if TOOLKIT == 'kivy':
            self.game.top.waitAnimation()
        return n


class DealBaseCard_StackMethods:
    def dealSingleBaseCard(self, frames=-1, update_saveinfo=1):
        c = self.cards[-1]
        self.dealBaseCards(ncards=1, frames=frames, update_saveinfo=0)
        for s in self.game.s.foundations:
            s.cap.base_rank = c.rank
            if update_saveinfo:
                cap = Struct(base_rank=c.rank)
                self.game.saveinfo.stack_caps.append((s.id, cap))
        return c

    def dealBaseCards(self, ncards=1, frames=-1, update_saveinfo=1):
        assert self.game.moves.state == self.game.S_INIT
        assert not self.base_cards
        while ncards > 0:
            assert self.cards
            c = self.cards[-1]
            for s in self.game.s.foundations:
                if (not s.cards and
                        (s.cap.base_suit < 0 or s.cap.base_suit == c.suit)):
                    break
            else:
                assert 0
                s = None
            s.cap.base_rank = c.rank
            if update_saveinfo:
                cap = Struct(base_rank=c.rank)
                self.game.saveinfo.stack_caps.append((s.id, cap))
            if not c.face_up:
                self.game.flipMove(self)
            self.game.moveMove(1, self, s, frames=frames)
            ncards -= 1


class RedealCards_StackMethods:

    def _redeal(self, rows=None, reverse=False, frames=0):
        # move all cards to the Talon
        num_cards = 0
        assert len(self.cards) == 0
        if rows is None:
            rows = self.game.s.rows
        rows = list(rows)
        if reverse:
            rows.reverse()
        for r in rows:
            for i in range(len(r.cards)):
                num_cards += 1
                self.game.moveMove(1, r, self, frames=frames, shadow=0)
                if self.cards[-1].face_up:
                    self.game.flipMove(self)
        assert len(self.cards) == num_cards
        return num_cards

    def redealCards(self, rows=None, sound=False,
                    shuffle=False, reverse=False, frames=0):
        if sound and self.game.app.opt.animations:
            self.game.startDealSample()
        num_cards = self._redeal(rows=rows, reverse=reverse, frames=frames)
        if num_cards == 0:          # game already finished
            return 0
        if shuffle:
            # shuffle
            self.game.shuffleStackMove(self)
        # redeal
        self.game.nextRoundMove(self)
        self.game.redealCards()
        if sound:
            self.game.stopSamples()
        return num_cards


# ************************************************************************
# * The Talon is a stack with support for dealing.
# ************************************************************************

class TalonStack(Stack,
                 DealRow_StackMethods,
                 DealBaseCard_StackMethods,
                 ):
    def __init__(self, x, y, game, max_rounds=1, num_deal=1, **cap):
        Stack.__init__(self, x, y, game, cap=cap)
        self.max_rounds = max_rounds
        self.num_deal = num_deal
        self.init_redeal = Struct(
            top_bottom=None,
            img_coord=None,
            txt_coord=None,
            )
        self.resetGame()

    def resetGame(self):
        Stack.resetGame(self)
        self.round = 1
        self.base_cards = []        # for DealBaseCard_StackMethods

    def assertStack(self):
        Stack.assertStack(self)
        n = self.game.gameinfo.redeals
        if n < 0:
            assert self.max_rounds == n
        else:
            assert self.max_rounds == n + 1

    # Control of dealing is transferred to the game which usually
    # transfers it back to the Talon - see dealCards() below.
    def clickHandler(self, event):
        return self.game.dealCards(sound=True)

    def rightclickHandler(self, event):
        return self.clickHandler(event)

    # Usually called by Game.canDealCards()
    def canDealCards(self):
        return len(self.cards) > 0

    # Actual dealing, usually called by Game.dealCards().
    # Either deal all cards in Game.startGame(), or subclass responsibility.
    def dealCards(self, sound=False):
        pass

    # remove all cards from all stacks
    def removeAllCards(self):
        for stack in self.game.allstacks:
            while stack.cards:
                stack.removeCard(update=0)
                # stack.removeCard(unhide=0, update=0)
        for stack in self.game.allstacks:
            stack.updateText()

    def updateText(self, update_rounds=1, update_redeal=1):
        # assertView(self)
        Stack.updateText(self)
        if update_rounds and self.game.preview <= 1:
            if self.texts.rounds is not None:
                t = _("Round %d") % self.round
                self.texts.rounds.config(text=t)
        if update_redeal:
            deal = self.canDealCards() != 0
            if self.images.redeal is not None:
                img = (self.getRedealImages())[deal]
                if img is not None and img is not self.images.redeal_img:
                    self.images.redeal.config(image=img)
                    self.images.redeal_img = img
                t = ("", _("Redeal"))[deal]
            else:
                t = (_("Stop"), _("Redeal"))[deal]
            if self.texts.redeal is not None and self.game.preview <= 1:
                if t != self.texts.redeal_str:
                    self.texts.redeal.config(text=t)
                    self.texts.redeal_str = t

    def _addRedealImage(self):
        # add or remove the redeal image/text
        if not self.is_visible or self.images.bottom is None:
            return
        if self.game.preview > 1:
            return
        images = self.game.app.images
        cw, ch = images.getSize()
        cx, cy = self.init_redeal.img_coord
        ca = 'center'
        tx, ty = self.init_redeal.txt_coord

        if self.images.redeal:
            self.canvas.delete(self.images.redeal)
            self.images.redeal = None
            self.images.redeal_img = None
        if self.texts.redeal:
            self.canvas.delete(self.texts.redeal)
            self.texts.redeal = None
            self.texts.redeal_str = ''
        self.top_bottom = self.init_redeal.top_bottom

        if cw >= 60 and ch >= 60:
            # add a redeal image above the bottom image
            img = (self.getRedealImages())[self.max_rounds != 1]
            if img is not None:
                self.images.redeal_img = img
                self.images.redeal = MfxCanvasImage(self.canvas,
                                                    cx, cy, image=img,
                                                    anchor="center",
                                                    group=self.group)
                if TOOLKIT == 'tk':
                    self.images.redeal.tkraise(self.top_bottom)
                elif TOOLKIT == 'kivy':
                    self.images.redeal.tkraise(self.top_bottom)
                elif TOOLKIT == 'gtk':
                    # FIXME
                    pass
                self.top_bottom = self.images.redeal
                if ch >= 90:
                    cy, ca = ty, "s"
                else:
                    ca = None
        font = self.game.app.getFont("canvas_default")
        text_width = get_text_width(_('Redeal'), font=font,
                                    root=self.canvas)
        if cw >= text_width+4 and ca:
            # add a redeal text below the bottom image
            if self.max_rounds != 1:
                # FIXME: sometimes canvas do not show the text
                # print 'add txt', cx, cy
                self.texts.redeal_str = ""
                images = self.game.app.images
                self.texts.redeal = MfxCanvasText(self.canvas, cx, cy,
                                                  anchor=ca, font=font,
                                                  group=self.group)
                if TOOLKIT == 'tk':
                    self.texts.redeal.tkraise(self.top_bottom)
                elif TOOLKIT == 'kivy':
                    self.texts.redeal.tkraise(self.top_bottom)
                elif TOOLKIT == 'gtk':
                    # FIXME
                    pass
                self.top_bottom = self.texts.redeal

    def prepareView(self):
        Stack.prepareView(self)
        if 0:
            if not self.is_visible or self.images.bottom is None:
                return
            if self.images.redeal is not None or self.texts.redeal is not None:
                return
            if self.game.preview > 1:
                return
        images = self.game.app.images
        self.init_redeal.top_bottom = self.top_bottom
        cx, cy = self.x + images.CARDW//2, self.y + images.CARDH//2
        ty = self.y + images.CARDH - 4
        self.init_redeal.img_coord = cx, cy
        self.init_redeal.txt_coord = cx, ty

        # At least display a redealImage at start, if USE_PIL is not set.
        if USE_PIL is False:
            self._addRedealImage()

    getBottomImage = Stack._getTalonBottomImage

    def getRedealImages(self):
        # returns a tuple of two PhotoImages
        return self.game.app.gimages.redeal

    def getHelp(self):
        from pysollib.mygettext import ungettext
        if self.max_rounds == -2:
            nredeals = _('Variable redeals.')
        elif self.max_rounds == -1:
            nredeals = _('Unlimited redeals.')
        else:
            n = self.max_rounds-1
            nredeals = ungettext('%d redeal', '%d redeals', n) % n
        # round = _('Round #%d.') % self.round
        return _('Talon.')+' '+nredeals  # +' '+round

    # def getBaseCard(self):
    #    return self._getBaseCard()

    def resize(self, xf, yf):
        self._addRedealImage()
        Stack.resize(self, xf, yf)


# A single click deals one card to each of the RowStacks.
class DealRowTalonStack(TalonStack):
    def dealCards(self, sound=False):
        return self.dealRowAvail(sound=sound)


# For games where the Talon is only used for the initial dealing.
class InitialDealTalonStack(TalonStack):
    # no bindings
    def initBindings(self):
        pass
    # no bottom
    getBottomImage = Stack._getNoneBottomImage


class RedealTalonStack(TalonStack, RedealCards_StackMethods):
    def canDealCards(self):
        if self.round == self.max_rounds:
            return False
        return not self.game.isGameWon()

    def dealCards(self, sound=False):
        RedealCards_StackMethods.redealCards(self, sound=sound)


class DealRowRedealTalonStack(TalonStack, RedealCards_StackMethods):

    def canDealCards(self, rows=None):
        if rows is None:
            rows = self.game.s.rows
        r_cards = sum([len(r.cards) for r in rows])
        if self.cards:
            return True
        elif r_cards and self.round != self.max_rounds:
            return True
        return False

    def dealCards(self, sound=False, rows=None, shuffle=False):
        num_cards = 0
        if rows is None:
            rows = self.game.s.rows
        if sound and self.game.app.opt.animations:
            self.game.startDealSample()
        if not self.cards:
            # move all cards to talon
            num_cards = self._redeal(rows=rows, frames=4)
            if shuffle:
                # shuffle
                self.game.shuffleStackMove(self)
            self.game.nextRoundMove(self)
        num_cards += self.dealRowAvail(rows=rows, sound=False)
        if sound:
            self.game.stopSamples()
        return num_cards

    def shuffleAndDealCards(self, sound=False, rows=None):
        DealRowRedealTalonStack.dealCards(self, sound=sound,
                                          rows=rows, shuffle=True)


class DealReserveRedealTalonStack(DealRowRedealTalonStack):

    def canDealCards(self, rows=None):
        return DealRowRedealTalonStack.canDealCards(
            self, rows=self.game.s.reserves)

    def dealCards(self, sound=False, rows=None):
        return DealRowRedealTalonStack.dealCards(
            self, sound=sound, rows=self.game.s.reserves)


# Spider Talons
class SpiderTalonStack(DealRowRedealTalonStack):
    def canDealCards(self):
        if not DealRowRedealTalonStack.canDealCards(self):
            return False
        # no row may be empty
        for r in self.game.s.rows:
            if not r.cards:
                return False
        return True


class GroundsForADivorceTalonStack(DealRowRedealTalonStack):
    # A single click deals a new cards to each non-empty row.
    def dealCards(self, sound=True):
        if self.cards:
            rows = [r for r in self.game.s.rows if r.cards]
            # if not rows:
            #     # deal one card to first row if all rows are empty
            #     rows = self.game.s.rows[:1]
            return DealRowRedealTalonStack.dealRowAvail(self, rows=rows,
                                                        sound=sound)
        return 0


# ************************************************************************
# * An OpenStack is a stack where cards can be placed and dragged
# * (i.e. FoundationStack, RowStack, ReserveStack, ...)
# *
# * Note that it defaults to max_move=1 and max_accept=0.
# ************************************************************************

class OpenStack(Stack):
    def __init__(self, x, y, game, **cap):
        kwdefault(cap, max_move=1, max_accept=0, max_cards=999999)
        Stack.__init__(self, x, y, game, cap=cap)

    #
    # Capabilities {model}
    #

    def acceptsCards(self, from_stack, cards):
        # default for OpenStack: we cannot accept
        # cards (max_accept defaults to 0)
        return self.basicAcceptsCards(from_stack, cards)

    def canMoveCards(self, cards):
        # import pdb
        # pdb.set_trace()
        # print('OpenStack.canMoveCards()', cards)
        # default for OpenStack: we can move the top card
        # (max_move defaults to 1)
        return self.basicCanMoveCards(cards)

    def canFlipCard(self):
        # default for OpenStack: we can flip the top card
        if self.basicIsBlocked() or not self.cards:
            return False
        return not self.cards[-1].face_up

    def canDropCards(self, stacks):
        if self.basicIsBlocked() or not self.cards:
            return (None, 0)
        cards = self.cards[-1:]
        if self.canMoveCards(cards):
            for s in stacks:
                if s is not self and s.acceptsCards(self, cards):
                    return (s, 1)
        return (None, 0)

    #
    # Mouse handlers {controller}
    #

    def clickHandler(self, event):
        flipstacks, dropstacks, quickstacks = self.game.getAutoStacks(event)
        if self in flipstacks and self.canFlipCard():
            self.playFlipMove(animation=True)
            # return -1                   # continue this event (start a drag)
            return 1                    # break
        return 0

    def rightclickHandler(self, event):
        if self.doubleclickHandler(event):
            return 1
        if self.game.app.opt.quickplay:
            flipstacks, dropstacks, quickstacks = \
                self.game.getAutoStacks(event)
            if self in quickstacks:
                n = self.quickPlayHandler(event)
                self.game.stats.quickplay_moves += n
                return n
        return 0

    def doubleclickHandler(self, event):
        # flip or drop a card
        flipstacks, dropstacks, quickstacks = self.game.getAutoStacks(event)
        if self in flipstacks and self.canFlipCard():
            self.playFlipMove(animation=True)
            return -1               # continue this event (start a drag)
        if self in dropstacks:
            to_stack, ncards = self.canDropCards(self.game.s.foundations)
            if to_stack:
                self.game.playSample("autodrop", priority=30)
                self.playMoveMove(ncards, to_stack, sound=False)
                return 1
        return 0

    def controlclickHandler(self, event):
        # highlight matching cards
        if self.game.app.opt.highlight_cards:
            return self.highlightMatchingCards(event)
        return 0

    def dragMove(self, drag, stack, sound=True):
        if self.game.app.opt.mouse_type == 'point-n-click':
            self.playMoveMove(len(drag.cards), stack, sound=sound)
        else:
            # self.playMoveMove(len(drag.cards), stack, frames=0, sound=sound)
            self.playMoveMove(len(drag.cards), stack, frames=-2, sound=sound)

    def releaseHandler(self, event, drag, sound=True):
        cards = drag.cards
        # check if we moved the card by at least 10 pixels
        if event is not None:
            dx, dy = event.x - drag.start_x, event.y - drag.start_y
            if abs(dx) < 10 and abs(dy) < 10:
                # move cards back to their origin stack
                Stack.releaseHandler(self, event, drag, sound=sound)
                return
            # print dx, dy
        # get destination stack
        if self.game.app.opt.mouse_type == 'point-n-click':
            from_stack = drag.stack
            to_stack = self
        else:
            from_stack = self
            to_stack = self.game.getClosestStack(cards[0], self)
        # move cards
        if (not to_stack or from_stack is to_stack or
                not to_stack.acceptsCards(from_stack, cards)):
            # move cards back to their origin stack
            Stack.releaseHandler(self, event, drag, sound=sound)
        else:
            # this code actually moves the cards to the new stack
            # self.playMoveMove(len(cards), stack, frames=0, sound=sound)
            from_stack.dragMove(drag, to_stack, sound=sound)

    def quickPlayHandler(self, event, from_stacks=None, to_stacks=None):
        # from_stacks and to_stacks are meant for possible
        # use in a subclasses
        if from_stacks is None:
            from_stacks = self.game.sg.dropstacks
        if to_stacks is None:
            # to_stacks = self.game.s.rows + self.game.s.reserves
            # to_stacks = self.game.sg.dropstacks
            to_stacks = self.game.s.foundations + self.game.sg.dropstacks
            # from pprint import pprint; pprint(to_stacks)
        moves = []
        #
        if not self.cards:
            for s in from_stacks:
                if s is not self and s.cards:
                    pile = s.getPile()
                    if pile and self.acceptsCards(s, pile):
                        score = self.game.getQuickPlayScore(len(pile), s, self)
                        moves.append((score, -len(moves), len(pile), s, self))
        else:
            pile1, pile2 = None, self.getPile()
            if pile2:
                i = self._findCard(event)
                if i >= 0:
                    pile = self.cards[i:]
                    if len(pile) != len(pile2) and self.canMoveCards(pile):
                        pile1 = pile
            for pile in (pile1, pile2):
                if not pile:
                    continue
                for s in to_stacks:
                    if s is not self and s.acceptsCards(self, pile):
                        score = self.game.getQuickPlayScore(len(pile), self, s)
                        moves.append((score, -len(moves), len(pile), self, s))
        #
        if moves:
            moves.sort()
            # from pprint import pprint; pprint(moves)
            score, len_moves, ncards, from_stack, to_stack = moves[-1]
            if score >= 0:
                # self.game.playSample("startdrag")
                from_stack.playMoveMove(ncards, to_stack)
                return 1
        return 0

    def getHelp(self):
        if self.cap.max_accept == 0:
            return _('Reserve. No building.')
        return ''


# ************************************************************************
# * Foundations stacks
# ************************************************************************

class AbstractFoundationStack(OpenStack):
    def __init__(self, x, y, game, suit, **cap):
        kwdefault(cap, suit=suit, base_suit=suit, base_rank=ACE,
                  dir=1, max_accept=1, max_cards=13)
        OpenStack.__init__(self, x, y, game, **cap)

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

    def clickHandler(self, event):
        return 0

    def rightclickHandler(self, event):
        # return 0
        if self.game.app.opt.quickplay:
            n = self.quickPlayHandler(event)
            self.game.stats.quickplay_moves += n
            return n
        return 0

    def quickPlayHandler(self, event):
        # return 0
        from_stacks = self.game.sg.dropstacks + self.game.s.foundations
        # to_stacks = self.game.sg.dropstacks
        to_stacks = from_stacks
        return OpenStack.quickPlayHandler(self, event, from_stacks, to_stacks)

    getBottomImage = Stack._getSuitBottomImage

    def getBaseCard(self):
        return self._getBaseCard()

    def closeStack(self):
        if len(self.cards) == self.cap.max_cards:
            self.is_filled = True
            self._shadeStack()

    def getHelp(self):
        return _('Foundation.')

    def varyAcceptsCards(self, from_stack, cards):
        # if base rank of foundations is vary
        subclass = self.__class__    # derived class (SS_FoundationStack, etc)
        assert subclass is not AbstractFoundationStack
        if self.cards:
            return subclass.acceptsCards(self, from_stack, cards)
        if not subclass.acceptsCards(self, from_stack, cards):
            return False
        # this stack don't have cards: check base rank of other stacks
        for s in self.game.s.foundations:
            if s.cards:
                base_card = s.cards[0]
                return base_card.rank == cards[0].rank
        return True                     # all foundations is empty

    def varyGetBaseCard(self):
        rank = None
        for s in self.game.s.foundations:
            if s.cards:
                rank = s.cards[0].rank
        return self._getBaseCard(rank=rank)


# A SameSuit_FoundationStack is the typical Foundation stack.
# It builds up in rank and suit.
class SS_FoundationStack(AbstractFoundationStack):
    def acceptsCards(self, from_stack, cards):
        if not AbstractFoundationStack.acceptsCards(self, from_stack, cards):
            return False
        if self.cards:
            # check the rank
            if ((self.cards[-1].rank + self.cap.dir) % self.cap.mod !=
                    cards[0].rank):
                return False
        return True

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Foundation. Build up by suit.')
        elif self.cap.dir < 0:
            return _('Foundation. Build down by suit.')
        else:
            return _('Foundation. Build by same rank.')


# A Rank_FoundationStack builds up in rank and ignores color and suit.
class RK_FoundationStack(SS_FoundationStack):
    def __init__(self, x, y, game, suit=ANY_SUIT, **cap):
        SS_FoundationStack.__init__(self, x, y, game, ANY_SUIT, **cap)

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Foundation. Build up regardless of suit.')
        elif self.cap.dir < 0:
            return _('Foundation. Build down regardless of suit.')
        else:
            return _('Foundation. Build by same rank.')


# A AlternateColor_FoundationStack builds up in rank and alternate color.
# It is used in only a few games.
class AC_FoundationStack(SS_FoundationStack):
    def __init__(self, x, y, game, suit, **cap):
        kwdefault(cap, base_suit=suit)
        SS_FoundationStack.__init__(self, x, y, game, ANY_SUIT, **cap)

    def acceptsCards(self, from_stack, cards):
        if not SS_FoundationStack.acceptsCards(self, from_stack, cards):
            return False
        if self.cards:
            # check the color
            if cards[0].color == self.cards[-1].color:
                return False
        return True

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Foundation. Build up by alternate color.')
        elif self.cap.dir < 0:
            return _('Foundation. Build down by alternate color.')
        else:
            return _('Foundation. Build by same rank.')


# A SameColor_FoundationStack builds up in rank and alternate color.
# It is used in only a few games.
class SC_FoundationStack(SS_FoundationStack):
    def __init__(self, x, y, game, suit, **cap):
        kwdefault(cap, base_suit=suit)
        SS_FoundationStack.__init__(self, x, y, game, ANY_SUIT, **cap)

    def acceptsCards(self, from_stack, cards):
        if not SS_FoundationStack.acceptsCards(self, from_stack, cards):
            return False
        if self.cards:
            # check the color
            if cards[0].color != self.cards[-1].color:
                return False
        return True

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Foundation. Build up by color.')
        elif self.cap.dir < 0:
            return _('Foundation. Build down by color.')
        else:
            return _('Foundation. Build by same rank.')


# Spider-type foundations
class Spider_SS_Foundation(AbstractFoundationStack):
    def __init__(self, x, y, game, suit=ANY_SUIT, **cap):
        kwdefault(cap, dir=-1, base_rank=KING,
                  min_accept=13, max_accept=13, max_move=0)
        AbstractFoundationStack.__init__(self, x, y, game, suit, **cap)

    def acceptsCards(self, from_stack, cards):
        if not AbstractFoundationStack.acceptsCards(self, from_stack, cards):
            return False
        # now check the cards
        return isSameSuitSequence(cards, self.cap.mod, self.cap.dir)


class Spider_AC_Foundation(Spider_SS_Foundation):
    def acceptsCards(self, from_stack, cards):
        if not AbstractFoundationStack.acceptsCards(self, from_stack, cards):
            return False
        # now check the cards
        return isAlternateColorSequence(cards, self.cap.mod, self.cap.dir)


class Spider_RK_Foundation(Spider_SS_Foundation):
    def acceptsCards(self, from_stack, cards):
        if not AbstractFoundationStack.acceptsCards(self, from_stack, cards):
            return False
        # now check the cards
        return isRankSequence(cards, self.cap.mod, self.cap.dir)


# ************************************************************************
# * Abstract classes for row stacks.
# ************************************************************************


# Abstract class.
class SequenceStack_StackMethods:
    def _isSequence(self, cards):
        # Are the cards in a basic sequence for our stack ?
        raise SubclassResponsibility

    def _isAcceptableSequence(self, cards):
        return self._isSequence(cards)

    def _isMoveableSequence(self, cards):
        # import pdb; pdb.set_trace()
        return self._isSequence(cards)

    def acceptsCards(self, from_stack, cards):
        if not self.basicAcceptsCards(from_stack, cards):
            return False
        # cards must be an acceptable sequence
        if not self._isAcceptableSequence(cards):
            return False
        # [topcard + cards] must be an acceptable sequence
        if (self.cards and not
                self._isAcceptableSequence([self.cards[-1]] + cards)):
            return False
        return True

    def canMoveCards(self, cards):
        return self.basicCanMoveCards(cards) and \
            self._isMoveableSequence(cards)


# Abstract class.
class BasicRowStack(OpenStack):
    def __init__(self, x, y, game, **cap):
        kwdefault(cap, dir=-1, base_rank=ANY_RANK)
        OpenStack.__init__(self, x, y, game, **cap)
        self.CARD_YOFFSET = game.app.images.CARD_YOFFSET

    def getHelp(self):
        if self.cap.max_accept == 0:
            return _('Tableau. No building.')
        return ''

    # def getBaseCard(self):
    #    return self._getBaseCard()

    def spiderCanDropCards(self, stacks):
        # print('spiderCanDropCards()', stacks)
        # drop whole sequence
        if len(self.cards) < 13:
            return (None, 0)
        cards = self.cards[-13:]
        for s in stacks:
            if s is not self and s.acceptsCards(self, cards):
                return (s, 13)
        return (None, 0)

    def getReserveBottomImage(self):
        return self.game.app.images.getReserveBottom()


# Abstract class.
class SequenceRowStack(SequenceStack_StackMethods, BasicRowStack):
    # canMoveCards = OpenStack.canMoveCards

    def __init__(self, x, y, game, **cap):
        kwdefault(cap, max_move=999999, max_accept=999999)
        BasicRowStack.__init__(self, x, y, game, **cap)

    def getBaseCard(self):
        return self._getBaseCard()


# ************************************************************************
# * Row stacks (the main playing stacks on the Tableau).
# ************************************************************************

#
# Implementation of common row stacks follows here.
#

# An AlternateColor_RowStack builds down by rank and alternate color.
# e.g. Klondike
class AC_RowStack(SequenceRowStack):
    def _isSequence(self, cards):
        return isAlternateColorSequence(cards, self.cap.mod, self.cap.dir)

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Tableau. Build up by alternate color.')
        elif self.cap.dir < 0:
            return _('Tableau. Build down by alternate color.')
        else:
            return _('Tableau. Build by same rank.')


# A SameColor_RowStack builds down by rank and same color.
# e.g. Klondike
class SC_RowStack(SequenceRowStack):
    def _isSequence(self, cards):
        return isSameColorSequence(cards, self.cap.mod, self.cap.dir)

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Tableau. Build up by color.')
        elif self.cap.dir < 0:
            return _('Tableau. Build down by color.')
        else:
            return _('Tableau. Build by same rank.')


# A SameSuit_RowStack builds down by rank and suit.
class SS_RowStack(SequenceRowStack):
    def _isSequence(self, cards):
        return isSameSuitSequence(cards, self.cap.mod, self.cap.dir)

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Tableau. Build up by suit.')
        elif self.cap.dir < 0:
            return _('Tableau. Build down by suit.')
        else:
            return _('Tableau. Build by same rank.')


# A Rank_RowStack builds down by rank ignoring suit.
class RK_RowStack(SequenceRowStack):
    def _isSequence(self, cards):
        return isRankSequence(cards, self.cap.mod, self.cap.dir)

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Tableau. Build up regardless of suit.')
        elif self.cap.dir < 0:
            return _('Tableau. Build down regardless of suit.')
        else:
            return _('Tableau. Build by same rank.')


# ButOwn_RowStack
class BO_RowStack(SequenceRowStack):
    def _isSequence(self, cards):
        return isAnySuitButOwnSequence(cards, self.cap.mod, self.cap.dir)

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Tableau. Build up in any suit but the same.')
        elif self.cap.dir < 0:
            return _('Tableau. Build down in any suit but the same.')
        else:
            return _('Tableau. Build by same rank.')


# A Freecell_AlternateColor_RowStack
class FreeCell_AC_RowStack(AC_RowStack):
    def canMoveCards(self, cards):
        max_move = getNumberOfFreeStacks(self.game.s.reserves) + 1
        return len(cards) <= max_move and AC_RowStack.canMoveCards(self, cards)


# A Freecell_SameSuit_RowStack (i.e. Baker's Game)
class FreeCell_SS_RowStack(SS_RowStack):
    def canMoveCards(self, cards):
        max_move = getNumberOfFreeStacks(self.game.s.reserves) + 1
        return len(cards) <= max_move and SS_RowStack.canMoveCards(self, cards)


# A Freecell_Rank_RowStack
class FreeCell_RK_RowStack(RK_RowStack):
    def canMoveCards(self, cards):
        max_move = getNumberOfFreeStacks(self.game.s.reserves) + 1
        return len(cards) <= max_move and RK_RowStack.canMoveCards(self, cards)


# A Spider_AlternateColor_RowStack builds down by rank and alternate color,
# but accepts sequences that match by rank only.
class Spider_AC_RowStack(AC_RowStack):
    def _isAcceptableSequence(self, cards):
        return isRankSequence(cards, self.cap.mod, self.cap.dir)

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Tableau. Build up regardless of suit. '
                     'Sequences of cards in alternate color '
                     'can be moved as a unit.')
        elif self.cap.dir < 0:
            return _('Tableau. Build down regardless of suit. '
                     'Sequences of cards in alternate color can be moved '
                     'as a unit.')
        else:
            return _('Tableau. Build by same rank.')


# A Spider_SameSuit_RowStack builds down by rank and suit,
# but accepts sequences that match by rank only.
class Spider_SS_RowStack(SS_RowStack):
    def _isAcceptableSequence(self, cards):
        return isRankSequence(cards, self.cap.mod, self.cap.dir)

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Tableau. Build up regardless of suit. '
                     'Sequences of cards in the same suit can be moved '
                     'as a unit.')
        elif self.cap.dir < 0:
            return _('Tableau. Build down regardless of suit. '
                     'Sequences of cards in the same suit can be moved '
                     'as a unit.')
        else:
            return _('Tableau. Build by same rank.')


# A Yukon_AlternateColor_RowStack builds down by rank and alternate color,
# but can move any face-up cards regardless of sequence.
class Yukon_AC_RowStack(BasicRowStack):
    def __init__(self, x, y, game, **cap):
        kwdefault(cap, max_move=999999, max_accept=999999)
        BasicRowStack.__init__(self, x, y, game, **cap)

    def _isYukonSequence(self, c1, c2):
        # print('Yukon_AC_RowStack._isYukonSequence()', c1, c2)
        return ((c1.rank + self.cap.dir) % self.cap.mod == c2.rank and
                c1.color != c2.color)

    def acceptsCards(self, from_stack, cards):
        # print('Yukon_AC_RowStack.acceptsCards()', from_stack, cards)
        if not self.basicAcceptsCards(from_stack, cards):
            return False
        # [topcard + card[0]] must be acceptable
        if self.cards and not self._isYukonSequence(self.cards[-1], cards[0]):
            return False
        return True

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Tableau. Build up by alternate color, '
                     'can move any face-up cards regardless of sequence.')
        elif self.cap.dir < 0:
            return _('Tableau. Build down by alternate color, '
                     'can move any face-up cards regardless of sequence.')
        else:
            return _('Tableau. Build by same rank, can move '
                     'any face-up cards regardless of sequence.')

    def getBaseCard(self):
        return self._getBaseCard()


# A Yukon_SameSuit_RowStack builds down by rank and suit,
# but can move any face-up cards regardless of sequence.
class Yukon_SS_RowStack(Yukon_AC_RowStack):
    def _isYukonSequence(self, c1, c2):
        return ((c1.rank + self.cap.dir) % self.cap.mod == c2.rank and
                c1.suit == c2.suit)

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Tableau. Build up by suit, can move any face-up cards '
                     'regardless of sequence.')
        elif self.cap.dir < 0:
            return _('Tableau. Build down by suit, can move any '
                     'face-up cards regardless of sequence.')
        else:
            return _('Tableau. Build by same rank, can move any '
                     'face-up cards regardless of sequence.')


# A Yukon_Rank_RowStack builds down by rank
# but can move any face-up cards regardless of sequence.
class Yukon_RK_RowStack(Yukon_AC_RowStack):
    def _isYukonSequence(self, c1, c2):
        return (c1.rank + self.cap.dir) % self.cap.mod == c2.rank

    def getHelp(self):
        if self.cap.dir > 0:
            return _('Tableau. Build up regardless of suit, '
                     'can move any face-up cards regardless of sequence.')
        elif self.cap.dir < 0:
            return _('Tableau. Build up regardless of suit, can move any '
                     'face-up cards regardless of sequence.')
        else:
            return _('Tableau. Build by same rank, can move any '
                     'face-up cards regardless of sequence.')

#
# King-versions of some of the above stacks: they accepts only Kings or
# sequences starting with a King as base_rank cards (i.e. when empty).
#


class KingAC_RowStack(AC_RowStack):
    def __init__(self, x, y, game, **cap):
        kwdefault(cap, base_rank=KING)
        AC_RowStack.__init__(self, x, y, game, **cap)


class KingSS_RowStack(SS_RowStack):
    def __init__(self, x, y, game, **cap):
        kwdefault(cap, base_rank=KING)
        SS_RowStack.__init__(self, x, y, game, **cap)


class KingRK_RowStack(RK_RowStack):
    def __init__(self, x, y, game, **cap):
        kwdefault(cap, base_rank=KING)
        RK_RowStack.__init__(self, x, y, game, **cap)


# up or down by color
class UD_SC_RowStack(SequenceRowStack):
    def __init__(self, x, y, game, **cap):
        kwdefault(cap, max_move=1, max_accept=1)
        SequenceRowStack.__init__(self, x, y, game, **cap)

    def _isSequence(self, cards):
        return (isSameColorSequence(cards, self.cap.mod, 1) or
                isSameColorSequence(cards, self.cap.mod, -1))

    def getHelp(self):
        return _('Tableau. Build up or down by color.')


# up or down by alternate color
class UD_AC_RowStack(SequenceRowStack):
    def __init__(self, x, y, game, **cap):
        kwdefault(cap, max_move=1, max_accept=1)
        SequenceRowStack.__init__(self, x, y, game, **cap)

    def _isSequence(self, cards):
        return (isAlternateColorSequence(cards, self.cap.mod, 1) or
                isAlternateColorSequence(cards, self.cap.mod, -1))

    def getHelp(self):
        return _('Tableau. Build up or down by alternate color.')


# up or down by suit
class UD_SS_RowStack(SequenceRowStack):
    def __init__(self, x, y, game, **cap):
        kwdefault(cap, max_move=1, max_accept=1)
        SequenceRowStack.__init__(self, x, y, game, **cap)

    def _isSequence(self, cards):
        return (isSameSuitSequence(cards, self.cap.mod, 1) or
                isSameSuitSequence(cards, self.cap.mod, -1))

    def getHelp(self):
        return _('Tableau. Build up or down by suit.')


# up or down by rank ignoring suit
class UD_RK_RowStack(SequenceRowStack):
    def __init__(self, x, y, game, **cap):
        kwdefault(cap, max_move=1, max_accept=1)
        SequenceRowStack.__init__(self, x, y, game, **cap)

    def _isSequence(self, cards):
        return (isRankSequence(cards, self.cap.mod, 1) or
                isRankSequence(cards, self.cap.mod, -1))

    def getHelp(self):
        return _('Tableau. Build up or down regardless of suit.')


# To simplify playing we also consider the number of free rows.
# See also the "SuperMove" section in the FreeCell FAQ.
class SuperMoveStack_StackMethods:
    def _getMaxMove(self, to_stack_ncards):
        max_move = getNumberOfFreeStacks(self.game.s.reserves) + 1
        if self.cap.base_rank != ANY_RANK:
            return max_move
        n = getNumberOfFreeStacks(self.game.s.rows)
        if to_stack_ncards == 0:
            n -= 1
        return max_move << max(n, 0)

    def _getNumSSSeq(self, cards):
        # num of same-suit sequences (for SuperMoveSpider_RowStack)
        if not cards:
            return 0
        n = 1
        suit = cards[-1].suit
        for c in cards[-2::-1]:
            if c.suit != suit:
                suit = c.suit
                n += 1
        return n


class SuperMoveSS_RowStack(SuperMoveStack_StackMethods, SS_RowStack):
    def canMoveCards(self, cards):
        if not SS_RowStack.canMoveCards(self, cards):
            return False
        return len(cards) <= self._getMaxMove(1)

    def acceptsCards(self, from_stack, cards):
        if not SS_RowStack.acceptsCards(self, from_stack, cards):
            return False
        return len(cards) <= self._getMaxMove(len(self.cards))


class SuperMoveAC_RowStack(SuperMoveStack_StackMethods, AC_RowStack):
    def canMoveCards(self, cards):
        if not AC_RowStack.canMoveCards(self, cards):
            return False
        return len(cards) <= self._getMaxMove(1)

    def acceptsCards(self, from_stack, cards):
        if not AC_RowStack.acceptsCards(self, from_stack, cards):
            return False
        return len(cards) <= self._getMaxMove(len(self.cards))


class SuperMoveRK_RowStack(SuperMoveStack_StackMethods, RK_RowStack):
    def canMoveCards(self, cards):
        if not RK_RowStack.canMoveCards(self, cards):
            return False
        return len(cards) <= self._getMaxMove(1)

    def acceptsCards(self, from_stack, cards):
        if not RK_RowStack.acceptsCards(self, from_stack, cards):
            return False
        return len(cards) <= self._getMaxMove(len(self.cards))


class SuperMoveSC_RowStack(SuperMoveStack_StackMethods, SC_RowStack):
    def canMoveCards(self, cards):
        if not SC_RowStack.canMoveCards(self, cards):
            return False
        return len(cards) <= self._getMaxMove(1)

    def acceptsCards(self, from_stack, cards):
        if not SC_RowStack.acceptsCards(self, from_stack, cards):
            return False
        return len(cards) <= self._getMaxMove(len(self.cards))


class SuperMoveBO_RowStack(SuperMoveStack_StackMethods, BO_RowStack):
    def canMoveCards(self, cards):
        if not BO_RowStack.canMoveCards(self, cards):
            return False
        return len(cards) <= self._getMaxMove(1)

    def acceptsCards(self, from_stack, cards):
        if not BO_RowStack.acceptsCards(self, from_stack, cards):
            return False
        return len(cards) <= self._getMaxMove(len(self.cards))


# ************************************************************************
# * WasteStack (a helper stack for the Talon, e.g. in Klondike)
# ************************************************************************

class WasteStack(OpenStack):
    def getHelp(self):
        return _('Waste.')


class WasteTalonStack(TalonStack):
    # A single click moves the top cards to the game's waste and
    # moves it face up; if we're out of cards, it moves the waste
    # back to the talon and increases the number of rounds (redeals).
    def __init__(self, x, y, game, max_rounds, num_deal=1, waste=None, **cap):
        TalonStack.__init__(self, x, y, game, max_rounds, num_deal, **cap)
        self.waste = waste

    def prepareStack(self):
        TalonStack.prepareStack(self)
        if self.waste is None:
            self.waste = self.game.s.waste

    def canDealCards(self):
        waste = self.waste
        if self.cards:
            num_cards = min(len(self.cards), self.num_deal)
            return len(waste.cards) + num_cards <= waste.cap.max_cards
        elif waste.cards and self.round != self.max_rounds:
            return True
        return False

    def dealCards(self, sound=False, shuffle=False):
        old_state = self.game.enterState(self.game.S_DEAL)
        num_cards = 0
        waste = self.waste
        if self.cards:
            if sound and not self.game.demo:
                self.game.playSample("dealwaste")
            num_cards = min(len(self.cards), self.num_deal)
            assert len(waste.cards) + num_cards <= waste.cap.max_cards
            for i in range(num_cards):
                if not self.cards[-1].face_up:
                    if 1:
                        self.game.flipAndMoveMove(self, waste)
                    else:
                        self.game.flipMove(self)
                        self.game.moveMove(1, self, waste, frames=4, shadow=0)
                else:
                    self.game.moveMove(1, self, waste, frames=4, shadow=0)
                self.fillStack()
                if TOOLKIT == 'kivy':
                    self.game.top.waitAnimation()
        elif waste.cards and self.round != self.max_rounds:
            if sound:
                self.game.playSample("turnwaste", priority=20)
            num_cards = len(waste.cards)
            self.game.turnStackMove(waste, self)
            if shuffle:
                # shuffle
                self.game.shuffleStackMove(self)
            self.game.nextRoundMove(self)
        self.game.leaveState(old_state)
        return num_cards

    def shuffleAndDealCards(self, sound=False):
        WasteTalonStack.dealCards(self, sound=sound, shuffle=True)


class FaceUpWasteTalonStack(WasteTalonStack):
    def canFlipCard(self):
        return len(self.cards) > 0 and not self.cards[-1].face_up

    def fillStack(self):
        if self.canFlipCard():
            self.game.singleFlipMove(self)
        self.game.fillStack(self)

    def dealCards(self, sound=False):
        retval = WasteTalonStack.dealCards(self, sound=sound)
        if self.canFlipCard():
            self.flipMove()
        return retval


class OpenTalonStack(TalonStack, OpenStack):
    canMoveCards = OpenStack.canMoveCards
    canDropCards = OpenStack.canDropCards
    releaseHandler = OpenStack.releaseHandler

    def __init__(self, x, y, game, **cap):
        kwdefault(cap, max_move=1)
        TalonStack.__init__(self, x, y, game, **cap)

    def canDealCards(self):
        return False

    def canFlipCard(self):
        return len(self.cards) > 0 and not self.cards[-1].face_up

    def fillStack(self):
        if self.canFlipCard():
            self.game.singleFlipMove(self)
        self.game.fillStack(self)

    def clickHandler(self, event):
        if self.canDealCards():
            return TalonStack.clickHandler(self, event)
        else:
            return OpenStack.clickHandler(self, event)


# ************************************************************************
# * ReserveStack (free cell)
# ************************************************************************

class ReserveStack(OpenStack):
    def __init__(self, x, y, game, **cap):
        kwdefault(cap, max_accept=1, max_cards=1)
        OpenStack.__init__(self, x, y, game, **cap)

    getBottomImage = Stack._getReserveBottomImage

    def getHelp(self):
        if self.cap.max_accept == 0:
            return _('Reserve. No building.')
        return _('Free cell.')


# ************************************************************************
# * InvisibleStack (an internal off-screen stack to hold cards)
# ************************************************************************

class InvisibleStack(Stack):
    def __init__(self, game, **cap):
        x, y = game.getInvisibleCoords()
        kwdefault(cap, max_move=0, max_accept=0)
        Stack.__init__(self, x, y, game, cap=cap)

    def assertStack(self):
        Stack.assertStack(self)
        assert not self.is_visible

    # no bindings
    def initBindings(self):
        pass

    # no bottom
    getBottomImage = Stack._getNoneBottomImage


# ************************************************************************
# * ArbitraryStack (stack with arbitrary access)
# *
# * NB: don't support hint and demo for non-top cards
# * NB: this stack only for CARD_XOFFSET == 0
# ************************************************************************

class ArbitraryStack(OpenStack):

    def __init__(self, x, y, game, **cap):
        kwdefault(cap, max_accept=0)
        OpenStack.__init__(self, x, y, game, **cap)
        self.CARD_YOFFSET = game.app.images.CARD_YOFFSET

    def canMoveCards(self, cards):
        return True

    def getDragCards(self, index):
        return [self.cards[index]]

    def startDrag(self, event, sound=True):
        OpenStack.startDrag(self, event, sound=sound)
        if self.game.app.opt.mouse_type == 'point-n-click':
            self.cards[self.game.drag.index].tkraise()
            self.game.drag.shadows[0].tkraise()
        else:
            for c in self.cards[self.game.drag.index+1:]:
                c.moveBy(0, -self.CARD_YOFFSET[0])

    def doubleclickHandler(self, event):
        # flip or drop a card
        flipstacks, dropstacks, quickstacks = self.game.getAutoStacks(event)
        if self in flipstacks and self.canFlipCard():
            self.playFlipMove(animation=True)
            return -1               # continue this event (start a drag)
        if self in dropstacks:
            i = self._findCard(event)
            if i < 0:
                return 0
            cards = [self.cards[i]]
            for s in self.game.s.foundations:
                if s is not self and s.acceptsCards(self, cards):
                    self.game.playSample("autodrop", priority=30)
                    self.playSingleCardMove(i, s, sound=False)
                    return 1
        return 0

    def moveCardsBackHandler(self, event, drag):
        i = self.cards.index(drag.cards[0])
        for card in self.cards[i:]:
            self._position(card)
            card.tkraise()

    def singleCardMove(self, index, to_stack, frames=-1, shadow=-1):
        self.game.singleCardMove(
            self, to_stack, index, frames=frames, shadow=shadow)
        self.fillStack()

    def dragMove(self, drag, to_stack, sound=True):
        self.playSingleCardMove(drag.index, to_stack, frames=0, sound=sound)

    def playSingleCardMove(self, index, to_stack, frames=-1, shadow=-1,
                           sound=True):
        if sound:
            if to_stack in self.game.s.foundations:
                self.game.playSample("drop", priority=30)
            else:
                self.game.playSample("move", priority=10)
        self.singleCardMove(index, to_stack, frames=frames, shadow=shadow)
        if not self.game.checkForWin():
            # let the player put cards back from the foundations
            if self not in self.game.s.foundations:
                self.game.autoPlay()
        self.game.finishMove()

    def quickPlayHandler(self, event, from_stacks=None, to_stacks=None):
        if to_stacks is None:
            to_stacks = self.game.s.foundations + self.game.sg.dropstacks
        if not self.cards:
            return 0
        #
        moves = []
        i = self._findCard(event)
        if i < 0:
            return 0
        pile = [self.cards[i]]
        for s in to_stacks:
            if s is not self and s.acceptsCards(self, pile):
                score = self.game.getQuickPlayScore(1, self, s)
                moves.append((score, -len(moves), i, s))
        #
        if moves:
            moves.sort()
            # from pprint import pprint; pprint(moves)
            score, len_moves, index, to_stack = moves[-1]
            if score >= 0:
                # self.game.playSample("startdrag")
                self.playSingleCardMove(index, to_stack)
                return 1
        return 0


# ************************************************************************
# * A StackWrapper is a functor (function object) that creates a
# * new stack when called, i.e. it wraps the constructor.
# *
# * "cap" are the capabilites, see class Stack above.
# ************************************************************************

# self.cap override any call-time cap
class StackWrapper:
    def __init__(self, stack_class, **cap):
        assert issubclass(stack_class, Stack)
        self.stack_class = stack_class
        self.cap = cap

    # return a new stack (an instance of the stack class)
    def __call__(self, x, y, game, **cap):
        # must preserve self.cap, so create a shallow copy
        # import pdb
        # pdb.set_trace()
        c = self.cap.copy()
        kwdefault(c, **cap)
        return self.stack_class(x, y, game, **c)


# call-time cap override self.cap
class WeakStackWrapper(StackWrapper):
    def __call__(self, x, y, game, **cap):
        kwdefault(cap, **self.cap)
        return self.stack_class(x, y, game, **cap)


# self.cap only, call-time cap is completely ignored
class FullStackWrapper(StackWrapper):
    def __call__(self, x, y, game, **cap):
        return self.stack_class(x, y, game, **self.cap)
