#!/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.game import Game
from pysollib.gamedb import GI, GameInfo, registerGame
from pysollib.hint import DefaultHint
from pysollib.layout import Layout
from pysollib.stack import \
        BasicRowStack, \
        InitialDealTalonStack, \
        InvisibleStack, \
        Stack, \
        StackWrapper, \
        TalonStack, \
        WasteStack, \
        WasteTalonStack
from pysollib.util import ACE, NO_SUIT

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


class Montana_Hint(DefaultHint):
    def computeHints(self):
        game = self.game
        RSTEP, RBASE = game.RSTEP, game.RBASE
        freerows = [s for s in game.s.rows if not s.cards]
        # for each stack
        for r in game.s.rows:
            if not r.cards:
                continue
            assert len(r.cards) == 1 and r.cards[-1].face_up
            c, pile, rpile = r.cards[0], r.cards, []
            if r.id % RSTEP > 0:
                left = game.s.rows[r.id - 1]
            else:
                left = None
                if c.rank == RBASE:
                    # do not move the leftmost card of a row if the
                    # rank is correct
                    continue
            for t in freerows:
                if self.shallMovePile(r, t, pile, rpile):
                    # FIXME: this scoring is completely simple
                    if left and left.cards:
                        # prefer low-rank left neighbours
                        score = 40000 + (self.K - left.cards[-1].rank)
                    else:
                        score = 50000
                    self.addHint(score, 1, r, t)


# ************************************************************************
# * Montana
# ************************************************************************

class Montana_Talon(TalonStack):
    def canDealCards(self):
        return self.round != self.max_rounds and not self.game.isGameWon()

    def _inSequence(self, card, suit, rank):
        return card.suit == suit and card.rank == rank

    def dealCards(self, sound=False):
        # move cards to the Talon, shuffle and redeal
        game = self.game
        decks = game.gameinfo.decks
        RSTEP, RBASE = game.RSTEP, game.RBASE
        num_cards = 0
        assert len(self.cards) == 0
        rows = game.s.rows
        # move out-of-sequence cards from the Tableau to the Talon
        stacks = []
        gaps = [None] * 4 * decks
        for g in range(4 * decks):
            i = g * RSTEP
            r = rows[i]
            if r.cards and r.cards[-1].rank == RBASE:
                in_sequence, suit = 1, r.cards[-1].suit
            else:
                in_sequence, suit = 0, NO_SUIT
            for j in range(RSTEP):
                r = rows[i + j]
                if in_sequence:
                    if (not r.cards or
                            not self._inSequence(r.cards[-1], suit, RBASE+j)):
                        in_sequence = 0
                if not in_sequence:
                    stacks.append(r)
                    if gaps[g] is None:
                        gaps[g] = r
                    if r.cards:
                        game.moveMove(1, r, self, frames=0)
                        num_cards = num_cards + 1
        assert len(self.cards) == num_cards
        assert len(stacks) == num_cards + len(gaps)
        if num_cards == 0:          # game already finished
            return 0
        if sound:
            game.startDealSample()
        # shuffle
        game.shuffleStackMove(self)
        # redeal
        game.nextRoundMove(self)
        spaces = self.getRedealSpaces(stacks, gaps)
        for r in stacks:
            if r not in spaces:
                self.game.moveMove(1, self, r, frames=4)
        # done
        assert len(self.cards) == 0
        if sound:
            game.stopSamples()
        return num_cards

    def getRedealSpaces(self, stacks, gaps):
        # the spaces are directly after the sorted sequence in each row
        return gaps


class Montana_RowStack(BasicRowStack):
    def acceptsCards(self, from_stack, cards):
        if not BasicRowStack.acceptsCards(self, from_stack, cards):
            return False
        if self.id % self.game.RSTEP == 0:
            return cards[0].rank == self.game.RBASE
        left = self.game.s.rows[self.id - 1]
        return left.cards and left.cards[-1].suit == cards[0].suit \
            and left.cards[-1].rank + 1 == cards[0].rank

    def clickHandler(self, event):
        if not self.cards:
            return self.quickPlayHandler(event)
        return BasicRowStack.clickHandler(self, event)

    getBottomImage = Stack._getBlankBottomImage


class Montana(Game):
    Talon_Class = StackWrapper(Montana_Talon, max_rounds=3)
    RowStack_Class = Montana_RowStack
    Hint_Class = Montana_Hint

    RLEN, RSTEP, RBASE = 52, 13, 1

    def createGame(self, round_text=True):
        decks = self.gameinfo.decks

        # create layout
        l, s = Layout(self, card_x_space=4), self.s

        # set window
        w, h = l.XM + self.RSTEP*l.XS, l.YM + (4*decks)*l.YS
        if round_text:
            h += l.YS
        self.setSize(w, h)

        # create stacks
        for k in range(decks):
            for i in range(4):
                x, y = l.XM, l.YM + (i+k*4)*l.YS
                for j in range(self.RSTEP):
                    s.rows.append(self.RowStack_Class(x, y, self,
                                  max_accept=1, max_cards=1))
                    x += l.XS
        if round_text:
            x, y = l.XM + (self.RSTEP-1)*l.XS//2, self.height-l.YS
            s.talon = self.Talon_Class(x, y, self)
            l.createRoundText(s.talon, 'se')
        else:
            # Talon is invisible
            x, y = self.getInvisibleCoords()
            s.talon = self.Talon_Class(x, y, self)
        if self.RBASE:
            # create an invisible stack to hold the four Aces
            s.internals.append(InvisibleStack(self))

        # define stack-groups
        l.defaultStackGroups()

    #
    # game overrides
    #

    def startGame(self):
        frames = 0
        toprows = len(self.s.talon.cards) * .75
        for i in range(len(self.s.talon.cards)):
            c = self.s.talon.cards[-1]
            if c.rank == ACE:
                self.s.talon.dealRow(rows=self.s.internals, frames=0)
            else:
                if frames == 0 and i >= toprows:
                    self.startDealSample()
                    frames = 4
                self.s.talon.dealRow(rows=(self.s.rows[i],), frames=frames)

    def isGameWon(self):
        rows = self.s.rows
        for i in range(0, self.RLEN, self.RSTEP):
            if not rows[i].cards:
                return False
            suit = rows[i].cards[-1].suit
            for j in range(self.RSTEP - 1):
                r = rows[i + j]
                if not r.cards or r.cards[-1].rank != self.RBASE + j \
                        or r.cards[-1].suit != suit:
                    return False
        return True

    def getHighlightPilesStacks(self):
        return ()

    def getAutoStacks(self, event=None):
        return (self.sg.dropstacks, (), self.sg.dropstacks)

    shallHighlightMatch = Game._shallHighlightMatch_SS

    def getQuickPlayScore(self, ncards, from_stack, to_stack):
        if from_stack.cards:
            if from_stack.id % self.RSTEP == 0 and \
                    from_stack.cards[-1].rank == self.RBASE:
                # do not move the leftmost card of a row if the rank is correct
                return -1
        return 1


# ************************************************************************
# * Spaces
# ************************************************************************

class Spaces_Talon(Montana_Talon):
    def getRedealSpaces(self, stacks, gaps):
        # use four random spaces, ignore gaps
        # note: the random.seed is already saved in shuffleStackMove
        spaces = []
        while len(spaces) != 4:
            r = self.game.random.choice(stacks)
            if r not in spaces:
                spaces.append(r)
        return spaces


class Spaces(Montana):
    Talon_Class = StackWrapper(Spaces_Talon, max_rounds=3)


# ************************************************************************
# * Station
# ************************************************************************

class Station_Talon(Montana_Talon):
    def getRedealSpaces(self, stacks, gaps):
        # use the last space of each row.
        return [self.game.s.rows[12], self.game.s.rows[25],
                self.game.s.rows[38], self.game.s.rows[51]]


class Station(Montana):
    Talon_Class = StackWrapper(Station_Talon, max_rounds=3)


# ************************************************************************
# * Blue Moon
# ************************************************************************

class BlueMoon(Montana):
    RLEN, RSTEP, RBASE = 56, 14, 0

    def startGame(self):
        frames = 0
        for i in range(self.RLEN):
            if i == self.RLEN-self.RSTEP:  # last row
                self.startDealSample()
                frames = -1
            if i % self.RSTEP == 0:     # left column
                continue
            self.s.talon.dealRow(rows=(self.s.rows[i],), frames=frames)
        ace_rows = [r for r in self.s.rows
                    if r.cards and r.cards[-1].rank == ACE]
        j = 0
        for r in ace_rows:
            self.moveMove(1, r, self.s.rows[j])
            j += self.RSTEP


# ************************************************************************
# * Red Moon
# ************************************************************************

class RedMoon(BlueMoon):
    def _shuffleHook(self, cards):
        # move Aces to top of the Talon (i.e. first cards to be dealt)
        return self._shuffleHookMoveToTop(
            cards, lambda c: (c.rank == 0, c.suit))

    def startGame(self):
        decks = self.gameinfo.decks
        frames = 0
        r = self.s.rows
        self.s.talon.dealRow(rows=(r[::14]), frames=frames)
        for i in range(4*decks):
            if i == 4*decks-1:
                self.startDealSample()
                frames = 4
            n = i * 14 + 2
            self.s.talon.dealRow(rows=r[n:n+12], frames=frames)


# ************************************************************************
# * Galary
# ************************************************************************


class Galary_Hint(Montana_Hint):
    def shallMovePile(self, from_stack, to_stack, pile, rpile):
        if from_stack is to_stack or \
                not to_stack.acceptsCards(from_stack, pile):
            return False
        # now check for loops
        rr = self.ClonedStack(from_stack, stackcards=rpile)
        if rr.acceptsCards(to_stack, pile):
            # the pile we are going to move could be moved back -
            # this is dangerous as we can create endless loops...
            return False
        return True


class Galary_RowStack(Montana_RowStack):
    def acceptsCards(self, from_stack, cards):
        if not BasicRowStack.acceptsCards(self, from_stack, cards):
            return False
        if self.id % self.game.RSTEP == 0:
            return cards[0].rank == self.game.RBASE
        r = self.game.s.rows
        left = r[self.id - 1]
        if left.cards and left.cards[-1].suit == cards[0].suit \
                and left.cards[-1].rank + 1 == cards[0].rank:
            return True
        if self.id < len(r)-1:
            right = r[self.id + 1]
            if right.cards and right.cards[-1].suit == cards[0].suit \
                    and right.cards[-1].rank - 1 == cards[0].rank:
                return True
        return False


class Galary(RedMoon):
    RowStack_Class = Galary_RowStack
    Hint_Class = Galary_Hint


# ************************************************************************
# * Free Parking
# ************************************************************************

class FreeParking(Montana):
    RowStack_Class = Galary_RowStack
    Hint_Class = Galary_Hint


# ************************************************************************
# * Jungle
# ************************************************************************

class Jungle_RowStack(Montana_RowStack):
    def acceptsCards(self, from_stack, cards):
        if not BasicRowStack.acceptsCards(self, from_stack, cards):
            return False
        if self.id % self.game.RSTEP == 0:
            return cards[0].rank == self.game.RBASE
        left = self.game.s.rows[self.id - 1]
        return left.cards and left.cards[-1].rank + 1 == cards[0].rank


class Jungle(BlueMoon):
    Talon_Class = StackWrapper(Montana_Talon, max_rounds=2)
    RowStack_Class = Jungle_RowStack
    Hint_Class = Galary_Hint


# ************************************************************************
# * Spaces and Aces
# ************************************************************************

class SpacesAndAces_RowStack(Montana_RowStack):
    def acceptsCards(self, from_stack, cards):
        if not BasicRowStack.acceptsCards(self, from_stack, cards):
            return False
        if self.id % self.game.RSTEP == 0:
            return cards[0].rank == self.game.RBASE
        left = self.game.s.rows[self.id - 1]
        return left.cards and left.cards[-1].suit == cards[0].suit \
            and left.cards[-1].rank < cards[0].rank


class SpacesAndAces(BlueMoon):
    Hint_Class = Galary_Hint
    Talon_Class = InitialDealTalonStack
    RowStack_Class = SpacesAndAces_RowStack

    def createGame(self):
        Montana.createGame(self, round_text=False)

    def startGame(self):
        frames = 0
        for i in range(self.RLEN):
            if i == self.RLEN-self.RSTEP:  # last row
                self.startDealSample()
                frames = -1
            if i % self.RSTEP == 0:     # left column
                continue
            self.s.talon.dealRow(rows=(self.s.rows[i],), frames=frames)

# ************************************************************************
# * Paganini
# ************************************************************************


class Paganini_Talon(Montana_Talon):
    def _inSequence(self, card, suit, rank):
        card_rank = card.rank
        if card_rank >= 5:
            card_rank -= 4
        return card.suit == suit and card_rank == rank


class Paganini_RowStack(Montana_RowStack):
    def acceptsCards(self, from_stack, cards):
        if not BasicRowStack.acceptsCards(self, from_stack, cards):
            return False
        if self.id % self.game.RSTEP == 0:
            return cards[0].rank == self.game.RBASE
        left = self.game.s.rows[self.id - 1]
        if not left.cards:
            return False
        if left.cards[-1].suit != cards[0].suit:
            return False
        if left.cards[-1].rank == ACE:
            return cards[0].rank == 5
        return left.cards[-1].rank+1 == cards[0].rank


class Paganini(BlueMoon):
    RLEN, RSTEP, RBASE = 40, 10, 0

    Talon_Class = StackWrapper(Paganini_Talon, max_rounds=2)
    RowStack_Class = Paganini_RowStack

    def isGameWon(self):
        rows = self.s.rows
        for i in range(0, self.RLEN, self.RSTEP):
            if not rows[i].cards:
                return False
            suit = rows[i].cards[-1].suit
            for j in range(self.RSTEP - 1):
                r = rows[i + j]
                if not r.cards:
                    return False
                card = r.cards[-1]
                card_rank = card.rank
                if card_rank >= 5:
                    card_rank -= 4
                if card_rank != self.RBASE + j or card.suit != suit:
                    return False
        return True


# ************************************************************************
# * Spoilt
# ************************************************************************

class Spoilt_RowStack(BasicRowStack):
    def acceptsCards(self, from_stack, cards):
        # if not BasicRowStack.acceptsCards(self, from_stack, cards):
        #    return False

        card = cards[0]
        RSTEP = self.game.RSTEP
        RBASE = self.game.RBASE
        row, col = divmod(self.id, RSTEP)
        # check rank
        if card.rank == ACE:
            if col != RSTEP-1:
                return False
        else:
            if card.rank - RBASE != col:
                return False
        # check suit
        suit = None
        for i in range(row*RSTEP, (row+1)*RSTEP):
            r = self.game.s.rows[i]
            if r.cards and r.cards[0].face_up:
                suit = r.cards[0].suit
                break
        if suit is not None:
            return card.suit == suit
        for r in self.game.s.rows:      # check other rows
            if r.cards and r.cards[0].face_up and r.cards[0].suit == card.suit:
                return False
        return True

    def canFlipCard(self):
        return False


class Spoilt_Waste(WasteStack):

    def moveMove(self, ncards, to_stack, frames=-1, shadow=-1):
        assert ncards == 1 and to_stack in self.game.s.rows
        if to_stack.cards:
            self._swapPairMove(ncards, to_stack, frames=-1, shadow=0)
        else:
            WasteStack.moveMove(self, ncards, to_stack, frames, shadow)

    def _swapPairMove(self, n, other_stack, frames=-1, shadow=-1):
        game = self.game
        old_state = game.enterState(game.S_FILL)
        swap = game.s.internals[0]
        game.flipMove(other_stack)
        game.moveMove(n, self, swap, frames=0)
        game.moveMove(n, other_stack, self, frames=frames, shadow=shadow)
        game.moveMove(n, swap, other_stack, frames=0)
        game.leaveState(old_state)


class Spoilt(Game):
    RSTEP, RBASE = 8, 6

    def createGame(self):
        # create layout
        l, s = Layout(self), self.s

        # set window
        self.setSize(l.XM + self.RSTEP*l.XS, l.YM + 5.5*l.YS)

        # create stacks
        for i in range(4):
            x, y, = l.XM, l.YM + i*l.YS
            for j in range(self.RSTEP):
                s.rows.append(Spoilt_RowStack(x, y, self,
                              max_accept=1, max_cards=2, min_cards=1))
                x += l.XS
        x, y = self.width//2 - l.XS, self.height-l.YS
        s.talon = WasteTalonStack(x, y, self, max_rounds=1)
        l.createText(s.talon, 'n')
        x += l.XS
        s.waste = Spoilt_Waste(x, y, self, max_cards=1)

        # create an invisible stack
        s.internals.append(InvisibleStack(self))

        # define stack-groups
        l.defaultStackGroups()

    def startGame(self):
        self.startDealSample()
        for i in range(4):
            rows = self.s.rows[self.RSTEP*i+1:self.RSTEP*(i+1)]
            self.s.talon.dealRow(rows=rows, frames=4, flip=False)
        self.s.talon.dealCards()

    def isGameWon(self):
        for r in self.s.rows:
            if not r.cards:
                return False
            if not r.cards[0].face_up:
                return False
        return True

    def getHighlightPilesStacks(self):
        return ()

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


# ************************************************************************
# * Double Montana
# * Paganini II
# * Double Blue Moon
# * Double Red Moon
# ************************************************************************

class DoubleMontana(Montana):
    Talon_Class = StackWrapper(Montana_Talon, max_rounds=3)
    RLEN, RSTEP, RBASE = 104, 13, 1


class PaganiniII(DoubleMontana):
    Talon_Class = InitialDealTalonStack
    Hint_Class = Galary_Hint
    RLEN, RSTEP, RBASE = 112, 14, 0

    def createGame(self):
        Montana.createGame(self, round_text=False)

    def startGame(self):
        frames = 0
        for i in range(self.RLEN):
            if i == self.RLEN - self.RSTEP:  # last row
                self.startDealSample()
                frames = -1
            if i % self.RSTEP == 0:     # left column
                continue
            self.s.talon.dealRow(rows=(self.s.rows[i],), frames=frames)


class DoubleBlueMoon(PaganiniII, BlueMoon):
    Talon_Class = StackWrapper(Montana_Talon, max_rounds=3)
    RLEN, RSTEP, RBASE = 112, 14, 0

    def createGame(self):
        BlueMoon.createGame(self, round_text=True)
    startGame = BlueMoon.startGame


class DoubleRedMoon(PaganiniII, RedMoon):
    Talon_Class = StackWrapper(Montana_Talon, max_rounds=3)
    RLEN, RROWS = 112, 8
    _shuffleHook = RedMoon._shuffleHook

    def createGame(self):
        RedMoon.createGame(self, round_text=True)
    startGame = RedMoon.startGame


# ************************************************************************
# * House of Commons
# * Pretzel
# ************************************************************************


class HouseOfCommons(Montana):
    Talon_Class = StackWrapper(Montana_Talon, max_rounds=2)
    RLEN, RSTEP, RBASE = 40, 10, 1

    def createGame(self):
        Montana.createGame(self, round_text=True)


class Pretzel(Montana):
    Talon_Class = InitialDealTalonStack
    RLEN, RSTEP, RBASE = 20, 5, 1

    def createGame(self):
        Montana.createGame(self, round_text=False)


# register the game
registerGame(GameInfo(53, Montana, "Montana",
                      GI.GT_MONTANA | GI.GT_OPEN, 1, 2, GI.SL_MOSTLY_SKILL,
                      si={"ncards": 48}, altnames=("Gaps", "Vacancies")))
registerGame(GameInfo(116, Spaces, "Spaces",
                      GI.GT_MONTANA | GI.GT_OPEN, 1, 2, GI.SL_MOSTLY_SKILL,
                      si={"ncards": 48}, altnames="Addiction"))
registerGame(GameInfo(63, BlueMoon, "Blue Moon",
                      GI.GT_MONTANA | GI.GT_OPEN, 1, 2, GI.SL_MOSTLY_SKILL,
                      altnames=("Rangoon",)))
registerGame(GameInfo(117, RedMoon, "Red Moon",
                      GI.GT_MONTANA | GI.GT_OPEN, 1, 2, GI.SL_MOSTLY_SKILL))
registerGame(GameInfo(275, Galary, "Galary",
                      GI.GT_MONTANA | GI.GT_OPEN | GI.GT_ORIGINAL, 1, 2,
                      GI.SL_MOSTLY_SKILL))
registerGame(GameInfo(276, FreeParking, "Free Parking",
                      GI.GT_MONTANA | GI.GT_OPEN, 1, 2, GI.SL_MOSTLY_SKILL,
                      si={"ncards": 48}, altnames=("Moonlight",
                                                   "Park and Ride")))
registerGame(GameInfo(380, Jungle, "Jungle",
                      GI.GT_MONTANA | GI.GT_OPEN, 1, 1, GI.SL_MOSTLY_SKILL))
registerGame(GameInfo(381, SpacesAndAces, "Spaces and Aces",
                      GI.GT_MONTANA | GI.GT_OPEN, 1, 0, GI.SL_MOSTLY_SKILL))
registerGame(GameInfo(706, Paganini, "Paganini",
                      GI.GT_MONTANA | GI.GT_OPEN | GI.GT_STRIPPED, 1, 1,
                      GI.SL_MOSTLY_SKILL, ranks=(0, 5, 6, 7, 8, 9, 10, 11, 12),
                      altnames=('Long Trip',)))
registerGame(GameInfo(736, Spoilt, "Spoilt",
                      GI.GT_MONTANA | GI.GT_STRIPPED, 1, 0, GI.SL_MOSTLY_LUCK,
                      ranks=(0, 6, 7, 8, 9, 10, 11, 12)))
registerGame(GameInfo(759, PaganiniII, "Paganini II",
                      GI.GT_MONTANA | GI.GT_OPEN, 2, 0, GI.SL_MOSTLY_SKILL))
registerGame(GameInfo(770, DoubleBlueMoon, "Double Blue Moon",
                      GI.GT_MONTANA | GI.GT_OPEN, 2, 2, GI.SL_MOSTLY_SKILL))
registerGame(GameInfo(771, DoubleRedMoon, "Double Red Moon",
                      GI.GT_MONTANA | GI.GT_OPEN, 2, 2, GI.SL_MOSTLY_SKILL))
registerGame(GameInfo(794, HouseOfCommons, "House of Commons",
                      GI.GT_MONTANA | GI.GT_OPEN | GI.GT_STRIPPED, 1, 1,
                      GI.SL_MOSTLY_SKILL, ranks=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
                      si={"ncards": 36}))
registerGame(GameInfo(795, Pretzel, "Pretzel",
                      GI.GT_MONTANA | GI.GT_OPEN | GI.GT_STRIPPED, 1, 0,
                      GI.SL_MOSTLY_SKILL, ranks=(0, 1, 2, 3, 4),
                      si={"ncards": 16}))
registerGame(GameInfo(858, Station, "Station",
                      GI.GT_MONTANA | GI.GT_OPEN, 1, 2, GI.SL_MOSTLY_SKILL,
                      si={"ncards": 48}))
registerGame(GameInfo(877, DoubleMontana, "Double Montana",
                      GI.GT_MONTANA | GI.GT_OPEN, 2, 2, GI.SL_MOSTLY_SKILL,
                      si={"ncards": 96}))