import os
import settings
import typing
import threading
import base64
import random
from copy import deepcopy
from typing import TextIO

from Utils import __version__
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification, LocationProgressType
from Fill import fill_restrictive, FillError, sweep_from_pool
from worlds.AutoWorld import World, WebWorld
from worlds.generic.Rules import add_item_rule
from .items import item_table, item_groups
from .locations import location_data, PokemonRBLocation
from .regions import create_regions
from .options import PokemonRBOptions
from .rom_addresses import rom_addresses
from .text import encode_text
from .rom import generate_output, PokemonRedProcedurePatch, PokemonBlueProcedurePatch
from .pokemon import process_pokemon_data, process_move_data, verify_hm_moves
from .encounters import process_pokemon_locations, process_trainer_data
from .rules import set_rules
from .level_scaling import level_scaling
from . import logic
from . import poke_data
from . import client


class PokemonSettings(settings.Group):
    class RedRomFile(settings.UserFilePath):
        """File names of the Pokemon Red and Blue roms"""
        description = "Pokemon Red (UE) ROM File"
        copy_to = "Pokemon Red (UE) [S][!].gb"
        md5s = [PokemonRedProcedurePatch.hash]

    class BlueRomFile(settings.UserFilePath):
        description = "Pokemon Blue (UE) ROM File"
        copy_to = "Pokemon Blue (UE) [S][!].gb"
        md5s = [PokemonBlueProcedurePatch.hash]

    red_rom_file: RedRomFile = RedRomFile(RedRomFile.copy_to)
    blue_rom_file: BlueRomFile = BlueRomFile(BlueRomFile.copy_to)


class PokemonWebWorld(WebWorld):
    setup_en = Tutorial(
        "Multiworld Setup Guide",
        "A guide to playing Pokémon Red and Blue with Archipelago.",
        "English",
        "setup_en.md",
        "setup/en",
        ["Alchav"]
    )

    setup_es = Tutorial(
        setup_en.tutorial_name,
        setup_en.description,
        "Español",
        "setup_es.md",
        "setup/es",
        ["Shiny"]
    )

    tutorials = [setup_en, setup_es]


class PokemonRedBlueWorld(World):
    """Pokémon Red and Pokémon Blue are the original monster-collecting turn-based RPGs.  Explore the Kanto region with
    your Pokémon, catch more than 150 unique creatures, earn badges from the region's Gym Leaders, and challenge the
    Elite Four to become the champion!"""
    # -MuffinJets#4559
    game = "Pokemon Red and Blue"

    options_dataclass = PokemonRBOptions
    options: PokemonRBOptions

    settings: typing.ClassVar[PokemonSettings]

    required_client_version = (0, 4, 2)

    topology_present = True

    item_name_to_id = {name: data.id for name, data in item_table.items()}
    location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"
                           and location.address is not None}
    item_name_groups = item_groups

    web = PokemonWebWorld()

    def __init__(self, multiworld: MultiWorld, player: int):
        super().__init__(multiworld, player)
        self.item_pool = []
        self.total_key_items = None
        self.fly_map = None
        self.fly_map_code = None
        self.town_map_fly_map = None
        self.town_map_fly_map_code = None
        self.extra_badges = {}
        self.type_chart = None
        self.local_poke_data = None
        self.local_move_data = None
        self.local_tms = None
        self.learnsets = None
        self.trainer_name = None
        self.rival_name = None
        self.traps = None
        self.trade_mons = {}
        self.finished_level_scaling = threading.Event()
        self.dexsanity_table = []
        self.trainersanity_table = []
        self.local_locs = []
        self.pc_item = None

    @classmethod
    def stage_generate_early(cls, multiworld: MultiWorld):

        seed_groups = {}
        pokemon_rb_worlds = multiworld.get_game_worlds("Pokemon Red and Blue")

        for world in pokemon_rb_worlds:
            if not (world.options.type_chart_seed.value.isdigit() or world.options.type_chart_seed.value == "random"):
                seed_groups[world.options.type_chart_seed.value] = seed_groups.get(world.options.type_chart_seed.value,
                                                                                   []) + [world]

        copy_chart_worlds = {}

        for worlds in seed_groups.values():
            chosen_world = multiworld.random.choice(worlds)
            for world in worlds:
                if world is not chosen_world:
                    copy_chart_worlds[world.player] = chosen_world

        for world in pokemon_rb_worlds:
            if world.player in copy_chart_worlds:
                continue
            tc_random = world.random
            if world.options.type_chart_seed.value.isdigit():
                tc_random = random.Random()
                tc_random.seed(int(world.options.type_chart_seed.value))

            if world.options.randomize_type_chart == "vanilla":
                chart = deepcopy(poke_data.type_chart)
            elif world.options.randomize_type_chart == "randomize":
                types = poke_data.type_names.values()
                matchups = []
                for type1 in types:
                    for type2 in types:
                        matchups.append([type1, type2])
                tc_random.shuffle(matchups)
                immunities = world.options.immunity_matchups.value
                super_effectives = world.options.super_effective_matchups.value
                not_very_effectives = world.options.not_very_effective_matchups.value
                normals = world.options.normal_matchups.value
                while super_effectives + not_very_effectives + normals < 225 - immunities:
                    if super_effectives == not_very_effectives == normals == 0:
                        super_effectives = 225
                        not_very_effectives = 225
                        normals = 225
                    else:
                        super_effectives += world.options.super_effective_matchups.value
                        not_very_effectives += world.options.not_very_effective_matchups.value
                        normals += world.options.normal_matchups.value
                if super_effectives + not_very_effectives + normals > 225 - immunities:
                    total = super_effectives + not_very_effectives + normals
                    excess = total - (225 - immunities)
                    subtract_amounts = (
                        int((excess / (super_effectives + not_very_effectives + normals)) * super_effectives),
                        int((excess / (super_effectives + not_very_effectives + normals)) * not_very_effectives),
                        int((excess / (super_effectives + not_very_effectives + normals)) * normals))
                    super_effectives -= subtract_amounts[0]
                    not_very_effectives -= subtract_amounts[1]
                    normals -= subtract_amounts[2]
                    while super_effectives + not_very_effectives + normals > 225 - immunities:
                        r = tc_random.randint(0, 2)
                        if r == 0 and super_effectives:
                            super_effectives -= 1
                        elif r == 1 and not_very_effectives:
                            not_very_effectives -= 1
                        elif normals:
                            normals -= 1
                chart = []
                for matchup_list, matchup_value in zip([immunities, normals, super_effectives, not_very_effectives],
                                                       [0, 10, 20, 5]):
                    for _ in range(matchup_list):
                        matchup = matchups.pop()
                        matchup.append(matchup_value)
                        chart.append(matchup)
            elif world.options.randomize_type_chart == "chaos":
                types = poke_data.type_names.values()
                matchups = []
                for type1 in types:
                    for type2 in types:
                        matchups.append([type1, type2])
                chart = []
                values = list(range(21))
                tc_random.shuffle(matchups)
                tc_random.shuffle(values)
                for matchup in matchups:
                    value = values.pop(0)
                    values.append(value)
                    matchup.append(value)
                    chart.append(matchup)
            # sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective"
            # matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to
            # damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes
            # to the way effectiveness messages are generated.
            world.type_chart = sorted(chart, key=lambda matchup: -matchup[2])

        for player in copy_chart_worlds:
            multiworld.worlds[player].type_chart = copy_chart_worlds[player].type_chart

    def generate_early(self):
        def encode_name(name, t):
            try:
                if len(encode_text(name)) > 7:
                    raise IndexError(f"{t} name too long for player {self.multiworld.player_name[self.player]}. Must be 7 characters or fewer.")
                return encode_text(name, length=8, whitespace="@", safety=True)
            except KeyError as e:
                raise KeyError(f"Invalid character(s) in {t} name for player {self.multiworld.player_name[self.player]}") from e
        if self.options.trainer_name == "choose_in_game":
            self.trainer_name = "choose_in_game"
        else:
            self.trainer_name = encode_name(self.options.trainer_name.value, "Player")
        if self.options.rival_name == "choose_in_game":
            self.rival_name = "choose_in_game"
        else:
            self.rival_name = encode_name(self.options.rival_name.value, "Rival")

        if not self.options.badgesanity:
            self.options.non_local_items.value -= self.item_name_groups["Badges"]

        if self.options.key_items_only:
            self.options.trainersanity.value = 0
            self.options.dexsanity.value = 0
            self.options.randomize_hidden_items = \
                self.options.randomize_hidden_items.from_text("off")

        if self.options.badges_needed_for_hm_moves.value >= 2:
            badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"]
            if self.options.badges_needed_for_hm_moves.value == 3:
                badges = ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge",
                          "Soul Badge", "Volcano Badge", "Earth Badge"]
                self.random.shuffle(badges)
                badges_to_add += [badges.pop(), badges.pop()]
            hm_moves = ["Cut", "Fly", "Surf", "Strength", "Flash"]
            self.random.shuffle(hm_moves)
            self.extra_badges = {}
            for badge in badges_to_add:
                self.extra_badges[hm_moves.pop()] = badge

        process_move_data(self)
        process_pokemon_data(self)

        self.dexsanity_table = [
            *(True for _ in range(round(self.options.dexsanity.value))),
            *(False for _ in range(151 - round(self.options.dexsanity.value)))
        ]
        self.random.shuffle(self.dexsanity_table)

        self.trainersanity_table = [
            *(True for _ in range(self.options.trainersanity.value)),
            *(False for _ in range(317 - self.options.trainersanity.value))
        ]
        self.random.shuffle(self.trainersanity_table)

    def create_items(self):
        self.multiworld.itempool += self.item_pool

    @classmethod
    def stage_fill_hook(cls, multiworld, progitempool, usefulitempool, filleritempool, fill_locations):
        locs = []
        for world in multiworld.get_game_worlds("Pokemon Red and Blue"):
            locs += world.local_locs
        for loc in sorted(locs):
            if loc.item:
                continue
            itempool = progitempool + usefulitempool + filleritempool
            multiworld.random.shuffle(itempool)
            unplaced_items = []
            for i, item in enumerate(itempool):
                if ((item.player == loc.player or (item.player in multiworld.groups
                                                   and loc.player in multiworld.groups[item.player]["players"]))
                        and loc.can_fill(multiworld.state, item, False)):
                    if item.advancement:
                        pool = progitempool
                    elif item.useful:
                        pool = usefulitempool
                    else:
                        pool = filleritempool
                    for i, check_item in enumerate(pool):
                        if item is check_item:
                            pool.pop(i)
                            break
                    if item.advancement:
                        state = sweep_from_pool(multiworld.state, progitempool + unplaced_items)
                    if (not item.advancement) or state.can_reach(loc, "Location", loc.player):
                        multiworld.push_item(loc, item, False)
                        fill_locations.remove(loc)
                        break
                    else:
                        unplaced_items.append(item)
            progitempool += [item for item in unplaced_items if item.advancement]
            usefulitempool += [item for item in unplaced_items if item.useful]
            filleritempool += [item for item in unplaced_items if (not item.advancement) and (not item.useful)]

    def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations):
        if not self.options.badgesanity:
            # Door Shuffle options besides Simple place badges during door shuffling
            if self.options.door_shuffle in ("off", "simple"):
                badges = [item for item in progitempool if "Badge" in item.name and item.player == self.player]
                for badge in badges:
                    self.multiworld.itempool.remove(badge)
                    progitempool.remove(badge)
                for attempt in range(6):
                    badgelocs = [
                        self.multiworld.get_location(loc, self.player) for loc in [
                            "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize",
                            "Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize",
                            "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize",
                            "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"
                        ] if self.multiworld.get_location(loc, self.player).item is None]
                    state = self.multiworld.get_all_state(False, True, False)
                    # Give it two tries to place badges with wild Pokemon and learnsets as-is.
                    # If it can't, then try with all Pokemon collected, and we'll try to fix HM move availability after.
                    if attempt > 1:
                        for mon in poke_data.pokemon_data.keys():
                            state.collect(self.create_item(mon), True)
                    state.sweep_for_advancements()
                    self.random.shuffle(badges)
                    self.random.shuffle(badgelocs)
                    badgelocs_copy = badgelocs.copy()
                    # allow_partial so that unplaced badges aren't lost, for debugging purposes
                    fill_restrictive(self.multiworld, state, badgelocs_copy, badges, True, True, allow_partial=True)
                    if len(badges) > 8 - len(badgelocs):
                        for location in badgelocs:
                            if location.item:
                                badges.append(location.item)
                                location.item = None
                        continue
                    else:
                        for location in badgelocs:
                            if location.item:
                                fill_locations.remove(location)
                        progitempool += badges
                        break
                else:
                    raise FillError(f"Failed to place badges for player {self.player}")
            verify_hm_moves(self.multiworld, self, self.player)

        if self.options.key_items_only:
            return

        tms = [item for item in usefulitempool + filleritempool if item.name.startswith("TM") and (item.player ==
               self.player or (item.player in self.multiworld.groups and self.player in
                               self.multiworld.groups[item.player]["players"]))]
        if len(tms) > 7:
            for gym_leader in (("Pewter Gym", "Brock"), ("Cerulean Gym", "Misty"), ("Vermilion Gym", "Lt. Surge"),
                               ("Celadon Gym-C", "Erika"), ("Fuchsia Gym", "Koga"), ("Saffron Gym-C", "Sabrina"),
                               ("Cinnabar Gym", "Blaine"), ("Viridian Gym", "Giovanni")):
                loc = self.multiworld.get_location(f"{gym_leader[0].split('-')[0]} - {gym_leader[1]} TM",
                                                   self.player)
                if loc.item:
                    continue
                for party in self.multiworld.get_location(gym_leader[0] + " - Trainer Parties", self.player).party_data:
                    if party["party_address"] == \
                            f"Trainer_Party_{gym_leader[1].replace('. ', '').replace('Giovanni', 'Viridian_Gym_Giovanni')}_A":
                        mon = party["party"][-1]
                        learnable_tms = [tm for tm in tms if self.local_poke_data[mon]["tms"][
                            int((int(tm.name[2:4]) - 1) / 8)] & 1 << ((int(tm.name[2:4]) - 1) % 8)]
                        if not learnable_tms:
                            learnable_tms = tms
                        tm = self.random.choice(learnable_tms)

                        loc.place_locked_item(tm)
                        fill_locations.remove(loc)
                        tms.remove(tm)
                        if tm.useful:
                            usefulitempool.remove(tm)
                        else:
                            filleritempool.remove(tm)
                        break
                else:
                    raise Exception("Missing Gym Leader data")

    def pre_fill(self) -> None:
        process_pokemon_locations(self)
        process_trainer_data(self)
        locs = [location.name for location in location_data if location.type != "Item"]
        for location in self.multiworld.get_locations(self.player):
            if location.name in locs:
                location.show_in_spoiler = False
        verify_hm_moves(self.multiworld, self, self.player)

        # Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not
        # fail. Re-use test_state from previous final loop.
        all_state = self.multiworld.get_all_state(False, True, False)
        evolutions_region = self.multiworld.get_region("Evolution", self.player)
        for location in evolutions_region.locations.copy():
            if not all_state.can_reach(location, player=self.player):
                evolutions_region.locations.remove(location)

        if self.options.old_man == "early_parcel":
            self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1
            if self.options.dexsanity:
                for i, mon in enumerate(poke_data.pokemon_data):
                    if self.dexsanity_table[i]:
                        location = self.multiworld.get_location(f"Pokedex - {mon}", self.player)
                        add_item_rule(location, lambda item: item.name != "Oak's Parcel" or item.player != self.player)

        # Place local items in some locations to prevent save-scumming. Also Oak's PC to prevent an "AP Item" from
        # entering the player's inventory.

        locs = {self.multiworld.get_location("Fossil - Choice A", self.player),
                self.multiworld.get_location("Fossil - Choice B", self.player)}

        if not self.options.key_items_only:
            rule = None
            if self.options.fossil_check_item_types == "key_items":
                rule = lambda i: i.advancement
            elif self.options.fossil_check_item_types == "unique_items":
                rule = lambda i: i.name in item_groups["Unique"]
            elif self.options.fossil_check_item_types == "no_key_items":
                rule = lambda i: not i.advancement
            if rule:
                for loc in locs:
                    add_item_rule(loc, rule)

        for mon in ([" ".join(self.multiworld.get_location(
                f"Oak's Lab - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)]
                + [" ".join(self.multiworld.get_location(
                f"Saffron Fighting Dojo - Gift {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 3)]
                + ["Vaporeon", "Jolteon", "Flareon"]):
            if self.dexsanity_table[poke_data.pokemon_dex[mon] - 1]:
                loc = self.multiworld.get_location(f"Pokedex - {mon}", self.player)
                if loc.item is None:
                    locs.add(loc)

        for loc in sorted(locs):
            if loc.name in self.options.priority_locations.value:
                add_item_rule(loc, lambda i: i.advancement)
            add_item_rule(loc, lambda i: i.player == self.player
                                         or (i.player in self.multiworld.groups
                                             and self.player in self.multiworld.groups[i.player]["players"]))
            if self.options.old_man == "early_parcel" and loc.name != "Player's House 2F - Player's PC":
                add_item_rule(loc, lambda i: i.name != "Oak's Parcel")

        self.local_locs = locs

        all_state = self.multiworld.get_all_state(False, True, False)

        reachable_mons = set()
        for mon in poke_data.pokemon_data:
            if all_state.has(mon, self.player) or all_state.has(f"Static {mon}", self.player):
                reachable_mons.add(mon)

        # The large number of wild Pokemon can make sweeping for events time-consuming, and is especially bad in
        # the spoiler playthrough calculation because it removes each advancement item one at a time to verify
        # if the game is beatable without it. We go through each zone and flag any duplicates as useful.
        # Especially with area 1-to-1 mapping / vanilla wild Pokémon, this should cut down significantly on wasted time.
        for region in self.multiworld.get_regions(self.player):
            region_mons = set()
            for location in region.locations:
                if "Wild Pokemon" in location.name:
                    if location.item.name in region_mons:
                        location.item.classification = ItemClassification.useful
                    else:
                        region_mons.add(location.item.name)

        self.options.elite_four_pokedex_condition.total = \
            int((len(reachable_mons) / 100) * self.options.elite_four_pokedex_condition.value)

        if self.options.accessibility == "full":
            balls = [self.create_item(ball) for ball in ["Poke Ball", "Great Ball", "Ultra Ball"]]
            traps = [self.create_item(trap) for trap in item_groups["Traps"]]
            locations = [location for location in self.multiworld.get_locations(self.player) if "Pokedex - " in
                         location.name]
            pokedex = self.multiworld.get_region("Pokedex", self.player)
            remove_items = 0

            for location in locations:
                if not location.can_reach(all_state):
                    pokedex.locations.remove(location)
                    if location in self.local_locs:
                        self.local_locs.remove(location)
                    self.dexsanity_table[poke_data.pokemon_dex[location.name.split(" - ")[1]] - 1] = False
                    remove_items += 1

            for _ in range(remove_items):
                balls.append(balls.pop(0))
                for ball in balls:
                    try:
                        self.multiworld.itempool.remove(ball)
                    except ValueError:
                        continue
                    else:
                        break
                else:
                    self.random.shuffle(traps)
                    for trap in traps:
                        try:
                            self.multiworld.itempool.remove(trap)
                        except ValueError:
                            continue
                        else:
                            break
                    else:
                        raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location")

        if not self.options.key_items_only:
            loc = self.multiworld.get_location("Player's House 2F - Player's PC", self.player)
            # Absolutely cannot have another player's item
            if loc.item is not None and loc.item.player != self.player:
                self.multiworld.itempool.append(loc.item)
                loc.item = None
            loc.place_locked_item(self.pc_item)

    def get_pre_fill_items(self) -> typing.List["Item"]:
        pool = [self.create_item(mon) for mon in poke_data.pokemon_data]
        pool.append(self.pc_item)
        return pool

    @classmethod
    def stage_post_fill(cls, multiworld):
        # Convert all but one of each instance of a wild Pokemon to useful classification.
        # This cuts down on time spent calculating the spoiler playthrough.
        found_mons = set()
        for sphere in multiworld.get_spheres():
            mon_locations_in_sphere = {}
            for location in sphere:
                if (location.game == location.item.game == "Pokemon Red and Blue"
                        and (location.item.name in poke_data.pokemon_data.keys() or "Static " in location.item.name)
                        and location.item.advancement):
                    key = (location.player, location.item.name)
                    if key in found_mons:
                        location.item.classification = ItemClassification.useful
                    else:
                        mon_locations_in_sphere.setdefault(key, []).append(location)
            for key, mon_locations in mon_locations_in_sphere.items():
                found_mons.add(key)
                if len(mon_locations) > 1:
                    # Sort for deterministic results.
                    mon_locations.sort()
                    # Convert all but the first to useful classification.
                    for location in mon_locations[1:]:
                        location.item.classification = ItemClassification.useful

    def create_regions(self):
        if (self.options.old_man == "vanilla" or
                self.options.door_shuffle in ("full", "insanity")):
            fly_map_codes = self.random.sample(range(2, 11), 2)
        elif (self.options.door_shuffle == "simple" or
                self.options.route_3_condition == "boulder_badge" or
              (self.options.route_3_condition == "any_badge" and
               self.options.badgesanity)):
            fly_map_codes = self.random.sample(range(3, 11), 2)

        else:
            fly_map_codes = self.random.sample([4, 6, 7, 8, 9, 10], 2)
        if self.options.free_fly_location:
            fly_map_code = fly_map_codes[0]
        else:
            fly_map_code = 0
        if self.options.town_map_fly_location:
            town_map_fly_map_code = fly_map_codes[1]
        else:
            town_map_fly_map_code = 0
        fly_maps = ["Pallet Town", "Viridian City", "Pewter City", "Cerulean City", "Lavender Town",
                    "Vermilion City", "Celadon City", "Fuchsia City", "Cinnabar Island", "Indigo Plateau",
                    "Saffron City"]
        self.fly_map = fly_maps[fly_map_code]
        self.town_map_fly_map = fly_maps[town_map_fly_map_code]
        self.fly_map_code = fly_map_code
        self.town_map_fly_map_code = town_map_fly_map_code

        create_regions(self)
        self.multiworld.completion_condition[self.player] = lambda state, player=self.player: state.has("Become Champion", player=player)

    def set_rules(self):
        set_rules(self.multiworld, self, self.player)

    def create_item(self, name: str) -> Item:
        return PokemonRBItem(name, self.player)

    @classmethod
    def stage_generate_output(cls, multiworld, output_directory):
        level_scaling(multiworld)

    def generate_output(self, output_directory: str):
        generate_output(self, output_directory)

    def modify_multidata(self, multidata: dict):
        rom_name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0',
                             'utf8')[:21]
        rom_name.extend([0] * (21 - len(rom_name)))
        new_name = base64.b64encode(bytes(rom_name)).decode()
        multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]

    def write_spoiler_header(self, spoiler_handle: TextIO):
        spoiler_handle.write(f"Cerulean Cave Total Key Items:   {self.options.cerulean_cave_key_items_condition.total}\n")
        spoiler_handle.write(f"Elite Four Total Key Items:      {self.options.elite_four_key_items_condition.total}\n")
        spoiler_handle.write(f"Elite Four Total Pokemon:        {self.options.elite_four_pokedex_condition.total}\n")
        if self.options.free_fly_location:
            spoiler_handle.write(f"Free Fly Location:               {self.fly_map}\n")
        if self.options.town_map_fly_location:
            spoiler_handle.write(f"Town Map Fly Location:           {self.town_map_fly_map}\n")
        if self.extra_badges:
            for hm_move, badge in self.extra_badges.items():
                spoiler_handle.write(hm_move + " enabled by: " + (" " * 20)[:20 - len(hm_move)] + badge + "\n")

    def write_spoiler(self, spoiler_handle):
        if self.options.randomize_type_chart:
            spoiler_handle.write(f"\n\nType matchups ({self.multiworld.player_name[self.player]}):\n\n")
            for matchup in self.type_chart:
                spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n")
        spoiler_handle.write(f"\n\nPokémon locations ({self.multiworld.player_name[self.player]}):\n\n")
        pokemon_locs = [location.name for location in location_data if location.type not in ("Item", "Trainer Parties")]
        for location in self.multiworld.get_locations(self.player):
            if location.name in pokemon_locs:
                spoiler_handle.write(location.name + ": " + location.item.name + "\n")

    def get_filler_item_name(self) -> str:
        combined_traps = (self.options.poison_trap_weight.value
                          + self.options.fire_trap_weight.value
                          + self.options.paralyze_trap_weight.value
                          + self.options.ice_trap_weight.value
                          + self.options.sleep_trap_weight.value)
        if (combined_traps > 0 and
                self.random.randint(1, 100) <= self.options.trap_percentage.value):
            return self.select_trap()
        banned_items = item_groups["Unique"]
        if (((not self.options.tea) or "Saffron City" not in [self.fly_map, self.town_map_fly_map])
                and (not self.options.door_shuffle)):
            # under these conditions, you should never be able to reach the Copycat or Pokémon Tower without being
            # able to reach the Celadon Department Store, so Poké Dolls would not allow early access to anything
            banned_items.append("Poke Doll")
        if not self.options.tea:
            banned_items += item_groups["Vending Machine Drinks"]
        return self.random.choice([item for item in item_table if item_table[item].id and item_table[
            item].classification == ItemClassification.filler and item not in banned_items])

    def select_trap(self):
        if self.traps is None:
            self.traps = []
            self.traps += ["Poison Trap"] * self.options.poison_trap_weight.value
            self.traps += ["Fire Trap"] * self.options.fire_trap_weight.value
            self.traps += ["Paralyze Trap"] * self.options.paralyze_trap_weight.value
            self.traps += ["Ice Trap"] * self.options.ice_trap_weight.value
            self.traps += ["Sleep Trap"] * self.options.sleep_trap_weight.value
        return self.random.choice(self.traps)

    def extend_hint_information(self, hint_data):
        if self.options.dexsanity or self.options.door_shuffle:
            hint_data[self.player] = {}
        if self.options.dexsanity:
            mon_locations = {mon: set() for mon in poke_data.pokemon_data.keys()}
            for loc in location_data:
                if loc.type in ["Wild Encounter", "Static Pokemon", "Legendary Pokemon", "Missable Pokemon"]:
                    mon = self.multiworld.get_location(loc.name, self.player).item.name
                    if mon.startswith("Static ") or mon.startswith("Missable "):
                        mon = " ".join(mon.split(" ")[1:])
                    mon_locations[mon].add(loc.name.split(" -")[0])
            for i, mon in enumerate(mon_locations):
                if self.dexsanity_table[i] and mon_locations[mon]:
                    hint_data[self.player][self.multiworld.get_location(f"Pokedex - {mon}", self.player).address] =\
                        ", ".join(mon_locations[mon])

        if self.options.door_shuffle:
            for location in self.multiworld.get_locations(self.player):
                if location.parent_region.entrance_hint and location.address:
                    hint_data[self.player][location.address] = location.parent_region.entrance_hint

    def fill_slot_data(self) -> dict:
        ret = {
            "second_fossil_check_condition": self.options.second_fossil_check_condition.value,
            "require_item_finder": self.options.require_item_finder.value,
            "randomize_hidden_items": self.options.randomize_hidden_items.value,
            "badges_needed_for_hm_moves": self.options.badges_needed_for_hm_moves.value,
            "oaks_aide_rt_2": self.options.oaks_aide_rt_2.value,
            "oaks_aide_rt_11": self.options.oaks_aide_rt_11.value,
            "oaks_aide_rt_15": self.options.oaks_aide_rt_15.value,
            "extra_key_items": self.options.extra_key_items.value,
            "extra_strength_boulders": self.options.extra_strength_boulders.value,
            "tea": self.options.tea.value,
            "old_man": self.options.old_man.value,
            "elite_four_badges_condition": self.options.elite_four_badges_condition.value,
            "elite_four_key_items_condition": self.options.elite_four_key_items_condition.total,
            "elite_four_pokedex_condition": self.options.elite_four_pokedex_condition.total,
            "victory_road_condition": self.options.victory_road_condition.value,
            "route_22_gate_condition": self.options.route_22_gate_condition.value,
            "route_3_condition": self.options.route_3_condition.value,
            "robbed_house_officer": self.options.robbed_house_officer.value,
            "viridian_gym_condition": self.options.viridian_gym_condition.value,
            "cerulean_cave_badges_condition": self.options.cerulean_cave_badges_condition.value,
            "cerulean_cave_key_items_condition": self.options.cerulean_cave_key_items_condition.total,
            "free_fly_map": self.fly_map_code,
            "town_map_fly_map": self.town_map_fly_map_code,
            "extra_badges": self.extra_badges,
            "randomize_pokedex": self.options.randomize_pokedex.value,
            "trainersanity": self.options.trainersanity.value,
            "death_link": self.options.death_link.value,
            "prizesanity": self.options.prizesanity.value,
            "key_items_only": self.options.key_items_only.value,
            "poke_doll_skip": self.options.poke_doll_skip.value,
            "bicycle_gate_skips": self.options.bicycle_gate_skips.value,
            "stonesanity": self.options.stonesanity.value,
            "door_shuffle": self.options.door_shuffle.value,
            "warp_tile_shuffle": self.options.warp_tile_shuffle.value,
            "dark_rock_tunnel_logic": self.options.dark_rock_tunnel_logic.value,
            "split_card_key": self.options.split_card_key.value,
            "all_elevators_locked": self.options.all_elevators_locked.value,
            "require_pokedex": self.options.require_pokedex.value,
            "area_1_to_1_mapping": self.options.area_1_to_1_mapping.value,
            "blind_trainers": self.options.blind_trainers.value,
            "game_version": self.options.game_version.value,
            "exp_all": self.options.exp_all.value,

        }
        if self.options.type_chart_seed == "random" or self.options.type_chart_seed.value.isdigit():
            ret["type_chart"] = self.type_chart

        return ret

class PokemonRBItem(Item):
    game = "Pokemon Red and Blue"
    type = None

    def __init__(self, name, player: int = None):
        item_data = item_table[name]
        super(PokemonRBItem, self).__init__(
            name,
            item_data.classification,
            item_data.id, player
        )
