"""
Defines the rules by which locations can be accessed,
depending on the items received
"""

from typing import TYPE_CHECKING, Callable, FrozenSet

from BaseClasses import CollectionState
from .player_logic import WitnessPlayerLogic
from .locations import WitnessPlayerLocations
from . import StaticWitnessLogic, WitnessRegions
from worlds.generic.Rules import set_rule

if TYPE_CHECKING:
    from . import WitnessWorld

laser_hexes = [
    "0x028A4",
    "0x00274",
    "0x032F9",
    "0x01539",
    "0x181B3",
    "0x0C2B2",
    "0x00509",
    "0x00BF6",
    "0x014BB",
    "0x012FB",
    "0x17C65",
]


def _has_laser(laser_hex: str, world: "WitnessWorld", player: int) -> Callable[[CollectionState], bool]:
    if laser_hex == "0x012FB":
        return lambda state: (
            _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat)(state)
            and state.has("Desert Laser Redirection", player)
        )
    else:
        return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat)


def _has_lasers(amount: int, world: "WitnessWorld") -> Callable[[CollectionState], bool]:
    laser_lambdas = []

    for laser_hex in laser_hexes:
        has_laser_lambda = _has_laser(laser_hex, world, world.player)

        laser_lambdas.append(has_laser_lambda)

    return lambda state: sum(laser_lambda(state) for laser_lambda in laser_lambdas) >= amount


def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic,
                     locat: WitnessPlayerLocations) -> Callable[[CollectionState], bool]:
    """
    Determines whether a panel can be solved
    """

    panel_obj = player_logic.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]
    entity_name = panel_obj["checkName"]

    if entity_name + " Solved" in locat.EVENT_LOCATION_TABLE:
        return lambda state: state.has(player_logic.EVENT_ITEM_PAIRS[entity_name + " Solved"], player)
    else:
        return make_lambda(panel, world)


def _can_move_either_direction(state: CollectionState, source: str, target: str, regio: WitnessRegions) -> bool:
    entrance_forward = regio.created_entrances[source, target]
    entrance_backward = regio.created_entrances[target, source]

    return (
        any(entrance.can_reach(state) for entrance in entrance_forward)
        or
        any(entrance.can_reach(state) for entrance in entrance_backward)
    )


def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool:
    player = world.player

    hedge_2_access = (
        _can_move_either_direction(state, "Keep 2nd Maze", "Keep", world.regio)
    )

    hedge_3_access = (
            _can_move_either_direction(state, "Keep 3rd Maze", "Keep", world.regio)
            or _can_move_either_direction(state, "Keep 3rd Maze", "Keep 2nd Maze", world.regio)
            and hedge_2_access
    )

    hedge_4_access = (
            _can_move_either_direction(state, "Keep 4th Maze", "Keep", world.regio)
            or _can_move_either_direction(state, "Keep 4th Maze", "Keep 3rd Maze", world.regio)
            and hedge_3_access
    )

    hedge_access = (
            _can_move_either_direction(state, "Keep 4th Maze", "Keep Tower", world.regio)
            and state.can_reach("Keep", "Region", player)
            and hedge_4_access
    )

    backwards_to_fourth = (
            state.can_reach("Keep", "Region", player)
            and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Keep Tower", world.regio)
            and (
                    _can_move_either_direction(state, "Keep", "Keep Tower", world.regio)
                    or hedge_access
            )
    )

    shadows_shortcut = (
            state.can_reach("Main Island", "Region", player)
            and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Shadows", world.regio)
    )

    backwards_access = (
            _can_move_either_direction(state, "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate", world.regio)
            and (backwards_to_fourth or shadows_shortcut)
    )

    front_access = (
            _can_move_either_direction(state, "Keep 2nd Pressure Plate", "Keep", world.regio)
            and state.can_reach("Keep", "Region", player)
    )

    return front_access and backwards_access


def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> bool:
    direct_access = (
            _can_move_either_direction(state, "Tunnels", "Windmill Interior", world.regio)
            and _can_move_either_direction(state, "Theater", "Windmill Interior", world.regio)
    )

    theater_from_town = (
            _can_move_either_direction(state, "Town", "Windmill Interior", world.regio)
            and _can_move_either_direction(state, "Theater", "Windmill Interior", world.regio)
            or _can_move_either_direction(state, "Town", "Theater", world.regio)
    )

    tunnels_from_town = (
            _can_move_either_direction(state, "Tunnels", "Windmill Interior", world.regio)
            and _can_move_either_direction(state, "Town", "Windmill Interior", world.regio)
            or _can_move_either_direction(state, "Tunnels", "Town", world.regio)
    )

    return direct_access or theater_from_town and tunnels_from_town


def _has_item(item: str, world: "WitnessWorld", player: int,
              player_logic: WitnessPlayerLogic, locat: WitnessPlayerLocations) -> Callable[[CollectionState], bool]:
    if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME:
        return lambda state: state.can_reach(item, "Region", player)
    if item == "7 Lasers":
        laser_req = world.options.mountain_lasers.value
        return _has_lasers(laser_req, world)
    if item == "11 Lasers":
        laser_req = world.options.challenge_lasers.value
        return _has_lasers(laser_req, world)
    elif item == "PP2 Weirdness":
        return lambda state: _can_do_expert_pp2(state, world)
    elif item == "Theater to Tunnels":
        return lambda state: _can_do_theater_to_tunnels(state, world)
    if item in player_logic.EVENT_PANELS:
        return _can_solve_panel(item, world, player, player_logic, locat)

    prog_item = StaticWitnessLogic.get_parent_progressive_item(item)
    return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item])


def _meets_item_requirements(requirements: FrozenSet[FrozenSet[str]],
                             world: "WitnessWorld") -> Callable[[CollectionState], bool]:
    """
    Checks whether item and panel requirements are met for
    a panel
    """

    lambda_conversion = [
        [_has_item(item, world, world.player, world.player_logic, world.locat) for item in subset]
        for subset in requirements
    ]

    return lambda state: any(
        all(condition(state) for condition in sub_requirement)
        for sub_requirement in lambda_conversion
    )


def make_lambda(entity_hex: str, world: "WitnessWorld") -> Callable[[CollectionState], bool]:
    """
    Lambdas are created in a for loop so values need to be captured
    """
    entity_req = world.player_logic.REQUIREMENTS_BY_HEX[entity_hex]

    return _meets_item_requirements(entity_req, world)


def set_rules(world: "WitnessWorld"):
    """
    Sets all rules for all locations
    """

    for location in world.locat.CHECK_LOCATION_TABLE:
        real_location = location

        if location in world.locat.EVENT_LOCATION_TABLE:
            real_location = location[:-7]

        associated_entity = world.player_logic.REFERENCE_LOGIC.ENTITIES_BY_NAME[real_location]
        entity_hex = associated_entity["entity_hex"]

        rule = make_lambda(entity_hex, world)

        location = world.multiworld.get_location(location, world.player)

        set_rule(location, rule)

    world.multiworld.completion_condition[world.player] = lambda state: state.has('Victory', world.player)
