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 MemoryDecay(MetaPlayer):
    """
    A player utilizes the (default) Tit for Tat strategy for the first (default) 15 turns,
    at the same time memorizing the opponent's decisions. After the 15 turns have
    passed, the player calculates a 'net cooperation score' (NCS) for their opponent,
    weighing decisions to Cooperate as (default) 1, and to Defect as (default)
    -2. If the opponent's NCS is below 0, the player defects; otherwise,
    they cooperate.

    The player's memories of the opponent's decisions have a random chance to be
    altered (i.e., a C decision becomes D or vice versa; default probability
    is 0.03) or deleted (default probability is 0.1).

    It is possible to pass a different axelrod player class to change the initial
    player behavior.

    Name: Memory Decay
    """

    name = "Memory Decay"
    classifier = {
        "memory_depth": float("inf"),
        "long_run_time": False,
        "stochastic": True,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(
        self,
        p_memory_delete: float = 0.1,
        p_memory_alter: float = 0.03,
        loss_value: float = -2,
        gain_value: float = 1,
        memory: list = None,
        start_strategy: Player = TitForTat,
        start_strategy_duration: int = 15,
    ):
        super().__init__(team=[start_strategy])
        self.p_memory_delete = p_memory_delete
        self.p_memory_alter = p_memory_alter
        self.loss_value = loss_value
        self.gain_value = gain_value
        self.memory = [] if not memory else memory
        self.start_strategy_duration = start_strategy_duration
        self.gloss_values = None

    def _post_init(self):
        super()._post_init()
        # This strategy is stochastic even if none of the team is.  The
        # MetaPlayer initializer will set stochastic to be False in that case.
        self.classifier["stochastic"] = True

    def __repr__(self):
        return Player.__repr__(self)

    def gain_loss_translate(self):
        """
        Translates the actions (D and C) to numeric values (loss_value and
        gain_value).
        """
        values = {C: self.gain_value, D: self.loss_value}
        self.gloss_values = [values[action] for action in self.memory]

    def memory_alter(self):
        """
        Alters memory entry, i.e. puts C if there's a D and vice versa.
        """
        alter = self._random.choice(range(0, len(self.memory)))
        self.memory[alter] = self.memory[alter].flip()

    def memory_delete(self):
        """
        Deletes memory entry.
        """
        self.memory.pop(self._random.choice(range(0, len(self.memory))))

    def meta_strategy(self, results, opponent):
        try:
            self.memory.append(opponent.history[-1])
        except IndexError:
            pass
        if len(self.history) < self.start_strategy_duration:
            return results[0]
        else:
            if self._random.random() <= self.p_memory_alter:
                self.memory_alter()
            if self._random.random() <= self.p_memory_delete:
                self.memory_delete()
            self.gain_loss_translate()
            if sum(self.gloss_values) < 0:
                return D
            else:
                return C