from collections import defaultdict as dd
import itertools
import progressbar
import random


def ResistanceGame(n_players):
    full_set = [("G", True), ("G", True), ("G", True),
                ("E", False), ("E", False), ("G", True),
                ("E", False), ("G", True), ("G", True), ("E", False)]
    return full_set[:n_players]


def AvalonGame(n_players):
    full_set = [("Merlin", True), ("Percival", True), ("G", True),
                ("Morgana", False), ("ELance", False), ("GLance", True),
                ("Mordred", False), ("G", True), ("G", True),
                ("Oberon", False)]
    return full_set[:n_players]


class DeceptionGame(object):
    def __init__(self, player_array, model_class):
        self.player_array = player_array
        self.model = model_class(self)
        self.all_permutations = list(set(itertools.permutations(player_array)))
        self.quick_permutations = list(set(itertools.permutations(
            [("", p[1]) for p in player_array])))
        self.n_players = len(player_array)
        self.n_good = len([x for x in player_array if x[1] is True])
        self.trace = None
        self.trace_score = 0.0
        self.observations = []
        self.seen = []
        self.tid = 0
        self.lancelots_switch_at = []

    def get_fail_req(self, rnd):
        if rnd is not 4:
            return 1
        if self.n_players >= 7:
            return 2
        return 1

    def player_is_good(self, deal, player, round):
        if round is None:
            return deal[player][1]
        if self.player_role(deal, player) is not "GLance" and \
           self.player_role(deal, player) is not "ELance":
            return deal[player][1]
        if len(self.lancelots_switch_at) == 0:
            return deal[player][1]
        if len(self.lancelots_switch_at) == 1:
            if round >= self.lancelots_switch_at[0]:
                return not deal[player][1]
            else:
                return deal[player][1]
        if len(self.lancelots_switch_at) == 2:
            if round >= self.lancelots_switch_at[0] and \
               round < self.lancelots_switch_at[1]:
                return not deal[player][1]
            else:
                return deal[player][1]
        return deal[player][1]

    def player_role(self, deal, player):
        return deal[player][0]

    def add_known_alliance(self, player_id, is_good):
        transaction = []

        def obs(deal):
            if self.player_is_good(deal, player_id, None) == is_good:
                return 1.0
            else:
                return None
        transaction.append(obs)
        self.observations.append(transaction)
        self.seen.append({"type": "known_side",
                          "player": player_id,
                          "is good": is_good,
                          "print_order": ["player",
                                          "is good"]})
        self.tid += 1

    def switch_lancelots(self, round):
        transaction = []
        rnd = round - 1

        def obs(deal):
            return 1.0

        self.lancelots_switch_at.append(rnd)

        transaction.append(obs)
        self.observations.append(transaction)
        self.seen.append({"type": "switch",
                          "round": rnd,
                          "print_order": ["round"]})
        self.tid += 1

    def add_known_role(self, player_id, role_str):
        transaction = []

        def obs(deal):
            if self.player_role(deal, player_id) == role_str:
                return 1.0
            else:
                return None
        transaction.append(obs)
        self.observations.append(transaction)
        self.seen.append({"type": "known_role",
                          "player": player_id,
                          "role": role_str,
                          "print_order": ["player",
                                          "role"]})
        self.tid += 1

    def player_sees_player_and_claims(self, p1, p2, claim, round):
        transaction = []
        rnd = round - 1

        def obs(deal):
            self.model.set_deal(deal)
            return self.model.player_sees_player_and_claims(p1, p2, claim, rnd)
        transaction.append(obs)
        self.observations.append(transaction)
        self.seen.append({"type": "lady",
                          "p1": p1,
                          "p2": p2,
                          "is good": claim,
                          "round": round,
                          "print_order": ["p1",
                                          "p2",
                                          "round",
                                          "is good"]})
        self.tid += 1

    def do_mission(self, team, fails, must_fail, r):
        transaction = []
        rnd = r - 1

        def obs(deal):
            self.model.set_deal(deal)
            return self.model.mission(team, fails, must_fail, rnd)

        transaction.append(obs)
        self.observations.append(transaction)
        self.seen.append({"type": "mission",
                          "team": team,
                          "fails": fails,
                          "round": r,
                          "must fail": must_fail,
                          "print_order": ["team",
                                          "fails",
                                          "must fail",
                                          "round"]})
        self.tid += 1

    def do_vote(self, team, votes, fail_req, r, v, proposer):
        transaction = []
        rnd = r - 1
        voten = v - 1

        def obs(deal):
            self.model.set_deal(deal)
            return self.model.votes(team, votes, fail_req, rnd, voten, proposer)

        transaction.append(obs)
        self.observations.append(transaction)
        self.seen.append({"type": "vote",
                          "team": team,
                          "votes": votes,
                          "round": r,
                          "voten": v,
                          "proposer": proposer,
                          "fails required": fail_req,
                          "print_order": ["team", "votes", "round"]})
        self.tid += 1

    def load_save(self, input_list):
        for statement in input_list:
            type = statement["type"]
            if type == "vote":
                self.do_vote(statement["team"],
                             statement["votes"],
                             statement["fails required"],
                             statement["round"],
                             statement["voten"],
                             statement["proposer"])
            if type == "switch":
                self.switch_lancelots(statement["round"])
            elif type == "mission":
                self.do_mission(statement["team"],
                                statement["fails"],
                                statement["must fail"],
                                statement["round"])
            elif type == "lady":
                self.player_sees_player_and_claims(statement["p1"],
                                                   statement["p2"],
                                                   statement["is good"],
                                                   statement.get("round", -1))
            elif type == "known_side":
                self.add_known_alliance(statement["player"],
                                        statement["is good"])
            elif type == "known_role":
                self.add_known_role(statement["player"],
                                    statement["role"])

    def eval(self, length=10, quick=False):
        random.seed()
        if quick:
            deck = self.quick_permutations[:]
        else:
            deck = self.all_permutations[:]
        trace = {}
        progress = progressbar.ProgressBar(
            widgets=["Simulating games: ",
                     progressbar.Bar(marker="*"),
                     " ", progressbar.ETA()])
        f_list = []
        for obs in self.observations:
            for tid in obs:
                f_list.append(tid)
        total_score = 0.0
        for deal in progress(deck):
            this_score = 1.0
            for f in f_list:
                score = f(deal)
                if score is None:
                    this_score = 0.0
                    break
                this_score = this_score * score
            total_score += this_score
            trace[deal] = this_score

        self.trace = trace
        self.trace_score = total_score

    def report(self):
        return self.get_player_data()

    def get_player_data(self):
        out = []
        for i in range(self.n_players):
            out.append({})
            out[i]["role"] = dd(float)
            out[i]["side"] = dd(float)
        top_k = [(0.0, None)] * 5
        for deal, score in self.trace.items():
            weighted_score = score / self.trace_score
            for i, card in enumerate(deal):
                role, side = card
                out[i]["role"][role] += weighted_score
                out[i]["side"][side] += weighted_score
            if weighted_score > top_k[-1][0]:
                top_k.append((weighted_score, deal))
                top_k = sorted(top_k, reverse=True)[:-1]

        for i in range(self.n_players):
            out[i]["role"] = dict(out[i]["role"])
            out[i]["side"] = dict(out[i]["side"])

        return out, top_k

    def __str__(self):
        return "%d Player Game (%d constraints)" % (self.n_players,
                                                    len(self.seen))

    def disbelieve(self, i):
        self.observations = self.observations[:i] + self.observations[i + 1:]
        self.seen = self.seen[:i] + self.seen[i + 1:]
        self.trace = {}
