from typing import TYPE_CHECKING, Any, Optional

from BaseClasses import CollectionState, Item, Location, MultiWorld
from Fill import fill_restrictive
from worlds.generic.Rules import add_item_rule

from ..Items import item_factory

if TYPE_CHECKING:
    from .. import TWWWorld


class Dungeon:
    """
    This class represents a dungeon in The Wind Waker, including its dungeon items.

    :param name: The name of the dungeon.
    :param big_key: The big key item for the dungeon.
    :param small_keys: A list of small key items for the dungeon.
    :param dungeon_items: A list of other items specific to the dungeon.
    :param player: The ID of the player associated with the dungeon.
    """

    def __init__(
        self,
        name: str,
        big_key: Optional[Item],
        small_keys: list[Item],
        dungeon_items: list[Item],
        player: int,
    ):
        self.name = name
        self.big_key = big_key
        self.small_keys = small_keys
        self.dungeon_items = dungeon_items
        self.player = player

    @property
    def keys(self) -> list[Item]:
        """
        Retrieve all the keys for the dungeon.

        :return: A list of Small Keys and the Big Key (if it exists).
        """
        return self.small_keys + ([self.big_key] if self.big_key else [])

    @property
    def all_items(self) -> list[Item]:
        """
        Retrieve all items associated with the dungeon.

        :return: A list of all items associated with the dungeon.
        """
        return self.dungeon_items + self.keys

    def __eq__(self, other: Any) -> bool:
        """
        Check equality between this dungeon and another object.

        :param other: The object to compare.
        :return: `True` if the other object is a Dungeon with the same name and player, `False` otherwise.
        """
        if isinstance(other, Dungeon):
            return self.name == other.name and self.player == other.player
        return False

    def __repr__(self) -> str:
        """
        Provide a string representation of the dungeon.

        :return: A string representing the dungeon.
        """
        return self.__str__()

    def __str__(self) -> str:
        """
        Convert the dungeon to a human-readable string.

        :return: A string in the format "<name> (Player <player>)".
        """
        return f"{self.name} (Player {self.player})"


def create_dungeons(world: "TWWWorld") -> None:
    """
    Create and assign dungeons to the given world based on game options.

    :param world: The Wind Waker game world.
    """
    player = world.player
    options = world.options

    def make_dungeon(name: str, big_key: Optional[Item], small_keys: list[Item], dungeon_items: list[Item]) -> Dungeon:
        dungeon = Dungeon(name, big_key, small_keys, dungeon_items, player)
        for item in dungeon.all_items:
            item.dungeon = dungeon
        return dungeon

    if options.progression_dungeons:
        if not options.required_bosses or "Dragon Roost Cavern" in world.boss_reqs.required_dungeons:
            world.dungeons["Dragon Roost Cavern"] = make_dungeon(
                "Dragon Roost Cavern",
                item_factory("DRC Big Key", world),
                item_factory(["DRC Small Key"] * 4, world),
                item_factory(["DRC Dungeon Map", "DRC Compass"], world),
            )

        if not options.required_bosses or "Forbidden Woods" in world.boss_reqs.required_dungeons:
            world.dungeons["Forbidden Woods"] = make_dungeon(
                "Forbidden Woods",
                item_factory("FW Big Key", world),
                item_factory(["FW Small Key"] * 1, world),
                item_factory(["FW Dungeon Map", "FW Compass"], world),
            )

        if not options.required_bosses or "Tower of the Gods" in world.boss_reqs.required_dungeons:
            world.dungeons["Tower of the Gods"] = make_dungeon(
                "Tower of the Gods",
                item_factory("TotG Big Key", world),
                item_factory(["TotG Small Key"] * 2, world),
                item_factory(["TotG Dungeon Map", "TotG Compass"], world),
            )

        if not options.required_bosses or "Forsaken Fortress" in world.boss_reqs.required_dungeons:
            world.dungeons["Forsaken Fortress"] = make_dungeon(
                "Forsaken Fortress",
                None,
                [],
                item_factory(["FF Dungeon Map", "FF Compass"], world),
            )

        if not options.required_bosses or "Earth Temple" in world.boss_reqs.required_dungeons:
            world.dungeons["Earth Temple"] = make_dungeon(
                "Earth Temple",
                item_factory("ET Big Key", world),
                item_factory(["ET Small Key"] * 3, world),
                item_factory(["ET Dungeon Map", "ET Compass"], world),
            )

        if not options.required_bosses or "Wind Temple" in world.boss_reqs.required_dungeons:
            world.dungeons["Wind Temple"] = make_dungeon(
                "Wind Temple",
                item_factory("WT Big Key", world),
                item_factory(["WT Small Key"] * 2, world),
                item_factory(["WT Dungeon Map", "WT Compass"], world),
            )


def get_dungeon_item_pool(multiworld: MultiWorld) -> list[Item]:
    """
    Retrieve the item pool for all The Wind Waker dungeons in the multiworld.

    :param multiworld: The MultiWorld instance.
    :return: List of dungeon items across all The Wind Waker dungeons.
    """
    return [
        item for world in multiworld.get_game_worlds("The Wind Waker") for item in get_dungeon_item_pool_player(world)
    ]


def get_dungeon_item_pool_player(world: "TWWWorld") -> list[Item]:
    """
    Retrieve the item pool for all dungeons specific to a player.

    :param world: The Wind Waker game world.
    :return: List of items in the player's dungeons.
    """
    return [item for dungeon in world.dungeons.values() for item in dungeon.all_items]


def get_unfilled_dungeon_locations(multiworld: MultiWorld) -> list[Location]:
    """
    Retrieve all unfilled The Wind Waker dungeon locations in the multiworld.

    :param multiworld: The MultiWorld instance.
    :return: List of unfilled The Wind Waker dungeon locations.
    """
    return [
        location
        for world in multiworld.get_game_worlds("The Wind Waker")
        for location in multiworld.get_locations(world.player)
        if location.dungeon and not location.item
    ]


def modify_dungeon_location_rules(multiworld: MultiWorld) -> None:
    """
    Modify the rules for The Wind Waker dungeon locations based on specific player-requested constraints.

    :param multiworld: The MultiWorld instance.
    """
    localized: set[tuple[int, str]] = set()
    dungeon_specific: set[tuple[int, str]] = set()
    for subworld in multiworld.get_game_worlds("The Wind Waker"):
        player = subworld.player
        if player not in multiworld.groups:
            localized |= {(player, item_name) for item_name in subworld.dungeon_local_item_names}
            dungeon_specific |= {(player, item_name) for item_name in subworld.dungeon_specific_item_names}

    if localized:
        in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized]
        if in_dungeon_items:
            locations = [location for location in get_unfilled_dungeon_locations(multiworld)]

            for location in locations:
                if dungeon_specific:
                    # Special case: If Dragon Roost Cavern has its own small keys, then ensure the first chest isn't the
                    # Big Key. This is to avoid placing the Big Key there during fill and resulting in a costly swap.
                    if location.name == "Dragon Roost Cavern - First Room":
                        add_item_rule(
                            location,
                            lambda item: item.name != "DRC Big Key"
                            or (item.player, "DRC Small Key") in dungeon_specific,
                        )

                    # Add item rule to ensure dungeon items are in their own dungeon when they should be.
                    add_item_rule(
                        location,
                        lambda item, dungeon=location.dungeon: not (item.player, item.name) in dungeon_specific
                        or item.dungeon is dungeon,
                    )


def fill_dungeons_restrictive(multiworld: MultiWorld) -> None:
    """
    Correctly fill The Wind Waker dungeons in the multiworld.

    :param multiworld: The MultiWorld instance.
    """
    localized: set[tuple[int, str]] = set()
    dungeon_specific: set[tuple[int, str]] = set()
    for subworld in multiworld.get_game_worlds("The Wind Waker"):
        player = subworld.player
        if player not in multiworld.groups:
            localized |= {(player, item_name) for item_name in subworld.dungeon_local_item_names}
            dungeon_specific |= {(player, item_name) for item_name in subworld.dungeon_specific_item_names}

    if localized:
        in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized]
        if in_dungeon_items:
            locations = [location for location in get_unfilled_dungeon_locations(multiworld)]
            multiworld.random.shuffle(locations)

            # Dungeon-locked items have to be placed first so as not to run out of space for dungeon-locked items.
            # Subsort in the order Big Key, Small Key, Other before placing dungeon items.
            sort_order = {"Big Key": 3, "Small Key": 2}
            in_dungeon_items.sort(
                key=lambda item: sort_order.get(item.type, 1)
                + (5 if (item.player, item.name) in dungeon_specific else 0)
            )

            # Construct a partial `all_state` that contains only the items from `get_pre_fill_items` that aren't in a
            # dungeon.
            in_dungeon_player_ids = {item.player for item in in_dungeon_items}
            all_state_base = CollectionState(multiworld)
            for item in multiworld.itempool:
                all_state_base.collect(item, prevent_sweep=True)
            pre_fill_items = []
            for player in in_dungeon_player_ids:
                pre_fill_items += multiworld.worlds[player].get_pre_fill_items()
            for item in in_dungeon_items:
                try:
                    pre_fill_items.remove(item)
                except ValueError:
                    # `pre_fill_items` should be a subset of `in_dungeon_items`, but just in case.
                    pass
            for item in pre_fill_items:
                all_state_base.collect(item, prevent_sweep=True)
            all_state_base.sweep_for_advancements()

            # Remove the completion condition so that minimal-accessibility words place keys correctly.
            for player in (item.player for item in in_dungeon_items):
                if all_state_base.has("Victory", player):
                    all_state_base.remove(multiworld.worlds[player].create_item("Victory"))

            fill_restrictive(
                multiworld,
                all_state_base,
                locations,
                in_dungeon_items,
                lock=True,
                allow_excluded=True,
                name="TWW Dungeon Items",
            )
