"""
Defines Region for The Witness, assigns locations to them,
and connects them with the proper requirements
"""
from typing import FrozenSet, TYPE_CHECKING, Dict, Tuple, List

from BaseClasses import Entrance, Region
from Utils import KeyedDefaultDict
from .static_logic import StaticWitnessLogic
from .locations import WitnessPlayerLocations, StaticWitnessLocations
from .player_logic import WitnessPlayerLogic

if TYPE_CHECKING:
    from . import WitnessWorld


class WitnessRegions:
    """Class that defines Witness Regions"""

    locat = None
    logic = None

    @staticmethod
    def make_lambda(item_requirement: FrozenSet[FrozenSet[str]], world: "WitnessWorld"):
        from .rules import _meets_item_requirements

        """
        Lambdas are made in a for loop, so the values have to be captured
        This function is for that purpose
        """

        return _meets_item_requirements(item_requirement, world)

    def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: FrozenSet[FrozenSet[str]],
                            regions_by_name: Dict[str, Region], backwards: bool = False):
        """
        connect two regions and set the corresponding requirement
        """

        # Remove any possibilities where being in the target region would be required anyway.
        real_requirement = frozenset({option for option in req if target not in option})

        # There are some connections that should only be done one way. If this is a backwards connection, check for that
        if backwards:
            real_requirement = frozenset({option for option in real_requirement if "TrueOneWay" not in option})

        # Dissolve any "True" or "TrueOneWay"
        real_requirement = frozenset({option - {"True", "TrueOneWay"} for option in real_requirement})

        # If there is no way to actually use this connection, don't even bother making it.
        if not real_requirement:
            return

        # We don't need to check for the accessibility of the source region.
        final_requirement = frozenset({option - frozenset({source}) for option in real_requirement})

        source_region = regions_by_name[source]
        target_region = regions_by_name[target]

        backwards = " Backwards" if backwards else ""
        connection_name = source + " to " + target + backwards

        connection = Entrance(
            world.player,
            connection_name,
            source_region
        )

        connection.access_rule = self.make_lambda(final_requirement, world)

        source_region.exits.append(connection)
        connection.connect(target_region)

        self.created_entrances[source, target].append(connection)

        # Register any necessary indirect connections
        mentioned_regions = {
            single_unlock for option in final_requirement for single_unlock in option
            if single_unlock in self.reference_logic.ALL_REGIONS_BY_NAME
        }

        for dependent_region in mentioned_regions:
            world.multiworld.register_indirect_condition(regions_by_name[dependent_region], connection)

    def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic):
        """
        Creates all the regions for The Witness
        """
        from . import create_region

        all_locations = set()
        regions_by_name = dict()

        for region_name, region in self.reference_logic.ALL_REGIONS_BY_NAME.items():
            locations_for_this_region = [
                self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["panels"]
                if self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE
            ]
            locations_for_this_region += [
                StaticWitnessLocations.get_event_name(panel) for panel in region["panels"]
                if StaticWitnessLocations.get_event_name(panel) in self.locat.EVENT_LOCATION_TABLE
            ]

            all_locations = all_locations | set(locations_for_this_region)

            new_region = create_region(world, region_name, self.locat, locations_for_this_region)

            regions_by_name[region_name] = new_region

        for region_name, region in self.reference_logic.ALL_REGIONS_BY_NAME.items():
            for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]:
                self.connect_if_possible(world, region_name, connection[0], connection[1], regions_by_name)
                self.connect_if_possible(world, connection[0], region_name, connection[1], regions_by_name, True)

        # find regions that are completely disconnected from the start node and remove them
        regions_to_check = {"Menu"}
        reachable_regions = {"Menu"}

        while regions_to_check:
            next_region = regions_to_check.pop()
            region_obj = regions_by_name[next_region]

            for exit in region_obj.exits:
                target = exit.connected_region

                if target.name in reachable_regions:
                    continue

                regions_to_check.add(target.name)
                reachable_regions.add(target.name)

        final_regions_list = [v for k, v in regions_by_name.items() if k in reachable_regions]

        world.multiworld.regions += final_regions_list

    def __init__(self, locat: WitnessPlayerLocations, world: "WitnessWorld"):
        difficulty = world.options.puzzle_randomization.value

        if difficulty == 0:
            self.reference_logic = StaticWitnessLogic.sigma_normal
        elif difficulty == 1:
            self.reference_logic = StaticWitnessLogic.sigma_expert
        elif difficulty == 2:
            self.reference_logic = StaticWitnessLogic.vanilla

        self.locat = locat
        self.created_entrances: Dict[Tuple[str, str], List[Entrance]] = KeyedDefaultDict(lambda _: [])
