import numpy as np

from axelrod.action import Action

from axelrod.classifier import Classifiers

from axelrod.player import Player

from axelrod.strategies import TitForTat

from axelrod.strategy_transformers import NiceTransformer

from ._strategies import all_strategies

from .hunter import (
    AlternatorHunter,
    CooperatorHunter,
    CycleHunter,
    DefectorHunter,
    EventualCycleHunter,
    MathConstantHunter,
    RandomHunter,
)

ordinary_strategies = [
    s for s in all_strategies if Classifiers.obey_axelrod(s())
]

C, D = Action.C, Action.D

NiceMetaWinner = NiceTransformer()(MetaWinner)

NiceMetaWinnerEnsemble = NiceTransformer()(MetaWinnerEnsemble)

class MetaPlayer(Player):
    """
    A generic player that has its own team of players.

    Names:

    - Meta Player: Original name by Karol Langner
    """

    name = "Meta Player"
    classifier = {
        "memory_depth": float("inf"),  # Long memory
        "stochastic": True,
        "long_run_time": True,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self, team=None):
        # The default is to use all strategies available, but we need to import
        # the list at runtime, since _strategies import also _this_ module
        # before defining the list.
        if team:
            self.team = team
        else:
            self.team = ordinary_strategies
        # Make sure we don't use any meta players to avoid infinite recursion.
        self.team = [t for t in self.team if not issubclass(t, MetaPlayer)]
        # Initiate all the players in our team.
        self.team = [t() for t in self.team]
        self._last_results = None
        super().__init__()

    def _post_init(self):
        # The player's classification characteristics are derived from the team.
        # Note that memory_depth is not simply the max memory_depth of the team.
        for key in [
            "stochastic",
            "inspects_source",
            "manipulates_source",
            "manipulates_state",
        ]:
            self.classifier[key] = any(map(Classifiers[key], self.team))

        self.classifier["makes_use_of"] = set()
        for t in self.team:
            new_uses = Classifiers["makes_use_of"](t)
            if new_uses:
                self.classifier["makes_use_of"].update(new_uses)

    def set_seed(self, seed=None):
        super().set_seed(seed=seed)
        # Seed the team as well
        for t in self.team:
            t.set_seed(self._random.random_seed_int())

    def receive_match_attributes(self):
        for t in self.team:
            t.set_match_attributes(**self.match_attributes)

    def __repr__(self):
        team_size = len(self.team)
        return "{}: {} player{}".format(
            self.name, team_size, "s" if team_size > 1 else ""
        )

    def update_histories(self, coplay):
        # Update team histories.
        try:
            for player, play in zip(self.team, self._last_results):
                player.update_history(play, coplay)
        except TypeError:
            # If the Meta class is decorated by the Joss-Ann transformer,
            # such that the decorated class is now deterministic, the underlying
            # strategy isn't called. In that case, updating the history of all the
            # team members doesn't matter.
            # As a sanity check, look for at least one reclassifier, otherwise
            # this try-except clause could hide a bug.
            if len(self._reclassifiers) == 0:
                raise TypeError(
                    "MetaClass update_histories issue, expected a reclassifier."
                )
            # Otherwise just update with C always, so at least the histories have the
            # expected length.
            for player in self.team:
                player.update_history(C, coplay)

    def update_history(self, play, coplay):
        super().update_history(play, coplay)
        self.update_histories(coplay)

    def strategy(self, opponent):
        """Actual strategy definition that determines player's action."""
        # Get the results of all our players.
        results = []
        for player in self.team:
            play = player.strategy(opponent)
            results.append(play)
        self._last_results = results
        # A subclass should just define a way to choose the result based on
        # team results.
        return self.meta_strategy(results, opponent)

    def meta_strategy(self, results, opponent):
        """Determine the meta result based on results of all players.
        Override this function in child classes."""
        return C

class MetaWinner(MetaPlayer):
    """A player who goes by the strategy of the current winner.

    Names:

    - Meta Winner: Original name by Karol Langner
    """

    name = "Meta Winner"

    def __init__(self, team=None):
        super().__init__(team=team)
        # For each player, we will keep the history of proposed moves and
        # a running score since the beginning of the game.
        self.scores = np.zeros(len(self.team))
        self.classifier["long_run_time"] = True

    def _update_scores(self, coplay):
        # Update the running score for each player, before determining the
        # next move.
        game = self.match_attributes["game"]
        scores = []
        for player in self.team:
            last_round = (player.history[-1], coplay)
            s = game.scores[last_round][0]
            scores.append(s)
        self.scores += np.array(scores)

    def update_histories(self, coplay):
        super().update_histories(coplay)
        self._update_scores(coplay)

    def meta_strategy(self, results, opponent):
        # Choose an action based on the collection of scores
        bestscore = max(self.scores)
        beststrategies = [
            i for (i, score) in enumerate(self.scores) if score == bestscore
        ]
        bestproposals = [results[i] for i in beststrategies]
        bestresult = C if C in bestproposals else D
        return bestresult

class MetaWinnerEnsemble(MetaWinner):
    """A variant of MetaWinner that chooses one of the top scoring strategies
    at random against each opponent. Note this strategy is always stochastic
    regardless of the team, if team larger than 1, and the players are distinct.

    Names:

    - Meta Winner Ensemble: Original name by Marc Harper
    """

    name = "Meta Winner Ensemble"

    def _post_init(self):
        super()._post_init()
        team = list(t.__class__ for t in self.team)
        if len(team) > 1:
            self.classifier["stochastic"] = True
            self.singular = False
        else:
            self.singular = True
        # If the team has repeated identical members, then it reduces to a singular team
        # and it may not actually be stochastic.
        if team and len(set(team)) == 1:
            self.classifier["stochastic"] = Classifiers["stochastic"](
                self.team[0]
            )
            self.singular = True

    def meta_strategy(self, results, opponent):
        # If the team consists of identical players, just take the first result.
        # This prevents an unnecessary call to _random below.
        if self.singular:
            return results[0]
        # Sort by score
        scores = [(score, i) for (i, score) in enumerate(self.scores)]
        # Choose one of the best scorers at random
        scores.sort(reverse=True)
        prop = max(1, int(len(scores) * 0.08))
        best_scorers = [i for (s, i) in scores[:prop]]
        index = self._random.choice(best_scorers)
        return results[index]