"""Base Pokemon."""
import operator
from typing import Callable, Sequence, Type, TypeVar, Union

import attr

from pokemaster2.prng import PRNG

S = TypeVar("S", bound="Stats")
P = TypeVar("P", bound="BasePokemon")


STAT_NAMES = ["hp", "atk", "def_", "spatk", "spdef", "spd"]
STAT_NAMES_FULL = {
    "hp": "hp",
    "attack": "atk",
    "defense": "def_",
    "special-attack": "spatk",
    "special-defense": "spdef",
    "speed": "spd",
}

prng = PRNG()


@attr.s(auto_attribs=True)
class Stats:
    """Generic stats, can be used for Pokemon stats/IV/EV."""

    hp: int
    atk: int
    def_: int
    spatk: int
    spdef: int
    spd: int

    def __add__(self: S, other: Union[S, int]) -> S:
        """Pointwise addition."""
        return self._make_operator(operator.add, other)

    def __sub__(self: S, other: Union[S, int]) -> S:
        """Pointwise subtraction."""
        return self._make_operator(operator.sub, other)

    def __mul__(self: S, other: Union[S, int]) -> S:
        """Pointwise multiplication."""
        return self._make_operator(operator.mul, other)

    def __floordiv__(self: S, other: Union[S, int]) -> S:
        """Pointwise floor division."""
        return self._make_operator(operator.floordiv, other)

    __radd__ = __add__
    __rmul__ = __mul__

    def _make_operator(
        self: S,
        operator: Callable[[int, int], int],
        other: Union[S, int],
    ) -> S:
        """Programmatically create point-wise operators.

        Args:
            operator: A callable (Real, Real) -> Real.
            other: If `other` is a `Stats` instance, then the
                operator will be applied point-wisely. If `other` is a
                number, then a scalar operation will be applied.

        Raises:
            TypeError: `other` should be either another `Stats` or `int`.

        Returns:
            A `Stats` instance.
        """
        names = (
            "hp",
            "atk",
            "def_",
            "spatk",
            "spdef",
            "spd",
        )

        if not isinstance(other, type(self)) and not isinstance(other, int):
            raise TypeError(
                f"unsupported operand type(s) for {operator}: "
                f"'{type(self)}' and '{type(other)}'"
            )
        result_stats = {}
        for stat in names:
            if isinstance(other, type(self)):
                result_stats[stat] = int(operator(getattr(self, stat), getattr(other, stat)))
            elif isinstance(other, int):
                result_stats[stat] = int(operator(getattr(self, stat), other))
        return self.__class__(**result_stats)

    def validate_iv(self: S) -> bool:
        """Check if each IV is between 0 and 32."""
        for stat in STAT_NAMES:
            if not 0 <= getattr(self, stat) <= 32:
                raise ValueError(
                    f"The {stat} IV ({getattr(self, stat)}) must be a number "
                    "between 0 and 32 inclusive."
                )
        return True

    @classmethod
    def create_iv(cls: Type[S], gene: int) -> S:
        """Create IV stats from a Pokémon's gene.

        Args:
            gene: An `int` generated by the PRNG.

        Returns:
            A `Stats` instance.
        """
        return cls(
            hp=gene % 32,
            atk=(gene >> 5) % 32,
            def_=(gene >> 10) % 32,
            spd=(gene >> 16) % 32,
            spatk=(gene >> 21) % 32,
            spdef=(gene >> 26) % 32,
        )

    @classmethod
    def zeros(cls: Type[S]) -> S:
        """Empty Stats."""
        return cls(
            hp=0,
            atk=0,
            def_=0,
            spatk=0,
            spdef=0,
            spd=0,
        )

    @classmethod
    def nature_modifiers(cls: Type[S], nature: str) -> S:
        """Generate nature modifier Stats."""
        # nature_data = _db.get_nature(identifier=nature)
        # modifiers = {}
        # for stat in STAT_NAMES:
        #     modifiers[stat] = 1
        # if nature_data.is_neutral:
        #     return cls(**modifiers)
        # modifiers[STAT_NAMES_FULL[nature_data.increased_stat.identifier]] = 1.1
        # modifiers[STAT_NAMES_FULL[nature_data.decreased_stat.identifier]] = 0.9
        # return cls(**modifiers)


@attr.s(auto_attribs=True)
class BasePokemon:
    """The underlying structure of a Pokémon.

    No fancy initializations, no consistency checks, just a very basic
    Pokémon model. Anything is possible with this BasePokemon. This
    class also contains common and basic behaviors of Pokémon, such as
    leveling-up, learning/forgetting moves, evolving into another
    Pokémon, etc.

    This class is never meant to be instantiated directly.
    """

    national_id: int
    species: str
    types: Sequence[str]
    item_held: str
    exp: int
    level: int

    base_stats: Stats
    iv: Stats
    current_stats: Stats
    stats: Stats
    ev: Stats

    # move_set = Mapping[int, Mapping[str, Union[str, int]]]
    pid: str
    gender: str
    nature: str
    ability: str

    # def evolve(self: P) -> None:
    #     """
    #     Evolve into another Pokémon.

    #     1. Statistics are updated.
    #     2. Learnset is updated.
    #     3. Evolution tree is updated.

    #     Returns:
    #         Nothing

    #     """
    #     pass

    # def level_up(self: P) -> None:
    #     """Increase `Pokemon`'s level by 1.

    #     Returns:
    #         Nothing
    #     """
    #     pass

    # @classmethod
    # def _from_pokedex_by_id(
    #     cls: "BasePokemon",
    #     national_id: int,
    #     level: int,
    #     item_held: str = None,
    #     iv: Stats = None,
    #     ev: Stats = None,
    #     pid: int = None,
    #     nature: str = None,
    #     ability: str = None,
    #     gender: str = None,
    # ) -> "BasePokemon":
    #     """Instantiate a `BasePokemon` by its national id.

    #     Everything else is randomized.

    #     Args:
    #         national_id: the Pokemon's ID in the National Pokedex.
    #         level: Pokemon's level.
    #         item_held: Pokemon's holding item.
    #         iv: Pokemon's individual values, `Stats`, used to determine its permanent stats.
    #             A random IV will be set if not provided.
    #         ev: Pokemon's effort values, `Stats`, used to determine its permanent stats. An
    #             all-zero ev will be set if not provided.
    #         pid: Pokemon's personality id. `nature`, `ability`, and `gender` will use
    #             their provided value first. A random `pid` will be set if not provided.
    #         nature: Pokemon's nature, used to determine its permanent stats. If nothing is
    #             provided, then the function will use `pid` to determine its `nature`.
    #         ability: Pokemon's ability, `str`. If nothing is provided, then the function
    #             will use `pid` to determine its `nature`.
    #         gender: Pokemon's gender.  If nothing is provided, then the function will use
    #             `pid` to determine its `nature`.

    #     Returns:
    #         A `BasePokemon` instance.
    #     """
    # # Build pokemon data
    # pokemon_data = _db.get_pokemon(national_id=national_id)
    # growth_data = _db.get_experience(national_id=national_id, level=level)
    # species_data = pokemon_data.species
    # species = species_data.identifier

    # # Determine stats
    # gene = prng.create_gene()
    # iv = iv or Stats.create_iv(gene=gene)
    # ev = ev or Stats.zeros()
    # base_stats = {}
    # for i, stat in enumerate(STAT_NAMES):
    #     base_stats[stat] = pokemon_data.stats[i].base_stat
    # stats = _calc_stats(level=level, base_stats=base_stats, iv=iv, ev=ev, nature=nature)
    # current_stats = stats

    # # PID related attributes
    # pid = pid or prng.create_personality()
    # nature = nature or _db.get_nature(pid).identifier
    # ability = ability or _db.get_ability(species=species, personality=pid).identifier
    # gender = gender or _db.get_pokemon_gender(species=species, personality=pid).identifier

    # return cls(
    #     pid=pid,
    #     national_id=species_data.id,
    #     species=species,
    #     types=list(map(lambda x: x.identifier, pokemon_data.types)),
    #     item_held=item_held,
    #     exp=growth_data.experience,
    #     level=growth_data.level,
    #     stats=stats,
    #     current_stats=current_stats,
    #     ev=ev,
    #     iv=iv,
    #     nature=nature,
    #     ability=ability,
    #     gender=gender,
    # )


def _calc_stats(level: int, base_stats: Stats, iv: Stats, ev: Stats, nature: str) -> Stats:
    """Calculate the Pokemon's stats."""
    nature_modifiers = Stats.nature_modifiers(nature)
    residual_stats = Stats(
        hp=10 + level,
        atk=5,
        def_=5,
        spatk=5,
        spdef=5,
        spd=5,
    )

    stats = ((base_stats * 2 + iv + ev // 4) * level // 100 + residual_stats) * nature_modifiers
    if base_stats.hp == 1:
        stats.hp = 1
    return stats
