# -*- coding: utf-8 -*-
# @Time    : 2019/9/5 13:42
# @Author  : duofeng Wu
# @Site    : 
# @File    : card_type.py
# @Software: PyCharm

from collections import defaultdict, Counter
from itertools import combinations, product
from constants import JOKER
from utils import Rank, Card, Cards


def get_star_num(card_list, star):
    """计算card_list中的级牌数量

    Args:
        card_list: 卡牌列表
        star: 级牌

    Return:
        级牌在卡牌列表中出现的张数
    """
    num = 0
    for card in card_list:
        if card == star:
            num += 1
    return num


class Single(object):

    """单张

    通过调用extract静态方法，提取卡牌列表中的所有可选单张

    Staticmethod:
    - extract: 提取卡牌列表中的所有可选单张
    """

    @staticmethod
    def extract(card_list, star, include=False, num=1, list_form=True):
        """提取给定卡牌列表中的所有可选单张

        Args:
            card_list: 给定的卡牌列表
            star: 变牌
            include: 每个点数的列表中是否包含级牌
            num: 给定卡牌列表中级牌的数量
            list_form: 是否将每张牌都单独形成一个列表，用于后续牌型的配对

        Return:
            字典，以卡牌列表中出现的点数为键，对应的值为所有相同点数的卡牌列表
        """

        single = defaultdict(list)
        for card in card_list:
            if list_form:
                single[card.rank].append([card, ])
            else:
                single[card.rank].append(card)
        if include:
            for rank in ('A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'):
                # 级牌点数的列表不可再重复添加级牌
                if rank == star.rank:
                    continue
                for _ in range(num):
                    single[rank].append(star)
        return single


class Pair(object):

    """对子

    通过调用extract静态方法，提取卡牌列表中的所有可选的对子

    Staticmethod:
    - extract: 提取卡牌列表中的所有可选对子
    """

    @staticmethod
    def extract(card_list, star):
        """提取给定卡牌列中中的所有可选对子

        Args:
            card_list: 卡牌列表
            star: 级牌

        Thoughts:
        - 不考虑级牌情况下，将所有去重后的单张两两组合为一种对子组合（大小王要单独判断），重复的单张与自身组合为一种对子
        - 考虑级牌情况下，对去重过后的单张进行级牌配对（跳过级牌和大小王）
        """

        pair = defaultdict(list)
        single = Single.extract(card_list, star, list_form=False)
        for rank, cards in single.items():
            if len(cards) < 2 or (rank == JOKER and len(cards) == 2 and len(set(cards)) == 2):
                continue
            elif not rank == JOKER:
                for card in combinations(set(cards), 2):
                    pair[rank].append(list(card))
            for card, count in Counter(cards).items():
                if count > 1:
                    pair[rank].append([card, card])
        if star in card_list:
            for rank, cards in single.items():
                if rank == JOKER or rank == star.rank:
                    continue
                for card in set(cards):
                    pair[rank].append([star, card])
        return pair


class Trips(object):

    """"三张

    通过调用extract静态方法，提取卡牌列表中的所有可选的三张

    Staticmethod:
    - extract: 提取卡牌列表中的所有可选三张
    """

    @staticmethod
    def extract(card_list, star):
        """提取给定卡牌列中中的所有可选三张

        Args:
            card_list: 卡牌列表
            star: 级牌

        Thoughts:
        - 不考虑级牌情况下，遍历所有单张，每三张组合为一种三张, 为防止重复组合，需要先对组合过后的三张排序，再添加
        - 考虑级牌情况下：
            - 1张级牌： 与对子配对为一种三张（去除大小王，跳过有级牌的对子）
            - 2张级牌：与单张配对为一种三张（去除级牌）
        """

        trips = defaultdict(list)
        single = Single.extract(card_list, star, list_form=False)

        for rank, cards in single.items():
            for _trips in combinations(cards, 3):
                order_triple = sorted(list(_trips), key=lambda x: x.suit)
                if order_triple not in trips[rank]:
                    trips[rank].append(order_triple)
        if JOKER in trips:
            del trips[JOKER]

        num = get_star_num(card_list, star)
        if num:
            pair = Pair.extract(card_list, star)
            for rank, cards in pair.items():
                if rank == JOKER:
                    continue
                for card in cards:
                    # 与手中级牌的数量无关，除级牌点数外，对子最多只有一张级牌
                    if star in card:
                        continue
                    trips[rank].append([star, *card])
        if num == 2:
            single = Single.extract(card_list, star, list_form=False)
            for rank, cards in single.items():
                if rank == JOKER:
                    continue
                for card in set(cards):
                    if card == star:
                        continue
                    trips[rank].append([star, star, card])
        return trips


class ThreePair(object):

    """三连对

    通过调用extract静态方法，提取卡牌列表中的所有可选的三连对

    Staticmethod:
    - extract: 提取卡牌列表中的所有可选三连对
    """

    @staticmethod
    def extract(card_list, star):
        """提取给定卡牌列中中的所有可选三张

        Args:
            card_list: 卡牌列表
            star: 级牌

        Thoughts:
        - 从正确的对子配对中每三个组合为一个三连对，避免重合要进行排序筛选，最后再剔除其中不符合级牌数量的三连对
        - 针对有2张级牌的情况：分别与XXBBCC（缺对子）, AXXBCC（缺单张）配对
        """

        three_pair = defaultdict(list)
        pair = Pair.extract(card_list, star)
        num = get_star_num(card_list, star)

        # 全部组合。从形成三连对的对子中任意选取形成三连对
        # (i, j, k)表示三连对中的三个点数
        ranks = ('A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A')
        for rank_i, rank_j, rank_k in ((ranks[h], ranks[h + 1], ranks[h + 2]) for h in range(12)):
            if rank_i in pair and rank_j in pair and rank_k in pair:
                _filter = []
                for pair_i, pair_j, pair_k in product(pair[rank_i],  pair[rank_j], pair[rank_k]):
                    _three_pair = pair_i + pair_j + pair_k
                    order_tp = sorted(_three_pair, key=lambda x: x.suit)
                    if order_tp not in _filter:
                        _filter.append(order_tp)
                        three_pair[rank_i].append(_three_pair)

        # 删除。删除其中中不符合条件的三连对： 每个三连对含有的级牌不能超过拥有的级牌数量
        # 循环时改动列表的最好方法之一为逆序遍历列表
        for rank, tp_list in three_pair.items():
            for h in range(len(tp_list) - 1, -1, -1):
                if get_star_num(tp_list[h], star) > num:
                    tp_list.remove(tp_list[h])

        # 清除。将空三连对点数删除
        three_pair = defaultdict(list, {rank: tp_list for rank, tp_list in three_pair.items() if tp_list})

        # 上述情况中已经默认包含了有1种级牌的情况，故这里只需对2张级牌进行额外判定
        # 2张级牌时有特殊情况，需要额外判定
        if num == 2:
            for rank_i, rank_j, rank_k in ((ranks[h], ranks[h + 1], ranks[h + 2]) for h in range(12)):

                # i, j, k 没有j的情况
                if rank_i in pair and rank_j not in pair and rank_k in pair:
                    for _pair_i, _pair_k in product(pair[rank_i],  pair[rank_k]):
                        _tp = [*_pair_i, star, star, *_pair_k]
                        if get_star_num(_tp, star) > num:
                            continue
                        three_pair[rank_i].append(_tp)

                # 两张级牌与i, j形成新的三连对。与(i, j, k没有i)、(i, j, k没有k)的情况重合，即只有2对的情况
                if pair[rank_i] and pair[rank_j]:
                    for _pair_i, _pair_j in product(pair[rank_i], pair[rank_j]):
                        if star not in _pair_i and star not in _pair_j:
                            # 存在两连对情况时，AABB配上级牌XX可视做点数不一样的三连对，故需要对其进行定位
                            index = ranks.index(rank_i)
                            _tp = [*_pair_i, *_pair_j, star, star]
                            three_pair[rank_i].append(_tp)
                            if index - 1 > 0:
                                _tp = [star, star, *_pair_i, *_pair_j]
                                three_pair[ranks[index - 1]].append(_tp)

            # 上述for循环遍历的最终结尾为QQKKAA，需要对KKAA情况单独进行判断
            if pair['K'] and pair['A']:
                for _pair_i, _pair_j in product(pair['K'], pair['A']):
                    if star not in _pair_i and star not in _pair_j:
                        _tp = [*_pair_i, *_pair_j, star, star]
                        three_pair['Q'].append(_tp)
        return three_pair


class ThreeWithTwo(object):

    """三连二

    通过调用extract静态方法，提取卡牌列表中的所有可选的三连二

    Staticmethod:
    - extract: 提取卡牌列表中的所有可选三连二
    """

    @staticmethod
    def extract(card_list, star):
        """提取给定卡牌列中中的所有可选三带二

        Args:
            card_list: 卡牌列表
            star: 级牌

        Thoughts:
        - 从正确的对子和三张中进新组合，并对其进行级牌数量的筛选即可
        """

        three_with_two = defaultdict(list)
        trips = Trips.extract(card_list, star)
        pair = Pair.extract(card_list, star)
        num = get_star_num(card_list, star)
        for rank_i, _trips in trips.items():
            for rank_j, _pair in pair.items():
                if rank_i != rank_j:
                    _filter = []
                    for three_i, two_j in product(_trips, _pair):
                        _three_with_two = three_i + two_j
                        if get_star_num(_three_with_two, star) > num:
                            continue
                        _order_tp = sorted(_three_with_two, key=lambda x: x.suit)
                        if _order_tp not in _filter:
                            _filter.append(_order_tp)
                            three_with_two[rank_i].append(_three_with_two)
        return three_with_two


class TripsPair(object):

    """钢板

    通过调用extract静态方法，提取卡牌列表中的所有可选的钢板

    Staticmethod:
    - extract: 提取卡牌列表中的所有可选钢板
    """

    @staticmethod
    def extract(card_list, star):
        """提取给定卡牌列中中的所有可选三带二

        Args:
            card_list: 卡牌列表
            star: 级牌

        Thoughts:
        - 从正确三张中进行组合，并对其进行级牌数量的筛选即可
        """

        trips_pair = defaultdict(list)
        trips = Trips.extract(card_list, star)
        num = get_star_num(card_list, star)
        ranks = ('A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A')
        for rank_i, rank_j in ((ranks[h], ranks[h + 1]) for h in range(13)):
            if rank_i in trips and rank_j in trips:
                _filter = []
                for trips_i, trips_j in product(trips[rank_i], trips[rank_j]):
                    _trips_pair = trips_i + trips_j
                    # 对组合的牌型进行筛选，如果其中级牌数量超过拥有的级牌数量，则跳过
                    if get_star_num(_trips_pair, star) > num:
                        continue
                    _order_tp = sorted(_trips_pair, key=lambda x: x.suit)
                    if _order_tp not in _filter:
                        _filter.append(_order_tp)
                        trips_pair[rank_i].append(_trips_pair)
        return trips_pair


class Bomb(object):
    """炸弹

    通过调用extract静态方法，提取卡牌列表中的所有可选炸弹

    Staticmethod:
    - extract: 提取卡牌列表中的所有可选炸弹
    """

    @staticmethod
    def extract(card_list, star):
        """提取给定卡牌列表中的所有可选单张

        Args:
            card_list: 给定的卡牌列表
            star: 级牌

        Thoughts:
        - 从包含级牌的单张列表开始，每4个开始组合，直到上限
        - 因为大小王的rank统一为JOKER，所以无需单独判断，拥有四张JOKER时自动会被提取为炸弹
        """

        # 更简便的方法 为单张中的每个点数添加对应的数量的级牌
        bomb = defaultdict(list)
        num = get_star_num(card_list, star)
        # include参数： 为每个单张列表添加级牌
        single = Single.extract(card_list, star, include=True, num=num, list_form=False)
        for rank, cards in single.items():
            for h in range(4, len(cards) + 1):
                _filter = []
                for _bomb in combinations(cards, h):
                    _order_bomb = sorted(_bomb, key=lambda x: x.suit)
                    if _order_bomb not in _filter:
                        _filter.append(_order_bomb)
                        bomb[rank].append(list(_bomb))
        return bomb


class Straight(object):
    """顺子

    通过调用extract静态方法，提取卡牌列表中的所有可选顺子

    Staticmethod:
    - extract: 提取卡牌列表中的所有可选顺子
    """

    @staticmethod
    def extract(card_list, star):
        """提取给定卡牌列表中的所有可选顺子

        Args:
            card_list: 给定的卡牌列表
            star: 级牌

        Thoughts:
        - 从包含级牌的单张列表开始，从A开始逐个后移至10，再对获得的顺子进行筛选
        - for循环会自动跳过空列表
        """

        straight = defaultdict(list)
        num = get_star_num(card_list, star)
        single = Single.extract(card_list, star, include=True, num=num, list_form=False)
        ranks = ('A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A')
        for m, n, k, p, q in ((ranks[h], ranks[h + 1], ranks[h + 2], ranks[h + 3], ranks[h + 4]) for h in range(10)):
            _filter = []
            for _straight in product(single[m], single[n], single[k], single[p], single[q]):
                if get_star_num(_straight, star) > num:
                    continue
                _order_straight = sorted(_straight, key=lambda x: x.suit)
                if _order_straight not in _filter:
                    _filter.append(_order_straight)
                    straight[m].append(list(_straight))
        return straight


class StraightFlush(object):
    """同花顺

    通过调用extract静态方法，提取卡牌列表中的所有可选同花顺

    Staticmethod:
    - extract: 提取卡牌列表中的所有可选同花顺
    """

    @staticmethod
    def extract(card_list, star):
        """提取给定卡牌列表中的所有可选顺子

        Args:
            card_list: 给定的卡牌列表
            star: 级牌

        Thoughts:
        - 从正确的顺子开始，使用集合去重，判断花色是否相等，即为同花顺
        - 有1张级牌，针对1张级牌的顺子进行花色判定
        - 有2张级牌，包含上述情况（故需要两个单独的if），针对2张级牌的顺子进行判定
        """

        straight_flush = defaultdict(list)
        num = get_star_num(card_list, star)
        single = Single.extract(card_list, star, include=True, num=num, list_form=False)
        ranks = ('A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A')
        for m, n, k, p, q in ((ranks[h], ranks[h + 1], ranks[h + 2], ranks[h + 3], ranks[h + 4]) for h in range(10)):
            _filter = []
            for flush in product(single[m], single[n], single[k], single[p], single[q]):
                if star not in flush and flush not in _filter and len(set([flush[j].suit for j in range(5)])) == 1:
                    straight_flush[m].append(list(flush))
                    _filter.append(flush)
                if num and get_star_num(flush, star) == 1:
                    index = flush.index(star)
                    if flush not in _filter and len(set([flush[(index + j) % 5].suit for j in range(1, 5)])) == 1:
                        straight_flush[m].append(list(flush))
                        _filter.append(flush)
                if num == get_star_num(flush, star) == 2:
                    rest = [card.suit for card in flush if card != star]
                    if flush not in _filter and len(set(rest)) == 1:
                        straight_flush[m].append(list(flush))
                        _filter.append(flush)
        return straight_flush


class Pass(object):

    @staticmethod
    def extract(card_list, star):
        return {'PASS': [[0, 'PASS']]}


extractor = (Single, Pair, Trips, TripsPair, ThreePair, ThreeWithTwo, Straight, StraightFlush, Bomb)
extractor_map = {
    "Single": Single,
    "Pair": Pair,
    "Trips": Trips,
    "TripsPair": TripsPair,
    "ThreePair": ThreePair,
    "ThreeWithTwo": ThreeWithTwo,
    "Straight": Straight,
    "StraightFlush": StraightFlush,
    "Bomb": Bomb,
    "PASS": Pass
}

type_weight = {
    "Single": 1,
    "Pair": 2,
    "Trips": 3,
    "TripsPair": 6,
    "ThreePair": 6,
    "ThreeWithTwo": 5,
    "Straight": 5,
    "StraightFlush": 5,
    "Bomb": -1
}


class CardType(object):

    def __init__(self, card_type, rank, cards):
        self.card_type = card_type
        self.rank = rank
        self.cards = cards

    @property
    def length(self):
        return len(self.cards)

    def __repr__(self):
        return tuple(self.cards)


def remove(cards, card_list):
    if len(cards) == 1:
        return None, 1
    else:
        for card in card_list:
            cards.remove(card)
        return cards, 1


def main():
    foo = Cards()
    _rank = Rank()
    _rank.update('4')
    test_cards = foo.deal(_rank.order_func)
    print(test_cards)

    card_types = []
    for type_name, type_extractor in extractor_map.items():
        for rank, card_list in type_extractor.extract(test_cards, Card(1, '4')).items():
            for cards in card_list:
                card_types.append(CardType(type_name, rank, cards))

    divisions = []
    for card_type in card_types:
        cards, hand_num = remove(test_cards, card_type.cards)






    # test = Single.extract(test_cards, Card(1, '4'))
    # test = Pair.extract(test_cards, Card(1, '4'))
    # test = Trips.extract(test_cards, Card(1, '4'))
    # test = ThreePair.extract(test_cards, Card(1, 'A'))
    # test = ThreeWithTwo.extract(test_cards, Card(1, '4'))
    # test = TripsPair.extract(test_cards, Card(1, 'A'))
    # test = Bomb.extract(test_cards, Card(1, '4'))
    # test = StraightFlush.extract(test_cards, Card(1, '4'))
    # test = Straight.extract(test_cards, Card(1, 'A'))
    # print(test_cards)
    # print(test)
    # for i in test:
    #     print("{}: {}, {}".format(i, test[i], len(test[i])))


if __name__ == "__main__":
    main()
