from collections import namedtuple

from itertools import product

from typing import Any, TypeVar

from axelrod.action import Action, actions_to_str, str_to_actions

from axelrod.evolvable_player import (
    EvolvablePlayer,
    InsufficientParametersError,
    crossover_dictionaries,
)

from axelrod.player import Player

C, D = Action.C, Action.D

actions = (C, D)

Plays = namedtuple("Plays", "self_plays, op_plays, op_openings")

Reaction = TypeVar("Reaction", Action, float)

def make_keys_into_plays(lookup_table: dict) -> dict:
    """Returns a dict where all keys are Plays."""
    new_table = lookup_table.copy()
    if any(not isinstance(key, Plays) for key in new_table):
        new_table = {Plays(*key): value for key, value in new_table.items()}
    return new_table

def create_lookup_table_keys(
    player_depth: int, op_depth: int, op_openings_depth: int
) -> list:
    """Returns a list of Plays that has all possible permutations of C's and
    D's for each specified depth. the list is in order,
    C < D sorted by ((player_tuple), (op_tuple), (op_openings_tuple)).
    create_lookup_keys(2, 1, 0) returns::

        [Plays(self_plays=(C, C), op_plays=(C,), op_openings=()),
         Plays(self_plays=(C, C), op_plays=(D,), op_openings=()),
         Plays(self_plays=(C, D), op_plays=(C,), op_openings=()),
         Plays(self_plays=(C, D), op_plays=(D,), op_openings=()),
         Plays(self_plays=(D, C), op_plays=(C,), op_openings=()),
         Plays(self_plays=(D, C), op_plays=(D,), op_openings=()),
         Plays(self_plays=(D, D), op_plays=(C,), op_openings=()),
         Plays(self_plays=(D, D), op_plays=(D,), op_openings=())]

    """
    self_plays = product((C, D), repeat=player_depth)
    op_plays = product((C, D), repeat=op_depth)
    op_openings = product((C, D), repeat=op_openings_depth)

    iterator = product(self_plays, op_plays, op_openings)
    return [Plays(*plays_tuple) for plays_tuple in iterator]

default_tft_lookup_table = {
    Plays(self_plays=(), op_plays=(D,), op_openings=()): D,
    Plays(self_plays=(), op_plays=(C,), op_openings=()): C,
}

def get_last_n_plays(player: Player, depth: int) -> tuple:
    """Returns the last N plays of player as a tuple."""
    if depth == 0:
        return ()
    return tuple(player.history[-1 * depth :])

class LookerUp(Player):
    """
    This strategy uses a LookupTable to decide its next action. If there is not
    enough history to use the table, it calls from a list of
    self.initial_actions.

    if self_depth=2, op_depth=3, op_openings_depth=5, LookerUp finds the last 2
    plays of self, the last 3 plays of opponent and the opening 5 plays of
    opponent. It then looks those up on the LookupTable and returns the
    appropriate action. If 5 rounds have not been played (the minimum required
    for op_openings_depth), it calls from self.initial_actions.

    LookerUp can be instantiated with a dictionary. The dictionary uses
    tuple(tuple, tuple, tuple) or Plays as keys. for example.

    - self_plays: depth=2
    - op_plays: depth=1
    - op_openings: depth=0::

        {Plays((C, C), (C), ()): C,
         Plays((C, C), (D), ()): D,
         Plays((C, D), (C), ()): D,  <- example below
         Plays((C, D), (D), ()): D,
         Plays((D, C), (C), ()): C,
         Plays((D, C), (D), ()): D,
         Plays((D, D), (C), ()): C,
         Plays((D, D), (D), ()): D}

    From the above table, if the player last played C, D and the opponent last
    played C (here the initial opponent play is ignored) then this round,
    the player would play D.

    The dictionary must contain all possible permutations of C's and D's.

    LookerUp can also be instantiated with `pattern=str/tuple` of actions, and::

        parameters=Plays(
            self_plays=player_depth: int,
            op_plays=op_depth: int,
            op_openings=op_openings_depth: int)

    It will create keys of len=2 ** (sum(parameters)) and map the pattern to
    the keys.

    initial_actions is a tuple such as (C, C, D). A table needs initial actions
    equal to max(self_plays depth, opponent_plays depth, opponent_initial_plays
    depth). If provided initial_actions is too long, the extra will be ignored.
    If provided initial_actions is too short, the shortfall will be made up
    with C's.

    Some well-known strategies can be expressed as special cases; for example
    Cooperator is given by the dict (All history is ignored and always play C)::

        {Plays((), (), ()) : C}


    Tit-For-Tat is given by (The only history that is important is the
    opponent's last play.)::

       {Plays((), (D,), ()): D,
        Plays((), (C,), ()): C}


    LookerUp's LookupTable defaults to Tit-For-Tat.  The initial_actions
    defaults to playing C.

    Names:

    - Lookerup: Original name by Martin Jones
    """

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

    default_tft_lookup_table = {
        Plays(self_plays=(), op_plays=(D,), op_openings=()): D,
        Plays(self_plays=(), op_plays=(C,), op_openings=()): C,
    }

    def __init__(
        self,
        lookup_dict: dict = None,
        initial_actions: tuple = None,
        pattern: Any = None,  # pattern is str or tuple of Action's.
        parameters: Plays = None,
    ) -> None:

        Player.__init__(self)
        self.parameters = parameters
        self.pattern = pattern
        self._lookup = self._get_lookup_table(lookup_dict, pattern, parameters)
        self._set_memory_depth()
        self.initial_actions = self._get_initial_actions(initial_actions)
        self._initial_actions_pool = list(self.initial_actions)

    @classmethod
    def _get_lookup_table(
        cls, lookup_dict: dict, pattern: Any, parameters: tuple
    ) -> LookupTable:
        if lookup_dict:
            return LookupTable(lookup_dict=lookup_dict)
        if pattern is not None and parameters is not None:
            if isinstance(pattern, str):
                pattern = str_to_actions(pattern)
            self_depth, op_depth, op_openings_depth = parameters
            return LookupTable.from_pattern(
                pattern, self_depth, op_depth, op_openings_depth
            )
        return LookupTable(default_tft_lookup_table)

    def _set_memory_depth(self):
        if self._lookup.op_openings_depth == 0:
            self.classifier["memory_depth"] = self._lookup.table_depth
        else:
            self.classifier["memory_depth"] = float("inf")

    def _get_initial_actions(self, initial_actions: tuple) -> tuple:
        """Initial actions will always be cut down to table_depth."""
        table_depth = self._lookup.table_depth
        if not initial_actions:
            return tuple([C] * table_depth)
        initial_actions_shortfall = table_depth - len(initial_actions)
        if initial_actions_shortfall > 0:
            return initial_actions + tuple([C] * initial_actions_shortfall)
        return initial_actions[:table_depth]

    def strategy(self, opponent: Player) -> Reaction:
        turn_index = len(opponent.history)
        while turn_index < len(self._initial_actions_pool):
            return self._initial_actions_pool[turn_index]

        player_last_n_plays = get_last_n_plays(
            player=self, depth=self._lookup.player_depth
        )
        opponent_last_n_plays = get_last_n_plays(
            player=opponent, depth=self._lookup.op_depth
        )
        opponent_initial_plays = tuple(
            opponent.history[: self._lookup.op_openings_depth]
        )

        return self._lookup.get(
            player_last_n_plays, opponent_last_n_plays, opponent_initial_plays
        )

    @property
    def lookup_dict(self):
        return self._lookup.dictionary

    def lookup_table_display(
        self, sort_by: tuple = ("op_openings", "self_plays", "op_plays")
    ) -> str:
        """
        Returns a string for printing lookup_table info in specified order.

        :param sort_by: only_elements='self_plays', 'op_plays', 'op_openings'
        """
        return self._lookup.display(sort_by=sort_by)

class EvolvedLookerUp2_2_2(LookerUp):
    """
    A 2 2 2 Lookerup trained with an evolutionary algorithm.

    Names:

    - Evolved Lookerup 2 2 2: Original name by Marc Harper
    """

    name = "EvolvedLookerUp2_2_2"

    def __init__(self) -> None:
        params = Plays(self_plays=2, op_plays=2, op_openings=2)
        pattern = (
            "CDDCDCDDCDDDCDDDDDCDCDCCCDDCCDCDDDCCCCCDDDCDDDDDDDDDCCDDCDDDCCCD"
        )
        super().__init__(
            parameters=params, pattern=pattern, initial_actions=(C, C)
        )