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 MetaMajority(MetaPlayer):
    """A player who goes by the majority vote of all other non-meta players.

    Names:

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

    name = "Meta Majority"

    @staticmethod
    def meta_strategy(results, opponent):
        if results.count(D) > results.count(C):
            return D
        return C

class MetaMajorityLongMemory(MetaMajority):
    """MetaMajority with the team of Long (infinite) Memory Players

    Names

    - Meta Majority Long Memory: Original name by Marc Harper
    """

    name = "Meta Majority Long Memory"

    def __init__(self):
        team = [
            s
            for s in ordinary_strategies
            if Classifiers["memory_depth"](s()) == float("inf")
        ]
        super().__init__(team=team)