import math
from typing import Mapping, Any

from Options import OptionError
from .data.strings import OTHER, ITEMS, CATEGORY, LOCATIONS, SLOTDATA, GOALS, OPTIONS
from .items import item_descriptions, item_table, ShapezItem, \
    buildings_routing, buildings_processing, buildings_other, \
    buildings_top_row, buildings_wires, gameplay_unlocks, upgrades, \
    big_upgrades, filler, trap, bundles, belt_and_extractor, standard_traps, random_draining_trap, split_draining_traps, \
    whacky_upgrade_traps
from .locations import ShapezLocation, addlevels, addupgrades, addachievements, location_description, \
    addshapesanity, addshapesanity_ut, shapesanity_simple, init_shapesanity_pool, achievement_locations, \
    level_locations, upgrade_locations, shapesanity_locations, categories
from .presets import options_presets
from .options import ShapezOptions
from worlds.AutoWorld import World, WebWorld
from BaseClasses import Item, Tutorial, LocationProgressType, MultiWorld
from .regions import create_shapez_regions, has_x_belt_multiplier
from ..generic.Rules import add_rule


class ShapezWeb(WebWorld):
    options_presets = options_presets
    rich_text_options_doc = True
    theme = "stone"
    game_info_languages = ['en', 'de']
    setup_en = Tutorial(
        "Multiworld Setup Guide",
        "A guide to playing shapez with Archipelago:",
        "English",
        "setup_en.md",
        "setup/en",
        ["BlastSlimey"]
    )
    setup_de = Tutorial(
        setup_en.tutorial_name,
        setup_en.description,
        "Deutsch",
        "setup_de.md",
        "setup/de",
        ["BlastSlimey"]
    )
    datapackage_settings_en = Tutorial(
        "Changing datapackage settings",
        "3000 locations are too many or not enough? Here's how you can change that:",
        "English",
        "datapackage_settings_en.md",
        "datapackage_settings/en",
        ["BlastSlimey"]
    )
    datapackage_settings_de = Tutorial(
        datapackage_settings_en.tutorial_name,
        datapackage_settings_en.description,
        "Deutsch",
        "datapackage_settings_de.md",
        "datapackage_settings/de",
        ["BlastSlimey"]
    )
    tutorials = [setup_en, setup_de, datapackage_settings_en, datapackage_settings_de]
    item_descriptions = item_descriptions
    location_descriptions = location_description


class ShapezWorld(World):
    """
    shapez is an automation game about cutting, rotating, stacking, and painting shapes, that you extract from randomly
    generated patches on an infinite canvas, without the need to manage your infinite resources or to pay for building
    your factories.
    """
    game = OTHER.game_name
    options_dataclass = ShapezOptions
    options: ShapezOptions
    topology_present = True
    web = ShapezWeb()
    base_id = 20010707
    item_name_to_id = {name: id for id, name in enumerate(item_table.keys(), base_id)}
    location_name_to_id = {name: id for id, name in enumerate(level_locations + upgrade_locations
                                                              + achievement_locations + shapesanity_locations, base_id)}
    item_name_groups = {
        "Main Buildings": {ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker},
        "Processing Buildings": {*buildings_processing},
        "Goal Buildings": {ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.rotator_ccw, ITEMS.color_mixer,
                           ITEMS.stacker, ITEMS.cutter_quad, ITEMS.painter_double, ITEMS.painter_quad, ITEMS.wires,
                           ITEMS.switch, ITEMS.const_signal},
        "Most Useful Buildings": {ITEMS.balancer, ITEMS.tunnel, ITEMS.tunnel_tier_ii, ITEMS.comp_merger,
                                  ITEMS.comp_splitter, ITEMS.trash, ITEMS.extractor_chain},
        "Most Important Buildings": {*belt_and_extractor},
        "Top Row Buildings": {*buildings_top_row},
        "Wires Layer Buildings": {*buildings_wires},
        "Gameplay Mechanics": {ITEMS.blueprints, ITEMS.wires},
        "Upgrades": {*{ITEMS.upgrade(size, cat)
                       for size in {CATEGORY.big, CATEGORY.small, CATEGORY.gigantic, CATEGORY.rising}
                       for cat in {CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting}},
                     *{ITEMS.trap_upgrade(cat, size)
                       for cat in {CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting}
                       for size in {"", CATEGORY.demonic}},
                     *{ITEMS.upgrade(size, CATEGORY.random)
                       for size in {CATEGORY.big, CATEGORY.small}}},
        **{f"{cat} Upgrades": {*{ITEMS.upgrade(size, cat)
                                 for size in {CATEGORY.big, CATEGORY.small, CATEGORY.gigantic, CATEGORY.rising}},
                               *{ITEMS.trap_upgrade(cat, size)
                                 for size in {"", CATEGORY.demonic}}}
           for cat in {CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting}},
        "Bundles": {*bundles},
        "Traps": {*standard_traps, *random_draining_trap, *split_draining_traps, *whacky_upgrade_traps},
    }
    location_name_groups = {
        "Levels": {*level_locations},
        "Upgrades": {*upgrade_locations},
        "Achievements": {*achievement_locations},
        "Shapesanity": {*shapesanity_locations},
        **{f"{cat} Upgrades": {loc for loc in upgrade_locations if loc.startswith(cat)} for cat in categories},
        "Only Belt and Extractor": {LOCATIONS.level(1), LOCATIONS.level(1, 1),
                                    LOCATIONS.my_eyes, LOCATIONS.its_a_mess, LOCATIONS.getting_into_it,
                                    LOCATIONS.perfectionist, LOCATIONS.oops, LOCATIONS.i_need_trains, LOCATIONS.gps,
                                    LOCATIONS.a_long_time, LOCATIONS.addicted,
                                    LOCATIONS.shapesanity(1), LOCATIONS.shapesanity(2), LOCATIONS.shapesanity(3)},
    }

    def __init__(self, multiworld: MultiWorld, player: int):
        super().__init__(multiworld, player)

        # Defining instance attributes for each shapez world
        # These are set to default values that should fail unit tests if not replaced with correct values
        self.location_count: int = 0
        self.level_logic: list[str] = []
        self.upgrade_logic: list[str] = []
        self.level_logic_type: str = ""
        self.upgrade_logic_type: str = ""
        self.random_logic_phase_length: list[int] = []
        self.category_random_logic_amounts: dict[str, int] = {}
        self.maxlevel: int = 0
        self.finaltier: int = 0
        self.included_locations: dict[str, tuple[str, LocationProgressType]] = {}
        self.client_seed: int = 0
        self.shapesanity_names: list[str] = []
        self.upgrade_traps_allowed: bool = False

        # Universal Tracker support
        self.ut_active: bool = False
        self.passthrough: dict[str, Any] = {}
        self.location_id_to_alias: dict[int, str] = {}

    @classmethod
    def stage_generate_early(cls, multiworld: MultiWorld) -> None:
        # Import the 75800 entries long shapesanity pool only once and only if it's actually needed
        if len(shapesanity_simple) == 0:
            init_shapesanity_pool()

    def generate_early(self) -> None:
        # Calculate all the important values used for generating a shapez world, with some of them being random
        self.upgrade_traps_allowed: bool = (self.options.include_whacky_upgrades and
                                            (not self.options.goal == GOALS.efficiency_iii) and
                                            self.options.throughput_levels_ratio == 0)

        # Load values from UT if this is a regenerated world
        if hasattr(self.multiworld, "re_gen_passthrough"):
            if OTHER.game_name in self.multiworld.re_gen_passthrough:
                self.ut_active = True
                self.passthrough = self.multiworld.re_gen_passthrough[OTHER.game_name]
                self.maxlevel = self.passthrough[SLOTDATA.maxlevel]
                self.finaltier = self.passthrough[SLOTDATA.finaltier]
                self.client_seed = self.passthrough[SLOTDATA.seed]
                self.level_logic = [self.passthrough[SLOTDATA.level_building(i+1)] for i in range(5)]
                self.upgrade_logic = [self.passthrough[SLOTDATA.upgrade_building(i+1)] for i in range(5)]
                self.level_logic_type = self.passthrough[SLOTDATA.rand_level_logic]
                self.upgrade_logic_type = self.passthrough[SLOTDATA.rand_upgrade_logic]
                self.random_logic_phase_length = [self.passthrough[SLOTDATA.phase_length(i)] for i in range(5)]
                self.category_random_logic_amounts = {cat: self.passthrough[SLOTDATA.cat_buildings_amount(cat)]
                                                      for cat in [CATEGORY.belt_low, CATEGORY.miner_low,
                                                                  CATEGORY.processors_low, CATEGORY.painting_low]}
                # Forces balancers, tunnel, and trash to not appear in regen to make UT more accurate
                self.options.early_balancer_tunnel_and_trash.value = 0
                return

        # "MAM" goal is supposed to be longer than vanilla, but to not have more options than necessary,
        # both goal amounts for "MAM" and "Even fasterer" are set in a single option.
        if self.options.goal == GOALS.mam and self.options.goal_amount < 27:
            raise OptionError(self.player_name
                              + ": When setting goal to 1 ('mam'), goal_amount must be at least 27 and not "
                              + str(self.options.goal_amount.value))

        # If lock_belt_and_extractor is true, the only sphere 1 locations will be achievements
        if self.options.lock_belt_and_extractor and not self.options.include_achievements:
            raise OptionError(self.player_name + ": Achievements must be included when belt and extractor are locked")

        # Determines maxlevel and finaltier, which are needed for location and item generation
        if self.options.goal == GOALS.vanilla:
            self.maxlevel = 25
            self.finaltier = 8
        elif self.options.goal == GOALS.mam:
            self.maxlevel = self.options.goal_amount - 1
            self.finaltier = 8
        elif self.options.goal == GOALS.even_fasterer:
            self.maxlevel = 26
            self.finaltier = self.options.goal_amount.value
        else:  # goal == efficiency_iii
            self.maxlevel = 26
            self.finaltier = 8

        # Setting the seed for the game before any other randomization call is done
        self.client_seed = self.random.randint(0, 100000)

        # Determines the order of buildings for levels logic
        if self.options.randomize_level_requirements:
            self.level_logic_type = self.options.randomize_level_logic.current_key
            if self.level_logic_type.endswith(OPTIONS.logic_shuffled) or self.level_logic_type == OPTIONS.logic_dopamine:
                vanilla_list = [ITEMS.cutter, ITEMS.painter, ITEMS.stacker]
                while len(vanilla_list) > 0:
                    index = self.random.randint(0, len(vanilla_list)-1)
                    next_building = vanilla_list.pop(index)
                    if next_building == ITEMS.cutter:
                        vanilla_list.append(ITEMS.rotator)
                    if next_building == ITEMS.painter:
                        vanilla_list.append(ITEMS.color_mixer)
                    self.level_logic.append(next_building)
            else:
                self.level_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker]
        else:
            self.level_logic_type = OPTIONS.logic_vanilla
            self.level_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker]

        # Determines the order of buildings for upgrades logic
        if self.options.randomize_upgrade_requirements:
            self.upgrade_logic_type = self.options.randomize_upgrade_logic.current_key
            if self.upgrade_logic_type == OPTIONS.logic_hardcore:
                self.upgrade_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker]
            elif self.upgrade_logic_type == OPTIONS.logic_category:
                self.upgrade_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.stacker, ITEMS.painter, ITEMS.color_mixer]
            else:
                vanilla_list = [ITEMS.cutter, ITEMS.painter, ITEMS.stacker]
                while len(vanilla_list) > 0:
                    index = self.random.randint(0, len(vanilla_list)-1)
                    next_building = vanilla_list.pop(index)
                    if next_building == ITEMS.cutter:
                        vanilla_list.append(ITEMS.rotator)
                    if next_building == ITEMS.painter:
                        vanilla_list.append(ITEMS.color_mixer)
                    self.upgrade_logic.append(next_building)
        else:
            self.upgrade_logic_type = OPTIONS.logic_vanilla_like
            self.upgrade_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker]

        # Determine lenghts of phases in level logic type "random"
        self.random_logic_phase_length = [1, 1, 1, 1, 1]
        if self.level_logic_type.startswith(OPTIONS.logic_random_steps):
            remaininglength = self.maxlevel - 1
            for phase in range(0, 5):
                if self.random.random() < 0.1:  # Make sure that longer phases are less frequent
                    self.random_logic_phase_length[phase] = self.random.randint(0, remaininglength)
                else:
                    self.random_logic_phase_length[phase] = self.random.randint(0, remaininglength // (6 - phase))
                remaininglength -= self.random_logic_phase_length[phase]

        # Determine amount of needed buildings for each category in upgrade logic type "category_random"
        self.category_random_logic_amounts = {CATEGORY.belt_low: 0, CATEGORY.miner_low: 1,
                                              CATEGORY.processors_low: 2, CATEGORY.painting_low: 3}
        if self.upgrade_logic_type == OPTIONS.logic_category_random:
            cats = [CATEGORY.belt_low, CATEGORY.miner_low, CATEGORY.processors_low, CATEGORY.painting_low]
            nextcat = self.random.choice(cats)
            self.category_random_logic_amounts[nextcat] = 0
            cats.remove(nextcat)
            for cat in cats:
                self.category_random_logic_amounts[cat] = self.random.randint(0, 5)

    def create_item(self, name: str) -> Item:
        return ShapezItem(name, item_table[name](self.options), self.item_name_to_id[name], self.player)

    def get_filler_item_name(self) -> str:
        return filler(self.random.random(), bool(self.options.include_whacky_upgrades))

    def append_shapesanity(self, name: str) -> None:
        """This method is given as a parameter when creating the locations for shapesanity."""
        self.shapesanity_names.append(name)

    def add_alias(self, location_name: str, alias: str):
        """This method is given as a parameter when locations with helpful aliases for UT are created."""
        if self.ut_active:
            self.location_id_to_alias[self.location_name_to_id[location_name]] = alias

    def create_regions(self) -> None:
        # Create list of all included level and upgrade locations based on player options
        # This already includes the region to be placed in and the LocationProgressType
        self.included_locations = {**addlevels(self.maxlevel, self.level_logic_type,
                                               self.random_logic_phase_length),
                                   **addupgrades(self.finaltier, self.upgrade_logic_type,
                                                 self.category_random_logic_amounts)}

        # Add shapesanity to included location and creates the corresponding list based on player options
        if self.ut_active:
            self.shapesanity_names = self.passthrough[SLOTDATA.shapesanity]
            self.included_locations.update(addshapesanity_ut(self.shapesanity_names, self.add_alias))
        else:
            self.included_locations.update(addshapesanity(self.options.shapesanity_amount.value, self.random,
                                                          self.append_shapesanity, self.add_alias))

        # Add achievements to included locations based on player options
        if self.options.include_achievements:
            self.included_locations.update(addachievements(
                bool(self.options.exclude_softlock_achievements), bool(self.options.exclude_long_playtime_achievements),
                bool(self.options.exclude_progression_unreasonable), self.maxlevel, self.upgrade_logic_type,
                self.category_random_logic_amounts, self.options.goal.current_key, self.included_locations,
                self.add_alias, self.upgrade_traps_allowed))

        # Save the final amount of to-be-filled locations
        self.location_count = len(self.included_locations)

        # Create regions and entrances based on included locations and player options
        self.multiworld.regions.extend(create_shapez_regions(self.player, self.multiworld,
                                                             bool(self.options.allow_floating_layers.value),
                                                             self.included_locations, self.location_name_to_id,
                                                             self.level_logic, self.upgrade_logic,
                                                             self.options.early_balancer_tunnel_and_trash.current_key,
                                                             self.options.goal.current_key))

    def create_items(self) -> None:
        # Include guaranteed items (game mechanic unlocks and 7x4 big upgrades)
        included_items: list[Item] = ([self.create_item(name) for name in buildings_processing.keys()]
                                      + [self.create_item(name) for name in buildings_routing.keys()]
                                      + [self.create_item(name) for name in buildings_other.keys()]
                                      + [self.create_item(name) for name in buildings_top_row.keys()]
                                      + [self.create_item(name) for name in buildings_wires.keys()]
                                      + [self.create_item(name) for name in gameplay_unlocks.keys()]
                                      + [self.create_item(name) for name in big_upgrades for _ in range(7)])

        if not self.options.lock_belt_and_extractor:
            for name in belt_and_extractor:
                self.multiworld.push_precollected(self.create_item(name))
        else:  # This also requires self.options.include_achievements to be true
            included_items.extend([self.create_item(name) for name in belt_and_extractor.keys()])

        # Give a detailed error message if there are already more items than available locations.
        # At the moment, this won't happen, but it's better for debugging in case a future update breaks things.
        if len(included_items) > self.location_count:
            raise RuntimeError(self.player_name + ": There are more guaranteed items than available locations")

        # Get value from traps probability option and convert to float
        traps_probability = self.options.traps_percentage/100
        split_draining = bool(self.options.split_inventory_draining_trap)
        # Fill remaining locations with fillers
        for x in range(self.location_count - len(included_items)):
            if self.random.random() < traps_probability:
                # Fill with trap
                included_items.append(self.create_item(trap(self.random.random(), split_draining,
                                                            self.upgrade_traps_allowed)))
            else:
                # Fil with random filler item
                included_items.append(self.create_item(self.get_filler_item_name()))

        # Add correct number of items to itempool
        self.multiworld.itempool += included_items

        # Add balancer, tunnel, and trash to early items if player options say so
        if self.options.early_balancer_tunnel_and_trash == OPTIONS.sphere_1:
            self.multiworld.early_items[self.player][ITEMS.balancer] = 1
            self.multiworld.early_items[self.player][ITEMS.tunnel] = 1
            self.multiworld.early_items[self.player][ITEMS.trash] = 1

    def set_rules(self) -> None:
        # Levels might need more belt speed if they require throughput per second. As the randomization of what levels
        # need throughput happens in the client mod, this logic needs to be applied to all levels. This is applied to
        # every individual level instead of regions, because they would need a much more complex calculation to prevent
        # softlocks.

        def f(x: int, name: str):
            # These calculations are taken from the client mod
            if x < 26:
                throughput = math.ceil((2.999+x*0.333)*self.options.required_shapes_multiplier/10)
            else:
                throughput = min((4+(x-26)*0.25)*self.options.required_shapes_multiplier/10, 200)
            if throughput/32 >= 1:
                add_rule(self.get_location(name),
                         lambda state: has_x_belt_multiplier(state, self.player, throughput/32))

        if not self.options.throughput_levels_ratio == 0:
            f(0, LOCATIONS.level(1, 1))
            f(19, LOCATIONS.level(20, 1))
            f(19, LOCATIONS.level(20, 2))
            for _x in range(self.maxlevel):
                f(_x, LOCATIONS.level(_x+1))
            if self.options.goal.current_key in [GOALS.vanilla, GOALS.mam]:
                f(self.maxlevel, LOCATIONS.goal)

    def fill_slot_data(self) -> Mapping[str, Any]:
        # Buildings logic; all buildings as individual parameters
        level_logic_data = {SLOTDATA.level_building(x+1): self.level_logic[x] for x in range(5)}
        upgrade_logic_data = {SLOTDATA.upgrade_building(x+1): self.upgrade_logic[x] for x in range(5)}
        # Randomized values for certain logic types
        logic_type_random_data = {SLOTDATA.phase_length(x): self.random_logic_phase_length[x] for x in range(0, 5)}
        logic_type_cat_random_data = {SLOTDATA.cat_buildings_amount(cat): self.category_random_logic_amounts[cat]
                                      for cat in [CATEGORY.belt_low, CATEGORY.miner_low,
                                                  CATEGORY.processors_low, CATEGORY.painting_low]}

        # Options that are relevant to the mod
        option_data = {
            SLOTDATA.goal: self.options.goal.current_key,
            SLOTDATA.maxlevel: self.maxlevel,
            SLOTDATA.finaltier: self.finaltier,
            SLOTDATA.req_shapes_mult: self.options.required_shapes_multiplier.value,
            SLOTDATA.allow_float_layers: bool(self.options.allow_floating_layers),
            SLOTDATA.rand_level_req: bool(self.options.randomize_level_requirements),
            SLOTDATA.rand_upgrade_req: bool(self.options.randomize_upgrade_requirements),
            SLOTDATA.rand_level_logic: self.level_logic_type,
            SLOTDATA.rand_upgrade_logic: self.upgrade_logic_type,
            SLOTDATA.throughput_levels_ratio: self.options.throughput_levels_ratio.value,
            SLOTDATA.comp_growth_gradient: self.options.complexity_growth_gradient.value,
            SLOTDATA.same_late: bool(self.options.same_late_upgrade_requirements),
            SLOTDATA.toolbar_shuffling: bool(self.options.toolbar_shuffling),
        }

        return {**level_logic_data, **upgrade_logic_data, **option_data, **logic_type_random_data,
                **logic_type_cat_random_data, SLOTDATA.seed: self.client_seed,
                SLOTDATA.shapesanity: self.shapesanity_names}

    def interpret_slot_data(self, slot_data: dict[str, Any]) -> dict[str, Any]:
        """Helper function for Universal Tracker"""
        return slot_data
