import dataclasses
from typing import Any, Callable, List, Mapping, MutableMapping, Optional
import wcwidth

from pad_data import common
from pad_data.skill import Skill

@dataclasses.dataclass
class EnemySkillRef:
    enemy_skill_id: int
    enemy_ai: int
    enemy_rnd: int

def _unflatten(raw: List[Any], idx: int, width: int, replace: bool) -> None:
    """Unflatten a card array.

    Index is the slot containing the item count.
    Width is the number of slots per item.
    If replace is true, values are moved into an array at idx.
    If replace is false, values are deleted.
    """
    item_count = raw[idx]
    if item_count == 0:
        if replace:
            raw[idx] = list()
            return

    data_start = idx + 1
    flattened_item_count = width * item_count
    flattened_data_slice = slice(data_start, data_start + flattened_item_count)

    data = list(raw[flattened_data_slice])
    del raw[flattened_data_slice]

    if replace:
        raw[idx] = data

# pylint: disable=too-many-instance-attributes
class Card:
    def __init__(self, raw_data: List[str]):
        self._raw_data: List[Any] = raw_data
        self._parse_raw_data()
        self.skill = Skill('', '', [], 0, 0)
        self.leader_skill = Skill('', '', [], 0, 0)
        self.enemy_passive_resist: MutableMapping[int, Skill] = {}
        self.evo_skills: List[Skill] = []

    # Copied from
    # https://github.com/nachoapps/dadguide-data/blob/master/etl/pad/raw/card.py
    # and removed some fields for faster processing
    def _parse_raw_data(self) -> None:
        raw = self._raw_data

        _unflatten(raw, 57, 3, replace=True)
        _unflatten(raw, 58, 1, replace=True)

        self.card_id = int(raw[0])
        self.name = raw[1]
        self.attr_id = common.Orb(int(raw[2]))
        self.sub_attr_id = common.Orb(int(raw[3]))
        self.is_ult = int(raw[4])
        self.type = [common.Type(int(raw[5])),
                     common.Type(int(raw[6])),
                     common.Type.NO_TYPE]
        self.rarity = int(raw[7])
        self.cost = int(raw[8])

        # 9: Appears to be related to the size of the monster.
        # If 5, the monster always spawns alone. Needs more research.

        self.max_level = int(raw[10])
        self.feed_xp_per_level = int(raw[11]) // 4
        self.released_status = raw[12] == 100
        self.sell_gold_per_level = int(raw[13]) // 10

        self.min_hp = int(raw[14])
        self.max_hp = int(raw[15])
        self.hp_scale = float(raw[16])

        self.min_atk = int(raw[17])
        self.max_atk = int(raw[18])
        self.atk_scale = float(raw[19])

        self.min_rcv = int(raw[20])
        self.max_rcv = int(raw[21])
        self.rcv_scale = float(raw[22])

        self.xp_max = int(raw[23])
        self.xp_scale = float(raw[24])

        self.active_skill_id = int(raw[25])
        self.leader_skill_id = int(raw[26])

        # 27: Enemy turn timer for normal dungeons, and techs where
        # enemy_turns_alt is not populated.

        # 28~36: enemy hp/atk/def
        # Level range from 1 to 10

        # 37: enemy_max_level
        # 38: enemy_coins_per_level
        # 39: enemy_xp_per_level

        # 40: ancestor_id

        self.evo_mat = list(map(int, raw[41:46]))
        # 46~50: un-evo mat

        # enemy AI releated things
        # 51: enemy_turns_alt
        # When >0, the enemy turn timer for technical dungeons.
        # 52: use_new_ai
        # 53: enemy_skill_max_counter
        # 54: enemy_skill_counter_increment

        # 55~56: unknown

        es_data = list(map(int, raw[57]))
        self.enemy_skill_refs = [
            EnemySkillRef(*es_data[i : i + 3])
            for i in range(0, len(es_data) - 2, 3)]

        self.awakenings = [common.Awakening(x) for x in raw[58]]
        self.super_awakenings = [common.Awakening(int(x))
                                 for x in raw[59].split(',')
                                 if x.strip()]

        # 60: base_id
        # 61: group_id
        self.type[2] = common.Type(int(raw[62]))

        # 63: sell_mp
        # 64: latent_on_feed
        # 65: collab_id

        # Bitmap with some random flag values, not sure what they all do.
        self.flags = int(raw[66])

        self.furigana = str(raw[67])  # JP data only?
        self.limit_mult = int(raw[68])

        # 69: voice_id
        # 70: orb_skin_id
        # 71: tag string

        self.other_fields = raw[72:]

    def _stat_at_level(self, st: str, lv: Optional[int]) -> int:
        max_lv = self.max_level
        max_v: int = getattr(self, f'max_{st}')
        min_v: int = getattr(self, f'min_{st}')
        scale: int = getattr(self, f'{st}_scale')
        limit_mult = self.limit_mult

        if lv is None:
            lv = 120 if self.limit_mult > 0 else max_lv

        if max_lv == 1:
            # In this case, max_v may not equal to min_v.
            # e.g. http://pad.skyozora.com/pets/147
            # min_v is the correct number.
            return min_v

        # note that python's round implementation is different than the game's
        # implementation. So this function is not 100% accurate.
        if 1 <= lv <= max_lv:
            return round(
                min_v + (max_v - min_v) * ((lv - 1) / (max_lv - 1)) ** scale)

        if limit_mult <= 0 or not (100 <= lv <= 120):
            raise ValueError('level out of range')

        if 100 <= lv <= 110:
            return round(max_v * (1 + limit_mult * (lv - 99) / 1100))
        if 110 < lv <= 120:
            step = 1 if st == 'hp' else 0.5
            return round(
                    max_v * (1 + (limit_mult + step * (lv - 110)) / 100))

        assert False, 'should not reach here'
        return -1


    _FIELD_WHITELIST = frozenset([
        'attr_id', 'awakenings', 'card_id', 'cost', 'extra_latent_slot',
        'inheritable', 'limit_mult', 'max_atk', 'max_hp', 'max_level',
        'max_rcv', 'min_atk', 'min_hp', 'min_rcv', 'name', 'rarity',
        'sub_attr_id', 'super_awakenings', 'type',
    ])

    @property
    def inheritable(self) -> bool:
        return bool(self.flags & 1)

    @property
    def is_collab(self) -> bool:
        return bool(self.flags & 4)

    @property
    def extra_latent_slot(self) -> bool:
        return bool(self.flags & 32)

    @property
    def evo_type(self) -> common.EvoType:
        # guess the evolution type from card data
        if self.evo_mat[0] == 0:
            return common.EvoType.NO_EVO

        if self.extra_latent_slot:
            if self.evo_mat[0] == 5077: # イベントメダル【虹】の希石
                return common.EvoType.SUPER_REINCARNATION
            return common.EvoType.REINCARNATION

        if self.evo_mat[0] == 3826: # ドットリット
            return common.EvoType.PIXEL

        if self.awakenings.count(common.Awakening.AWOKEN_ASSIST):
            return common.EvoType.ASSIST

        if self.is_ult:
            return common.EvoType.ULTIMATE
        return common.EvoType.NORMAL_EVO

    @property
    def merged_json(self) -> Mapping[str, Any]:
        obj: MutableMapping[str, Any] = {}
        for k in self._FIELD_WHITELIST:
            obj[k] = getattr(self, k)
        obj['skill'] = self.skill.to_json()
        obj['leader_skill'] = self.leader_skill.to_json(is_leader_skill=True)
        obj['enemy_passive_resist'] = [
            {'name': s.name, 'effects': s.effects_to_tuples()}
            for s in self.enemy_passive_resist.values()
        ]
        if self.evo_skills:
            obj['evo_skills'] = [s.to_json() for s in self.evo_skills]
        return obj

    def atk_at_level(self, level: Optional[int]=None) -> int:
        return self._stat_at_level('atk', level)

    def hp_at_level(self, level: Optional[int]=None) -> int:
        return self._stat_at_level('hp', level)

    def rcv_at_level(self, level: Optional[int]=None) -> int:
        return self._stat_at_level('rcv', level)

    def has_evo_skill(self) -> bool:
        return bool(self.evo_skills)

    def __repr__(self) -> str:
        return str(self.__dict__)

    def color_code(self) -> str:
        if self.attr_id == common.Orb.JAMMER:
            return self.sub_attr_id.color_code()
        return self.attr_id.color_code()

    def dump(self, atk_eval: Callable[['Card'], int]=atk_at_level,
             rcv_eval: Callable[['Card'], int]=rcv_at_level,
             print_active_skill: bool=True, print_leader_skill: bool=False,
             evo_skill_index: Optional[int]=None,
             ) -> None:
        print(self.color_code(),
              self.name,
              common.Orb.NO_ORB.color_code(),
              ' ' * (50 - wcwidth.wcswidth(self.name)),
              f'{self.hp_at_level():8}',
              f'{atk_eval(self):8}',
              f'{rcv_eval(self):8}',
              sep='',
              end='  ')

        if print_active_skill:
            if evo_skill_index:
                desc = self.evo_skills[evo_skill_index].clean_description
            else:
                desc = self.skill.clean_description
            turn_max = self.skill.turn_max
            turn_min = self.skill.turn_min

            extra_turn = 0
            if evo_skill_index is not None:
                for i in range(1, evo_skill_index + 1):
                    extra_turn += self.evo_skills[i].turn_min_checked()

            print(f'{turn_max:2}/{turn_min:2}',
                  f'+{extra_turn:<2} ' if extra_turn else '    ',
                  desc,
                  sep='',
                  end='')
        if print_leader_skill:
            print(self.leader_skill.clean_description, end='')
        print()

import dataclasses
from typing import Any, Callable, List, Mapping, MutableMapping, Optional
import wcwidth

from pad_data import common
from pad_data.skill import Skill

@dataclasses.dataclass
class EnemySkillRef:
    enemy_skill_id: int
    enemy_ai: int
    enemy_rnd: int

def _unflatten(raw: List[Any], idx: int, width: int, replace: bool) -> None:
    """Unflatten a card array.

    Index is the slot containing the item count.
    Width is the number of slots per item.
    If replace is true, values are moved into an array at idx.
    If replace is false, values are deleted.
    """
    item_count = raw[idx]
    if item_count == 0:
        if replace:
            raw[idx] = list()
            return

    data_start = idx + 1
    flattened_item_count = width * item_count
    flattened_data_slice = slice(data_start, data_start + flattened_item_count)

    data = list(raw[flattened_data_slice])
    del raw[flattened_data_slice]

    if replace:
        raw[idx] = data

# pylint: disable=too-many-instance-attributes
class Card:
    def __init__(self, raw_data: List[str]):
        self._raw_data: List[Any] = raw_data
        self._parse_raw_data()
        self.skill = Skill('', '', [], 0, 0)
        self.leader_skill = Skill('', '', [], 0, 0)
        self.enemy_passive_resist: MutableMapping[int, Skill] = {}
        self.evo_skills: List[Skill] = []

    # Copied from
    # https://github.com/nachoapps/dadguide-data/blob/master/etl/pad/raw/card.py
    # and removed some fields for faster processing
    def _parse_raw_data(self) -> None:
        raw = self._raw_data

        _unflatten(raw, 57, 3, replace=True)
        _unflatten(raw, 58, 1, replace=True)

        self.card_id = int(raw[0])
        self.name = raw[1]
        self.attr_id = common.Orb(int(raw[2]))
        self.sub_attr_id = common.Orb(int(raw[3]))
        self.is_ult = int(raw[4])
        self.type = [common.Type(int(raw[5])),
                     common.Type(int(raw[6])),
                     common.Type.NO_TYPE]
        self.rarity = int(raw[7])
        self.cost = int(raw[8])

        # 9: Appears to be related to the size of the monster.
        # If 5, the monster always spawns alone. Needs more research.

        self.max_level = int(raw[10])
        self.feed_xp_per_level = int(raw[11]) // 4
        self.released_status = raw[12] == 100
        self.sell_gold_per_level = int(raw[13]) // 10

        self.min_hp = int(raw[14])
        self.max_hp = int(raw[15])
        self.hp_scale = float(raw[16])

        self.min_atk = int(raw[17])
        self.max_atk = int(raw[18])
        self.atk_scale = float(raw[19])

        self.min_rcv = int(raw[20])
        self.max_rcv = int(raw[21])
        self.rcv_scale = float(raw[22])

        self.xp_max = int(raw[23])
        self.xp_scale = float(raw[24])

        self.active_skill_id = int(raw[25])
        self.leader_skill_id = int(raw[26])

        # 27: Enemy turn timer for normal dungeons, and techs where
        # enemy_turns_alt is not populated.

        # 28~36: enemy hp/atk/def
        # Level range from 1 to 10

        # 37: enemy_max_level
        # 38: enemy_coins_per_level
        # 39: enemy_xp_per_level

        # 40: ancestor_id

        self.evo_mat = list(map(int, raw[41:46]))
        # 46~50: un-evo mat

        # enemy AI releated things
        # 51: enemy_turns_alt
        # When >0, the enemy turn timer for technical dungeons.
        # 52: use_new_ai
        # 53: enemy_skill_max_counter
        # 54: enemy_skill_counter_increment

        # 55~56: unknown

        es_data = list(map(int, raw[57]))
        self.enemy_skill_refs = [
            EnemySkillRef(*es_data[i : i + 3])
            for i in range(0, len(es_data) - 2, 3)]

        self.awakenings = [common.Awakening(x) for x in raw[58]]
        self.super_awakenings = [common.Awakening(int(x))
                                 for x in raw[59].split(',')
                                 if x.strip()]

        # 60: base_id
        # 61: group_id
        self.type[2] = common.Type(int(raw[62]))

        # 63: sell_mp
        # 64: latent_on_feed
        # 65: collab_id

        # Bitmap with some random flag values, not sure what they all do.
        self.flags = int(raw[66])

        self.furigana = str(raw[67])  # JP data only?
        self.limit_mult = int(raw[68])

        # 69: voice_id
        # 70: orb_skin_id
        # 71: tag string

        self.other_fields = raw[72:]

    def _stat_at_level(self, st: str, lv: Optional[int]) -> int:
        max_lv = self.max_level
        max_v: int = getattr(self, f'max_{st}')
        min_v: int = getattr(self, f'min_{st}')
        scale: int = getattr(self, f'{st}_scale')
        limit_mult = self.limit_mult

        if lv is None:
            lv = 120 if self.limit_mult > 0 else max_lv

        if max_lv == 1:
            # In this case, max_v may not equal to min_v.
            # e.g. http://pad.skyozora.com/pets/147
            # min_v is the correct number.
            return min_v

        # note that python's round implementation is different than the game's
        # implementation. So this function is not 100% accurate.
        if 1 <= lv <= max_lv:
            return round(
                min_v + (max_v - min_v) * ((lv - 1) / (max_lv - 1)) ** scale)

        if limit_mult <= 0 or not (100 <= lv <= 120):
            raise ValueError('level out of range')

        if 100 <= lv <= 110:
            return round(max_v * (1 + limit_mult * (lv - 99) / 1100))
        if 110 < lv <= 120:
            step = 1 if st == 'hp' else 0.5
            return round(
                    max_v * (1 + (limit_mult + step * (lv - 110)) / 100))

        assert False, 'should not reach here'
        return -1


    _FIELD_WHITELIST = frozenset([
        'attr_id', 'awakenings', 'card_id', 'cost', 'extra_latent_slot',
        'inheritable', 'limit_mult', 'max_atk', 'max_hp', 'max_level',
        'max_rcv', 'min_atk', 'min_hp', 'min_rcv', 'name', 'rarity',
        'sub_attr_id', 'super_awakenings', 'type',
    ])

    @property
    def inheritable(self) -> bool:
        return bool(self.flags & 1)

    @property
    def is_collab(self) -> bool:
        return bool(self.flags & 4)

    @property
    def extra_latent_slot(self) -> bool:
        return bool(self.flags & 32)

    @property
    def evo_type(self) -> common.EvoType:
        # guess the evolution type from card data
        if self.evo_mat[0] == 0:
            return common.EvoType.NO_EVO

        if self.extra_latent_slot:
            if self.evo_mat[0] == 5077: # イベントメダル【虹】の希石
                return common.EvoType.SUPER_REINCARNATION
            return common.EvoType.REINCARNATION

        if self.evo_mat[0] == 3826: # ドットリット
            return common.EvoType.PIXEL

        if self.awakenings.count(common.Awakening.AWOKEN_ASSIST):
            return common.EvoType.ASSIST

        if self.is_ult:
            return common.EvoType.ULTIMATE
        return common.EvoType.NORMAL_EVO

    @property
    def merged_json(self) -> Mapping[str, Any]:
        obj: MutableMapping[str, Any] = {}
        for k in self._FIELD_WHITELIST:
            obj[k] = getattr(self, k)
        obj['skill'] = self.skill.to_json()
        obj['leader_skill'] = self.leader_skill.to_json(is_leader_skill=True)
        obj['enemy_passive_resist'] = [
            {'name': s.name, 'effects': s.effects_to_tuples()}
            for s in self.enemy_passive_resist.values()
        ]
        if self.evo_skills:
            obj['evo_skills'] = [s.to_json() for s in self.evo_skills]
        return obj

    def atk_at_level(self, level: Optional[int]=None) -> int:
        return self._stat_at_level('atk', level)

    def hp_at_level(self, level: Optional[int]=None) -> int:
        return self._stat_at_level('hp', level)

    def rcv_at_level(self, level: Optional[int]=None) -> int:
        return self._stat_at_level('rcv', level)

    def has_evo_skill(self) -> bool:
        return bool(self.evo_skills)

    def __repr__(self) -> str:
        return str(self.__dict__)

    def color_code(self) -> str:
        if self.attr_id == common.Orb.JAMMER:
            return self.sub_attr_id.color_code()
        return self.attr_id.color_code()

    def dump(self, atk_eval: Callable[['Card'], int]=atk_at_level,
             rcv_eval: Callable[['Card'], int]=rcv_at_level,
             print_active_skill: bool=True, print_leader_skill: bool=False,
             evo_skill_index: Optional[int]=None,
             ) -> None:
        print(self.color_code(),
              self.name,
              common.Orb.NO_ORB.color_code(),
              ' ' * (50 - wcwidth.wcswidth(self.name)),
              f'{self.hp_at_level():8}',
              f'{atk_eval(self):8}',
              f'{rcv_eval(self):8}',
              sep='',
              end='  ')

        if print_active_skill:
            if evo_skill_index:
                desc = self.evo_skills[evo_skill_index].clean_description
            else:
                desc = self.skill.clean_description
            turn_max = self.skill.turn_max
            turn_min = self.skill.turn_min

            extra_turn = 0
            if evo_skill_index is not None:
                for i in range(1, evo_skill_index + 1):
                    extra_turn += self.evo_skills[i].turn_min_checked()

            print(f'{turn_max:2}/{turn_min:2}',
                  f'+{extra_turn:<2} ' if extra_turn else '    ',
                  desc,
                  sep='',
                  end='')
        if print_leader_skill:
            print(self.leader_skill.clean_description, end='')
        print()

Unflatten a card array.      Index is the slot containing the item count.     Width is the number of slots per item.     If replace is true, values are moved into an array at idx.     If replace is false, values are deleted. # pylint: disable=too-many-instance-attributes # Copied from # https://github.com/nachoapps/dadguide-data/blob/master/etl/pad/raw/card.py # and removed some fields for faster processing # 9: Appears to be related to the size of the monster. # If 5, the monster always spawns alone. Needs more research. # 27: Enemy turn timer for normal dungeons, and techs where # enemy_turns_alt is not populated. # 28~36: enemy hp/atk/def # Level range from 1 to 10 # 37: enemy_max_level # 38: enemy_coins_per_level # 39: enemy_xp_per_level # 40: ancestor_id # 46~50: un-evo mat # enemy AI releated things # 51: enemy_turns_alt # When >0, the enemy turn timer for technical dungeons. # 52: use_new_ai # 53: enemy_skill_max_counter # 54: enemy_skill_counter_increment # 55~56: unknown # 60: base_id # 61: group_id # 63: sell_mp # 64: latent_on_feed # 65: collab_id # Bitmap with some random flag values, not sure what they all do. # JP data only? # 69: voice_id # 70: orb_skin_id # 71: tag string # In this case, max_v may not equal to min_v. # e.g. http://pad.skyozora.com/pets/147 # min_v is the correct number. # note that python's round implementation is different than the game's # implementation. So this function is not 100% accurate. # guess the evolution type from card data # イベントメダル【虹】の希石 # ドットリット