from typing import Any, Dict, List

from BaseClasses import Item, Location, Tutorial, ItemClassification, MultiWorld
from worlds.AutoWorld import WebWorld, World
from . import Items, Locations, Regions, Rules
from .Options import FaxanaduOptions
from worlds.generic.Rules import set_rule


DAXANADU_VERSION = "0.3.0"


class FaxanaduLocation(Location):
    game: str = "Faxanadu"


class FaxanaduItem(Item):
    game: str = "Faxanadu"


class FaxanaduWeb(WebWorld):
    tutorials = [Tutorial(
        "Multiworld Setup Guide",
        "A guide to setting up the Faxanadu randomizer connected to an Archipelago Multiworld",
        "English",
        "setup_en.md",
        "setup/en",
        ["Daivuk"]
    )]
    theme = "dirt"


class FaxanaduWorld(World):
    """
    Faxanadu is an action role-playing platform video game developed by Hudson Soft for the Nintendo Entertainment System
    """
    options_dataclass = FaxanaduOptions
    options: FaxanaduOptions
    game = "Faxanadu"
    web = FaxanaduWeb()

    item_name_to_id = {item.name: item.id for item in Items.items if item.id is not None}
    item_name_to_item = {item.name: item for item in Items.items}
    location_name_to_id = {loc.name: loc.id for loc in Locations.locations if loc.id is not None}

    def __init__(self, world: MultiWorld, player: int):
        self.filler_ratios: Dict[str, int] = {
            item.name: item.count
            for item in Items.items
            if item.classification in [ItemClassification.filler, ItemClassification.trap]
        }
        # Remove poison by default to respect itemlinking
        self.filler_ratios["Poison"] = 0
        super().__init__(world, player)

    def create_regions(self):
        Regions.create_regions(self)

        # Add locations into regions
        for region in self.multiworld.get_regions(self.player):
            for loc in [location for location in Locations.locations if location.region == region.name]:
                location = FaxanaduLocation(self.player, loc.name, loc.id, region)

                # In Faxanadu, Poison hurts you when picked up. It makes no sense to sell them in shops
                if loc.type == Locations.LocationType.shop:
                    location.item_rule = lambda item, player=self.player: not (player == item.player and item.name == "Poison")
                
                region.locations.append(location)

    def set_rules(self):
        Rules.set_rules(self)
        self.multiworld.completion_condition[self.player] = lambda state: state.has("Killed Evil One", self.player)

    def create_item(self, name: str) -> FaxanaduItem:
        item: Items.ItemDef = self.item_name_to_item[name]
        return FaxanaduItem(name, item.classification, item.id, self.player)

    # Returns how many red potions were prefilled into shops
    def prefill_shop_red_potions(self) -> int:
        red_potion_in_shop_count = 0
        if self.options.keep_shop_red_potions:
            red_potion_item = self.item_name_to_item["Red Potion"]
            red_potion_shop_locations = [
                loc
                for loc in Locations.locations
                if loc.type == Locations.LocationType.shop and loc.original_item == Locations.ItemType.red_potion
            ]
            for loc in red_potion_shop_locations:
                location = self.get_location(loc.name)
                location.place_locked_item(FaxanaduItem(red_potion_item.name, red_potion_item.classification, red_potion_item.id, self.player))
                red_potion_in_shop_count += 1
        return red_potion_in_shop_count

    def put_wingboot_in_shop(self, shops, region_name):
        item = self.item_name_to_item["Wingboots"]
        shop = shops.pop(region_name)
        slot = self.random.randint(0, len(shop) - 1)
        loc = shop[slot]
        location = self.get_location(loc.name)
        location.place_locked_item(FaxanaduItem(item.name, item.classification, item.id, self.player))

        # Put a rule right away that we need to have to unlocked.
        set_rule(location, lambda state: state.has("Unlock Wingboots", self.player))

    # Returns how many wingboots were prefilled into shops
    def prefill_shop_wingboots(self) -> int:
        # Collect shops
        shops: Dict[str, List[Locations.LocationDef]] = {}
        for loc in Locations.locations:
            if loc.type == Locations.LocationType.shop:
                if self.options.keep_shop_red_potions and loc.original_item == Locations.ItemType.red_potion:
                    continue # Don't override our red potions
                shops.setdefault(loc.region, []).append(loc)
        
        shop_count = len(shops)
        wingboots_count = round(shop_count / 2.5) # On 10 shops, we should have about 4 shops with wingboots

        # At least one should be in the first 4 shops. Because we require wingboots to progress past that point.
        must_have_regions = [region for i, region in enumerate(shops) if i < 4]
        self.put_wingboot_in_shop(shops, self.random.choice(must_have_regions))

        # Fill in the rest randomly in remaining shops
        for i in range(wingboots_count - 1): # -1 because we added one already
            region = self.random.choice(list(shops.keys()))
            self.put_wingboot_in_shop(shops, region)

        return wingboots_count

    def create_items(self) -> None:
        itempool: List[FaxanaduItem] = []

        # Prefill red potions in shops if option is set
        red_potion_in_shop_count = self.prefill_shop_red_potions()

        # Prefill wingboots in shops
        wingboots_in_shop_count = self.prefill_shop_wingboots()

        # Create the item pool, excluding fillers.
        prefilled_count = red_potion_in_shop_count + wingboots_in_shop_count
        for item in Items.items:
            # Ignore pendant if turned off
            if item.name == "Pendant" and not self.options.include_pendant:
                continue

            # ignore fillers for now, we will fill them later
            if item.classification in [ItemClassification.filler, ItemClassification.trap] and \
               item.progression_count == 0:
                continue

            prefill_loc = None
            if item.prefill_location:
                prefill_loc = self.get_location(item.prefill_location)

            # if require dragon slayer is turned on, we need progressive shields to be progression
            item_classification = item.classification
            if self.options.require_dragon_slayer and item.name == "Progressive Shield":
                item_classification = ItemClassification.progression

            if prefill_loc:
                prefill_loc.place_locked_item(FaxanaduItem(item.name, item_classification, item.id, self.player))
                prefilled_count += 1
            else:
                for i in range(item.count - item.progression_count):
                    itempool.append(FaxanaduItem(item.name, item_classification, item.id, self.player))
                for i in range(item.progression_count):
                    itempool.append(FaxanaduItem(item.name, ItemClassification.progression, item.id, self.player))

        # Adjust filler ratios
        # If red potions are locked in shops, remove the count from the ratio.
        self.filler_ratios["Red Potion"] -= red_potion_in_shop_count

        # Add poisons if desired
        if self.options.include_poisons:
            self.filler_ratios["Poison"] = self.item_name_to_item["Poison"].count

        # Randomly add fillers to the pool with ratios based on og game occurrence counts.
        filler_count = len(Locations.locations) - len(itempool) - prefilled_count
        for i in range(filler_count):
            itempool.append(self.create_item(self.get_filler_item_name()))
                
        self.multiworld.itempool += itempool

    def get_filler_item_name(self) -> str:
        return self.random.choices(list(self.filler_ratios.keys()), weights=list(self.filler_ratios.values()))[0]
    
    def fill_slot_data(self) -> Dict[str, Any]:
        slot_data = self.options.as_dict("keep_shop_red_potions", "random_musics", "random_sounds", "random_npcs", "random_monsters", "random_rewards")
        slot_data["daxanadu_version"] = DAXANADU_VERSION
        return slot_data
