import random
from typing import Callable

from Class.exceptions import GeneratorException, SettingsException
from Class.itemClass import KH2Item
from List.configDict import LevelUpStatBonus, StartingMovementOption, AbilityPoolOption, StartingVisitMode, locationType
from List.inventory import growth, ability, storyunlock
from List.inventory.growth import GrowthAbility, GrowthType
from List.inventory.storyunlock import StoryUnlock


class SeedModifier:
    @staticmethod
    def ability_list_modifier(
        option: AbilityPoolOption,
    ) -> Callable[[list[KH2Item], list[KH2Item]], list[KH2Item]]:
        if option == AbilityPoolOption.DEFAULT:
            return SeedModifier.default_ability_pool
        elif option == AbilityPoolOption.RANDOMIZE:
            return SeedModifier.random_ability_pool
        elif option == AbilityPoolOption.RANDOMIZE_SUPPORT:
            return SeedModifier.random_support_ability_pool
        elif option == AbilityPoolOption.RANDOMIZE_STACKABLE:
            return SeedModifier.random_stackable_ability_pool
        else:
            raise SettingsException("Invalid ability pool option")

    @staticmethod
    def default_ability_pool(
        action: list[KH2Item], support: list[KH2Item]
    ) -> list[KH2Item]:
        return action + support

    @staticmethod
    def random_ability_pool(
        action: list[KH2Item], support: list[KH2Item]
    ) -> list[KH2Item]:
        randomizable_list = action + support
        randomizable_dict: dict[str, KH2Item] = {i.Name: i for i in randomizable_list}
        possible_abilities = list(
            set(
                i.Name
                for i in randomizable_list
                if i.Name not in [ability.SecondChance.name, ability.OnceMore.name]
            )
        )
        possible_abilities.sort()
        random_ability_pool = []
        for _ in range(len(randomizable_list) - 2):
            choice: str = random.choice(possible_abilities)
            random_ability_pool.append(randomizable_dict[choice])
            # Limit only 1 of each action ability in the pool, to make it more interesting
            if choice in [i.Name for i in action]:
                possible_abilities.remove(choice)

        # Make sure there is one OM and one SC so the tracker behaves
        random_ability_pool.append(randomizable_dict[ability.SecondChance.name])
        random_ability_pool.append(randomizable_dict[ability.OnceMore.name])
        return random_ability_pool

    @staticmethod
    def random_support_ability_pool(
        action: list[KH2Item], support: list[KH2Item]
    ) -> list[KH2Item]:
        randomizable_list = support
        randomizable_dict: dict[str, KH2Item] = {i.Name: i for i in randomizable_list}
        possible_abilities = list(
            set(
                i.Name
                for i in randomizable_list
                if i.Name not in [ability.SecondChance.name, ability.OnceMore.name]
            )
        )
        possible_abilities.sort()
        random_ability_pool = []
        for _ in range(len(randomizable_list) - 2):
            choice: str = random.choice(possible_abilities)
            random_ability_pool.append(randomizable_dict[choice])

        # Make sure there is one OM and one SC so the tracker behaves
        random_ability_pool.append(randomizable_dict[ability.SecondChance.name])
        random_ability_pool.append(randomizable_dict[ability.OnceMore.name])
        return random_ability_pool + action

    @staticmethod
    def random_stackable_ability_pool(
        action: list[KH2Item], support: list[KH2Item]
    ) -> list[KH2Item]:
        stackable_abilities = [
            ability.ComboPlus.name,
            ability.AirComboPlus.name,
            ability.ComboBoost.name,
            ability.AirComboBoost.name,
            ability.ReactionBoost.name,
            ability.FinishingPlus.name,
            ability.NegativeCombo.name,
            ability.BerserkCharge.name,
            ability.DamageDrive.name,
            ability.DriveBoost.name,
            ability.FormBoost.name,
            ability.SummonBoost.name,
            ability.ExperienceBoost.name,
            ability.Draw.name,
            ability.Jackpot.name,
            ability.LuckyLucky.name,
            ability.DriveConverter.name,
            ability.FireBoost.name,
            ability.BlizzardBoost.name,
            ability.ThunderBoost.name,
            ability.ItemBoost.name,
            ability.MpRage.name,
            ability.MpHaste.name,
            ability.MpHastera.name,
            ability.MpHastega.name,
            ability.Defender.name,
            ability.DamageControl.name,
            ability.CombinationBoost.name,
        ]
        ability_list = support + action
        ability_dict: dict[str, KH2Item] = {i.Name: i for i in ability_list}
        unique_abilities = list(set(i.Name for i in ability_list))
        unique_abilities.sort()
        random_ability_pool = []
        for unique_ability in unique_abilities:
            random_ability_pool.append(ability_dict[unique_ability])
        for _ in range(len(ability_list) - len(unique_abilities)):
            choice = random.choice(stackable_abilities)
            random_ability_pool.append(ability_dict[choice])

        return random_ability_pool

    @staticmethod
    def level_up_stat_pool(glass_cannon: bool) -> list[tuple[LevelUpStatBonus, int]]:
        stat_pool = SeedModifier.level_up_stat_pool_weighted(
            def_rate=0 if glass_cannon else 25
        )
        return [(s[0], s[1]) for s in stat_pool]

    @staticmethod
    def level_up_stat_pool_weighted(
        str_rate: int = 25,
        mag_rate: int = 25,
        def_rate: int = 25,
        ap_rate: int = 25,
    ) -> list[tuple[LevelUpStatBonus, int, int]]:
        valid_stat_list = [
            (LevelUpStatBonus.STRENGTH, 2, str_rate),
            (LevelUpStatBonus.MAGIC, 2, mag_rate),
            (LevelUpStatBonus.DEFENSE, 1, def_rate),
            (LevelUpStatBonus.AP, 2, ap_rate),
        ]
        # remove any stats with zero weight
        valid_stat_list = [
            stat_config for stat_config in valid_stat_list if stat_config[2] > 0
        ]
        if len(valid_stat_list)<2:
            raise GeneratorException("Not enough valid stats for level ups")

        # rescale the weights to sum to 100
        total_sum = sum([stat_config[2] for stat_config in valid_stat_list])
        valid_stat_list = [
            (stat_config[0], stat_config[1], (100 * stat_config[2]) // total_sum)
            for stat_config in valid_stat_list
        ]
        new_total_sum = sum([stat_config[2] for stat_config in valid_stat_list])
        diff_adjustment = 0
        while new_total_sum + diff_adjustment < 100:
            old_tuple = valid_stat_list[diff_adjustment % len(valid_stat_list)]
            valid_stat_list[diff_adjustment % len(valid_stat_list)] = (
                old_tuple[0],
                old_tuple[1],
                old_tuple[2] + 1,
            )
            diff_adjustment += 1

        return valid_stat_list

    @staticmethod
    def starting_growth(option: StartingMovementOption) -> list[GrowthAbility]:
        if option == StartingMovementOption.DISABLED:
            return []
        elif option == StartingMovementOption.LEVEL_1:
            return growth.all_growth_to_level(1)
        elif option == StartingMovementOption.LEVEL_2:
            return growth.all_growth_to_level(2)
        elif option == StartingMovementOption.LEVEL_3:
            return growth.all_growth_to_level(3)
        elif option == StartingMovementOption.LEVEL_4:
            return growth.all_growth_to_level(4)
        elif option == StartingMovementOption.RANDOM_3:
            return SeedModifier._random_growth(3)
        elif option == StartingMovementOption.RANDOM_5:
            return SeedModifier._random_growth(5)
        elif option == StartingMovementOption.RANDOM_7:
            return SeedModifier._random_growth(7)
        elif option == StartingMovementOption.RANDOM_9:
            return SeedModifier._random_growth(9)
        else:
            raise GeneratorException(f"Unknown option {option}")

    @staticmethod
    def _random_growth(num_growth_abilities: int) -> list[GrowthAbility]:
        options = [growth_ability.growth_type for growth_ability in growth.all_growth()]
        levels_by_growth_type: dict[GrowthType, int] = {}
        for chosen_growth_type in random.sample(options, k=num_growth_abilities):
            if chosen_growth_type in levels_by_growth_type:
                levels_by_growth_type[chosen_growth_type] = (
                    levels_by_growth_type[chosen_growth_type] + 1
                )
            else:
                levels_by_growth_type[chosen_growth_type] = 1

        random_growth = []
        for growth_type, level in levels_by_growth_type.items():
            random_growth.extend(growth.growth_to_level(level, growth_type))
        return random_growth

    @staticmethod
    def starting_unlocks(
            mode: StartingVisitMode,
            random_range: tuple[int, int],
            specific_unlocks: dict[locationType, int],
    ) -> list[StoryUnlock]:
        if mode is StartingVisitMode.ALL:
            return storyunlock.all_individual_story_unlocks()
        elif mode is StartingVisitMode.FIRST:
            return storyunlock.all_story_unlocks()
        elif mode is StartingVisitMode.NONE:
            return []
        elif mode is StartingVisitMode.RANDOM:
            random_min, random_max = random_range
            random_count = random.randint(random_min, random_max)
            return random.sample(storyunlock.all_individual_story_unlocks(), k=random_count)
        elif mode is StartingVisitMode.SPECIFIC:
            result: list[StoryUnlock] = []
            for location, count in specific_unlocks.items():
                result.extend([storyunlock.story_unlock_for_location(location)] * count)
            return result
        elif mode is StartingVisitMode.CUSTOM:
            result: list[StoryUnlock] = []
            random_pool: list[StoryUnlock] = []
            for unlock in storyunlock.all_story_unlocks():
                specific_count = specific_unlocks[unlock.location]
                result.extend([unlock] * specific_count)
                random_pool.extend([unlock] * (unlock.visit_count - specific_count))
            random_min, random_max = random_range
            random_count = random.randint(random_min, random_max)
            result.extend(random.sample(random_pool, k=random_count))
            return result
        else:
            raise GeneratorException(f"Unknown mode {mode}")
