from enum import Enum
from random import shuffle
from json import JSONEncoder

class BridzikException(Exception):
    pass

class RuleException(Exception):
    pass

class Card_colors(Enum):
    HEARTS = 'červeň'
    LEAVES = 'zeleň'
    ACORNS = 'žaluď'
    BELLS = 'guľa'

    def __eq__(self, other):
        if self.__class__ is other.__class__:
            return self.name == other.name
        return NotImplemented

    # vlastne __eq__ rusi zdedeny __hash__ -- obnovit konzistentne s __eq__
    def __hash__(self):
        return hash(self.name)


class Card_values(Enum):
    C7 = 1
    C8 = 2
    C9 = 3
    C10 = 4
    LOWER = 5
    UPPER = 6
    KING = 7
    ACE = 8
    
    def __ge__(self, other):
        if self.__class__ is other.__class__:
            return self.value >= other.value
        return NotImplemented
    def __gt__(self, other):
        if self.__class__ is other.__class__:
            return self.value > other.value
        return NotImplemented
    def __le__(self, other):
        if self.__class__ is other.__class__:
            return self.value <= other.value
        return NotImplemented
    def __lt__(self, other):
        if self.__class__ is other.__class__:
            return self.value < other.value
        return NotImplemented
    def __eq__(self, other):
        if self.__class__ is other.__class__:
            return self.name == other.name
        return NotImplemented

    # vlastne __eq__ rusi zdedeny __hash__ -- obnovit konzistentne s __eq__
    def __hash__(self):
        return hash(self.name)


class Card():
    def __init__(self, color: Card_colors, value: Card_values):
        self.color = color
        self.value = value

    def __eq__(self, other):
        if self.__class__ is other.__class__:
            return self.color == other.color \
                and self.value == other.value
        return NotImplemented

    # vlastne __eq__ rusi zdedeny __hash__ -- obnovit konzistentne s __eq__
    def __hash__(self):
        return hash((self.color, self.value))

    def __str__(self):
        return '{}_{}'.format(self.color.name, self.value.name)

    def __repr__(self):
        return '<Card {}_{}>'.format(self.color.name, self.value.name)

    class JSONEncoder(JSONEncoder):
        def default(self, obj):
            if isinstance(obj, Card):
                return {
                    'color': obj.color.name,
                    'value': obj.value.name
                }
            return JSONEncoder.default(self, obj)


cards = [Card(color, value) for value in Card_values for color in Card_colors]


# Sturktura hry: kazda hra ma SERIES_PER_GAME serii, kazda seria ma
# ROUNDS_PER_SERIES kol. Jediny zdroj pravdy pre tieto cisla -- ina vrstva
# (napr. api/history.py) ich odvodzuje odtialto, nikdy si ich nevymysla sama.
SERIES_PER_GAME = 4
ROUNDS_PER_SERIES = 8


class Bridzik():
    def __init__(self, shuffler=shuffle):
        self.shuffler = shuffler
        self.series = [Series(0, 0, shuffler=self.shuffler)]

    def play_card(self, player: int, card: Card):
        if self.is_completed():
            raise BridzikException('Hra je ukoncena.')
        self.series[-1].play_card(player, card)
        if self.series[-1].is_completed() and not self.is_completed():
            self.series.append(Series(len(self.series), shuffler=self.shuffler))

    def add_player_guess(self, player: int, guess):
        if self.is_completed():
            raise BridzikException('Hra je ukoncena.')
        self.series[-1].add_player_guess(player, guess)

    def get_player_cards(self, player: int):
        player_cards = self.series[-1].get_last_round().player_cards[player]
        return {k: v for k, v in enumerate(player_cards)}

    def get_status(self):
        status = {}
        last_series = self.series[-1]
        if not self.is_completed():
            status['active_player'] = last_series.get_last_round().get_active_player()
            status['active_round_guesses'] = last_series.get_last_round().guesses
            if last_series.get_last_round().is_guessing_completed():
                status['active_round_stashes'] = last_series.get_last_round().get_stashes_winner_summary()
                status['active_stash'] = {
                    'first_player': last_series.get_last_round().get_last_stash().first_player,
                    'cards': last_series.get_last_round().get_last_stash().get_cards()
                }
        if self.get_previous_stash():
            status['previous_stash'] = {
                'first_player': self.get_previous_stash().first_player,
                'cards':  self.get_previous_stash().get_cards()
            }
        status['standings'] = [s.get_standings() for s in self.series]
        return status

    def is_completed(self):
        return len(self.series) == SERIES_PER_GAME and self.series[-1].is_completed()

    def get_previous_stash(self):
        if len(self.series[-1].get_last_round().stashes) > 1:
            return self.series[-1].get_last_round().stashes[-2]
        elif len(self.series[-1].rounds) > 1:
            return self.series[-1].rounds[-2].get_last_stash()
        elif len(self.series) > 1:
            return self.series[-2].get_last_round().get_last_stash()
        return None


class Series():
    def __init__(self, series_number: int, first_player: int = None, shuffler = shuffle):
        self.series_number = series_number
        self.first_player = first_player if first_player else series_number
        self.rounds = []
        self.shuffler = shuffler
        self.start_new_round()

    def add_player_guess(self, player: int, guess: int):
        if self.get_last_round().is_completed():
            raise BridzikException('Seria je ukoncena')
        if player not in [0, 1, 2, 3]:
            raise BridzikException('Cislo hraca musi byt 0, 1, 2 alebo 3.')
        if self.get_last_round().is_guessing_completed():
            raise BridzikException('Tipovanie ukoncene')
        if player != self.get_last_round().get_active_player():
            raise BridzikException('Hrac nie je na tahu')
        self.get_last_round().add_player_guess(player, guess)
    
    def play_card(self, player: int, card: Card):
        if self.get_last_round().is_completed():
            raise BridzikException('Seria je ukoncena')
        if player not in [0, 1, 2, 3]:
            raise BridzikException('Cislo hraca musi byt 0, 1, 2 alebo 3.')
        if not self.get_last_round().is_guessing_completed():
            raise BridzikException('Tipovanie nie je ukoncene.')
        if player != self.get_last_round().get_active_player():
            raise BridzikException('Hrac nie je na tahu')
        self.get_last_round().play_card(player, card)
        if self.get_last_round().is_completed() and not self.is_completed():
            self.start_new_round()

    def is_completed(self):
        return len(self.rounds) == ROUNDS_PER_SERIES and self.get_last_round().is_completed()

    def get_standings(self):
        return [r.get_points_summary() for r in self.rounds if r.is_completed()]

    def get_last_round(self):
        return self.rounds[-1] if self.rounds else None

    def start_new_round(self):
        if self.is_completed():
            raise BridzikException('Seria je ukoncena.')
        round_number = len(self.rounds)
        if round_number != 0 and not self.get_last_round().is_completed():
            raise BridzikException('Predchadzajuce kolo nie je ukoncene')
        self.rounds.append(
            Round(round_number, (self.first_player + round_number) % 4, shuffler=self.shuffler)
        )


class Round():
    def __init__(self, round_number: int, first_player: int, cards: []=cards, shuffler=shuffle):
        # vyrob kopku pre toto kolo a priprav prazdne objekty
        if round_number not in range(ROUNDS_PER_SERIES):
            raise BridzikException('Neplatne cislo kola.')
        if first_player not in [0, 1, 2, 3]:
            raise BridzikException('Cislo hraca musi byt 0, 1, 2 alebo 3.')
        self.round_number = round_number
        self.first_player = first_player
        self.deal_starting_cards(cards, shuffler)
        self.guesses = {}
        self.stashes = []

    def add_player_guess(self, player: int, guess: int):
        if player not in [0, 1, 2, 3]:
            raise BridzikException('Cislo hraca musi byt 0, 1, 2 alebo 3.')
        if player in self.guesses:
            raise BridzikException('Pre tohto hraca uz bol zadany tip.')
        if player != self.get_active_player():
            raise BridzikException('Nie je mozne pridat tip mimo poradia.')
        if guess < 0 or guess > 8 - self.round_number:
            raise BridzikException('Tip musi byt vacsi ako 0 a nie vacsia ako pocet kariet.')
        if len(self.guesses) < 3:
            self.guesses[player] = guess
        elif 8 - self.round_number != guess + sum([self.guesses[player] for player in self.guesses]):
            self.guesses[player] = guess
            self.start_new_stash()
        else:
            raise BridzikException('Nie je mozne zadat tip. Sucet sa rovna poctu kopok v hre.')

    def play_card(self, player, card: Card):
        if self.is_completed():
            raise BridzikException('Kolo je ukoncene.')
        if not self.is_guessing_completed():
            raise BridzikException('Tipovanie nie je ukoncene.')
        if self.get_last_stash().get_active_player() != player:
            raise BridzikException('Hrac nie je na tahu.')
        if card not in self.player_cards[player]:
            raise BridzikException('Hrac nema tuto kartu na ruke.')
        if self.get_last_stash().get_first_card():
            if [c for c in self.player_cards[player] if c.color == \
                            self.get_last_stash().get_first_card().color] == []: # nema farbu
                if [c for c in self.player_cards[player] if c.color == \
                                                Card_colors['HEARTS']] == []: # nema cerven
                    pass
                elif card.color != Card_colors['HEARTS']: # ma cerven ale nezahral
                    raise BridzikException('Musi zahrat cerven, ktory ma.')
            elif card.color != self.get_last_stash().get_first_card().color: # ma farbu ale nezahral
                raise BridzikException('Musi zahrat farbu.')            

        self.player_cards[player] = [c for c in self.player_cards[player] if c != card]
        self.get_last_stash().add_card(player, card)
        if self.get_last_stash().is_completed() and not self.is_completed():
            self.start_new_stash()

    def get_highest_guessing_player(self) -> int:
        if not self.is_guessing_completed():
            raise BridzikException('Tipovanie nebolo skoncene.')
        round_highest_guessing_player = self.first_player
        for i in range(1, 4):
            if self.guesses[round_highest_guessing_player] < self.guesses[(i+self.first_player) % 4]:
                round_highest_guessing_player = (i+self.first_player) % 4
        return round_highest_guessing_player

    def get_active_player(self):
        if not self.is_completed():
            if not self.is_guessing_completed():
                return (self.first_player + len(self.guesses)) % 4
            return self.get_last_stash().get_active_player()
        raise BridzikException('Kolo je uzavrete')

    def is_completed(self):
        """Round completion status"""
        return len(self.stashes) == 8 - self.round_number \
                and self.get_last_stash().is_completed()
    
    def is_guessing_completed(self):
        return len(self.guesses) == 4

    def get_points_summary(self) -> []:
        points_summary = [0]*4
        stashes_summary = self.get_stashes_winner_summary()
        if self.is_completed():
            for player in range(4):
                if self.guesses[player] == stashes_summary[player]:
                    points_summary[player] = 10 + self.guesses[player]
            return points_summary
        else:
            raise BridzikException('Kolo nie je ukoncene')

    def get_stashes_winner_summary(self) -> []:
        summary = [0]*4
        for stash in self.stashes:
            if stash.is_completed():
                summary[stash.get_winner()] += 1
        return summary

    def get_last_stash(self):
        return self.stashes[-1] if self.stashes else None

    def deal_starting_cards(self, cards: [], shuffler=shuffle):
        self.round_cards = cards.copy()
        shuffler(self.round_cards)
        self.round_cards = self.round_cards[(4*self.round_number):]
        self.player_cards = {}
        for player_number in range(4):
            self.player_cards[player_number] = self.round_cards[
                player_number * (8-self.round_number) : 
                (player_number + 1) * (8-self.round_number)
                ]
        self.player_starting_cards = self.player_cards.copy()

    def start_new_stash(self):
        if self.is_completed():
            raise BridzikException('Kolo je ukoncene.')
        if len(self.stashes) == 0:
            self.stashes.append(Stash(self.get_highest_guessing_player()))
        elif not self.get_last_stash().is_completed():
            raise BridzikException('Predchadzajuca kopka nie je ukoncena.')
        else:
            self.stashes.append(Stash(self.get_last_stash().get_winner()))


class Stash:
    def __init__(self, first_player: int):
        if first_player not in [0, 1, 2, 3]:
            raise BridzikException('Cislo hraca musi byt 0, 1, 2 alebo 3.')
        self._cards = {}
        self.first_player = first_player
    
    def add_card(self, player: int, card: Card):
        if player not in [0, 1, 2, 3]:
            raise BridzikException('Cislo hraca musi byt 0, 1, 2 alebo 3.')
        if player in self._cards:
            raise BridzikException('Pre tohto hraca uz bola pridana karta')
        self._cards[player] = card

    def get_first_card(self) -> Card:
        """Return copy of first card in stash"""
        return Card(
            self._cards[self.first_player].color,
            self._cards[self.first_player].value
        ) if len(self._cards) > 0 else None

    def get_winner(self) -> int:
        if not self.is_completed():
            raise BridzikException('Nie je mozne urcit vitaza, kopka nie je dohrana.')
        winner = self.first_player
        winning_card = self.get_first_card()
        for player_number in range(4):
            if self._cards[player_number].color == self.get_first_card().color \
                    or self._cards[player_number].color == Card_colors['HEARTS']:
                if self._cards[player_number].color == winning_card.color:
                    if self._cards[player_number].value >= winning_card.value:
                        winning_card = self._cards[player_number]
                        winner = player_number
                elif self._cards[player_number].color == Card_colors['HEARTS']:
                        winning_card = self._cards[player_number]
                        winner = player_number
        return winner

    def get_active_player(self) -> int:
        if not self.is_completed():
            return (self.first_player + len(self._cards)) % 4
        else:
            raise BridzikException('Kopka je ukoncena.')

    def get_cards(self) -> {}:
        return self._cards.copy()

    def is_completed(self) -> bool:
        return len(self._cards) == 4
