#!/usr/bin/env python3
#
# make_pysol_freecell_board.py - Program to generate the boards of
# PySol for input into Freecell Solver.
#
# Usage: make_pysol_freecell_board.py [board number] | fc-solve
#
# Or on non-UNIXes:
#
# python make_pysol_freecell_board.py [board number] | fc-solve
#
# This program is platform independent and will generate the same results
# on all architectures and operating systems.
#
# Based on the code by Markus Franz Xaver Johannes Oberhumer.
# Modified by Shlomi Fish, 2000
#
# Since much of the code here is ripped from the actual PySol code, this
# program is distributed under the GNU General Public License.

# Original PySol blurb:
#
# ---------------------------------------------------------------------------
#
# PySol -- a Python Solitaire game
#
# Copyright (C) 2000 Markus Franz Xaver Johannes Oberhumer
# Copyright (C) 1999 Markus Franz Xaver Johannes Oberhumer
# Copyright (C) 1998 Markus Franz Xaver Johannes Oberhumer
#
# 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@jk.uni-linz.ac.at>
# http://wildsau.idv.uni-linz.ac.at/mfx/pysol.html
#
# ---------------------------------------------------------------------------


# imports
import sys
import time
import random2

# /***********************************************************************
# // Abstract PySol Random number generator.
# //
# // We use a seed of type long in the range [0, MAX_SEED].
# ************************************************************************/


class PysolRandom:
    MAX_SEED = 0

    ORIGIN_UNKNOWN = 0
    ORIGIN_RANDOM = 1
    ORIGIN_PREVIEW = 2         # random from preview
    ORIGIN_SELECTED = 3         # manually entered
    ORIGIN_NEXT_GAME = 4        # "Next game number"

    DEALS_PYSOL = 0
    DEALS_PYSOLFC = 1
    DEALS_MS = 2

    def __init__(self, seed=None):
        if seed is None:
            seed = self._getRandomSeed()
        self.initial_seed = self.setSeed(seed)
        self.origin = self.ORIGIN_UNKNOWN

    def __str__(self):
        return self.str(self.initial_seed)

    def reset(self):
        self.seed = self.initial_seed

    def getSeed(self):
        return self.seed

    def setSeed(self, seed):
        seed = self._convertSeed(seed)
        if type(seed) is not int:
            raise TypeError("seeds must be longs")
        if not (0 <= seed <= self.MAX_SEED):
            raise ValueError("seed out of range")
        self.seed = seed
        return seed

    def copy(self):
        random = PysolRandom(0)
        random.__class__ = self.__class__
        random.__dict__.update(self.__dict__)
        return random

    #
    # implementation
    #

    def choice(self, seq):
        return seq[int(self.random() * len(seq))]

    # Get a random integer in the range [a, b] including both end points.
    def randint(self, a, b):
        return a + int(self.random() * (b+1-a))

    #
    # subclass responsibility
    #

    # Get the next random number in the range [0.0, 1.0).
    def random(self):
        raise ValueError("Subclass responsibility")

    #
    # subclass overrideable
    #

    def _convertSeed(self, seed):
        return int(seed)

    def increaseSeed(self, seed):
        if seed < self.MAX_SEED:
            return seed + 1
        return 0

    def _getRandomSeed(self):
        t = int(time.time() * 256.0)
        t = (t ^ (t >> 24)) % (self.MAX_SEED + 1)
        return t

    #
    # shuffle
    #   see: Knuth, Vol. 2, Chapter 3.4.2, Algorithm P
    #   see: FAQ of sci.crypt: "How do I shuffle cards ?"
    #

    def shuffle(self, seq):
        n = len(seq) - 1
        while n > 0:
            j = self.randint(0, n)
            seq[n], seq[j] = seq[j], seq[n]
            n -= 1


# /***********************************************************************
# // Linear Congruential random generator
# //
# // Knuth, Donald.E., "The Art of Computer Programming,", Vol 2,
# // Seminumerical Algorithms, Third Edition, Addison-Wesley, 1998,
# // p. 106 (line 26) & p. 108
# ************************************************************************/

class LCRandom64(PysolRandom):
    MAX_SEED = 0xffffffffffffffff  # 64 bits

    def str(self, seed):
        s = repr(int(seed))[:-1]
        s = "0"*(20-len(s)) + s
        return s

    def random(self):
        self.seed = (self.seed*6364136223846793005 + 1) & self.MAX_SEED
        return ((self.seed >> 21) & 0x7fffffff) / 2147483648.0


# /***********************************************************************
# // Linear Congruential random generator
# // In PySol this is only used for 0 <= seed <= 32000.
# ************************************************************************/

class LCRandom31(PysolRandom):
    MAX_SEED = ((1 << (32+2))-1)         # 34 bits

    def str(self, seed):
        return "%05d" % int(seed)

    def _convertSeed(self, seed):
        seed = int(seed)
        self.seedx = (seed if (seed < 0x100000000) else (seed - 0x100000000))
        return seed

    def _rando(self):
        self.seedx = (self.seedx*214013 + 2531011) & self.MAX_SEED
        return ((self.seedx >> 16) & 0x7fff)

    def _randp(self):
        self.seedx = (self.seedx*214013 + 2531011) & self.MAX_SEED
        return ((self.seedx >> 16) & 0xffff)

    def randint(self, a, b):
        if self.seed < 0x100000000:
            ret = self._rando()
            ret = (ret if (self.seed < 0x80000000) else (ret | 0x8000))
        else:
            ret = self._randp() + 1

        return a + (ret % (b+1-a))

# ************************************************************************
# * Mersenne Twister random number generator
# * uses standard python module `random'
# ************************************************************************


class BasicRandom:
    MAX_SEED = 100000000000000000000  # 20 digits

    ORIGIN_UNKNOWN = 0
    ORIGIN_RANDOM = 1
    ORIGIN_PREVIEW = 2         # random from preview
    ORIGIN_SELECTED = 3         # manually entered
    ORIGIN_NEXT_GAME = 4        # "Next game number"

    def __str__(self):
        return self.str(self.initial_seed)

    def str(self, seed):
        return '%020d' % seed

    def reset(self):
        raise ValueError("Subclass responsibility")

    def copy(self):
        random = self.__class__(0)
        random.__dict__.update(self.__dict__)
        return random

    def increaseSeed(self, seed):
        if seed < self.MAX_SEED:
            return seed + 1
        return 0

    def _getRandomSeed(self):
        t = int(time.time() * 256.0)
        t = (t ^ (t >> 24)) % (self.MAX_SEED + 1)
        return t


class MTRandom(BasicRandom, random2.Random):

    def setSeed(self, seed):
        random2.Random.__init__(self, seed)
        self.initial_seed = seed
        self.initial_state = self.getstate()
        self.origin = self.ORIGIN_UNKNOWN

    def reset(self):
        self.setstate(self.initial_state)


class Card:

    ACE = 1
    KING = 13

    def __init__(self, id, rank, suit, print_ts):
        self.id = id
        self.rank = rank
        self.suit = suit
        self.flipped = False
        self.print_ts = print_ts
        self.empty = False

    def is_king(self):
        return self.rank == self.KING

    def is_ace(self):
        return self.rank == self.ACE

    def rank_s(self):
        s = "0A23456789TJQK"[self.rank]
        if (not self.print_ts) and s == "T":
            s = "10"
        return s

    def suit_s(self):
        return "CSHD"[self.suit]

    def to_s(self):
        if self.empty:
            return "-"
        ret = ""
        ret = ret + self.rank_s()
        ret = ret + self.suit_s()
        if self.flipped:
            ret = "<" + ret + ">"

        return ret

    def found_s(self):
        return self.suit_s() + "-" + self.rank_s()

    def flip(self, flipped=True):
        new_card = Card(self.id, self.rank, self.suit, self.print_ts)
        new_card.flipped = flipped
        return new_card

    def is_empty(self):
        return self.empty


class Columns:

    def __init__(self, num):
        self.num = num
        cols = []
        for i in range(num):
            cols.append([])

        self.cols = cols

    def add(self, idx, card):
        self.cols[idx].append(card)

    def rev(self):
        self.cols.reverse()

    def output(self):
        for column in self.cols:
            print(column_to_string(column))


class Board:
    def __init__(self, num_columns, with_freecells=False,
                 with_talon=False, with_foundations=False):
        self.with_freecells = with_freecells
        self.with_talon = with_talon
        self.with_foundations = with_foundations
        self.columns = Columns(num_columns)
        if (self.with_freecells):
            self.freecells = []
        if (self.with_talon):
            self.talon = []
        if (self.with_foundations):
            self.foundations = [empty_card() for s in range(4)]

    def reverse_cols(self):
        return self.columns.rev()

    def add(self, idx, card):
        return self.columns.add(idx, card)

    def print_freecells(self):
        print("Freecells: " + column_to_string(self.freecells))

    def print_talon(self):
        print("Talon: " + column_to_string(self.talon))

    def print_foundations(self):
        cells = []
        for f in [2, 0, 3, 1]:
            if not self.foundations[f].is_empty():
                cells.append(self.foundations[f].found_s())

        if len(cells):
            print("Foundations:" + ("".join([" "+s for s in cells])))

    def output(self):
        if (self.with_talon):
            self.print_talon()
        if (self.with_foundations):
            self.print_foundations()
        if (self.with_freecells):
            self.print_freecells()
        self.columns.output()

    def add_freecell(self, card):
        if not self.with_freecells:
            raise AttributeError("Layout does not have freecells!")
        self.freecells.append(card)

    def add_talon(self, card):
        if not self.with_talon:
            raise AttributeError("Layout does not have a talon!")

        self.talon.append(card)

    def put_into_founds(self, card):
        if not self.with_foundations:
            raise AttributeError("Layout does not have foundations!")

        if ((self.foundations[card.suit].rank+1) == card.rank):
            self.foundations[card.suit] = card
            return True
        else:
            return False
        self.talon.append(card)


def empty_card():
    ret = Card(0, 0, 0, 1)
    ret.empty = True
    return ret


def createCards(num_decks, print_ts):
    cards = []
    for deck in range(num_decks):
        id = 0
        for suit in range(4):
            for rank in range(13):
                cards.append(Card(id, rank+1, suit, print_ts))
                id = id + 1
    return cards


def column_to_list_of_strings(col):
    return [c.to_s() for c in col]


def column_to_string(col):
    return " ".join(column_to_list_of_strings(col))


def flip_card(card_str, flip):
    if flip:
        return "<" + card_str + ">"
    else:
        return card_str


def shuffle(orig_cards, game_num, which_deals):
    if ((game_num <= 32000) or which_deals == PysolRandom.DEALS_MS):
        r = LCRandom31()
        r.setSeed(game_num)
        fcards = []
        if (len(orig_cards) == 52):
            for i in range(13):
                for j in (0, 39, 26, 13):
                    fcards.append(orig_cards[i + j])
            orig_cards = fcards
        r.shuffle(orig_cards)
    else:
        r = 0
        if (which_deals == PysolRandom.DEALS_PYSOLFC):
            r = MTRandom()
        else:
            r = LCRandom64()
        r.setSeed(game_num)
        r.shuffle(orig_cards)

    return orig_cards


class Game:
    REVERSE_MAP = \
        {
                "freecell":
                ["freecell", "forecell", "bakers_game",
                 "ko_bakers_game", "kings_only_bakers_game",
                 "relaxed_freecell", "eight_off"],
                "der_katz":
                ["der_katz", "der_katzenschwantz", "die_schlange"],
                "seahaven":
                ["seahaven_towers", "seahaven", "relaxed_seahaven",
                 "relaxed_seahaven_towers"],
                "bakers_dozen": None,
                "gypsy": None,
                "klondike":
                ["klondike", "klondike_by_threes",
                 "casino_klondike", "small_harp", "thumb_and_pouch",
                 "vegas_klondike", "whitehead"],
                "simple_simon": None,
                "yukon": None,
                "beleaguered_castle":
                ["beleaguered_castle", "streets_and_alleys", "citadel"],
                "fan": None,
                "black_hole": None,
                "all_in_a_row": None,
        }

    def __init__(self, game_id, game_num, which_deals, print_ts):
        mymap = {}
        for k in list(self.REVERSE_MAP.keys()):
            if self.REVERSE_MAP[k] is None:
                mymap[k] = k
            else:
                for alias in self.REVERSE_MAP[k]:
                    mymap[alias] = k
        self.games_map = mymap
        self.game_id = game_id
        self.game_num = game_num
        self.print_ts = print_ts
        self.which_deals = which_deals

    def print_layout(self):
        game_class = self.lookup()

        if not game_class:
            raise ValueError("Unknown game type " + self.game_id + "\n")

        self.deal()

        getattr(self, game_class)()

        self.board.output()

    def lookup(self):
        return self.games_map[self.game_id]

    def is_two_decks(self):
        return self.game_id in ("der_katz", "der_katzenschwantz",
                                "die_schlange", "gypsy")

    def get_num_decks(self):
        if self.is_two_decks():
            return 2
        else:
            return 1

    def deal(self):
        orig_cards = createCards(self.get_num_decks(), self.print_ts)

        orig_cards = shuffle(orig_cards, self.game_num, self.which_deals)

        cards = orig_cards
        cards.reverse()

        self.cards = cards
        self.card_idx = 0
        return True

    def __iter__(self):
        return self

    def no_more_cards(self):
        return self.card_idx >= len(self.cards)

    def __next__(self):
        if self.no_more_cards():
            raise StopIteration
        c = self.cards[self.card_idx]
        self.card_idx = self.card_idx + 1
        return c

    def new_cards(self, cards):
        self.cards = cards
        self.card_idx = 0

    def add(self, idx, card):
        return self.board.add(idx, card)

    def add_freecell(self, card):
        return self.board.add_freecell(card)

    def cyclical_deal(game, num_cards, num_cols, flipped=False):
        for i in range(num_cards):
            game.add(i % num_cols, next(game).flip(flipped=flipped))
        return i

    def add_all_to_talon(game):
        for card in game:
            game.board.add_talon(card)

    # These are the games variants:
    # Each one is a callback.
    def der_katz(game):
        if (game.game_id == "die_schlange"):
            print("Foundations: H-A S-A D-A C-A H-A S-A D-A C-A")

        game.board = Board(9)
        col_idx = 0

        for card in game:
            if card.is_king():
                col_idx = col_idx + 1
            if not ((game.game_id == "die_schlange") and (card.rank == 1)):
                game.add(col_idx, card)

    def freecell(game):
        is_fc = (game.game_id in ('forecell', 'eight_off'))

        game.board = Board(8, with_freecells=is_fc)

        if is_fc:
            game.cyclical_deal(48, 8)
            for card in game:
                game.add_freecell(card)
                if game.game_id == "eight_off":
                    game.add_freecell(empty_card())
        else:
            game.cyclical_deal(52, 8)

    def seahaven(game):
        game.board = Board(10, with_freecells=True)

        game.add_freecell(empty_card())

        game.cyclical_deal(50, 10)

        for card in game:
            game.add_freecell(card)

    def bakers_dozen(game):
        i, n = 0, 13
        kings = []
        cards = game.cards
        cards.reverse()
        for c in cards:
            if c.is_king():
                kings.append(i)
            i = i + 1
        for i in kings:
            j = i % n
            while j < i:
                if not cards[j].is_king():
                    cards[i], cards[j] = cards[j], cards[i]
                    break
                j = j + n

        game.new_cards(cards)

        game.board = Board(13)

        game.cyclical_deal(52, 13)

    def gypsy(game):
        num_cols = 8
        game.board = Board(num_cols, with_talon=True)

        game.cyclical_deal(num_cols*2, num_cols, flipped=True)
        game.cyclical_deal(num_cols, num_cols, flipped=False)

        game.add_all_to_talon()

    def klondike(game):
        num_cols = 7
        game.board = Board(num_cols, with_talon=True)

        for r in range(1, num_cols):
            for s in range(num_cols-r):
                game.add(s, next(game).flip())

        game.cyclical_deal(num_cols, num_cols)

        game.add_all_to_talon()

        if not (game.game_id == "small_harp"):
            game.board.reverse_cols()

    def simple_simon(game):
        game.board = Board(10)

        num_cards = 9

        while num_cards >= 3:
            for s in range(num_cards):
                game.add(s, next(game))
            num_cards = num_cards - 1

        for s in range(10):
            game.add(s, next(game))

    def fan(game):
        game.board = Board(18)

        game.cyclical_deal(52-1, 17)

        game.add(17, next(game))

    def _shuffleHookMoveSorter(self, cards, func, ncards):
        # note that we reverse the cards, so that smaller sort_orders
        # will be nearer to the top of the Talon
        sitems, i = [], len(cards)
        for c in cards[:]:
            select, sort_order = func(c)
            if select:
                cards.remove(c)
                sitems.append((sort_order, i, c))
                if len(sitems) >= ncards:
                    break
            i = i - 1
        sitems.sort()
        sitems.reverse()
        scards = [item[2] for item in sitems]
        return cards, scards

    def _shuffleHookMoveToBottom(self, cards, func, ncards=999999):
        # move cards to bottom of the Talon (i.e. last cards to be dealt)
        cards, scards = self._shuffleHookMoveSorter(cards, func, ncards)
        ret = scards + cards
        return ret

    def _shuffleHookMoveToTop(self, cards, func, ncards=999999):
        # move cards to top of the Talon (i.e. last cards to be dealt)
        cards, scards = self._shuffleHookMoveSorter(cards, func, ncards)
        return cards + scards

    def black_hole(game):
        game.board = Board(17)

        # move Ace to bottom of the Talon (i.e. last cards to be dealt)
        game.cards = game._shuffleHookMoveToBottom(
            game.cards,
            lambda c: (c.id == 13, c.suit),
            1)
        next(game)
        game.cyclical_deal(52-1, 17)

        print("Foundations: AS")

    def all_in_a_row(game):
        game.board = Board(13)

        # move Ace to bottom of the Talon (i.e. last cards to be dealt)
        game.cards = game._shuffleHookMoveToTop(
            game.cards,
            lambda c: (c.id == 13, c.suit),
            1)
        game.cyclical_deal(52, 13)
        print("Foundations: -")

    def beleaguered_castle(game):
        aces_up = game.game_id in ("beleaguered_castle", "citadel")

        game.board = Board(8, with_foundations=True)

        if aces_up:
            new_cards = []

            for c in game:
                if c.is_ace():
                    game.board.put_into_founds(c)
                else:
                    new_cards.append(c)

            game.new_cards(new_cards)

        for i in range(6):
            for s in range(8):
                c = next(game)
                if not ((game.game_id == "citadel") and
                        game.board.put_into_founds(c)):
                    game.add(s, c)
            if game.no_more_cards():
                break

        if (game.game_id == "streets_and_alleys"):
            game.cyclical_deal(4, 4)

    def yukon(game):
        num_cols = 7
        game.board = Board(num_cols)

        for i in range(1, num_cols):
            for j in range(i, num_cols):
                game.add(j, next(game).flip())

        for i in range(4):
            for j in range(1, num_cols):
                game.add(j, next(game))

        game.cyclical_deal(num_cols, num_cols)


def shlomif_main(args):
    print_ts = 0
    which_deals = PysolRandom.DEALS_PYSOL
    while args[1][0] == '-':
        if (args[1] == "-t"):
            print_ts = 1
            args.pop(0)
        elif ((args[1] == "--pysolfc") or (args[1] == "-F")):
            which_deals = PysolRandom.DEALS_PYSOLFC
            args.pop(0)
        elif ((args[1] == "--ms") or (args[1] == "-M")):
            which_deals = PysolRandom.DEALS_MS
            args.pop(0)
        else:
            raise ValueError("Unknown flag " + args[1] + "!")

    game_num = int(args[1])
    if (len(args) >= 3):
        which_game = args[2]
    else:
        which_game = "freecell"

    game = Game(which_game, game_num, which_deals, print_ts)
    game.print_layout()


if __name__ == "__main__":
    sys.exit(shlomif_main(sys.argv))
