"""
Archipelago World definition for Pokemon Emerald Version
"""
from collections import Counter
import copy
import logging
import os
import pkgutil
from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar, TextIO, Union

from BaseClasses import CollectionState, ItemClassification, MultiWorld, Tutorial, LocationProgressType
from Fill import FillError, fill_restrictive
from Options import OptionError, Toggle
import settings
from worlds.AutoWorld import WebWorld, World

from .client import PokemonEmeraldClient  # Unused, but required to register with BizHawkClient
from .data import LEGENDARY_POKEMON, MapData, SpeciesData, TrainerData, LocationCategory, data as emerald_data
from .groups import ITEM_GROUPS, LOCATION_GROUPS
from .items import PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification, offset_item_value
from .locations import (PokemonEmeraldLocation, create_location_label_to_id_map, create_locations_by_category,
                        set_free_fly, set_legendary_cave_entrances)
from .opponents import randomize_opponent_parties
from .options import (Goal, DarkCavesRequireFlash, HmRequirements, ItemPoolType, PokemonEmeraldOptions,
                      RandomizeWildPokemon, RandomizeBadges, RandomizeHms, NormanRequirement, OPTION_GROUPS)
from .pokemon import (get_random_move, get_species_id_by_label, randomize_abilities, randomize_learnsets,
                      randomize_legendary_encounters, randomize_misc_pokemon, randomize_starters,
                      randomize_tm_hm_compatibility,randomize_types, randomize_wild_encounters)
from .rom import PokemonEmeraldProcedurePatch, write_tokens
from .util import get_encounter_type_label

# Try adding the Pokemon Gen 3 Adjuster
try:
    from worlds._pokemon_gen3_adjuster import __init__
except:
    pass

class PokemonEmeraldWebWorld(WebWorld):
    """
    Webhost info for Pokemon Emerald
    """
    theme = "ocean"

    setup_en = Tutorial(
        "Multiworld Setup Guide",
        "A guide to playing Pokémon Emerald with Archipelago.",
        "English",
        "setup_en.md",
        "setup/en",
        ["Zunawe"]
    )

    setup_es = Tutorial(
        "Guía de configuración para Multiworld",
        "Una guía para jugar Pokémon Emerald en Archipelago",
        "Español",
        "setup_es.md",
        "setup/es",
        ["nachocua"]
    )

    setup_sv = Tutorial(
        "Multivärld Installations Guide",
        "En guide för att kunna spela Pokémon Emerald med Archipelago.",
        "Svenska",
        "setup_sv.md",
        "setup/sv",
        ["Tsukino"]
    )

    # Add this doc file when the adjuster is merged
    adjuster_en = Tutorial(
        "Usage Guide",
        "A guide to use the Pokemon Gen 3 Adjuster with Pokemon Emerald.",
        "English",
        "adjuster_en.md",
        "adjuster/en",
        ["RhenaudTheLukark"]
    )

    tutorials = [setup_en, setup_es, setup_sv]
    option_groups = OPTION_GROUPS


class PokemonEmeraldSettings(settings.Group):
    class PokemonEmeraldRomFile(settings.UserFilePath):
        """File name of your English Pokemon Emerald ROM"""
        description = "Pokemon Emerald ROM File"
        copy_to = "Pokemon - Emerald Version (USA, Europe).gba"
        md5s = [PokemonEmeraldProcedurePatch.hash]

    rom_file: PokemonEmeraldRomFile = PokemonEmeraldRomFile(PokemonEmeraldRomFile.copy_to)


class PokemonEmeraldWorld(World):
    """
    Pokémon Emerald is the definitive Gen III Pokémon game and one of the most beloved in the franchise.
    Catch, train, and battle Pokémon, explore the Hoenn region, thwart the plots
    of Team Magma and Team Aqua, challenge gyms, and become the Pokémon champion!
    """
    game = "Pokemon Emerald"
    web = PokemonEmeraldWebWorld()
    topology_present = True

    settings_key = "pokemon_emerald_settings"
    settings: ClassVar[PokemonEmeraldSettings]

    options_dataclass = PokemonEmeraldOptions
    options: PokemonEmeraldOptions

    item_name_to_id = create_item_label_to_code_map()
    location_name_to_id = create_location_label_to_id_map()
    item_name_groups = ITEM_GROUPS
    location_name_groups = LOCATION_GROUPS

    required_client_version = (0, 4, 6)

    item_pool: List[PokemonEmeraldItem]
    badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]]
    hm_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]]
    free_fly_location_id: int
    blacklisted_moves: Set[int]
    blacklisted_wilds: Set[int]
    blacklisted_starters: Set[int]
    blacklisted_opponent_pokemon: Set[int]
    hm_requirements: Dict[str, Union[int, List[str]]]
    auth: bytes

    modified_species: Dict[int, SpeciesData]
    modified_maps: Dict[str, MapData]
    modified_tmhm_moves: List[int]
    modified_legendary_encounters: List[int]
    modified_starters: Tuple[int, int, int]
    modified_trainers: List[TrainerData]

    def __init__(self, multiworld, player):
        super(PokemonEmeraldWorld, self).__init__(multiworld, player)
        self.badge_shuffle_info = None
        self.hm_shuffle_info = None
        self.free_fly_location_id = 0
        self.blacklisted_moves = set()
        self.blacklisted_wilds = set()
        self.blacklisted_starters = set()
        self.blacklisted_opponent_pokemon = set()
        self.modified_maps = copy.deepcopy(emerald_data.maps)
        self.modified_species = copy.deepcopy(emerald_data.species)
        self.modified_tmhm_moves = []
        self.modified_starters = emerald_data.starters
        self.modified_trainers = []
        self.modified_legendary_encounters = []

    @classmethod
    def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
        from .sanity_check import validate_regions, validate_group_maps

        assert validate_regions()
        assert validate_group_maps()

    def get_filler_item_name(self) -> str:
        return "Great Ball"

    def generate_early(self) -> None:
        self.hm_requirements = {
            "HM01 Cut": ["Stone Badge"],
            "HM02 Fly": ["Feather Badge"],
            "HM03 Surf": ["Balance Badge"],
            "HM04 Strength": ["Heat Badge"],
            "HM05 Flash": ["Knuckle Badge"],
            "HM06 Rock Smash": ["Dynamo Badge"],
            "HM07 Waterfall": ["Rain Badge"],
            "HM08 Dive": ["Mind Badge"],
        }
        if self.options.hm_requirements == HmRequirements.option_fly_without_badge:
            self.hm_requirements["HM02 Fly"] = 0

        self.blacklisted_moves = {emerald_data.move_labels[label] for label in self.options.move_blacklist.value}

        self.blacklisted_wilds = {
            get_species_id_by_label(species_name)
            for species_name in self.options.wild_encounter_blacklist.value
            if species_name != "_Legendaries"
        }
        if "_Legendaries" in self.options.wild_encounter_blacklist.value:
            self.blacklisted_wilds |= LEGENDARY_POKEMON

        self.blacklisted_starters = {
            get_species_id_by_label(species_name)
            for species_name in self.options.starter_blacklist.value
            if species_name != "_Legendaries"
        }
        if "_Legendaries" in self.options.starter_blacklist.value:
            self.blacklisted_starters |= LEGENDARY_POKEMON

        self.blacklisted_opponent_pokemon = {
            get_species_id_by_label(species_name)
            for species_name in self.options.trainer_party_blacklist.value
            if species_name != "_Legendaries"
        }
        if "_Legendaries" in self.options.trainer_party_blacklist.value:
            self.blacklisted_opponent_pokemon |= LEGENDARY_POKEMON

        # In race mode we don't patch any item location information into the ROM
        if self.multiworld.is_race and not self.options.remote_items:
            logging.warning("Pokemon Emerald: Forcing player %s (%s) to use remote items due to race mode.",
                            self.player, self.player_name)
            self.options.remote_items.value = Toggle.option_true

        if self.options.goal == Goal.option_legendary_hunt:
            # Prevent turning off all legendary encounters
            if len(self.options.allowed_legendary_hunt_encounters.value) == 0:
                raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.player_name}) needs to allow at "
                                   "least one legendary encounter when goal is legendary hunt.")

            # Prevent setting the number of required legendaries higher than the number of enabled legendaries
            if self.options.legendary_hunt_count.value > len(self.options.allowed_legendary_hunt_encounters.value):
                logging.warning("Pokemon Emerald: Legendary hunt count for player %s (%s) higher than number of allowed "
                                "legendary encounters. Reducing to number of allowed encounters.", self.player,
                                self.player_name)
                self.options.legendary_hunt_count.value = len(self.options.allowed_legendary_hunt_encounters.value)

        # Require random wild encounters if dexsanity is enabled
        if self.options.dexsanity and self.options.wild_pokemon == RandomizeWildPokemon.option_vanilla:
            raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.player_name}) must not leave wild "
                               "encounters vanilla if enabling dexsanity.")

        # If badges or HMs are vanilla, Norman locks you from using Surf,
        # which means you're not guaranteed to be able to reach Fortree Gym,
        # Mossdeep Gym, or Sootopolis Gym. So we can't require reaching those
        # gyms to challenge Norman or it creates a circular dependency.
        #
        # This is never a problem for completely random badges/hms because the
        # algo will not place Surf/Balance Badge on Norman on its own. It's
        # never a problem for shuffled badges/hms because there is no scenario
        # where Cut or the Stone Badge can be a lynchpin for access to any gyms,
        # so they can always be put on Norman in a worst case scenario.
        #
        # This will also be a problem in warp rando if direct access to Norman's
        # room requires Surf or if access any gym leader in general requires
        # Surf. We will probably have to force this to 0 in that case.
        max_norman_count = 7

        if self.options.badges == RandomizeBadges.option_vanilla:
            max_norman_count = 4

        if self.options.hms == RandomizeHms.option_vanilla:
            if self.options.norman_requirement == NormanRequirement.option_badges:
                if self.options.badges != RandomizeBadges.option_completely_random:
                    max_norman_count = 4
            if self.options.norman_requirement == NormanRequirement.option_gyms:
                max_norman_count = 4

        if self.options.norman_count.value > max_norman_count:
            logging.warning("Pokemon Emerald: Norman requirements for player %s (%s) are unsafe in combination with "
                            "other settings. Reducing to 4.", self.player, self.player_name)
            self.options.norman_count.value = max_norman_count

        # Shuffled badges/hms will always be placed locally, so add them to local_items
        if self.options.badges == RandomizeBadges.option_shuffle:
            self.options.local_items.value.update(self.item_name_groups["Badge"])

        if self.options.hms == RandomizeHms.option_shuffle:
            self.options.local_items.value.update(self.item_name_groups["HM"])

    def create_regions(self) -> None:
        from .regions import create_regions
        all_regions = create_regions(self)

        # Categories with progression items always included
        categories = {
            LocationCategory.BADGE,
            LocationCategory.HM,
            LocationCategory.KEY,
            LocationCategory.ROD,
            LocationCategory.BIKE,
            LocationCategory.TICKET
        }
        if self.options.overworld_items:
            categories.add(LocationCategory.OVERWORLD_ITEM)
        if self.options.hidden_items:
            categories.add(LocationCategory.HIDDEN_ITEM)
        if self.options.npc_gifts:
            categories.add(LocationCategory.GIFT)
        if self.options.berry_trees:
            categories.add(LocationCategory.BERRY_TREE)
        if self.options.dexsanity:
            categories.add(LocationCategory.POKEDEX)
        if self.options.trainersanity:
            categories.add(LocationCategory.TRAINER)
        create_locations_by_category(self, all_regions, categories)

        self.multiworld.regions.extend(all_regions.values())

        # Exclude locations which are always locked behind the player's goal
        def exclude_locations(location_names: List[str]):
            for location_name in location_names:
                try:
                    self.multiworld.get_location(location_name,
                                                 self.player).progress_type = LocationProgressType.EXCLUDED
                except KeyError:
                    continue  # Location not in multiworld

        if self.options.goal == Goal.option_champion:
            # Always required to beat champion before receiving these
            exclude_locations([
                "Littleroot Town - S.S. Ticket from Norman",
                "Littleroot Town - Aurora Ticket from Norman",
                "Littleroot Town - Eon Ticket from Norman",
                "Littleroot Town - Mystic Ticket from Norman",
                "Littleroot Town - Old Sea Map from Norman",
                "Ever Grande City - Champion Wallace",
                "Meteor Falls 1F - Rival Steven",
                "Trick House Puzzle 8 - Item",
            ])

            # Construction workers don't move until champion is defeated
            if "Safari Zone Construction Workers" not in self.options.remove_roadblocks.value:
                exclude_locations([
                    "Safari Zone NE - Hidden Item North",
                    "Safari Zone NE - Hidden Item East",
                    "Safari Zone NE - Item on Ledge",
                    "Safari Zone SE - Hidden Item in South Grass 1",
                    "Safari Zone SE - Hidden Item in South Grass 2",
                    "Safari Zone SE - Item in Grass",
                ])

            # Sacred ash is on Navel Rock, which is locked behind the event tickets
            if not self.options.event_tickets:
                exclude_locations([
                    "Navel Rock Top - Hidden Item Sacred Ash",
                ])
        elif self.options.goal == Goal.option_steven:
            exclude_locations([
                "Meteor Falls 1F - Rival Steven",
            ])
        elif self.options.goal == Goal.option_norman:
            # If the player sets their options such that Surf or the Balance
            # Badge is vanilla, a very large number of locations become
            # "post-Norman". Similarly, access to the E4 may require you to
            # defeat Norman as an event or to get his badge, making postgame
            # locations inaccessible. Detecting these situations isn't trivial
            # and excluding all locations requiring Surf would be a bad idea.
            # So for now we just won't touch it and blame the user for
            # constructing their options in this way. Players usually expect
            # to only partially complete their world when playing this goal
            # anyway.

            # Locations which are directly unlocked by defeating Norman.
            exclude_locations([
                "Petalburg Gym - Leader Norman",
                "Petalburg Gym - Balance Badge",
                "Petalburg Gym - TM42 from Norman",
                "Petalburg City - HM03 from Wally's Uncle",
                "Dewford Town - TM36 from Sludge Bomb Man",
                "Mauville City - Basement Key from Wattson",
                "Mauville City - TM24 from Wattson",
            ])

    def create_items(self) -> None:
        item_locations: List[PokemonEmeraldLocation] = [
            location
            for location in self.multiworld.get_locations(self.player)
            if location.address is not None
        ]

        # Filter progression items which shouldn't be shuffled into the itempool.
        # Their locations will still exist, but event items will be placed and
        # locked at their vanilla locations instead.
        filter_categories = set()

        if not self.options.key_items:
            filter_categories.add(LocationCategory.KEY)
        if not self.options.rods:
            filter_categories.add(LocationCategory.ROD)
        if not self.options.bikes:
            filter_categories.add(LocationCategory.BIKE)
        if not self.options.event_tickets:
            filter_categories.add(LocationCategory.TICKET)

        if self.options.badges in {RandomizeBadges.option_vanilla, RandomizeBadges.option_shuffle}:
            filter_categories.add(LocationCategory.BADGE)
        if self.options.hms in {RandomizeHms.option_vanilla, RandomizeHms.option_shuffle}:
            filter_categories.add(LocationCategory.HM)

        # If Badges and HMs are set to the `shuffle` option, don't add them to
        # the normal item pool, but do create their items and save them and
        # their locations for use in `pre_fill` later.
        if self.options.badges == RandomizeBadges.option_shuffle:
            self.badge_shuffle_info = [
                (location, self.create_item_by_code(location.default_item_code))
                for location in [l for l in item_locations if emerald_data.locations[l.key].category == LocationCategory.BADGE]
            ]
        if self.options.hms == RandomizeHms.option_shuffle:
            self.hm_shuffle_info = [
                (location, self.create_item_by_code(location.default_item_code))
                for location in [l for l in item_locations if emerald_data.locations[l.key].category == LocationCategory.HM]
            ]

        # Filter down locations to actual items that will be filled and create
        # the itempool.
        item_locations = [location for location in item_locations if emerald_data.locations[location.key].category not in filter_categories]
        default_itempool = [self.create_item_by_code(location.default_item_code) for location in item_locations]

        if self.options.item_pool_type == ItemPoolType.option_shuffled:
            # Take the itempool as-is
            self.item_pool = default_itempool
        elif self.options.item_pool_type in (ItemPoolType.option_diverse, ItemPoolType.option_diverse_balanced):
            # Recreate the itempool from random items
            item_categories = ["Ball", "Healing", "Rare Candy", "Vitamin", "Evolution Stone",
                               "Money", "TM", "Held", "Misc", "Berry"]

            # Count occurrences of types of vanilla items in pool
            item_category_counter = Counter()
            for item in default_itempool:
                if not item.advancement:
                    item_category_counter.update([tag for tag in item.tags if tag in item_categories])

            self.item_pool = []
            item_category_weights = [item_category_counter.get(category) for category in item_categories]
            item_category_weights = [weight if weight is not None else 0 for weight in item_category_weights]

            # Create lists of item codes that can be used to fill
            fill_item_candidates = emerald_data.items.values()

            fill_item_candidates = [item for item in fill_item_candidates if "Unique" not in item.tags]

            fill_item_candidates_by_category = {category: [] for category in item_categories}
            for item_data in fill_item_candidates:
                for category in item_categories:
                    if category in item_data.tags:
                        fill_item_candidates_by_category[category].append(offset_item_value(item_data.item_id))

            for category in fill_item_candidates_by_category:
                fill_item_candidates_by_category[category].sort()

            # Ignore vanilla occurrences and pick completely randomly
            if self.options.item_pool_type == ItemPoolType.option_diverse:
                item_category_weights = [
                    len(category_list)
                    for category_list in fill_item_candidates_by_category.values()
                ]

            # TMs should not have duplicates until every TM has been used already
            all_tm_choices = fill_item_candidates_by_category["TM"].copy()

            def refresh_tm_choices() -> None:
                fill_item_candidates_by_category["TM"] = all_tm_choices.copy()
                self.random.shuffle(fill_item_candidates_by_category["TM"])
            refresh_tm_choices()

            # Create items
            for item in default_itempool:
                if not item.advancement and "Unique" not in item.tags:
                    category = self.random.choices(item_categories, item_category_weights)[0]
                    if category == "TM":
                        if len(fill_item_candidates_by_category["TM"]) == 0:
                            refresh_tm_choices()
                        item_code = fill_item_candidates_by_category["TM"].pop()
                    else:
                        item_code = self.random.choice(fill_item_candidates_by_category[category])
                    item = self.create_item_by_code(item_code)

                self.item_pool.append(item)

        self.multiworld.itempool += self.item_pool

        set_free_fly(self)
        set_legendary_cave_entrances(self)

        # Key items which are considered in access rules but not randomized are converted to events and placed
        # in their vanilla locations so that the player can have them in their inventory for logic.
        def convert_unrandomized_items_to_events(category: LocationCategory) -> None:
            for location in self.multiworld.get_locations(self.player):
                assert isinstance(location, PokemonEmeraldLocation)
                if location.key is not None and emerald_data.locations[location.key].category == category:
                    location.place_locked_item(self.create_event(self.item_id_to_name[location.default_item_code]))
                    location.progress_type = LocationProgressType.DEFAULT
                    location.address = None

        if self.options.badges == RandomizeBadges.option_vanilla:
            convert_unrandomized_items_to_events(LocationCategory.BADGE)
        if self.options.hms == RandomizeHms.option_vanilla:
            convert_unrandomized_items_to_events(LocationCategory.HM)
        if not self.options.rods:
            convert_unrandomized_items_to_events(LocationCategory.ROD)
        if not self.options.bikes:
            convert_unrandomized_items_to_events(LocationCategory.BIKE)
        if not self.options.event_tickets:
            convert_unrandomized_items_to_events(LocationCategory.TICKET)
        if not self.options.key_items:
            convert_unrandomized_items_to_events(LocationCategory.KEY)

    def set_rules(self):
        from .rules import set_rules
        set_rules(self)

    def connect_entrances(self):
        randomize_wild_encounters(self)
        self.shuffle_badges_hms()
        # For entrance randomization, disconnect entrances here, randomize map, then
        # undo badge/HM placement and re-shuffle them in the new map.

    def shuffle_badges_hms(self) -> None:
        my_progression_items = [item for item in self.item_pool if item.advancement]
        my_locations = list(self.get_locations())

        if self.options.badges == RandomizeBadges.option_shuffle:
            badge_locations: List[PokemonEmeraldLocation]
            badge_items: List[PokemonEmeraldItem]

            # Sort order makes `fill_restrictive` try to place important badges later, which
            # makes it less likely to have to swap at all, and more likely for swaps to work.
            badge_locations, badge_items = [list(l) for l in zip(*self.badge_shuffle_info)]
            badge_priority = {
                "Knuckle Badge": 3,
                "Balance Badge": 1,
                "Dynamo Badge": 1,
                "Mind Badge": 2,
                "Heat Badge": 2,
                "Rain Badge": 3,
                "Stone Badge": 4,
                "Feather Badge": 5,
            }
            # In the case of vanilla HMs, navigating Granite Cave is required to access more than 2 gyms,
            # so Knuckle Badge deserves highest priority if Flash is logically required.
            if self.options.hms == RandomizeHms.option_vanilla and \
                    self.options.require_flash in (DarkCavesRequireFlash.option_both, DarkCavesRequireFlash.option_only_granite_cave):
                badge_priority["Knuckle Badge"] = 0
            badge_items.sort(key=lambda item: badge_priority.get(item.name, 0))

            # Build state
            state = CollectionState(self.multiworld)
            for item in my_progression_items:
                state.collect(item, True)
            # If HM shuffle is on, HMs are neither placed in locations nor in
            # the item pool, so we also need to collect them.
            if self.hm_shuffle_info is not None:
                for _, item in self.hm_shuffle_info:
                    state.collect(item, True)
            state.sweep_for_advancements(my_locations)

            # Shuffle badges
            self.fill_subset_with_retries(badge_items, badge_locations, state)

        if self.options.hms == RandomizeHms.option_shuffle:
            hm_locations: List[PokemonEmeraldLocation]
            hm_items: List[PokemonEmeraldItem]

            # Sort order makes `fill_restrictive` try to place important HMs later, which
            # makes it less likely to have to swap at all, and more likely for swaps to work.
            hm_locations, hm_items = [list(l) for l in zip(*self.hm_shuffle_info)]
            hm_priority = {
                "HM05 Flash": 3,
                "HM03 Surf": 1,
                "HM06 Rock Smash": 1,
                "HM08 Dive": 2,
                "HM04 Strength": 2,
                "HM07 Waterfall": 3,
                "HM01 Cut": 4,
                "HM02 Fly": 5,
            }
            # In the case of vanilla badges, navigating Granite Cave is required to access more than 2 gyms,
            # so Flash deserves highest priority if it's logically required.
            if self.options.badges == RandomizeBadges.option_vanilla and \
                    self.options.require_flash in (DarkCavesRequireFlash.option_both, DarkCavesRequireFlash.option_only_granite_cave):
                hm_priority["HM05 Flash"] = 0
            hm_items.sort(key=lambda item: hm_priority.get(item.name, 0), reverse=True)

            # Build state
            # Badges are either in the item pool, or already placed and collected during sweep
            state = CollectionState(self.multiworld)
            for item in my_progression_items:
                state.collect(item, True)
            state.sweep_for_advancements(my_locations)

            # Shuffle HMs
            self.fill_subset_with_retries(hm_items, hm_locations, state)

    def fill_subset_with_retries(self, items: list[PokemonEmeraldItem], locations: list[PokemonEmeraldLocation], state: CollectionState):
        # Un-exclude locations, since we need to put progression items on them
        for location in locations:
            location.progress_type = LocationProgressType.DEFAULT \
                if location.progress_type == LocationProgressType.EXCLUDED \
                else location.progress_type

        # In specific very constrained conditions, `fill_restrictive` may run
        # out of swaps before it finds a valid solution if it gets unlucky.
        attempts_remaining = 2
        while attempts_remaining > 0:
            attempts_remaining -= 1
            locations_copy = locations.copy()
            items_copy = items.copy()
            self.random.shuffle(locations_copy)
            try:
                fill_restrictive(self.multiworld, state, locations_copy, items_copy, single_player_placement=True,
                                 lock=True)
                break
            except FillError as exc:
                if attempts_remaining <= 0:
                    raise exc

                # Undo partial item placement
                for location in locations:
                    location.locked = False
                    if location.item is not None:
                        location.item.location = None
                        location.item = None

                logging.debug(f"Failed to shuffle items for player {self.player} ({self.player_name}). Retrying.")
                continue

    def generate_basic(self) -> None:
        # Create auth
        self.auth = self.random.randbytes(16)

        randomize_types(self)

    def generate_output(self, output_directory: str) -> None:
        self.modified_trainers = copy.deepcopy(emerald_data.trainers)
        self.modified_tmhm_moves = copy.deepcopy(emerald_data.tmhm_moves)
        self.modified_legendary_encounters = copy.deepcopy(emerald_data.legendary_encounters)
        self.modified_misc_pokemon = copy.deepcopy(emerald_data.misc_pokemon)
        self.modified_starters = copy.deepcopy(emerald_data.starters)

        # Modify catch rate
        min_catch_rate = min(self.options.min_catch_rate.value, 255)
        for species in self.modified_species.values():
            species.catch_rate = max(species.catch_rate, min_catch_rate)

        # Modify TM moves
        if self.options.tm_tutor_moves:
            new_moves: Set[int] = set()

            for i in range(50):
                new_move = get_random_move(self.random, new_moves | self.blacklisted_moves)
                new_moves.add(new_move)
                self.modified_tmhm_moves[i] = new_move

        randomize_abilities(self)
        randomize_learnsets(self)
        randomize_tm_hm_compatibility(self)
        randomize_legendary_encounters(self)
        randomize_misc_pokemon(self)
        randomize_opponent_parties(self)
        randomize_starters(self)

        patch = PokemonEmeraldProcedurePatch(player=self.player, player_name=self.player_name)
        patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "data/base_patch.bsdiff4"))
        write_tokens(self, patch)

        del self.modified_trainers
        del self.modified_tmhm_moves
        del self.modified_legendary_encounters
        del self.modified_misc_pokemon
        del self.modified_starters
        del self.modified_species

        # Write Output
        out_file_name = self.multiworld.get_out_file_name_base(self.player)
        patch.write(os.path.join(output_directory, f"{out_file_name}{patch.patch_file_ending}"))

    def write_spoiler(self, spoiler_handle: TextIO):
        if self.options.dexsanity:
            from collections import defaultdict

            spoiler_handle.write(f"\n\nWild Pokemon ({self.player_name}):\n\n")

            species_maps = defaultdict(set)
            for map_data in self.modified_maps.values():
                for encounter_type, encounter_data in map_data.encounters.items():
                    for i, encounter in enumerate(encounter_data.slots):
                        species_maps[encounter].add(f"{map_data.label} ({get_encounter_type_label(encounter_type, i)})")

            lines = [f"{emerald_data.species[species].label}: {', '.join(sorted(maps))}\n"
                     for species, maps in species_maps.items()]
            lines.sort()
            for line in lines:
                spoiler_handle.write(line)

        del self.modified_maps

    def extend_hint_information(self, hint_data):
        if self.options.dexsanity:
            from collections import defaultdict

            species_maps = defaultdict(set)
            for map_data in self.modified_maps.values():
                for encounter_type, encounter_data in map_data.encounters.items():
                    for i, encounter in enumerate(encounter_data.slots):
                        species_maps[encounter].add(f"{map_data.label} ({get_encounter_type_label(encounter_type, i)})")

            hint_data[self.player] = {
                self.location_name_to_id[f"Pokedex - {emerald_data.species[species].label}"]: ", ".join(sorted(maps))
                for species, maps in species_maps.items()
            }

    def modify_multidata(self, multidata: Dict[str, Any]):
        import base64
        multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = multidata["connect_names"][self.player_name]

    def fill_slot_data(self) -> Dict[str, Any]:
        slot_data = self.options.as_dict(
            "goal",
            "badges",
            "hms",
            "key_items",
            "bikes",
            "event_tickets",
            "rods",
            "overworld_items",
            "hidden_items",
            "npc_gifts",
            "berry_trees",
            "require_itemfinder",
            "require_flash",
            "elite_four_requirement",
            "elite_four_count",
            "norman_requirement",
            "norman_count",
            "legendary_hunt_catch",
            "legendary_hunt_count",
            "extra_boulders",
            "remove_roadblocks",
            "allowed_legendary_hunt_encounters",
            "extra_bumpy_slope",
            "free_fly_location",
            "remote_items",
            "dexsanity",
            "trainersanity",
            "modify_118",
            "death_link",
            "normalize_encounter_rates",
        )
        slot_data["free_fly_location_id"] = self.free_fly_location_id
        slot_data["hm_requirements"] = self.hm_requirements
        return slot_data

    def create_item(self, name: str) -> PokemonEmeraldItem:
        return self.create_item_by_code(self.item_name_to_id[name])

    def create_item_by_code(self, item_code: int) -> PokemonEmeraldItem:
        return PokemonEmeraldItem(
            self.item_id_to_name[item_code],
            get_item_classification(item_code),
            item_code,
            self.player
        )

    def create_event(self, name: str) -> PokemonEmeraldItem:
        return PokemonEmeraldItem(
            name,
            ItemClassification.progression,
            None,
            self.player
        )
