#!/usr/bin/env python
# -*- mode: python; coding: utf-8; -*-
# ---------------------------------------------------------------------------##
#
#  Copyright (C) 1998-2003 Markus Franz Xaver Johannes Oberhumer
#  Copyright (C) 2003 Mt. Hood Playing Card Co.
#  Copyright (C) 2005-2009 Skomoroh
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# ---------------------------------------------------------------------------##


import os
import re
import subprocess
import time
from io import BytesIO

from pysollib.mfxutil import destruct
from pysollib.pysolrandom import construct_random
from pysollib.settings import DEBUG, FCS_COMMAND
from pysollib.util import KING

FCS_VERSION = None

# ************************************************************************
# * HintInterface is an abstract class that defines the public
# * interface - it only consists of the constructor
# * and the getHints() method.
# *
# * The whole hint system is exclusively used by Game.getHints().
# ************************************************************************


class HintInterface:
    # level == 0: show hint (key `H')
    # level == 1: show hint and display score value (key `Ctrl-H')
    # level == 2: demo
    def __init__(self, game, level):
        pass

    # Compute all hints for the current position.
    # Subclass responsibility.
    #
    # Returns a list of "atomic hints" - an atomic hint is a 7-tuple
    # (score, pos, ncards, from_stack, to_stack, text_color, forced_move).
    #
    #    if ncards == 0: deal cards
    #    elif from_stack == to_stack: flip card
    #    else: move cards from from_stack to to_stack
    #
    #    score, pos and text_color are only for debugging.
    #    A forced_move is the next move that must be taken after this move
    #    in order to avoid endless loops during demo play.
    #
    # Deal and flip may only happen if self.level >= 2 (i.e. demo).
    #
    # See Game.showHint() for more information.
    def getHints(self, taken_hint=None):
        return []


# ************************************************************************
# * AbstractHint provides a useful framework for derived hint classes.
# *
# * Subclasses should override computeHints()
# ************************************************************************

class AbstractHint(HintInterface):
    def __init__(self, game, level):
        self.game = game
        self.level = level
        self.score_flatten_value = 0
        if self.level == 0:
            self.score_flatten_value = 10000
        # temporaries within getHints()
        self.bonus_color = None
        #
        self.__clones = []
        self.reset()

    def __del__(self):
        self.reset()

    def reset(self):
        self.hints = []
        self.max_score = 0
        self.__destructClones()
        self.solver_state = 'not_started'

    #
    # stack cloning
    #

    # Create a shallow copy of a stack.
    class AClonedStack:
        def __init__(self, stack, stackcards):
            # copy class identity
            self.__class__ = stack.__class__
            # copy model data (reference copy)
            stack.copyModel(self)
            # set new cards (shallow copy of the card list)
            self.cards = stackcards[:]

    def ClonedStack(self, stack, stackcards):
        s = self.AClonedStack(stack, stackcards)
        self.__clones.append(s)
        return s

    def __destructClones(self):
        for s in self.__clones:
            s.__class__ = self.AClonedStack     # restore orignal class
            destruct(s)
        self.__clones = []

    # When computing hints for level 0, the scores are flattened
    # (rounded down) to a multiple of score_flatten_value.
    #
    # The idea is that hints will appear equal within a certain score range
    # so that the player will not get confused by the demo-intelligence.
    #
    # Pressing `Ctrl-H' (level 1) will preserve the score.

    def addHint(self, score, ncards, from_stack,
                to_stack, text_color=None, forced_move=None):
        if score < 0:
            return
        self.max_score = max(self.max_score, score)
        # add an atomic hint
        if self.score_flatten_value > 0:
            score = (score // self.score_flatten_value) * \
                    self.score_flatten_value
        if text_color is None:
            text_color = self.BLACK
        assert forced_move is None or len(forced_move) == 7
        # pos is used for preserving the original sort order on equal scores
        pos = -len(self.hints)
        ah = (int(score), pos, ncards, from_stack, to_stack,
              text_color, forced_move)
        self.hints.append(ah)

    # clean up and return hints sorted by score
    def _returnHints(self):
        hints = self.hints
        self.reset()
        hints.sort()
        hints.reverse()
        return hints

    #
    # getHints() default implementation:
    #   - handle forced moves
    #   - try to flip face-down cards
    #   - call computeHints() to do something useful
    #   - try to deal cards
    #   - clean up and return hints sorted by score
    #

    # Default scores for flip and deal moves.
    SCORE_FLIP = 100000         # 0..100000
    SCORE_DEAL = 0              # 0..100000

    def getHints(self, taken_hint=None):
        # 0) setup
        self.reset()
        game = self.game
        # 1) forced moves of the prev. taken hint have absolute priority
        if taken_hint and taken_hint[6]:
            return [taken_hint[6]]
        # 2) try if we can flip a card
        if self.level >= 2:
            for r in game.allstacks:
                if r.canFlipCard():
                    self.addHint(self.SCORE_FLIP, 1, r, r)
                    if self.SCORE_FLIP >= 90000:
                        return self._returnHints()
        # 3) ask subclass to do something useful
        self.computeHints()
        # 4) try if we can deal cards
        if self.level >= 2:
            if game.canDealCards():
                self.addHint(self.SCORE_DEAL, 0, game.s.talon, None)
            # A few games have multiple waste stacks.  In these games,
            # reserves are used for the waste stacks.  This logic will
            # handle for those games.
            if (not game.canDealCards() and game.s.waste is not None and
                    len(game.s.waste.cards) > 0 and len(game.s.reserves) > 0):
                max_cards = 0
                reserve = None
                for r in game.s.reserves:
                    if r.acceptsCards(game.s.waste, game.s.waste.cards):
                        if len(r.cards) < max_cards or reserve is None:
                            max_cards = len(r.cards)
                            reserve = r

                if reserve is not None:
                    self.addHint(self.SCORE_DEAL, 1, game.s.waste, reserve)

        return self._returnHints()

    # subclass
    def computeHints(self):
        pass

    #
    # utility shallMovePile()
    #

    # we move the pile if it is accepted by the target stack
    def _defaultShallMovePile(self, from_stack, to_stack, pile, rpile):
        if from_stack is to_stack or not \
                to_stack.acceptsCards(from_stack, pile):
            return 0
        return 1

    # same, but check for loops
    def _cautiousShallMovePile(self, from_stack, to_stack, pile, rpile):
        if from_stack is to_stack or not \
                to_stack.acceptsCards(from_stack, pile):
            return 0
        #
        if len(rpile) == 0:
            return 1
        # 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 0
        return 1

    # same, but only check for loops only when in demo mode
    def _cautiousDemoShallMovePile(self, from_stack, to_stack, pile, rpile):
        if from_stack is to_stack or not \
                to_stack.acceptsCards(from_stack, pile):
            return 0
        if self.level >= 2:
            #
            if len(rpile) == 0:
                return 1
            # 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 0
        return 1

    shallMovePile = _defaultShallMovePile

    #
    # other utility methods
    #

    def _canDropAllCards(self, from_stack, stacks, stackcards):
        assert from_stack not in stacks
        return 0
        # FIXME: this does not account for cards which are dropped herein
        #         cards = pile[:]
        #         cards.reverse()
        #         for card in cards:
        #             for s in stacks:
        #                 if s is not from_stack:
        #                     if s.acceptsCards(from_stack, [card]):
        #                         break
        #             else:
        #                 return 0
        #         return 1

    #
    # misc. constants
    #

    # score value so that the scores look nicer
    K = KING + 1
    # text_color that will display the score (for debug with level 1)
    BLACK = "black"
    RED = "red"
    BLUE = "blue"


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

class DefaultHint(AbstractHint):

    # The DefaultHint is optimized for Klondike type games
    # and also deals quite ok with other simple variants.
    #
    # But it completely lacks any specific strategy about game
    # types like Forty Thieves, FreeCell, Golf, Spider, ...
    #
    # BTW, we do not cheat !

    #
    # bonus scoring used in _getXxxScore() below - subclass overrideable
    #

    def _preferHighRankMoves(self):
        return 0

    # Basic bonus for moving a card.
    # Bonus must be in range 0..999

    BONUS_DROP_CARD = 300        # 0..400
    BONUS_SAME_SUIT_MOVE = 200        # 0..400
    BONUS_NORMAL_MOVE = 100        # 0..400

    def _getMoveCardBonus(self, r, t, pile, rpile):
        assert pile
        bonus = 0
        if rpile:
            rr = self.ClonedStack(r, stackcards=rpile)
            if (rr.canDropCards(self.game.s.foundations))[0]:
                # the card below the pile can be dropped
                bonus = self.BONUS_DROP_CARD
        if t.cards and t.cards[-1].suit == pile[0].suit:
            # simple heuristics - prefer moving high-rank cards
            bonus += self.BONUS_SAME_SUIT_MOVE + (1 + pile[0].rank)
        elif self._preferHighRankMoves():
            # simple heuristics - prefer moving high-rank cards
            bonus += self.BONUS_NORMAL_MOVE + (1 + pile[0].rank)
        elif rpile:
            # simple heuristics - prefer low-rank cards in rpile
            bonus += self.BONUS_NORMAL_MOVE + (self.K - rpile[-1].rank)
        else:
            # simple heuristics - prefer moving high-rank cards
            bonus += self.BONUS_NORMAL_MOVE + (1 + pile[0].rank)
        return bonus

    # Special bonus for facing up a card after the current move.
    # Bonus must be in range 0..9000

    BONUS_FLIP_CARD = 1500        # 0..9000

    def _getFlipSpecialBonus(self, r, t, pile, rpile):
        assert pile and rpile
        # The card below the pile can be flipped
        # (do not cheat and look at it !)
        # default: prefer a short rpile
        bonus = max(self.BONUS_FLIP_CARD - len(rpile), 0)
        return bonus

    # Special bonus for moving a pile from stack r to stack t.
    # Bonus must be in range 0..9000

    BONUS_CREATE_EMPTY_ROW = 9000        # 0..9000
    BONUS_CAN_DROP_ALL_CARDS = 4000        # 0..4000
    BONUS_CAN_CREATE_EMPTY_ROW = 2000        # 0..4000

    def _getMoveSpecialBonus(self, r, t, pile, rpile):
        # check if we will create an empty row
        if not rpile:
            return self.BONUS_CREATE_EMPTY_ROW
        # check if the card below the pile can be flipped
        if not rpile[-1].face_up:
            return self._getFlipSpecialBonus(r, t, pile, rpile)
        # check if all the cards below our pile could be dropped
        if self._canDropAllCards(r, self.game.s.foundations, stackcards=rpile):
            # we can drop the whole remaining pile
            # (and will create an empty row in the next move)
            # print "BONUS_CAN_DROP_ALL_CARDS", r, pile, rpile
            self.bonus_color = self.RED
            return self.BONUS_CAN_DROP_ALL_CARDS + \
                self.BONUS_CAN_CREATE_EMPTY_ROW
        # check if the cards below our pile are a whole row
        if r.canMoveCards(rpile):
            # could we move the remaining pile ?
            for x in self.game.s.rows:
                # note: we allow x == r here, because the pile
                #       (currently at the top of r) will be
                #       available in the next move
                if x is t or not x.cards:
                    continue
                if x.acceptsCards(r, rpile):
                    # we can create an empty row in the next move
                    # print "BONUS_CAN_CREATE_EMPTY_ROW", r, x, pile, rpile
                    self.bonus_color = self.BLUE
                    return self.BONUS_CAN_CREATE_EMPTY_ROW
        return 0

    #
    # scoring used in getHints() - subclass overrideable
    #

    # Score for moving a pile from stack r to stack t.
    # Increased score should be in range 0..9999
    def _getMovePileScore(self, score, color, r, t, pile, rpile):
        assert pile
        self.bonus_color = color
        b1 = self._getMoveSpecialBonus(r, t, pile, rpile)
        assert 0 <= b1 <= 9000
        b2 = self._getMoveCardBonus(r, t, pile, rpile)
        assert 0 <= b2 <= 999
        return score + b1 + b2, self.bonus_color

    # Score for moving a pile (usually a single card) from the WasteStack.
    def _getMoveWasteScore(self, score, color, r, t, pile, rpile):
        assert pile
        self.bonus_color = color
        score = 30000
        if t.cards:
            score = 31000
        b2 = self._getMoveCardBonus(r, t, pile, rpile)
        assert 0 <= b2 <= 999
        return score + b2, self.bonus_color

    # Score for dropping ncards from stack r to stack t.
    def _getDropCardScore(self, score, color, r, t, ncards):
        assert t is not r
        if ncards > 1:
            # drop immediately (Spider)
            return 93000, color
        pile = r.cards
        c = pile[-1]
        # compute distance to t.cap.base_rank - compare Stack.getRankDir()
        if t.cap.base_rank < 0:
            d = len(t.cards)
        else:
            d = (c.rank - t.cap.base_rank) % t.cap.mod
            if d > t.cap.mod // 2:
                d -= t.cap.mod
        if abs(d) <= 1:
            # drop Ace and 2 immediately
            score = 92000
        elif r in self.game.sg.talonstacks:
            score = 25000              # less than _getMoveWasteScore()
        elif len(pile) == 1:
            # score = 50000
            score = 91000
        elif self._canDropAllCards(
                r, self.game.s.foundations, stackcards=pile[:-1]):
            score = 90000
            color = self.RED
        else:
            # don't drop this card too eagerly - we may need it
            # for pile moving
            score = 50000
        score += (self.K - c.rank)
        return score, color

    #
    # compute hints - main hint intelligence
    #

    def computeHints(self):
        game = self.game

        # 1) check Tableau piles
        self.step010(game.sg.dropstacks, game.s.rows)

        # 2) try if we can move part of a pile within the RowStacks
        #    so that we can drop a card afterwards
        if not self.hints and self.level >= 1:
            self.step020(game.s.rows, game.s.foundations)

        # 3) try if we should move a card from a Foundation to a RowStack
        if not self.hints and self.level >= 1:
            self.step030(game.s.foundations, game.s.rows, game.sg.dropstacks)

        # 4) try if we can move a card from a RowStack to a ReserveStack
        if not self.hints or self.level == 0:
            self.step040(game.s.rows, game.sg.reservestacks)

        # 5) try if we should move a card from a ReserveStack to a RowStack
        if not self.hints or self.level == 0:
            self.step050(game.sg.reservestacks, game.s.rows)

        # Don't be too clever and give up ;-)

    #
    # implementation of the hint steps
    #

    # 1) check Tableau piles

    def step010(self, dropstacks, rows):
        # for each stack
        for r in dropstacks:
            # 1a) try if we can drop cards
            t, ncards = r.canDropCards(self.game.s.foundations)
            if t:
                score, color = 0, None
                score, color = self._getDropCardScore(
                    score, color, r, t, ncards)
                self.addHint(score, ncards, r, t, color)
                if score >= 90000 and self.level >= 1:
                    break
            # 1b) try if we can move cards to one of the RowStacks
            for pile in self.step010b_getPiles(r):
                if pile:
                    self.step010_movePile(r, pile, rows)

    def step010b_getPiles(self, stack):
        # return all moveable piles for this stack, longest one first
        return (stack.getPile(), )

    def step010_movePile(self, r, pile, rows):
        lp = len(pile)
        lr = len(r.cards)
        assert 1 <= lp <= lr
        rpile = r.cards[: (lr-lp)]   # remaining pile

        empty_row_seen = 0
        r_is_waste = r in self.game.sg.talonstacks

        for t in rows:
            score, color = 0, None
            if not self.shallMovePile(r, t, pile, rpile):
                continue
            if r_is_waste:
                # moving a card from the WasteStack
                score, color = self._getMoveWasteScore(
                    score, color, r, t, pile, rpile)
            else:
                if not t.cards:
                    # the target stack is empty
                    if lp == lr:
                        # do not move a whole stack from row to row
                        continue
                    if empty_row_seen:
                        # only make one hint for moving to an empty stack
                        # (in case we have multiple empty stacks)
                        continue
                    score = 60000
                    empty_row_seen = 1
                else:
                    # the target stack is not empty
                    score = 80000
                score, color = self._getMovePileScore(
                    score, color, r, t, pile, rpile)
            self.addHint(score, lp, r, t, color)

    # 2) try if we can move part of a pile within the RowStacks
    #    so that we can drop a card afterwards
    #    score: 40000 .. 59999

    step020_getPiles = step010b_getPiles

    def step020(self, rows, foundations):
        for r in rows:
            for pile in self.step020_getPiles(r):
                if not pile or len(pile) < 2:
                    continue
                # is there a card in our pile that could be dropped ?
                drop_info = []
                i = 0
                for c in pile:
                    rr = self.ClonedStack(r, stackcards=[c])
                    stack, ncards = rr.canDropCards(foundations)
                    if stack and stack is not r:
                        assert ncards == 1
                        drop_info.append((c, stack, ncards, i))
                    i += 1
                # now try to make a move so that the drop-card will get free
                for di in drop_info:
                    c = di[0]
                    sub_pile = pile[di[3]+1:]
                    # print "trying drop move", c, pile, sub_pile
                    # assert r.canMoveCards(sub_pile)
                    if not r.canMoveCards(sub_pile):
                        continue
                    for t in rows:
                        if t is r or not t.acceptsCards(r, sub_pile):
                            continue
                        # print "drop move", r, t, sub_pile
                        score = 40000
                        score += 1000 + (self.K - r.getCard().rank)
                        # force the drop (to avoid loops)
                        force = (999999, 0, di[2], r, di[1], self.BLUE, None)
                        self.addHint(
                                score, len(sub_pile), r, t,
                                self.RED, forced_move=force)

    # 3) try if we should move a card from a Foundation to a RowStack
    #    score: 20000 .. 29999

    def step030(self, foundations, rows, dropstacks):
        for s in foundations:
            card = s.getCard()
            if not card or not s.canMoveCards([card]):
                continue
            # search a RowStack that would accept the card
            for t in rows:
                if t is s or not t.acceptsCards(s, [card]):
                    continue
                tt = self.ClonedStack(t, stackcards=t.cards+[card])
                # search a Stack that would benefit from this card
                for r in dropstacks:
                    if r is t:
                        continue
                    pile = r.getPile()
                    if not pile:
                        continue
                    if not tt.acceptsCards(r, pile):
                        continue
                    # compute remaining pile in r
                    rpile = r.cards[:(len(r.cards)-len(pile))]
                    rr = self.ClonedStack(r, stackcards=rpile)
                    if rr.acceptsCards(t, pile):
                        # the pile we are going to move from r to t
                        # could be moved back from t ro r - this is
                        # dangerous as we can create loops...
                        continue
                    score = 20000 + card.rank
                    # print score, s, t, r, pile, rpile
                    # force the move from r to t (to avoid loops)
                    force = (999999, 0, len(pile), r, t, self.BLUE, None)
                    self.addHint(score, 1, s, t, self.BLUE, forced_move=force)

    # 4) try if we can move a card from a RowStack to a ReserveStack
    #    score: 10000 .. 19999

    def step040(self, rows, reservestacks):
        if not reservestacks:
            return
        for r in rows:
            card = r.getCard()
            if not card or not r.canMoveCards([card]):
                continue
            pile = [card]
            # compute remaining pile in r
            rpile = r.cards[:(len(r.cards)-len(pile))]
            rr = self.ClonedStack(r, stackcards=rpile)
            for t in reservestacks:
                if t is r or not t.acceptsCards(r, pile):
                    continue
                if rr.acceptsCards(t, pile):
                    # the pile we are going to move from r to t
                    # could be moved back from t ro r - this is
                    # dangerous as we can create loops...
                    continue
                score = 10000
                score, color = self._getMovePileScore(
                    score, None, r, t, pile, rpile)
                self.addHint(score, len(pile), r, t, color)
                break

    # 5) try if we should move a card from a ReserveStack to a RowStack

    def step050(self, reservestacks, rows):
        if not reservestacks:
            return
        # FIXME


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

class CautiousDefaultHint(DefaultHint):
    shallMovePile = DefaultHint._cautiousShallMovePile
    # shallMovePile = DefaultHint._cautiousDemoShallMovePile

    def _preferHighRankMoves(self):
        return 1


# ************************************************************************
# * now some default hints for the various game types
# ************************************************************************

# DefaultHint is optimized for Klondike type games anyway
class KlondikeType_Hint(DefaultHint):
    pass


# this works for Yukon, but not too well for Russian Solitaire
class YukonType_Hint(CautiousDefaultHint):
    def step010b_getPiles(self, stack):
        # return all moveable piles for this stack, longest one first
        p = stack.getPile()
        piles = []
        while p:
            piles.append(p)
            p = p[1:]       # note: we need a fresh shallow copy
        return piles


class Yukon_Hint(YukonType_Hint):
    BONUS_FLIP_CARD = 9000
    BONUS_CREATE_EMPTY_ROW = 100

    # FIXME: this is only a rough approximation and doesn't seem to help
    #        for Russian Solitaire
    def _getMovePileScore(self, score, color, r, t, pile, rpile):
        s, color = YukonType_Hint._getMovePileScore(
            self, score, color, r, t, pile, rpile)
        bonus = s - score
        assert 0 <= bonus <= 9999
        # We must take care when moving piles that we won't block cards,
        # i.e. if there is a card in pile which would be needed
        # for a card in stack t.
        tpile = t.getPile()
        if tpile:
            for cr in pile:
                rr = self.ClonedStack(r, stackcards=[cr])
                for ct in tpile:
                    if rr.acceptsCards(t, [ct]):
                        d = bonus // 1000
                        bonus = (d * 1000) + bonus % 100
                        break
        return score + bonus, color


class FreeCellType_Hint(CautiousDefaultHint):
    pass


class GolfType_Hint(DefaultHint):
    pass


class SpiderType_Hint(DefaultHint):
    pass


class PySolHintLayoutImportError(Exception):

    def __init__(self, msg, cards, line_num):
        """docstring for __init__"""
        self.msg = msg
        self.cards = cards
        self.line_num = line_num

    def format(self):
        return self.msg + ":\n\n" + ', '.join(self.cards)


class Base_Solver_Hint:
    def __init__(self, game, dialog, **game_type):
        self.game = game
        self.dialog = dialog
        self.game_type = game_type
        self.options = {
            'iters_step': 100,
            'max_iters': 10000,
            'progress': False,
            'preset': None,
            }
        self.hints = []
        self.hints_index = 0

        # correct cards rank if foundations.base_rank != 0 (Penguin, Opus)
        if 'base_rank' in game_type:    # (Simple Simon)
            self.base_rank = game_type['base_rank']
        else:
            self.base_rank = game.s.foundations[0].cap.base_rank

    def _setText(self, **kw):
        return self.dialog.setText(**kw)

    def config(self, **kw):
        self.options.update(kw)

    def _card2str_format(self, fmt, rank, suit):
        # row and reserves
        rank = (rank-self.base_rank) % 13
        return fmt.format(R="A23456789TJQK"[rank], S="CSHD"[suit])

    def card2str1_(self, rank, suit):
        # row and reserves
        return self._card2str_format('{R}{S}', rank, suit)

    def card2str1(self, card):
        return self.card2str1_(card.rank, card.suit)

    def card2str2(self, card):
        # foundations
        return self._card2str_format('{S}-{R}', card.rank, card.suit)

# hard solvable: Freecell #47038300998351211829 (65539 iters)

    def getHints(self, taken_hint=None):
        if taken_hint and taken_hint[6]:
            return [taken_hint[6]]
        h = self.hints[self.hints_index]
        if h is None:
            return None
        ncards, src, dest = h
        thint = None
        if len(src.cards) > ncards and not src.cards[-ncards-1].face_up:
            # flip card
            thint = (999999, 0, 1, src, src, None, None)
        skip = False
        if dest is None:                 # foundation
            if src is self.game.s.talon:
                if not src.cards[-1].face_up:
                    self.game.flipMove(src)
                dest = self.game.s.foundations[0]
            else:
                cards = src.cards[-ncards:]
                for f in self.game.s.foundations:
                    if f.acceptsCards(src, cards):
                        dest = f
                        break
        assert dest
        self.hints_index += 1
        if skip:
            return []
        hint = (999999, 0, ncards, src, dest, None, thint)
        return [hint]

    def colonPrefixMatch(self, prefix, s):
        m = re.match(prefix + ': ([0-9]+)', s)
        if m:
            self._v = int(m.group(1))
            return True
        self._v = None
        return False

    def run_solver(self, command, board):
        if DEBUG:
            print(command)
        kw = {'shell': True,
              'stdin': subprocess.PIPE,
              'stdout': subprocess.PIPE,
              'stderr': subprocess.PIPE}
        if os.name != 'nt':
            kw['close_fds'] = True
        p = subprocess.Popen(command, **kw)
        bytes_board = bytes(board, 'utf-8')
        pout, perr = p.communicate(bytes_board)
        if p.returncode in (127, 1):
            # Linux and Windows return codes for "command not found" error
            raise RuntimeError('Solver exited with {}'.format(p.returncode))
        return BytesIO(pout), BytesIO(perr)

    def importFile(solver, fh, s_game, self):
        s_game.endGame()
        s_game.random = construct_random('Custom')
        s_game.newGame(
            shuffle=True,
            random=construct_random('Custom'),
            dealer=lambda: solver.importFileHelper(fh, s_game))
        s_game.random = construct_random('Custom')

    def importFileHelper(solver, fh, s_game):
        pass


use_fc_solve_lib = False

try:
    import freecell_solver
    fc_solve_lib_obj = freecell_solver.FreecellSolver()
    use_fc_solve_lib = True
except BaseException:
    pass

use_bh_solve_lib = False

try:
    import black_hole_solver
    bh_solve_lib_obj = black_hole_solver.BlackHoleSolver()
    use_bh_solve_lib = True
except BaseException:
    pass


class FreeCellSolver_Hint(Base_Solver_Hint):
    def _determineIfSolverState(self, line):
        if re.search('^(?:Iterations count exceeded)', line):
            self.solver_state = 'intractable'
            return True
        if re.search('^(?:I could not solve this game)', line):
            self.solver_state = 'unsolved'
            return True
        return False

    def _isSimpleSimon(self):
        game_type = self.game_type
        return ('preset' in game_type and
                game_type['preset'] == 'simple_simon')

    def _addBoardLine(self, line):
        self.board += line + '\n'

    def _addPrefixLine(self, prefix, b):
        if b:
            self._addBoardLine(prefix + b)

    def importFileHelper(solver, fh, s_game):
        game = s_game.s
        stack_idx = 0

        RANKS_S = "A23456789TJQK"
        RANKS0_S = '0' + RANKS_S
        RANKS_RE = '(?:' + '[' + RANKS_S + ']' + '|10)'
        SUITS_S = "CSHD"
        SUITS_RE = '[' + SUITS_S + ']'
        CARD_RE = r'(?:' + RANKS_RE + SUITS_RE + ')'
        line_num = 0

        def cards():
            return game.talon.cards

        def put(target, suit, rank):
            ret = [i for i, c in enumerate(cards())
                   if c.suit == suit and c.rank == rank]
            if len(ret) < 1:
                raise PySolHintLayoutImportError(
                    "Duplicate cards in input",
                    [solver.card2str1_(rank, suit)],
                    line_num
                )

            ret = ret[0]
            game.talon.cards = \
                cards()[0:ret] + cards()[(ret+1):] + [cards()[ret]]
            s_game.flipMove(game.talon)
            s_game.moveMove(1, game.talon, target, frames=0)

        def put_str(target, str_):
            put(target, SUITS_S.index(str_[-1]),
                (RANKS_S.index(str_[0]) if len(str_) == 2 else 9))

        def my_find_re(RE, m, msg):
            s = m.group(1)
            if not re.match(r'^\s*(?:' + RE + r')?(?:\s+' + RE + r')*\s*$', s):
                raise PySolHintLayoutImportError(
                    msg,
                    [],
                    line_num
                )
            return re.findall(r'\b' + RE + r'\b', s)

        # Based on https://stackoverflow.com/questions/8898294 - thanks!
        def mydecode(s):
            for encoding in "utf-8-sig", "utf-8":
                try:
                    return s.decode(encoding)
                except UnicodeDecodeError:
                    continue
            return s.decode("latin-1")  # will always work

        mytext = mydecode(fh.read())
        for line_p in mytext.splitlines():
            line_num += 1
            line = line_p.rstrip('\r\n')
            m = re.match(r'^(?:Foundations:|Founds?:)\s*(.*)', line)
            if m:
                for gm in my_find_re(
                        r'(' + SUITS_RE + r')-([' + RANKS0_S + r'])', m,
                        "Invalid Foundations line"):
                    for foundat in game.foundations:
                        suit = foundat.cap.suit
                        if SUITS_S[suit] == gm[0]:
                            rank = gm[1]
                            if len(rank) == 1:
                                lim = RANKS0_S.index(rank)
                            else:
                                lim = 10
                            for r in range(lim):
                                put(foundat, suit, r)
                            break
                continue
            m = re.match(r'^(?:FC:|Freecells:)\s*(.*)', line)
            if m:
                g = my_find_re(r'(' + CARD_RE + r'|\-)', m,
                               "Invalid Freecells line")
                while len(g) < len(game.reserves):
                    g.append('-')
                for i, gm in enumerate(g):
                    str_ = gm
                    if str_ != '-':
                        put_str(game.reserves[i], str_)
                continue
            m = re.match(r'^:?\s*(.*)', line)
            for str_ in my_find_re(r'(' + CARD_RE + r')', m,
                                   "Invalid column text"):
                put_str(game.rows[stack_idx], str_)

            stack_idx += 1
        if len(cards()) > 0:
            raise PySolHintLayoutImportError(
                "Missing cards in input",
                [solver.card2str1(c) for c in cards()],
                -1
            )

    def calcBoardString(self):
        game = self.game
        self.board = ''
        is_simple_simon = self._isSimpleSimon()
        b = ''
        for s in game.s.foundations:
            if s.cards:
                b += ' ' + self.card2str2(
                    s.cards[0 if is_simple_simon else -1])
        self._addPrefixLine('Founds:', b)

        b = ''
        for s in game.s.reserves:
            b += ' ' + (self.card2str1(s.cards[-1]) if s.cards else '-')
        self._addPrefixLine('FC:', b)

        for s in game.s.rows:
            b = ''
            for c in s.cards:
                cs = self.card2str1(c)
                if not c.face_up:
                    cs = '<%s>' % cs
                b += cs + ' '
            self._addBoardLine(b.strip())

        return self.board

    def computeHints(self):
        game = self.game
        game_type = self.game_type
        global FCS_VERSION
        if FCS_VERSION is None:
            if use_fc_solve_lib:
                FCS_VERSION = (5, 0, 0)
            else:
                pout, _ = self.run_solver(FCS_COMMAND + ' --version', '')
                s = str(pout.read(), encoding='utf-8')
                m = re.search(r'version ([0-9]+)\.([0-9]+)\.([0-9]+)', s)
                if m:
                    FCS_VERSION = (int(m.group(1)), int(m.group(2)),
                                   int(m.group(3)))
                else:
                    FCS_VERSION = (0, 0, 0)

        progress = self.options['progress']

        board = self.calcBoardString()
        if DEBUG:
            print('--------------------\n', board, '--------------------')
        args = []
        if use_fc_solve_lib:
            args += ['--reset', '-opt', ]
        else:
            args += ['-m', '-p', '-opt', '-sel']
            if FCS_VERSION >= (4, 20, 0):
                args += ['-hoi']
        if (not use_fc_solve_lib) and progress:
            args += ['--iter-output']
            fcs_iter_output_step = None
            if FCS_VERSION >= (4, 20, 0):
                fcs_iter_output_step = self.options['iters_step']
                args += ['--iter-output-step', str(fcs_iter_output_step)]
            if DEBUG:
                args += ['-s']
        if self.options['preset'] and self.options['preset'] != 'none':
            args += ['--load-config', self.options['preset']]
        args += ['--max-iters', str(self.options['max_iters']),
                 '--decks-num', str(game.gameinfo.decks),
                 '--stacks-num', str(len(game.s.rows)),
                 '--freecells-num', str(len(game.s.reserves)),
                 ]
        if 'preset' in game_type:
            args += ['--preset', game_type['preset']]
        if 'sbb' in game_type:
            args += ['--sequences-are-built-by', game_type['sbb']]
        if 'sm' in game_type:
            args += ['--sequence-move', game_type['sm']]
        if 'esf' in game_type:
            args += ['--empty-stacks-filled-by', game_type['esf']]

        if use_fc_solve_lib:
            fc_solve_lib_obj.input_cmd_line(args)
            status = fc_solve_lib_obj.solve_board(board)
        else:
            command = FCS_COMMAND+' '+' '.join(args)
            pout, perr = self.run_solver(command, board)
        self.solver_state = 'unknown'
        stack_types = {
            'the': game.s.foundations,
            'stack': game.s.rows,
            'freecell': game.s.reserves,
            }
        if DEBUG:
            start_time = time.time()
        if not (use_fc_solve_lib) and progress:
            iter_ = 0
            depth = 0
            states = 0

            for sbytes in pout:
                s = str(sbytes, encoding='utf-8')
                if DEBUG >= 5:
                    print(s)

                if self.colonPrefixMatch('Iteration', s):
                    iter_ = self._v
                elif self.colonPrefixMatch('Depth', s):
                    depth = self._v
                elif self.colonPrefixMatch('Stored-States', s):
                    states = self._v
                    if iter_ % 100 == 0 or fcs_iter_output_step:
                        self._setText(iter=iter_, depth=depth, states=states)
                elif re.search('^(?:-=-=)', s):
                    break
                elif self._determineIfSolverState(s):
                    break
            self._setText(iter=iter_, depth=depth, states=states)

        hints = []
        if use_fc_solve_lib:
            self._setText(
                iter=fc_solve_lib_obj.get_num_times(),
                depth=0,
                states=fc_solve_lib_obj.get_num_states_in_collection(),
            )
            if status == 0:
                m = fc_solve_lib_obj.get_next_move()
                while m:
                    type_ = ord(m.s[0])
                    src = ord(m.s[1])
                    dest = ord(m.s[2])
                    hints.append([
                        (ord(m.s[3]) if type_ == 0
                         else (13 if type_ == 11 else 1)),
                        (game.s.rows if (type_ in [0, 1, 4, 11, ])
                         else game.s.reserves)[src],
                        (game.s.rows[dest] if (type_ in [0, 2])
                         else (game.s.reserves[dest]
                               if (type_ in [1, 3]) else None))])

                    m = fc_solve_lib_obj.get_next_move()
            else:
                self.solver_state = 'unsolved'
        else:
            for sbytes in pout:
                s = str(sbytes, encoding='utf-8')
                if DEBUG:
                    print(s)
                if self._determineIfSolverState(s):
                    break
                m = re.match(
                    'Total number of states checked is ([0-9]+)\\.', s)
                if m:
                    self._setText(iter=int(m.group(1)))

                m = re.match('This scan generated ([0-9]+) states\\.', s)

                if m:
                    self._setText(states=int(m.group(1)))

                m = re.match('Move (.*)', s)
                if not m:
                    continue

                move_s = m.group(1)

                m = re.match(
                    'the sequence on top of Stack ([0-9]+) to the foundations',
                    move_s)

                if m:
                    ncards = 13
                    st = stack_types['stack']
                    sn = int(m.group(1))
                    src = st[sn]
                    dest = None
                else:
                    m = re.match(
                        '(?P<ncards>a card|(?P<count>[0-9]+) cards) '
                        'from (?P<source_type>stack|freecell) '
                        '(?P<source_idx>[0-9]+) to '
                        '(?P<dest>the foundations|'
                        '(?P<dest_type>freecell|stack) '
                        '(?P<dest_idx>[0-9]+))\\s*', move_s)

                    if not m:
                        continue

                    if m.group('ncards') == 'a card':
                        ncards = 1
                    else:
                        ncards = int(m.group('count'))

                    st = stack_types[m.group('source_type')]
                    sn = int(m.group('source_idx'))
                    src = st[sn]

                    dest_s = m.group('dest')
                    if dest_s == 'the foundations':
                        dest = None
                    else:
                        dt = stack_types[m.group('dest_type')]
                        dest = dt[int(m.group('dest_idx'))]

                hints.append([ncards, src, dest])

        if DEBUG:
            print('time:', time.time()-start_time)

        self.hints = hints
        if len(hints) > 0:
            if self.solver_state != 'intractable':
                self.solver_state = 'solved'
        self.hints.append(None)

        if not use_fc_solve_lib:
            pout.close()
            perr.close()


class BlackHoleSolver_Hint(Base_Solver_Hint):
    BLACK_HOLE_SOLVER_COMMAND = 'black-hole-solve'

    def importFileHelper(solver, fh, s_game):
        game = s_game.s
        stack_idx = 0
        found_idx = 0

        RANKS_S = "A23456789TJQK"
        RANKS_RE = '(?:' + '[' + RANKS_S + ']' + '|10)'
        SUITS_S = "CSHD"
        SUITS_RE = '[' + SUITS_S + ']'
        CARD_RE = r'(?:' + RANKS_RE + SUITS_RE + ')'
        line_num = 0

        def cards():
            return game.talon.cards

        def put(target, suit, rank):
            ret = [i for i, c in enumerate(cards())
                   if c.suit == suit and c.rank == rank]
            if len(ret) < 1:
                raise PySolHintLayoutImportError(
                    "Duplicate cards in input",
                    [solver.card2str1_(rank, suit)],
                    line_num
                )

            ret = ret[0]
            game.talon.cards = \
                cards()[0:ret] + cards()[(ret+1):] + [cards()[ret]]
            s_game.flipMove(game.talon)
            s_game.moveMove(1, game.talon, target, frames=0)

        def put_str(target, str_):
            put(target, SUITS_S.index(str_[-1]),
                (RANKS_S.index(str_[0]) if len(str_) == 2 else 9))

        def my_find_re(RE, m, msg):
            s = m.group(1)
            if not re.match(r'^\s*(?:' + RE + r')?(?:\s+' + RE + r')*\s*$', s):
                raise PySolHintLayoutImportError(
                    msg,
                    [],
                    line_num
                )
            return re.findall(r'\b' + RE + r'\b', s)

        # Based on https://stackoverflow.com/questions/8898294 - thanks!
        def mydecode(s):
            for encoding in "utf-8-sig", "utf-8":
                try:
                    return s.decode(encoding)
                except UnicodeDecodeError:
                    continue
            return s.decode("latin-1")  # will always work

        mytext = mydecode(fh.read())
        for line_p in mytext.splitlines():
            line_num += 1
            line = line_p.rstrip('\r\n')
            m = re.match(r'^(?:Foundations:|Founds?:)\s*(.*)', line)
            if m:
                for gm in my_find_re(r'(' + CARD_RE + r')', m,
                                     "Invalid Foundations line"):
                    put_str(game.foundations[found_idx], gm)
                    found_idx += 1
                continue
            m = re.match(r'^:?\s*(.*)', line)
            for str_ in my_find_re(r'(' + CARD_RE + r')', m,
                                   "Invalid column text"):
                put_str(game.rows[stack_idx], str_)

            stack_idx += 1
        if len(cards()) > 0:
            # A bit hacky, but normally, this move would require an internal.
            # We don't want to have to add an internal stack to all Black
            # Hole Solver games just for the import.
            s_game.moveMove(1, game.foundations[0], game.rows[0], frames=0)
            s_game.moveMove(len(cards()), game.talon, game.foundations[0],
                            frames=0)
            s_game.moveMove(1, game.rows[0], game.foundations[0], frames=0)

    def calcBoardString(self):
        board = ''
        cards = self.game.s.talon.cards
        if len(cards) > 0:
            board += ' '.join(['Talon:'] +
                              [self.card2str1(x) for x in reversed(cards)])
            board += '\n'
        board += 'Foundations:'
        for f in self.game.s.foundations:
            cards = f.cards
            s = '-'
            if len(cards) > 0:
                s = self.card2str1(cards[-1])
            board += ' ' + s
        board += '\n'

        for s in self.game.s.rows:
            b = ''
            for c in s.cards:
                cs = self.card2str1(c)
                if not c.face_up:
                    cs = '<%s>' % cs
                b += cs + ' '
            board += b.strip() + '\n'

        return board

    def computeHints(self):
        game = self.game
        game_type = self.game_type

        board = self.calcBoardString()
        if DEBUG:
            print('--------------------\n', board, '--------------------')
        if use_bh_solve_lib:
            # global bh_solve_lib_obj
            # bh_solve_lib_obj = bh_solve_lib_obj.new_bhs_user_handle()
            bh_solve_lib_obj.recycle()
            bh_solve_lib_obj.read_board(
                board=board,
                game_type=game_type['preset'],
                place_queens_on_kings=(
                    game_type['queens_on_kings']
                    if ('queens_on_kings' in game_type) else True),
                wrap_ranks=(
                    game_type['wrap_ranks']
                    if ('wrap_ranks' in game_type) else True),
            )
            bh_solve_lib_obj.limit_iterations(self.options['max_iters'])
        else:
            args = []
            args += ['--game', game_type['preset'], '--rank-reach-prune']
            args += ['--max-iters', str(self.options['max_iters'])]
            if 'queens_on_kings' in game_type:
                args += ['--queens-on-kings']
            if 'wrap_ranks' in game_type:
                args += ['--wrap-ranks']

            command = self.BLACK_HOLE_SOLVER_COMMAND + ' ' + ' '.join(args)

        if DEBUG:
            start_time = time.time()

        result = ''

        if use_bh_solve_lib:
            ret_code = bh_solve_lib_obj.resume_solution()
        else:
            pout, perr = self.run_solver(command, board)

            for sbytes in pout:
                s = str(sbytes, encoding='utf-8')
                if DEBUG >= 5:
                    print(s)

                m = re.search('^(Intractable|Unsolved|Solved)!', s.rstrip())
                if m:
                    result = m.group(1)
                    break

        self._setText(iter=0, depth=0, states=0)
        hints = []
        if use_bh_solve_lib:
            self.solver_state = (
                'solved' if ret_code == 0 else
                ('intractable'
                 if bh_solve_lib_obj.ret_code_is_suspend(ret_code)
                 else 'unsolved'))
            self._setText(iter=bh_solve_lib_obj.get_num_times())
            self._setText(
                states=bh_solve_lib_obj.get_num_states_in_collection())
            if self.solver_state == 'solved':
                m = bh_solve_lib_obj.get_next_move()
                while m:
                    found_stack_idx = m.get_column_idx()
                    if len(game.s.rows) > found_stack_idx >= 0:
                        src = game.s.rows[found_stack_idx]

                        hints.append([1, src, None])
                    else:
                        hints.append([1, game.s.talon, None])
                    m = bh_solve_lib_obj.get_next_move()
        else:
            self.solver_state = result.lower()
            for sbytes in pout:
                s = str(sbytes, encoding='utf-8')
                if DEBUG:
                    print(s)

                if s.strip() == 'Deal talon':
                    hints.append([1, game.s.talon, None])
                    continue

                m = re.match(
                    'Total number of states checked is ([0-9]+)\\.', s)
                if m:
                    self._setText(iter=int(m.group(1)))
                    continue

                m = re.match('This scan generated ([0-9]+) states\\.', s)

                if m:
                    self._setText(states=int(m.group(1)))
                    continue

                m = re.match(
                    'Move a card from stack ([0-9]+) to the foundations', s)
                if not m:
                    continue

                found_stack_idx = int(m.group(1))
                src = game.s.rows[found_stack_idx]

                hints.append([1, src, None])
            pout.close()
            perr.close()

        if DEBUG:
            print('time:', time.time()-start_time)

        hints.append(None)
        self.hints = hints


class FreeCellSolverWrapper:

    def __init__(self, **game_type):
        self.game_type = game_type

    def __call__(self, game, dialog):
        hint = FreeCellSolver_Hint(game, dialog, **self.game_type)
        return hint


class BlackHoleSolverWrapper:

    def __init__(self, **game_type):
        self.game_type = game_type

    def __call__(self, game, dialog):
        hint = BlackHoleSolver_Hint(game, dialog, **self.game_type)
        return hint
