## vim:ts=4:et:nowrap
##
##---------------------------------------------------------------------------##
##
## PySol -- a Python Solitaire game
##
## Copyright (C) 2003 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 2002 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 2001 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 2000 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 1999 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 1998 Markus Franz Xaver Johannes Oberhumer
## All Rights Reserved.
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; see the file COPYING.
## If not, write to the Free Software Foundation, Inc.,
## 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
##
## Markus F.X.J. Oberhumer
## <markus@oberhumer.com>
## http://www.oberhumer.com/pysol
##
##---------------------------------------------------------------------------##


# imports
import sys

from mfxtools import *


# /***********************************************************************
# // moves (undo / redo)
# ************************************************************************/

## Currently we have the following atomic moves:
## - move the top cards from one stack on the top of another
## - flip the top card of a stack
## - turn a whole stack onto another stack
## - update the model or complete view a stack
## - increase the round (the number of redeals)
## - save the seed of game.random
## - shuffle a stack

class AtomicMove:
    def do(self, game):
        self.redo(game)

    def __repr__(self):
        return str(self.__dict__)
    def __str__(self):
        return str(self.__dict__)

    # Custom comparision for detecting redo moves. See Game.finishMove().
    def cmpForRedo(self, other):
        return -1


# /***********************************************************************
# // Move the top N cards from a stack to another stack.
# ************************************************************************/

class AMoveMove(AtomicMove):
    def __init__(self, ncards, from_stack, to_stack, frames, shadow=-1):
        assert from_stack is not to_stack
        self.ncards = ncards
        self.from_stack_id = from_stack.id
        self.to_stack_id = to_stack.id
        self.frames = frames
        self.shadow = shadow

    # do the actual move
    def __doMove(self, game, ncards, from_stack, to_stack):
        if game.moves.state == game.S_PLAY:
            assert to_stack.acceptsCards(from_stack, from_stack.cards[-ncards:])
        cards = []
        for i in range(ncards):
            card = from_stack.removeCard()
            cards.append(card)
        cards.reverse()
        if self.frames != 0:
            x, y = to_stack.getPositionFor(cards[0])
            game.animatedMoveTo(from_stack, to_stack, cards, x, y, frames=self.frames, shadow=self.shadow)
        for c in cards:
            to_stack.addCard(c)

    def redo(self, game):
        self.__doMove(game, self.ncards, game.allstacks[self.from_stack_id],
                      game.allstacks[self.to_stack_id])

    def undo(self, game):
        self.__doMove(game, self.ncards, game.allstacks[self.to_stack_id],
                      game.allstacks[self.from_stack_id])

    def cmpForRedo(self, other):
        return (cmp(self.ncards, other.ncards) or
                cmp(self.from_stack_id, other.from_stack_id) or
                cmp(self.to_stack_id, other.to_stack_id))


# /***********************************************************************
# // Flip the top card of a stack.
# ************************************************************************/

class AFlipMove(AtomicMove):
    def __init__(self, stack):
        self.stack_id = stack.id

    # do the actual move
    def __doMove(self, game, stack):
        card = stack.cards[-1]
        if card.face_up:
            card.showBack()
        else:
            card.showFace()

    def redo(self, game):
        self.__doMove(game, game.allstacks[self.stack_id])

    def undo(self, game):
        self.__doMove(game, game.allstacks[self.stack_id])

    def cmpForRedo(self, other):
        return cmp(self.stack_id, other.stack_id)


# /***********************************************************************
# // Turn the Waste stack onto the empty Talon.
# ************************************************************************/

class ATurnStackMove(AtomicMove):
    def __init__(self, from_stack, to_stack, update_flags=1):
        assert from_stack is not to_stack
        self.from_stack_id = from_stack.id
        self.to_stack_id = to_stack.id
        self.update_flags = update_flags

    def redo(self, game):
        from_stack = game.allstacks[self.from_stack_id]
        to_stack = game.allstacks[self.to_stack_id]
        assert len(from_stack.cards) > 0
        assert len(to_stack.cards) == 0
        l = len(from_stack.cards)
        for i in range(l):
            ##unhide = (i >= l - 2)
            unhide = 1
            ##print 1, unhide, from_stack.getCard().__dict__
            card = from_stack.removeCard(unhide=unhide, update=0)
            ##print 2, unhide, card.__dict__
            assert card.face_up
            to_stack.addCard(card, unhide=unhide, update=0)
            card.showBack(unhide=unhide)
            ##print 3, unhide, to_stack.getCard().__dict__
        if self.update_flags & 2:
            ### not used yet
            assert 0
            from_stack.round = from_stack.round + 1
        if self.update_flags & 1:
            assert to_stack is game.s.talon
            assert to_stack.round < to_stack.max_rounds or to_stack.max_rounds < 0
            to_stack.round = to_stack.round + 1
        from_stack.updateText()
        to_stack.updateText()

    def undo(self, game):
        from_stack = game.allstacks[self.to_stack_id]
        to_stack = game.allstacks[self.from_stack_id]
        assert len(from_stack.cards) > 0
        assert len(to_stack.cards) == 0
        l = len(from_stack.cards)
        for i in range(l):
            ##unhide = (i >= l - 2)
            unhide = 1
            card = from_stack.removeCard(unhide=unhide, update=0)
            assert not card.face_up
            card.showFace(unhide=unhide)
            to_stack.addCard(card, unhide=unhide, update=0)
        if self.update_flags & 2:
            ### not used yet
            assert 0
            assert to_stack.round > 1
            to_stack.round = to_stack.round - 1
        if self.update_flags & 1:
            assert from_stack is game.s.talon
            assert from_stack.round > 1
            from_stack.round = from_stack.round - 1
        from_stack.updateText()
        to_stack.updateText()

    def cmpForRedo(self, other):
        return (cmp(self.from_stack_id, other.from_stack_id) or
                cmp(self.to_stack_id, other.to_stack_id) or
                cmp(self.update_flags, other.update_flags))


# /***********************************************************************
# // ATurnStackMove is somewhat optimized to avoid unnecessary
# // unhide and hide operations.
# // FIXME: doesn't work yet
# ************************************************************************/

class NEW_ATurnStackMove(AtomicMove):
    def __init__(self, from_stack, to_stack, update_flags=1):
        assert from_stack is not to_stack
        self.from_stack_id = from_stack.id
        self.to_stack_id = to_stack.id
        self.update_flags = update_flags

    # do the actual turning move
    def __doMove(self, from_stack, to_stack, show_face):
        assert len(from_stack.cards) > 0
        assert len(to_stack.cards) == 0
        for card in from_stack.cards:
            card.item.dtag(from_stack.group)
            card.item.addtag(to_stack.group)
            if show_face:
                assert not card.face_up
                card.showFace(unhide=0)
            else:
                assert card.face_up
                card.showBack(unhide=0)
        to_stack.cards = from_stack.cards
        from_stack.cards = []
        from_stack.refreshView()
        from_stack.updateText()
        to_stack.refreshView()
        to_stack.updateText()

    def redo(self, game):
        from_stack = game.allstacks[self.from_stack_id]
        to_stack = game.allstacks[self.to_stack_id]
        if self.update_flags & 1:
            assert to_stack is game.s.talon
            assert to_stack.round < to_stack.max_rounds or to_stack.max_rounds < 0
            to_stack.round = to_stack.round + 1
        self.__doMove(from_stack, to_stack, 0)

    def undo(self, game):
        from_stack = game.allstacks[self.from_stack_id]
        to_stack = game.allstacks[self.to_stack_id]
        if self.update_flags & 1:
            assert to_stack is game.s.talon
            assert to_stack.round > 1
            to_stack.round = to_stack.round - 1
        self.__doMove(to_stack, from_stack, 1)

    def cmpForRedo(self, other):
        return (cmp(self.from_stack_id, other.from_stack_id) or
                cmp(self.to_stack_id, other.to_stack_id) or
                cmp(self.update_flags, other.update_flags))


# /***********************************************************************
# // Update the view or model of a stack. Only needed for complex
# // games in combination with undo.
# ************************************************************************/

class AUpdateStackMove(AtomicMove):
    def __init__(self, stack, flags):
        self.stack_id = stack.id
        self.flags = flags

    # do the actual move
    def __doMove(self, game, stack, undo):
        if self.flags & 64:
            # model
            stack.updateModel(undo, self.flags)
        else:
            # view
            if self.flags & 16:
                stack.updateText()
            if self.flags & 32:
                stack.refreshView()

    def redo(self, game):
        if (self.flags & 3) in (1, 3):
            self.__doMove(game, game.allstacks[self.stack_id], 0)

    def undo(self, game):
        if (self.flags & 3) in (2, 3):
            self.__doMove(game, game.allstacks[self.stack_id], 1)

    def cmpForRedo(self, other):
        return cmp(self.stack_id, other.stack_id) or cmp(self.flags, other.flags)


AUpdateStackModelMove = AUpdateStackMove
AUpdateStackViewMove = AUpdateStackMove


# /***********************************************************************
# // Increase the `round' member variable of a Talon stack.
# ************************************************************************/

class ANextRoundMove(AtomicMove):
    def __init__(self, stack):
        self.stack_id = stack.id

    def redo(self, game):
        stack = game.allstacks[self.stack_id]
        assert stack is game.s.talon
        assert stack.round < stack.max_rounds or stack.max_rounds < 0
        stack.round = stack.round + 1
        stack.updateText()

    def undo(self, game):
        stack = game.allstacks[self.stack_id]
        assert stack is game.s.talon
        assert stack.round > 1
        stack.round = stack.round - 1
        stack.updateText()

    def cmpForRedo(self, other):
        return cmp(self.stack_id, other.stack_id)


# /***********************************************************************
# // Save the current seed (needed for undo in some games).
# ************************************************************************/

class ASaveSeedMove(AtomicMove):
    def __init__(self, game):
        self.seed = game.random.getSeed()

    def redo(self, game):
        game.random.setSeed(self.seed)

    def undo(self, game):
        game.random.setSeed(self.seed)

    def cmpForRedo(self, other):
        return cmp(self.seed, other.seed)


# /***********************************************************************
# // Shuffle all cards of a stack. Saves the seed. Does not flip any cards.
# ************************************************************************/

class AShuffleStackMove(AtomicMove):
    def __init__(self, stack, game):
        self.stack_id = stack.id
        # save cards and seed
        self.card_ids = tuple(map(lambda c: c.id, stack.cards))
        self.seed = game.random.getSeed()

    def redo(self, game):
        stack = game.allstacks[self.stack_id]
        # paranoia
        assert stack is game.s.talon
        assert self.card_ids == tuple(map(lambda c: c.id, stack.cards))
        # shuffle (see random)
        game.random.setSeed(self.seed)
        seq = stack.cards
        n = len(seq) - 1
        while n > 0:
            j = game.random.randint(0, n)
            seq[n], seq[j] = seq[j], seq[n]
            n = n - 1
        stack.refreshView()

    def undo(self, game):
        stack = game.allstacks[self.stack_id]
        # restore cards
        cards = []
        for id in self.card_ids:
            c = game.cards[id]
            assert c.id == id
            cards.append(c)
        stack.cards = cards
        # restore the seed
        game.random.setSeed(self.seed)
        stack.refreshView()

    def cmpForRedo(self, other):
        return (cmp(self.stack_id, other.stack_id) or
                cmp(self.card_ids, other.card_ids) or
                cmp(self.seed, other.seed))

