"""Typing in RSTT

This modules defines different componnents types for the package. RSTT user level functions and class methods are typehecked.
It helps raising meaningfull errors at runtime, write understandable functions.
The module purpose is also to help develloper to extent and integrate their own component that fit in the RSTT framework.

When a type name clashes with an existing class, the type name is prefixed by a capital `S`.
For example: :class:`rstt.stypes.SPlayer` should only be used for typehint in functions signatures. :class:`rstt.player.player.Player` is a class representing competitors.

.. note::
    RSTT relies on typeguard 3.0.0 and `Duck-Typing` for function signatures.
    Which means that Protocol are defined by the existence of attributes/methods, and not their type signatures.
"""


from dataclasses import dataclass
from typing import List, Dict, Union,  Any, Iterable, Protocol, runtime_checkable


# TODO: work on Scheduler typing and similarities/differences between 'Tournament' & 'MatchMaking'


@runtime_checkable
class SPlayer(Protocol):
    """Player interface

    Runtime_checkable Protocol representating the notion of a participant in a :class:`rstt.stypes.SMatch` and an element in a :class:`rstt.ranking.ranking.Ranking`.
    Examples of SPlayer are implemented in the subpackage :class:`rstt.player`
    """

    def name(self) -> str:
        """return the name of the competitors. Assumed to be a unique identifier"""

    def level(self) -> float:
        """return the level (or strenght, skills) of the splayer"""


Score = list[float]
"""Outcome of match

A list of float value, each representing the result of a corresponding participant in the Match.
"""


@runtime_checkable
class SMatch(Protocol):
    """Match interface

    Runtime_checkable Protocol representing a confrontation between :class:`rstt.stypes.SPlayer`. 
    Examples of SMatch can be found in the :class:`rstt.game.match` module. 

    """

    def players(self) -> List[SPlayer]:
        """getter for match participants

        Returns
        -------
        List[SPlayer]
            The list of SPlayer who participates in the match. The order of players matches the order of float value in the :func:`rstt.stypes.SMatch.scores`
        """

    def scores(self) -> Score:
        """getter for the match score

        Returns
        -------
        Score
           The outcome of the match. The order of players returned by :func:`rstt.stypes.SMatch.players` matches the order of float value in the returned Score.
        """


@runtime_checkable
class Event(Protocol):
    """Event interface

    Runtime_checkable Protocol for competitive events.

    Events are automated game generators. Typical events are tournament inheriting from the :class:`rstt.scheduler.competition.Competition` class.
    Examples are :class:`rstt.scheduler.tournament.knockout.SingleEliminationBracket` and :class:`rstt.scheduler.tournament.groups.RoundRobin`
    """

    def name(self) -> str:
        """getter for the name event

        Name are assumed to be unique identifier

        Returns
        -------
        str
            The name of the event
        """

    def games(self, by_rounds: bool) -> Union[List[SMatch], List[List[SMatch]]]:
        """getter for SMatch

        Parameters
        ----------
        by_rounds : bool
            a boolean value indicating if the return should be a List[Smatch] (false), or a List[List[SMatch]]

        Returns
        -------
        Union[List[SMatch], List[List[SMatch]]]
            All SMatch played during the event.
        """

    def standing(self) -> Dict[SPlayer, int]:
        """getter for the event standing

        A standing represent the result of the event (and not a given SMatch). The place 1 indicates the winner of the event.

        Returns
        -------
        Dict[SPlayer, int]
            A dictionary indicating the final placement of all SPlayer that participated in the Event.
        """


@dataclass
class Achievement:
    """DataClass indicating player's result in event.

    Achievements are object generated by :class:`rstt.scheduler.tournament.competition.Competition`.
    It is collected by :class:`rstt.player.palyer.Player` to track their history in events.

    .. note::
        Since achievements are automaticaly stored by Player, there is no field indicating the 'who'
    """
    event_name: str
    """The name of the event"""
    place: int
    """The place of the player in the final standing of the event"""
    prize: float
    """The earnings of the player during the event"""
    # ??? categorie/label(s) (world event, domestic league, playoffs etc.)
    # ??? event_type (SEB, Snake, MM etc)
    # ??? Match type (duel, many-versus-many, other not yet implemented ...)
    # ??? teammates


@runtime_checkable
class Solver(Protocol):
    """Solver interface

    Runtime_checkable Protocol responsible of SMatch outcome generation. For a :class:`rstt.game.match.Duel`.
    An example is a deterministic Solver where the player with the highest level wins the duel :class:`rstt.solver.solvers.BetterWin`
    """

    def solve(self, match: SMatch, *agrs, **kwargs) -> None:
        """Method that decide an SMatch outcome.

        This method has three responsabilities.
            - First it should generate a suitable game result (Score).
            - Assign the result to the game
            - add the game to the history of the game participants that tracks their games.

        .. note::
            An SMatch can only be assigned an outcome once, but the Solver does not need to take any precaution in that regards.
            It is the responsability of the Smatch.
            The same can be said about player tracking their game history, a match can only be added once,
            but it is the player responsability to enforce it.

        Parameters
        ----------
        match : SMatch
            A game to generate an outcome for.
        """


# -------------------------- #
# --- Typing for Ranking --- #
# -------------------------- #
@runtime_checkable
class Inference(Protocol):
    """Inference Interface

    Runtime_checkable Protocol responsible to estimate the level of SPlayer. Inference is a core component of :class:`rstt.ranking.ranking.Ranking`.
    Examples are :class:`rstt.ranking.inferer.Elo` or :class:`rstt.ranking.inferer.EventStanding`.
    """

    # NOTE: name inspiration: https://en.wikipedia.org/wiki/Statistical_inference
    def rate(self, *args, **kwargs) -> Any:
        """A method to rate Splayer based on 'observation'

        Different Inference can have different parameters and returns values/types. There is no general patterns that can be extracted.
        However, the existence of this method has the benefit to sementicly fit other famous package in the field of ranking system in competition, such as
        `Openskill <https://openskill.me/en/stable/>`_ and `TrueSkill <https://trueskill.org>`_.

        .. note::
            For devellopers: The type-signature of the rate method is a 'contract' with the other components of a Ranking instance, namely a :class:`rstt.stypes.Observer` instance.
            For example if the ranking uses a :class:`rstt.ranking.observer.GameByGame` Observer, then the inferer.rate() method should accept a List[Rating] and either a Score or a List[int] as parameters.
            The GameByGame expect a List[Rating] as return value. The type of Rating has to be compatible with the :class:`rstt.stypes.RatingSystem` of the Ranking.

        Parameters:
        -----------
        Any
            The input of the rate method is usually some sort of prior 'Rating' or collection of 'Rating' and an indication of why/how the ratings should change.

        Returns
        -------
        Any
            The return of the rate method is usually a sort of  updated 'Rating' or collection of 'Rating'.
        """


@runtime_checkable
class RatingSystem(Protocol):
    """RatingSystem interface

    Runtime_checkable Protocol acting as a sort of Dict[SPlayer, Rating] where Rating can be of Any type. RatingSystem are componnent of :class:`rstt.ranking.ranking.Ranking`
    and provde an 'ordinal()' method used by the ranking to sort automatically players (assign a rank). 

    An example is the :class:`rstt.ranking.datamodel.KeyModel` that acts as a defaultdictionary, ensuring that any SPlayer has at least a default rating such that it can be ranked in a Ranking.

    """

    def get(self, key: SPlayer) -> Any:
        """getter method for rating


        Parameters
        ----------
        key : SPlayer
            A key to get the corresponding value (rating)

        Returns
        -------
        Any
            A corresponding value, meant has the player's rating
        """

    def set(self, key: SPlayer, rating: Any) -> None:
        """setter method for rating

        Allows a user to tune the rating of a SPlayer at will

        Parameters
        ----------
        key : SPlayer
            A key for which the associated value should change
        rating : Any
            The new value associated to the provided key
        """

    def ordinal(self, rating: Any) -> float:
        """Convert a value to a float

        This methods is used to compare keys to each others based on their associated values.

        Parameters
        ----------
        rating : Any
            A rating to convert

        Returns
        -------
        float
            A single floating value. :class:`rstt.ranking.ranking.Ranking` assumes that a higher returned value means 'better'.
        """

    def keys(self):
        """Dict like keys method

        Returns
        -------
        view object
            view on the RatingSystem entries
        """

    def __delitem__(self, key: SPlayer):
        """delitem magic method

        Parameters
        ----------
        key : SPlayer
            a player to remove from the RatingSystem
        """


@runtime_checkable
class Observer(Protocol):
    """Obersever Interface

    Runtime_checkable Protocol implementing a workfow triggered by :func:`rstt.ranking.ranking.Ranking.update`. 
    The observer component of a ranking is responsible to update ratings of SPlayer based on 'observation' passed to the update() function.
    Examples are :class:`rstt.ranking.observer.GameByGame` and :class:`rstt.ranking.observer.BatchGame`.

    It is possible to entirely change the behaviour of Ranking by simply changing the observer component.
    Indeed both the `Elo <https://en.wikipedia.org/wiki/Elo_rating_system>`_ and `Glicko <https://en.wikipedia.org/wiki/Glicko_rating_system>`_ rating systems can be used either with the GameByGame or BatchGame observer.
    Yet, for identical input, the resulting updated can be different.

    .. note::
        'Observation' is an abstract arbitrary notion, it does not even need to be something passed as a parameters to a :func:`rstt.ranking.ranking.Ranking.update` method.
        For example the :class:`rstt.ranking.standard.BTRanking` rank players based on the returned value of their level() method, which could change silently during a simulation.
        In this case the ranking.update() method is called without parameters.

    """

    '''
        # TODO:
            - work on observer as a pipeline of transformation and operation
            - common components are:
                * transform/format the observation(s)
                * QUERY the datamodel
                * call the Inference
                * POST updates in the datamodel
                * ...
    '''

    def handle_observations(self, infer: Inference, datamodel: RatingSystem, *args, **kwargs) -> None:
        """method that triggers a rating update routine

        The method uses a RatingSystem to get prior rating of Player and store updated ratings.
        It uses an Inference instance to compute the updated ratings, usually from the prior ratings and some additional parameters

        Parameters
        ----------
        infer : Inference
            An inference object responsible to update ratings
        datamodel : RatingSystem
            A ratingSystem object storing rating compatible with the passed infer.
        """


# ---------------------------- #
# --- Typing for Shceduler --- #
# ---------------------------- #
@runtime_checkable
class Shuffler(Protocol):
    def rearange(status: List[int]) -> List[int]:
        """Reorder elements in a 'meaningfull' fashion

        Parameters
        ----------
        status : List[int]
            A list of integers [e0, e1, e2, ...]

        Returns
        -------
        List[int]
            A list of the same integers, in a different order.
        """


@runtime_checkable
class Seeder(Protocol):
    def seed(players: List[SPlayer], initial_seeds: Iterable, results: Any, **kwargs) -> List[SPlayer]:
        """Reorder players in a 'meaningfull' fashion

        Parameters
        ----------
        players : List[Player]
            A list of Player to order
        initial_seeds : Iterable
            An original ordering of the players
        results : Any
            Any data structure containing games (type ´Game´) related to the involved players

        Returns
        -------
        List[Player]
            A list of the same players, in a different order
        """


@runtime_checkable
class Generator(Protocol):
    def generate(status: Union[List[int], List[SPlayer]]) -> Union[List[int], List[SPlayer]]:
        """Generate different ordering version of a given List


        the different version produced are refered as 'options'
        For short input, this function could just produce all possible orderings.
        For long inout, this function could implement greedy strategy.
        Thus the input is a list, and the initial ordering is meaningfull for the strategy

        Parameters
        ----------
        status : Union[List[int], List[Player]]
            A list of element to compute options

        Returns
        -------
        Union[List[int], List[Player]]
            the options, A list of different 'option' which are reordering of the given 'status'
            Idealy the first option is a direct, and 'obvious' transformation of status
        """


@runtime_checkable
class Evaluator(Protocol):
    def eval(options: List[List[SPlayer]], initial_seeds: Iterable, results: Any, **kwargs) -> List[List[SPlayer]]:
        """reorder options based on criteria

        The options can be evaluated based on an history of Game(s), or Player(s) appreciation.
        Usually the eval function does some 'sorting' operations

        Parameters
        ----------
        options : List[List[Player]]
            Different ordering of players possibility to evaluate
        initial_seeds : Iterable
            An apriori 'preference' on the player.
        results : Any
            A collection of results indicating palyer's performaces so far.

        Returns
        -------
        List[List[Player]]
            A reordered version of the parameter 'options'.
        """
