"""Stochastic variants of Lookup table based-strategies, trained with particle
swarm algorithms.

For the original see:
 https://gist.github.com/GDKO/60c3d0fd423598f3c4e4
"""

from typing import Any

from axelrod.action import Action, actions_to_str, str_to_actions

from axelrod.load_data_ import load_pso_tables

from axelrod.player import Player

from .lookerup import (
    EvolvableLookerUp,
    LookerUp,
    LookupTable,
    Plays,
    create_lookup_table_keys,
)

C, D = Action.C, Action.D

tables = load_pso_tables("pso_gambler.csv", directory="data")

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 Gambler(LookerUp):
    """
    A stochastic version of LookerUp which will select randomly an action in
    some cases.

    Names:

    - Gambler: Original name by Georgios Koutsovoulos
    """

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

    def strategy(self, opponent: Player) -> Action:
        """Actual strategy definition that determines player's action."""
        actions_or_float = super(Gambler, self).strategy(opponent)
        if isinstance(actions_or_float, Action):
            return actions_or_float
        return self._random.random_choice(actions_or_float)

class PSOGambler2_2_2_Noise05(Gambler):
    """
    A 2x2x2 PSOGambler trained with pyswarm with noise=0.05.

    Names:

    - PSO Gambler 2_2_2 Noise 05: Original name by Marc Harper
    """

    name = "PSO Gambler 2_2_2 Noise 05"

    def __init__(self) -> None:
        pattern = tables[("PSO Gambler 2_2_2 Noise 05", 2, 2, 2)]
        parameters = Plays(self_plays=2, op_plays=2, op_openings=2)

        super().__init__(parameters=parameters, pattern=pattern)