import itertools
import logging
from collections import defaultdict, deque

from BaseClasses import DoorType, KeyRuleType, RegionType, dungeon_keys
from DungeonGenerator import (
    ExplorationState,
    blind_boss_unavail,
    count_locations_exclude_big_chest,
    get_special_big_key_doors,
    prize_or_event,
    reserved_location,
)
from Dungeons import dungeon_bigs, dungeon_keys, dungeon_table
from Regions import location_events


class KeyLayout(object):

    def __init__(self, sector, starts, proposal):
        self.sector = sector
        self.start_regions = starts
        self.event_starts = []
        self.proposal = proposal
        self.key_logic = KeyLogic(sector.name)

        self.key_counters = None
        self.flat_prop = None
        self.max_chests = None
        self.max_drops = None
        self.all_chest_locations = {}
        self.big_key_special = False
        self.all_locations = set()
        self.item_locations = set()

        self.found_doors = set()
        self.prize_relevant = None
        self.prize_can_lock = None  # if true, then you may need to beat the bo
        # bk special?
        # bk required? True if big chests or big doors exists

    def reset(self, proposal, builder, world, player):
        self.proposal = proposal
        self.flat_prop = flatten_pair_list(self.proposal)
        self.key_logic = KeyLogic(self.sector.name)
        self.max_chests = calc_max_chests(builder, self, world, player)
        self.all_locations = set()
        self.item_locations = set()
        self.prize_relevant = None


class KeyLogic(object):

    def __init__(self, dungeon_name):
        self.door_rules = {}
        self.bk_restricted = set()  # subset of free locations
        self.bk_locked = set()      # includes potentially other locations and key only locations
        self.sm_restricted = set()
        self.small_key_name = dungeon_keys[dungeon_name]
        self.bk_name = dungeon_bigs[dungeon_name]
        self.bk_doors = set()
        self.bk_chests = set()
        self.logic_min = {}
        self.logic_max = {}
        self.placement_rules = []
        self.location_rules = {}
        self.outside_keys = 0
        self.outside_keys_locations = set()
        self.dungeon = dungeon_name
        self.sm_doors = {}
        self.prize_location = None

    def check_placement(self, unplaced_keys, wild_keys, reached_keys, self_locking_keys,
                        big_key_loc=None, prize_loc=None, cr_count=7):
        for rule in self.placement_rules:
            if not rule.is_satisfiable(self.outside_keys_locations, wild_keys, reached_keys, self_locking_keys,
                                       unplaced_keys, big_key_loc, prize_loc, cr_count):
                return False
            if big_key_loc:
                for rule_a, rule_b in itertools.combinations(self.placement_rules, 2):
                    if rule_a.contradicts(rule_b, unplaced_keys, big_key_loc):
                        return False
        return True

    def reset(self):
        self.door_rules.clear()
        self.bk_restricted.clear()
        self.bk_locked.clear()
        self.sm_restricted.clear()
        self.bk_doors.clear()
        self.bk_chests.clear()
        self.placement_rules.clear()


class DoorRules(object):

    def __init__(self, number, is_valid):
        self.small_key_num = number
        self.is_valid = is_valid
        # allowing a different number if bk is behind this door in a set of locations
        self.alternate_small_key = None
        self.alternate_big_key_loc = set()
        # for a place with only 1 free location/key_only_location behind it ... no goals and locations
        self.allow_small = False
        self.small_location = None
        self.opposite = None

        self.new_rules = {}  # keyed by type, or type+lock_item -> number


class LocationRule(object):
    def __init__(self):
        self.small_key_num = 0
        self.conditional_sets = []


class ConditionalLocationRule(object):
    def __init__(self, conditional_set):
        self.conditional_set = conditional_set
        self.small_key_num = 0


class PlacementRule(object):

    def __init__(self):
        self.door_reference = None
        self.small_key = None
        self.bk_conditional_set = None  # the location that means
        self.needed_keys_w_bk = None
        self.needed_keys_wo_bk = None
        self.check_locations_w_bk = None
        self.special_bk_avail = False
        self.check_locations_wo_bk = None
        self.bk_relevant = True
        self.key_reduced = False
        self.prize_relevance = None

    def contradicts(self, rule, unplaced_keys, big_key_loc):
        bk_blocked = big_key_loc in self.bk_conditional_set if self.bk_conditional_set else False
        rule_blocked = big_key_loc in rule.bk_conditional_set if rule.bk_conditional_set else False
        check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
        rule_locations = rule.check_locations_wo_bk if rule_blocked else rule.check_locations_w_bk
        if check_locations is None or rule_locations is None:
            return False
        if not bk_blocked and big_key_loc not in check_locations:  # bk is not available, so rule doesn't apply
            return False
        if not rule_blocked and big_key_loc not in rule_locations:  # bk is not available, so rule doesn't apply
            return False
        check_locations = check_locations - {big_key_loc}
        rule_locations = rule_locations - {big_key_loc}
        threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
        rule_threshold = rule.needed_keys_wo_bk if rule_blocked else rule.needed_keys_w_bk
        common_locations = rule_locations & check_locations
        shared = len(common_locations)
        if min(rule_threshold, threshold) - shared > 0:
            left = unplaced_keys - shared
            check_locations = check_locations - common_locations
            check_needed = threshold - shared
            if len(check_locations) < check_needed or left < check_needed:
                return True
            else:
                left -= check_needed
            rule_locations = rule_locations - common_locations
            rule_needed = rule_threshold - shared
            if len(rule_locations) < rule_needed or left < rule_needed:
                return True
            else:
                left -= rule_needed
        return False

    def is_satisfiable(self, outside_keys_locations, wild_keys, reached_keys, self_locking_keys, unplaced_keys,
                       big_key_loc, prize_location, cr_count):
        if self.prize_relevance and prize_location:
            if self.prize_relevance == 'BigBomb':
                 if prize_location.item.name not in ['Crystal 5', 'Crystal 6']:
                     return True
            elif self.prize_relevance == 'GT':
                if 'Crystal' not in prize_location.item.name or cr_count < 7:
                    return True
        bk_blocked = False
        if self.bk_conditional_set:
            for loc in self.bk_conditional_set:
                if loc.item and loc.item.bigkey:
                    bk_blocked = True
                    break
        elif len(self.check_locations_w_bk) > self.needed_keys_w_bk:
            def loc_has_bk(l):
                return (big_key_loc is not None and big_key_loc == l) or (l.item and l.item.bigkey)

            # todo: sometimes the bk avail rule doesn't mean the bk must be avail or this rule is invalid
            # but sometimes it certainly does
            # check threshold vs len(check_loc) maybe to determine bk isn't relevant?
            bk_found = self.special_bk_avail or any(loc for loc in self.check_locations_w_bk if loc_has_bk(loc))
            if not bk_found:
                return True
        check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
        if not bk_blocked and check_locations is None:
            return True
        available_keys = len(outside_keys_locations)
        # todo: sometimes we need an extra empty chest to accomodate the big key too
        # dungeon bias seed 563518200 for example
        threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
        threshold -= self_locking_keys
        if not wild_keys:
            empty_chests = 0
            for loc in check_locations:
                if not loc.item:
                    empty_chests += 1
                elif loc.item and loc.item.name == self.small_key:
                    available_keys += 1
            place_able_keys = min(empty_chests, unplaced_keys)
            available_keys += place_able_keys
        else:
            available_keys += len(reached_keys.difference(outside_keys_locations))  # already placed small keys
            available_keys += unplaced_keys  # small keys not yet placed
        return available_keys >= threshold


class KeyCounter(object):

    def __init__(self, max_chests):
        self.max_chests = max_chests
        self.free_locations = {}
        self.key_only_locations = {}
        self.child_doors = {}
        self.open_doors = {}
        self.used_keys = 0
        self.big_key_opened = False
        self.important_location = False
        self.other_locations = {}
        self.important_locations = {}
        self.prize_doors_opened = False
        self.prize_received = False

    def used_smalls_loc(self, reserve=0):
        return max(self.used_keys + reserve - len(self.key_only_locations), 0)


def build_key_layout(builder, start_regions, proposal, event_starts, world, player):
    key_layout = KeyLayout(builder.master_sector, start_regions, proposal)
    key_layout.flat_prop = flatten_pair_list(key_layout.proposal)
    key_layout.max_drops = count_key_drops(key_layout.sector)
    key_layout.max_chests = calc_max_chests(builder, key_layout, world, player)
    key_layout.big_key_special = check_bk_special(key_layout.sector.region_set(), world, player)
    key_layout.all_locations = find_all_locations(key_layout.sector)
    key_layout.event_starts = list(event_starts.keys())
    return key_layout


def count_key_drops(sector):
    cnt = 0
    for region in sector.regions:
        for loc in region.locations:
            if loc.forced_item and 'Small Key' in loc.item.name:
                cnt += 1
    return cnt


def find_all_locations(sector):
    all_locations = set()
    for region in sector.regions:
        for loc in region.locations:
            all_locations.add(loc)
    return all_locations


def calc_max_chests(builder, key_layout, world, player):
    if world.doorShuffle[player] in ['basic', 'vanilla']:
        return len(world.get_dungeon(key_layout.sector.name, player).small_keys)
    return max(0, builder.key_doors_num - key_layout.max_drops)


def analyze_dungeon(key_layout, world, player):
    key_layout.key_logic.reset()
    key_layout.key_counters = create_key_counters(key_layout, world, player)
    key_logic = key_layout.key_logic
    for door in key_layout.proposal:
        if isinstance(door, tuple):
            key_logic.sm_doors[door[0]] = door[1]
            key_logic.sm_doors[door[1]] = door[0]
        else:
            if door.dest and door.type != DoorType.SpiralStairs:
                key_logic.sm_doors[door] = door.dest
                key_logic.sm_doors[door.dest] = door
            else:
                key_logic.sm_doors[door] = None

    find_bk_locked_sections(key_layout, world, player)
    key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
    key_logic.bk_chests.update(find_big_key_locked_locations(key_layout.all_chest_locations))
    key_logic.prize_location = dungeon_table[key_layout.sector.name].prize
    if world.keyshuffle[player] == 'universal' and world.mode[player] != 'standard':
        return

    original_key_counter = find_counter({}, False, key_layout, False)
    if key_layout.big_key_special and forced_big_key_avail(original_key_counter.other_locations) is not None:
        original_key_counter = find_counter({}, True, key_layout, False)
    queue = deque([(None, original_key_counter)])
    doors_completed = set()
    visited_cid = set()
    visited_cid.add(cid(original_key_counter, key_layout))

    while len(queue) > 0:
        queue = deque(sorted(queue, key=queue_sorter))
        parent_door, key_counter = queue.popleft()
        chest_keys = available_chest_small_keys(key_counter, world, player)
        raw_avail = chest_keys + len(key_counter.key_only_locations)
        available = raw_avail - key_counter.used_keys
        possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
        avail_bigs = exist_relevant_big_doors(key_counter, key_layout) or exist_big_chest(key_counter)
        non_big_locs = count_locations_big_optional(key_counter.free_locations)
        big_avail = key_counter.big_key_opened or (key_layout.big_key_special and any(x for x in key_counter.other_locations.keys() if x.forced_item and x.forced_item.bigkey))
        if not big_avail:
            if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs:
                key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
            # note to self: this is due to the enough_small_locations function in validate_key_layout_sub_loop
            # I don't like this exception here or there
            # elif available < possible_smalls and avail_bigs and non_big_locs > 0:
            #     max_ctr = find_max_counter(key_layout)
            #     bk_lockdown = [x for x in max_ctr.free_locations if x not in key_counter.free_locations]
            #     key_logic.bk_restricted.update(filter_big_chest(bk_lockdown))
        # try to relax the rules here? - smallest requirement that doesn't force a softlock
        child_queue = deque()
        for child in key_counter.child_doors.keys():
            if can_open_door_by_counter(child, key_counter, key_layout, world, player):
                odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player)
                empty_flag = empty_counter(odd_counter)
                child_queue.append((child, odd_counter, empty_flag))
        while len(child_queue) > 0:
            child, odd_counter, empty_flag = child_queue.popleft()
            prize_flag = key_counter.prize_doors_opened
            if child in key_layout.flat_prop and child not in doors_completed:
                best_counter = find_best_counter(child, key_layout, odd_counter, False, empty_flag)
                rule = create_rule(best_counter, key_counter, world, player)
                create_worst_case_rule(rule, best_counter, world, player)
                check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
                bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player)
                key_logic.door_rules[child.name] = rule
            elif not child.bigKey and child not in doors_completed:
                prize_flag = True
            doors_completed.add(child)
            next_counter = find_next_counter(child, key_counter, key_layout, prize_flag)
            ctr_id = cid(next_counter, key_layout)
            if ctr_id not in visited_cid:
                queue.append((child, next_counter))
                visited_cid.add(ctr_id)
    # todo: why is this commented out?
    # check_rules(original_key_counter, key_layout, world, player)

    # Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location
    for rule in key_logic.door_rules.values():
        if rule.alternate_small_key is not None and rule.alternate_small_key > rule.small_key_num:
            max_counter = find_max_counter(key_layout)
            rule.alternate_big_key_loc = set(max_counter.free_locations.keys()).difference(rule.alternate_big_key_loc)
            rule.small_key_num, rule.alternate_small_key = rule.alternate_small_key, rule.small_key_num
    create_exhaustive_placement_rules(key_layout, world, player)
    set_paired_rules(key_logic, world, player)


def create_exhaustive_placement_rules(key_layout, world, player):
    key_logic = key_layout.key_logic
    max_ctr = find_max_counter(key_layout)
    for code, key_counter in key_layout.key_counters.items():
        if skip_key_counter_due_to_prize(key_layout, key_counter):
            continue  # we have the prize, we are not concerned about this case
        accessible_loc = set()
        accessible_loc.update(key_counter.free_locations)
        accessible_loc.update(key_counter.key_only_locations)
        blocked_loc = key_layout.item_locations.difference(accessible_loc)
        valid_rule = True
        # min_keys = max(count_unique_sm_doors(key_counter.child_doors), key_counter.used_keys + 1)
        min_keys = key_counter.used_keys + 1
        if len(blocked_loc) > 0 and len(key_counter.key_only_locations) < min_keys:
            rule = PlacementRule()
            rule.door_reference = code
            rule.small_key = key_logic.small_key_name
            if key_counter.big_key_opened or not big_key_progress(key_counter):
                rule.needed_keys_w_bk = min_keys
                rule.bk_relevant = key_counter.big_key_opened
                if key_counter.big_key_opened and rule.needed_keys_w_bk + 1 > len(accessible_loc):
                    valid_rule = False      # indicates that the big key cannot be in the accessible locations
                    key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations))
                else:
                    placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
                    rule.check_locations_w_bk = accessible_loc
                    if key_layout.big_key_special:
                        rule.special_bk_avail = forced_big_key_avail(key_counter.important_locations) is not None
                    # check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
            else:
                if big_key_progress(key_counter) and only_sm_doors(key_counter):
                    create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player)
                rule.bk_conditional_set = blocked_loc
                rule.needed_keys_wo_bk = min_keys
                rule.check_locations_wo_bk = set(filter_big_chest(accessible_loc))
                rule.prize_relevance = key_layout.prize_relevant if world.prizeshuffle[player] == 'none' and rule_prize_relevant(key_counter) else None
            if valid_rule:
                key_logic.placement_rules.append(rule)
                adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr)
    refine_placement_rules(key_layout, max_ctr)
    refine_location_rules(key_layout)


def rule_prize_relevant(key_counter):
    return not key_counter.prize_doors_opened and not key_counter.prize_received


def skip_key_counter_due_to_prize(key_layout, key_counter):
    return key_layout.prize_relevant and key_counter.prize_received and not key_counter.prize_doors_opened


def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, player):
    if len(blocked_loc) == 1 and world.accessibility[player] != 'locations':
        blocked_others = set(max_ctr.other_locations).difference(set(ctr.other_locations))
        important_found = False
        for loc in blocked_others:
            if important_location(loc, world, player):
                important_found = True
                break
        if not important_found:
            rule.needed_keys_w_bk -= 1


# this rule is suspect - commented out usages for now
def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked):
    if rule.needed_keys_w_bk == key_layout.max_chests + len(max_ctr.key_only_locations):
        key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations))
        return True
    return False


def adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr):
    if rule.bk_conditional_set:
        test_set = (rule.bk_conditional_set - key_logic.bk_locked) - set(max_ctr.key_only_locations.keys())
        needed = rule.needed_keys_wo_bk if test_set else 0
    else:
        test_set = None
        needed = rule.needed_keys_w_bk
    if needed > 0:
        all_accessible = set(accessible_loc)
        all_accessible.update(key_counter.other_locations)
        blocked_loc = key_layout.all_locations-all_accessible
        for location in blocked_loc:
            if location not in key_logic.location_rules.keys():
                loc_rule = LocationRule()
                key_logic.location_rules[location] = loc_rule
            else:
                loc_rule = key_logic.location_rules[location]
            if test_set:
                if location not in key_logic.bk_locked:
                    cond_rule = None
                    for other in loc_rule.conditional_sets:
                        if other.conditional_set == test_set:
                            cond_rule = other
                            break
                    if not cond_rule:
                        cond_rule = ConditionalLocationRule(test_set)
                        loc_rule.conditional_sets.append(cond_rule)
                    cond_rule.small_key_num = max(needed, cond_rule.small_key_num)
            else:
                loc_rule.small_key_num = max(needed, loc_rule.small_key_num)


def refine_placement_rules(key_layout, max_ctr):
    key_logic = key_layout.key_logic
    changed = True
    while changed:
        changed = False
        rules_to_remove = {}
        for rule in key_logic.placement_rules:
            if rule.check_locations_w_bk:
                rule.check_locations_w_bk.difference_update(key_logic.sm_restricted)
                key_onlys = rule.check_locations_w_bk.intersection(max_ctr.key_only_locations)
                if len(key_onlys) > 0:
                    rule.check_locations_w_bk.difference_update(key_onlys)
                    rule.needed_keys_w_bk -= len(key_onlys)
                if rule.needed_keys_w_bk == 0:
                    rules_to_remove[rule] = None
                # todo: evaluate this usage
                # if rule.bk_relevant and len(rule.check_locations_w_bk) == rule.needed_keys_w_bk + 1:
                #     new_restricted = set(max_ctr.free_locations) - rule.check_locations_w_bk
                #     if len(new_restricted | key_logic.bk_restricted) < len(key_layout.all_chest_locations):
                #         if len(new_restricted - key_logic.bk_restricted) > 0:
                #             key_logic.bk_restricted.update(new_restricted)  # bk must be in one of the check_locations
                #             changed = True
                #     else:
                #         rules_to_remove.append(rule)
                #         changed = True
                if rule.needed_keys_w_bk > key_layout.max_chests or len(rule.check_locations_w_bk) < rule.needed_keys_w_bk:
                    logging.getLogger('').warning('Invalid rule - what went wrong here??')
                    rules_to_remove[rule] = None
                    changed = True
            if rule.bk_conditional_set is not None:
                rule.bk_conditional_set.difference_update(key_logic.bk_restricted)
                rule.bk_conditional_set.difference_update(max_ctr.key_only_locations)
                if len(rule.bk_conditional_set) == 0:
                    rules_to_remove[rule] = None
            if rule.check_locations_wo_bk:
                rule.check_locations_wo_bk.difference_update(key_logic.sm_restricted)
                key_onlys = rule.check_locations_wo_bk.intersection(max_ctr.key_only_locations)
                if len(key_onlys) > 0:
                    rule.check_locations_wo_bk.difference_update(key_onlys)
                    rule.needed_keys_wo_bk -= len(key_onlys)
                if rule.needed_keys_wo_bk == 0:
                    rules_to_remove[rule] = None
                if len(rule.check_locations_wo_bk) < rule.needed_keys_wo_bk or rule.needed_keys_wo_bk > key_layout.max_chests:
                    if not rule.prize_relevance and len(rule.bk_conditional_set) > 0:
                        key_logic.bk_restricted.update(rule.bk_conditional_set)
                        rules_to_remove[rule] = None
                        changed = True  # impossible for bk to be here, I think
        for rule_a, rule_b in itertools.combinations([x for x in key_logic.placement_rules if x not in rules_to_remove], 2):
            if rule_b.bk_conditional_set and rule_a.check_locations_w_bk:
                temp = rule_a
                rule_a = rule_b
                rule_b = temp
            if rule_a.bk_conditional_set and rule_b.check_locations_w_bk:
                common_needed = min(rule_a.needed_keys_wo_bk, rule_b.needed_keys_w_bk)
                common_locs = len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk)
                if (common_needed - common_locs) * 2 > key_layout.max_chests:
                    key_logic.bk_restricted.update(rule_a.bk_conditional_set)
                    rules_to_remove[rule_a] = None
                    changed = True
                    break
        equivalent_rules = []
        for rule in key_logic.placement_rules:
            for rule2 in key_logic.placement_rules:
                if rule != rule2 and rule not in rules_to_remove and rule2 not in rules_to_remove:
                    if rule.check_locations_w_bk and rule2.check_locations_w_bk:
                        if rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk > rule.needed_keys_w_bk:
                            rules_to_remove[rule] = None
                        elif rule2.needed_keys_w_bk == rule.needed_keys_w_bk and rule2.check_locations_w_bk < rule.check_locations_w_bk:
                            rules_to_remove[rule] = None
                        elif rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk == rule.needed_keys_w_bk:
                            equivalent_rules.append((rule, rule2))
                    if rule.check_locations_wo_bk and rule2.check_locations_wo_bk and rule.bk_conditional_set == rule2.bk_conditional_set:
                        if rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk > rule.needed_keys_wo_bk:
                            rules_to_remove[rule] = None
                        elif rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk and rule2.check_locations_wo_bk < rule.check_locations_wo_bk:
                            rules_to_remove[rule] = None
                        elif rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk:
                            equivalent_rules.append((rule, rule2))
        if len(rules_to_remove) > 0:
            key_logic.placement_rules = [x for x in key_logic.placement_rules if x not in rules_to_remove]
            equivalent_rules = [x for x in equivalent_rules if x[0] not in rules_to_remove and x[1] not in rules_to_remove]
        if len(equivalent_rules) > 0:
            removed_rules = {}
            for r1, r2 in equivalent_rules:
                if r1 in removed_rules.keys():
                    r1 = removed_rules[r1]
                if r2 in removed_rules.keys():
                    r2 = removed_rules[r2]
                if r1 != r2:
                    r1.door_reference += ','+r2.door_reference
                    key_logic.placement_rules.remove(r2)
                    removed_rules[r2] = r1


def refine_location_rules(key_layout):
    locs_to_remove = []
    for loc, rule in key_layout.key_logic.location_rules.items():
        conditions_to_remove = []
        for cond_rule in rule.conditional_sets:
            if cond_rule.small_key_num <= rule.small_key_num:
                conditions_to_remove.append(cond_rule)
        rule.conditional_sets = [x for x in rule.conditional_sets if x not in conditions_to_remove]
        if rule.small_key_num == 0 and len(rule.conditional_sets) == 0:
            locs_to_remove.append(loc)
    for loc in locs_to_remove:
        del key_layout.key_logic.location_rules[loc]


def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player):
    key_logic = key_layout.key_logic
    rule = PlacementRule()
    rule.door_reference = code
    rule.small_key = key_logic.small_key_name
    rule.needed_keys_w_bk = min_keys
    if key_counter.big_key_opened and rule.needed_keys_w_bk + 1 > len(accessible_loc):
        # indicates that the big key cannot be in the accessible locations
        key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations))
    else:
        placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
        rule.check_locations_w_bk = accessible_loc
        # check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
        key_logic.placement_rules.append(rule)
        adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr)


def queue_sorter(queue_item):
    door, counter = queue_item
    if door is None:
        return 0
    return 1 if door.bigKey else 0


def queue_sorter_2(queue_item):
    door, counter, key_only = queue_item
    if door is None:
        return 0
    return 1 if door.bigKey else 0


def find_bk_locked_sections(key_layout, world, player):
    key_counters = key_layout.key_counters
    key_logic = key_layout.key_logic

    bk_not_required = set()
    big_chest_allowed_big_key = world.accessibility[player] != 'locations'
    for counter in key_counters.values():
        key_layout.all_chest_locations.update(counter.free_locations)
        key_layout.item_locations.update(counter.free_locations)
        key_layout.item_locations.update(counter.key_only_locations)
        key_layout.all_locations.update(key_layout.item_locations)
        key_layout.all_locations.update(counter.other_locations)
        if counter.big_key_opened and counter.important_location:
            big_chest_allowed_big_key = False
        if not counter.big_key_opened:
            bk_not_required.update(counter.free_locations)
            bk_not_required.update(counter.key_only_locations)
            bk_not_required.update(counter.other_locations)
    # todo?: handle bk special differently in cross dungeon
    # notably: things behind bk doors - relying on the bk door logic atm
    if not key_layout.big_key_special:
        key_logic.bk_restricted.update(dict.fromkeys(set(key_layout.all_chest_locations).difference(bk_not_required)))
        key_logic.bk_locked.update(dict.fromkeys(set(key_layout.all_locations) - bk_not_required))
    if not big_chest_allowed_big_key:
        bk_required_locations = find_big_chest_locations(key_layout.all_chest_locations)
        bk_required_locations += find_big_key_locked_locations(key_layout.all_chest_locations)
        key_logic.bk_restricted.update(bk_required_locations)
        key_logic.bk_locked.update(bk_required_locations)


def empty_counter(counter):
    if len(counter.key_only_locations) != 0 or len(counter.free_locations) != 0 or len(counter.child_doors) != 0:
        return False
    return not counter.important_location


def relative_empty_counter(odd_counter, key_counter):
    if len(set(odd_counter.key_only_locations).difference(key_counter.key_only_locations)) > 0:
        return False
    if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
        return False
    if len(set(odd_counter.other_locations).difference(key_counter.other_locations)) > 0:
        return False
    # important only
    if len(set(odd_counter.important_locations).difference(key_counter.important_locations)) > 0:
        return False
    new_child_door = False
    for child in odd_counter.child_doors:
        if unique_child_door(child, key_counter):
            new_child_door = True
            break
    if new_child_door:
        return False
    return True


def relative_empty_counter_2(odd_counter, key_counter):
    if len(set(odd_counter.key_only_locations).difference(key_counter.key_only_locations)) > 0:
        return False
    if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
        return False
    # important only
    if len(set(odd_counter.important_locations).difference(key_counter.important_locations)) > 0:
        return False
    for child in odd_counter.child_doors:
        if unique_child_door_2(child, key_counter):
            return False
    return True


def progressive_ctr(new_counter, last_counter):
    if len(set(new_counter.key_only_locations).difference(last_counter.key_only_locations)) > 0:
        return True
    if len(set(new_counter.free_locations).difference(last_counter.free_locations)) > 0:
        return True
    for child in new_counter.child_doors:
        if unique_child_door_2(child, last_counter):
            return True
    return False


def unique_child_door(child, key_counter):
    if child in key_counter.child_doors or child.dest in key_counter.child_doors:
        return False
    if child in key_counter.open_doors or child.dest in key_counter.open_doors:
        return False
    if child.bigKey and key_counter.big_key_opened:
        return False
    return True


def unique_child_door_2(child, key_counter):
    if child in key_counter.child_doors or child.dest in key_counter.child_doors:
        return False
    if child in key_counter.open_doors or child.dest in key_counter.open_doors:
        return False
    return True


# def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag):  # try to waste as many keys as possible?
#     ignored_doors = {door, door.dest} if door is not None else {}
#     finished = False
#     opened_doors = dict(key_counter.open_doors)
#     bk_opened = key_counter.big_key_opened
#     # new_counter = key_counter
#     last_counter = key_counter
#     while not finished:
#         door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk)
#         if door_set is None or len(door_set) == 0:
#             finished = True
#             continue
#         for new_door in door_set:
#             proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
#             bk_open = bk_opened or new_door.bigKey
#             new_counter = find_counter(proposed_doors, bk_open, key_layout)
#             bk_open = new_counter.big_key_opened
#             # this means the new_door invalidates the door / leads to the same stuff
#             if not empty_flag and relative_empty_counter(odd_counter, new_counter):
#                 ignored_doors.add(new_door)
#             elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player):
#                 last_counter = new_counter
#                 opened_doors = proposed_doors
#                 bk_opened = bk_open
#             else:
#                 ignored_doors.add(new_door)
#     return last_counter


def find_best_counter(door, key_layout, odd_counter, skip_bk, empty_flag):
    best, best_ctr, locations = 0, None, 0
    for code, counter in key_layout.key_counters.items():
        if door not in counter.open_doors:
            if best_ctr is None or counter.used_keys > best or (counter.used_keys == best and count_locations(counter) > locations):
                if not skip_bk or not counter.big_key_opened:
                    if empty_flag or not relative_empty_counter(odd_counter, counter):
                        best = counter.used_keys
                        best_ctr = counter
                        locations = count_locations(counter)
    return best_ctr


def count_locations(ctr):
    return len(ctr.free_locations) + len(ctr.key_only_locations) + len(ctr.other_locations) + len(ctr.important_locations)


def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk):  # try to waste as many keys as possible?
    ignored_doors = {door, door.dest} if door is not None else {}
    finished = False
    opened_doors = dict(key_counter.open_doors)
    bk_opened = key_counter.big_key_opened
    # new_counter = key_counter
    last_counter = key_counter
    while not finished:
        door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk, 0)
        if door_set is None or len(door_set) == 0:
            finished = True
            continue
        for new_door in door_set:
            proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
            bk_open = bk_opened or new_door.bigKey
            new_counter = find_counter(proposed_doors, bk_open, key_layout, key_counter.prize_doors_opened)
            bk_open = new_counter.big_key_opened
            if not new_door.bigKey and progressive_ctr(new_counter, last_counter) and relative_empty_counter_2(odd_counter, new_counter):
                ignored_doors.add(new_door)
            else:
                last_counter = new_counter
                opened_doors = proposed_doors
                bk_opened = bk_open
            # this means the new_door invalidates the door / leads to the same stuff
    return last_counter


def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk, reserve=1):
    small_doors = []
    big_doors = []
    if key_layout.big_key_special:
        big_key_available = any(x for x in key_counter.other_locations.keys() if x.forced_item and x.forced_item.bigkey)
    else:
        big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(reserve) > 0
    for other in key_counter.child_doors:
        if other not in ignored_doors and other.dest not in ignored_doors:
            if other.bigKey:
                if not skip_bk and (not key_layout.big_key_special or big_key_available):
                    big_doors.append(other)
            elif other.dest not in small_doors:
                small_doors.append(other)
    if len(small_doors) == 0 and (not skip_bk and (len(big_doors) == 0 or not big_key_available)):
        return None
    return small_doors + big_doors


def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, player):
    if new_door.bigKey:  # big keys are not wastes - it uses up a location
        return True
    chest_keys = available_chest_small_keys(old_counter, world, player)
    old_key_diff = len(old_counter.key_only_locations) - old_counter.used_keys
    old_avail = chest_keys + old_key_diff
    new_chest_keys = available_chest_small_keys(new_counter, world, player)
    new_key_diff = len(new_counter.key_only_locations) - new_counter.used_keys
    new_avail = new_chest_keys + new_key_diff
    if new_key_diff < old_key_diff or new_avail < old_avail:
        return True
    if new_avail >= old_avail:
        wasted_keys = 0
        old_children = old_counter.child_doors.keys()
        new_children = [x for x in new_counter.child_doors.keys() if x != old_door and x.dest != old_door and (not x.bigKey or x not in old_children)]
        current_counter = new_counter
        opened_doors = dict(current_counter.open_doors)
        bk_opened = current_counter.big_key_opened
        for new_child in new_children:
            proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])}
            bk_open = bk_opened or new_door.bigKey
            new_counter = find_counter(proposed_doors, bk_open, key_layout, current_counter.prize_doors_opened)
            if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player):
                wasted_keys += 1
            if new_avail - wasted_keys < old_avail:
                return True  # waste is possible
    return False


def find_next_counter(new_door, old_counter, key_layout, prize_flag=None):
    prop_doors = next((item_or_tuple for item_or_tuple in key_layout.proposal
                       if new_door == item_or_tuple or (isinstance(item_or_tuple, tuple) and new_door in item_or_tuple)), None)
    if prop_doors:
        prop_doors = list(prop_doors) if isinstance(prop_doors, tuple) else [prop_doors]
        proposed_doors = {**old_counter.open_doors, **dict.fromkeys(prop_doors)}
    else:
        proposed_doors = {**old_counter.open_doors}
    bk_open = old_counter.big_key_opened or new_door.bigKey
    prize_flag = prize_flag if prize_flag else old_counter.prize_doors_opened
    return find_counter(proposed_doors, bk_open, key_layout, prize_flag)


def check_special_locations(locations):
    for loc in locations:
        if loc.name == 'Hyrule Castle - Zelda\'s Chest':
            return True
    return False


def calc_avail_keys(key_counter, world, player):
    chest_keys = available_chest_small_keys(key_counter, world, player)
    raw_avail = chest_keys + len(key_counter.key_only_locations)
    return raw_avail - key_counter.used_keys


def create_rule(key_counter, prev_counter, world, player):
    # prev_chest_keys = available_chest_small_keys(prev_counter, world)
    # prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
    chest_keys = available_chest_small_keys(key_counter, world, player)
    key_gain = len(key_counter.key_only_locations) - len(prev_counter.key_only_locations)
    # previous method
    # raw_avail = chest_keys + len(key_counter.key_only_locations)
    # available = raw_avail - key_counter.used_keys
    # possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
    # required_keys = min(available, possible_smalls) + key_counter.used_keys
    required_keys = key_counter.used_keys + 1  # this makes more sense, if key_counter has wasted all keys
    adj_chest_keys = min(chest_keys, required_keys)
    needed_chests = required_keys - len(key_counter.key_only_locations)
    is_valid = needed_chests <= chest_keys
    unneeded_chests = min(key_gain, max(0, adj_chest_keys - needed_chests))
    rule_num = required_keys - unneeded_chests
    return DoorRules(rule_num, is_valid)


def create_worst_case_rule(rules, key_counter, world, player):
    required_keys = key_counter.used_keys + 1  # this makes more sense, if key_counter has wasted all keys
    rules.new_rules[KeyRuleType.WorstCase] = required_keys


def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player):
    if world.accessibility[player] != 'locations':
        counter = find_inverted_counter(door, parent_counter, key_layout, world, player)
        if not self_lock_possible(counter):
            return
        if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location:
            rule.allow_small = True
            rule.small_location = next(iter(counter.free_locations))
            rule.new_rules[KeyRuleType.AllowSmall] = rule.new_rules[KeyRuleType.WorstCase] - 1


def find_inverted_counter(door, parent_counter, key_layout, world, player):
    # open all doors in counter
    counter = open_all_counter(parent_counter, key_layout, world, player, door=door)
    max_counter = find_max_counter(key_layout)
    # find the difference
    inverted_counter = KeyCounter(key_layout.max_chests)
    inverted_counter.free_locations = dict_difference(max_counter.free_locations, counter.free_locations)
    inverted_counter.key_only_locations = dict_difference(max_counter.key_only_locations, counter.key_only_locations)
    # child doors? used_keys?
    # inverted_counter.child_doors = dict_difference(max_counter.child_doors, counter.child_doors)
    inverted_counter.open_doors = dict_difference(max_counter.open_doors, counter.open_doors)
    inverted_counter.other_locations = dict_difference(max_counter.other_locations, counter.other_locations)
    for loc in inverted_counter.other_locations:
        if important_location(loc, world, player):
            inverted_counter.important_location = True
    return inverted_counter


def open_all_counter(parent_counter, key_layout, world, player, door=None, skipBk=False):
    changed = True
    counter = parent_counter
    proposed_doors = dict.fromkeys(parent_counter.open_doors.keys())
    while changed:
        changed = False
        doors_to_open = {}
        for child in counter.child_doors:
            if door is None or (child != door and child != door.dest):
                if skipBk:
                    if not child.bigKey:
                        doors_to_open[child] = None
                elif can_open_door_by_counter(child, counter, key_layout, world, player):
                    doors_to_open[child] = None
        if len(doors_to_open.keys()) > 0:
            proposed_doors = {**proposed_doors, **doors_to_open}
            bk_hint = counter.big_key_opened or any(d.bigKey for d in doors_to_open.keys())
            counter = find_counter(proposed_doors, bk_hint, key_layout, True)
            changed = True
    return counter


def open_some_counter(parent_counter, key_layout, ignored_doors):
    changed = True
    counter = parent_counter
    proposed_doors = dict.fromkeys(parent_counter.open_doors.keys())
    while changed:
        changed = False
        doors_to_open = {}
        for child in counter.child_doors:
            if child not in ignored_doors:
                if not child.bigKey:
                    doors_to_open[child] = None
        if len(doors_to_open.keys()) > 0:
            proposed_doors = {**proposed_doors, **doors_to_open}
            bk_hint = counter.big_key_opened
            for d in doors_to_open.keys():
                bk_hint = bk_hint or d.bigKey
            counter = find_counter(proposed_doors, bk_hint, key_layout, parent_counter.prize_doors_opened)
            changed = True
    return counter


def self_lock_possible(counter):
    return len(counter.free_locations) <= 1 and len(counter.key_only_locations) == 0 and not counter.important_location


def available_chest_small_keys(key_counter, world, player):
    if world.keyshuffle[player] == 'none':
        cnt = 0
        for loc in key_counter.free_locations:
            if key_counter.big_key_opened or '- Big Chest' not in loc.name:
                cnt += 1
        return min(cnt, key_counter.max_chests)
    else:
        return key_counter.max_chests


def available_chest_small_keys_logic(key_counter, world, player, sm_restricted):
    if world.keyshuffle[player] == 'none':
        cnt = 0
        for loc in key_counter.free_locations:
            if loc not in sm_restricted and (key_counter.big_key_opened or '- Big Chest' not in loc.name):
                cnt += 1
        return min(cnt, key_counter.max_chests)
    else:
        return key_counter.max_chests


def big_key_drop_available(key_counter):
    for loc in key_counter.other_locations:
        if loc.forced_big_key():
            return True
    return False


def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player):
    if key_counter.big_key_opened:
        return
    best_counter = find_best_counter(door, key_layout, odd_counter, True, empty_flag)
    bk_rule = create_rule(best_counter, key_counter, world, player)
    if bk_rule.small_key_num >= rule.small_key_num:
        return
    door_open = find_next_counter(door, best_counter, key_layout)
    ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors)
    dest_ignored = []
    for d in ignored_doors.keys():
        if d.dest not in ignored_doors:
            dest_ignored.append(d.dest)
    ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
    post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
    unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations)
    # todo: figure out the intention behind this change - better way to detect the big key is blocking needed key onlys?
    if len(unique_loc) > 0:  # and bk_rule.is_valid
        rule.alternate_small_key = bk_rule.small_key_num
        rule.alternate_big_key_loc.update(unique_loc)
        if not door.bigKey:
            rule.new_rules[(KeyRuleType.Lock, key_layout.key_logic.bk_name)] = best_counter.used_keys + 1


def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout):
    if key_counter.big_key_opened:
        return None, None, None
    worst_counter = find_worst_counter(door, odd_ctr, key_counter, key_layout, True)
    bk_rule_num = worst_counter.used_keys + 1
    bk_access_set = set()
    bk_access_set.update(worst_counter.free_locations)
    bk_access_set.update(worst_counter.key_only_locations)
    if bk_rule_num == small_key_num and len(bk_access_set ^ accessible_set) == 0:
        return None, None, None
    door_open = find_next_counter(door, worst_counter, key_layout)
    ignored_doors = dict_intersection(worst_counter.child_doors, door_open.child_doors)
    dest_ignored = []
    for door in ignored_doors.keys():
        if door.dest not in ignored_doors:
            dest_ignored.append(door.dest)
    ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
    post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
    return worst_counter, post_counter, bk_rule_num


def open_a_door(door, child_state, flat_proposal, world, player):
    if door.bigKey or door.name in get_special_big_key_doors(world, player):
        child_state.big_key_opened = True
        child_state.avail_doors.extend(child_state.big_doors)
        child_state.opened_doors.extend(set([d.door for d in child_state.big_doors]))
        child_state.big_doors.clear()
    elif door in child_state.prize_door_set:
        child_state.prize_doors_opened = True
        for exp_door in child_state.prize_doors:
            new_region = exp_door.door.entrance.parent_region
            child_state.visit_region(new_region, key_checks=True)
            child_state.add_all_doors_check_keys(new_region, flat_proposal, world, player)
        child_state.prize_doors.clear()
    else:
        child_state.opened_doors.append(door)
        doors_to_open = [x for x in child_state.small_doors if x.door == door]
        child_state.small_doors[:] = [x for x in child_state.small_doors if x.door != door]
        child_state.avail_doors.extend(doors_to_open)
        dest_door = door.dest
        if dest_door in flat_proposal and door.type != DoorType.SpiralStairs:
            child_state.opened_doors.append(dest_door)
            if child_state.in_door_list_ic(dest_door, child_state.small_doors):
                now_available = [x for x in child_state.small_doors if x.door == dest_door]
                child_state.small_doors[:] = [x for x in child_state.small_doors if x.door != dest_door]
                child_state.avail_doors.extend(now_available)


# allows dest doors
def unique_doors(doors):
    unique_d_set = []
    for d in doors:
        if d.door not in unique_d_set:
            unique_d_set.append(d.door)
    return unique_d_set


# does not allow dest doors
def count_unique_sm_doors(doors):
    unique_d_set = set()
    for d in doors:
        if d not in unique_d_set and (d.dest not in unique_d_set or d.type == DoorType.SpiralStairs) and not d.bigKey:
            unique_d_set.add(d)
    return len(unique_d_set)


def big_key_progress(key_counter):
    return not only_sm_doors(key_counter) or exist_big_chest(key_counter)


def only_sm_doors(key_counter):
    for door in key_counter.child_doors:
        if door.bigKey:
            return False
    return True


# doesn't count dest doors
def count_unique_small_doors(key_counter, proposal):
    cnt = 0
    counted = set()
    for door in key_counter.child_doors:
        if door in proposal and door not in counted:
            cnt += 1
            counted.add(door)
            if door.type != DoorType.SpiralStairs:
                counted.add(door.dest)
    return cnt


def exist_relevant_big_doors(key_counter, key_layout):
    bk_counter = find_counter(key_counter.open_doors, True, key_layout, key_counter.prize_doors_opened, False)
    if bk_counter is not None:
        diff = dict_difference(bk_counter.free_locations, key_counter.free_locations)
        if len(diff) > 0:
            return True
        diff = dict_difference(bk_counter.key_only_locations, key_counter.key_only_locations)
        if len(diff) > 0:
            return True
        diff = dict_difference(bk_counter.child_doors, key_counter.child_doors)
        if len(diff) > 0:
            return True
    return False


def exist_big_chest(key_counter):
    for loc in key_counter.free_locations:
        if '- Big Chest' in loc.name:
            return True
    return False


def count_locations_big_optional(locations, bk=False):
    cnt = 0
    for loc in locations:
        if bk or '- Big Chest' not in loc.name:
            cnt += 1
    return cnt


def filter_big_chest(locations):
    return [x for x in locations if '- Big Chest' not in x.name]


def location_is_bk_locked(loc, key_logic):
    return loc in key_logic.bk_chests or loc in key_logic.bk_locked


# todo: verfiy this code is defunct
# def prize_or_event(loc):
#     return loc.name in location_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
#
#
# def reserved_location(loc, world, player):
#     return loc in world.item_pool.config.reserved_locations[player]
#
#
# def blind_boss_unavail(loc, state, world, player):
#     if loc.name == "Thieves' Town - Boss":
#         return (loc.parent_region.dungeon.boss.name == 'Blind' and
#                 (not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or
#                  (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and
#                   not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor'))))
#     return False


# counts free locations for keys - hence why reserved locations don't count
def count_free_locations(state, world, player):
    cnt = 0
    for loc in state.found_locations:
        if (not prize_or_event(loc) and not loc.forced_item and not reserved_location(loc, world, player)
           and not blind_boss_unavail(loc, state.found_locations, world, player)):
            cnt += 1
    return cnt


def count_small_key_only_locations(state):
    cnt = 0
    for loc in state.found_locations:
        if loc.forced_item and loc.item.smallkey:
            cnt += 1
    return cnt


def big_chest_in_locations(locations):
    return len(find_big_chest_locations(locations)) > 0


def find_big_chest_locations(locations):
    ret = []
    for loc in locations:
        if 'Big Chest' in loc.name:
            ret.append(loc)
    return ret


def find_big_key_locked_locations(locations):
    ret = []
    for loc in locations:
        if loc.name in ["Thieves' Town - Blind's Cell", "Hyrule Castle - Zelda's Chest"]:
            ret.append(loc)
    return ret


def expand_key_state(state, flat_proposal, world, player):
    while len(state.avail_doors) > 0:
        exp_door = state.next_avail_door()
        door = exp_door.door
        connect_region = world.get_entrance(door.name, player).connected_region
        if state.validate(door, connect_region, world, player):
            state.visit_region(connect_region, key_checks=True)
            state.add_all_doors_check_keys(connect_region, flat_proposal, world, player)


def expand_big_key_state(state, flat_proposal, world, player):
    while len(state.avail_doors) > 0:
        exp_door = state.next_avail_door()
        door = exp_door.door
        connect_region = world.get_entrance(door.name, player).connected_region
        if state.validate(door, connect_region, world, player):
            state.visit_region(connect_region, key_checks=True)
            state.add_all_doors_check_big_keys(connect_region, flat_proposal, world, player)


def flatten_pair_list(paired_list):
    flat_list = []
    for d in paired_list:
        if type(d) is tuple:
            flat_list.append(d[0])
            flat_list.append(d[1])
        else:
            flat_list.append(d)
    return flat_list


def check_rules(original_counter, key_layout, world, player):
    all_key_only = set()
    key_only_map = {}
    queue = deque([(None, original_counter, original_counter.key_only_locations)])
    completed = set()
    completed.add(cid(original_counter, key_layout))
    while len(queue) > 0:
        queue = deque(sorted(queue, key=queue_sorter_2))
        access_door, counter, key_only_loc = queue.popleft()
        for loc in key_only_loc:
            if loc not in all_key_only:
                all_key_only.add(loc)
                access_rules = []
                key_only_map[loc] = access_rules
            else:
                access_rules = key_only_map[loc]
            if access_door is None or access_door.name not in key_layout.key_logic.door_rules.keys():
                if access_door is None or not access_door.bigKey:
                    access_rules.append(DoorRules(0, True))
            else:
                rule = key_layout.key_logic.door_rules[access_door.name]
                if rule not in access_rules:
                    access_rules.append(rule)
        for child in counter.child_doors.keys():
            if not child.bigKey or not key_layout.big_key_special or counter.big_key_opened:
                next_counter = find_next_counter(child, counter, key_layout)
                c_id = cid(next_counter, key_layout)
                if c_id not in completed:
                    completed.add(c_id)
                    new_key_only = dict_difference(next_counter.key_only_locations, counter.key_only_locations)
                    queue.append((child, next_counter, new_key_only))
    min_rule_bk = defaultdict(list)
    min_rule_non_bk = defaultdict(list)
    check_non_bk = False
    for loc, rule_list in key_only_map.items():
        m_bk = None
        m_nbk = None
        for rule in rule_list:
            if m_bk is None or rule.small_key_num <= m_bk:
                min_rule_bk[loc].append(rule)
                m_bk = rule.small_key_num
            if rule.alternate_small_key is None:
                ask = rule.small_key_num
            else:
                check_non_bk = True
                ask = rule.alternate_small_key
            if m_nbk is None or ask <= m_nbk:
                min_rule_non_bk[loc].append(rule)
                m_nbk = rule.alternate_small_key
    adjust_key_location_mins(key_layout, min_rule_bk, lambda r: r.small_key_num, lambda r, v: setattr(r, 'small_key_num', v))
    if check_non_bk:
        adjust_key_location_mins(key_layout, min_rule_non_bk, lambda r: r.small_key_num if r.alternate_small_key is None else r.alternate_small_key,
                                 lambda r, v: r if r.alternate_small_key is None else setattr(r, 'alternate_small_key', v))
    check_rules_deep(original_counter, key_layout, world, player)


def adjust_key_location_mins(key_layout, min_rules, getter, setter):
    collected_keys = key_layout.max_chests
    collected_locs = set()
    changed = True
    while changed:
        changed = False
        for_removal = []
        for loc, rules in min_rules.items():
            if loc in collected_locs:
                for_removal.append(loc)
            for rule in rules:
                if getter(rule) <= collected_keys and loc not in collected_locs:
                    changed = True
                    collected_keys += 1
                    collected_locs.add(loc)
                    for_removal.append(loc)
        for loc in for_removal:
            del min_rules[loc]
    if len(min_rules) > 0:
        for loc, rules in min_rules.items():
            for rule in rules:
                setter(rule, collected_keys)


def check_rules_deep(original_counter, key_layout, world, player):
    key_logic = key_layout.key_logic
    big_locations = {x for x in key_layout.all_chest_locations if x not in key_logic.bk_restricted}
    queue = deque([original_counter])
    completed = set()
    completed.add(cid(original_counter, key_layout))
    last_counter = None
    bail = 0
    while len(queue) > 0:
        counter = queue.popleft()
        if counter == last_counter:
            bail += 1
            if bail > 10:
                raise Exception('Key logic issue, during deep rule check: %s' % key_layout.sector.name)
        else:
            bail = 0
        last_counter = counter
        chest_keys = available_chest_small_keys_logic(counter, world, player, key_logic.sm_restricted)
        bk_drop = big_key_drop_available(counter)
        big_avail = counter.big_key_opened or bk_drop
        big_maybe_not_found = not counter.big_key_opened and not bk_drop  # better named as big_missing?
        if not key_layout.big_key_special and not big_avail:
            if world.bigkeyshuffle[player] != 'none':
                big_avail = True
            else:
                for location in counter.free_locations:
                    if location not in key_logic.bk_restricted:
                        big_avail = True
                        break
        outstanding_big_locs = {x for x in big_locations if x not in counter.free_locations}
        if big_maybe_not_found:
            if len(outstanding_big_locs) == 0 and not key_layout.big_key_special:
                big_maybe_not_found = False
        big_uses_chest = big_avail and not key_layout.big_key_special
        collected_alt = len(counter.key_only_locations) + chest_keys
        if big_uses_chest and chest_keys == count_locations_big_optional(counter.free_locations, counter.big_key_opened):
            chest_keys -= 1
        collected = len(counter.key_only_locations) + chest_keys
        can_progress = len(counter.child_doors) == 0
        smalls_opened, big_opened = False, False
        small_rules = []
        for door in counter.child_doors.keys():
            can_open = False
            if door.bigKey and big_avail:
                can_open = True
            elif door.name in key_logic.door_rules.keys():
                rule = key_logic.door_rules[door.name]
                small_rules.append(rule)
                if rule_satisfied(rule, collected, collected_alt, outstanding_big_locs, chest_keys, key_layout):
                    can_open = True
                    smalls_opened = True
            elif not door.bigKey:
                can_open = True
            if can_open:
                can_progress = (big_avail or not big_maybe_not_found) if door.bigKey else smalls_opened
                next_counter = find_next_counter(door, counter, key_layout)
                c_id = cid(next_counter, key_layout)
                if c_id not in completed:
                    completed.add(c_id)
                    queue.append(next_counter)
        if not can_progress:
            if len(small_rules) > 0:  # zero could be indicative of a problem, but also, the big key is now required
                reduce_rules(small_rules, collected, collected_alt)
                queue.append(counter)  # run it through again
            else:
                raise Exception('Possible problem with generation or bk rules')


def rule_satisfied(rule, collected, collected_alt, outstanding_big_locs, chest_keys, key_layout):
    if collected >= rule.small_key_num:
        return True
    if rule.allow_small and collected >= rule.small_key_num-1 and chest_keys < key_layout.max_chests:
        return True
    rule_diff = outstanding_big_locs.difference(rule.alternate_big_key_loc)
    if rule.alternate_small_key is not None and len(rule_diff) == 0 and collected >= rule.alternate_small_key:
        return True
    if collected_alt > collected:
        if collected_alt >= rule.small_key_num:
            return True
        if rule.allow_small and collected_alt >= rule.small_key_num-1 and chest_keys+1 < key_layout.max_chests:
            return True
        if rule.alternate_small_key is not None and len(rule_diff) == 0 and collected_alt >= rule.alternate_small_key:
            return True
    return False


def reduce_rules(small_rules, collected, collected_alt):
    smallest_rules = []
    min_num = None
    for rule in small_rules:
        if min_num is None or rule.small_key_num <= min_num:
            if min_num is not None and rule.small_key_num < min_num:
                min_num = rule.small_key_num
                smallest_rules.clear()
            elif min_num is None:
                min_num = rule.small_key_num
            smallest_rules.append(rule)
    for rule in smallest_rules:
        if rule.allow_small:  # we are already reducing it
            rule.allow_small = False
        if min_num > collected_alt > collected:
            rule.small_key_num = collected_alt
        else:
            rule.small_key_num = collected


def set_paired_rules(key_logic, world, player):
    for d_name, rule in key_logic.door_rules.items():
        door = world.get_door(d_name, player)
        if door.dest.name in key_logic.door_rules.keys():
            rule.opposite = key_logic.door_rules[door.dest.name]


def check_bk_special(regions, world, player):
    for r_name in regions:
        region = world.get_region(r_name, player)
        for loc in region.locations:
            if loc.forced_big_key():
                return True
    return False


def forced_big_key_avail(locations):
    for loc in locations:
        if loc.forced_big_key():
            return loc
    return None


def prize_relevance(key_layout, dungeon_entrance, is_atgt_swapped):
    if len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_table[key_layout.key_logic.dungeon].prize:
        if dungeon_entrance.name == ('Agahnims Tower' if is_atgt_swapped else 'Ganons Tower'):
            return 'GT'
        elif dungeon_entrance.name == 'Pyramid Fairy':
            return 'BigBomb'
    return None


def prize_relevance_sig2(start_regions, d_name, dungeon_entrance, is_atgt_swapped):
    if len(start_regions) > 1 and dungeon_entrance and dungeon_table[d_name].prize:
        if dungeon_entrance.name == ('Agahnims Tower' if is_atgt_swapped else 'Ganons Tower'):
            return 'GT'
        elif dungeon_entrance.name == 'Pyramid Fairy':
            return 'BigBomb'
    return None


def validate_bk_layout(proposal, builder, start_regions, world, player):
    bk_special = check_bk_special(builder.master_sector.regions, world, player)
    if world.bigkeyshuffle[player] != 'none' and (world.dropshuffle[player] != 'none' or not bk_special):
        return True
    flat_proposal = flatten_pair_list(proposal)
    state = ExplorationState(dungeon=builder.name)
    state.big_key_special = bk_special
    for region in start_regions:
        dungeon_entrance, portal_door = find_outside_connection(region)
        prize_relevant_flag = prize_relevance_sig2(start_regions, builder.name, dungeon_entrance, world.is_atgt_swapped(player))
        if prize_relevant_flag and world.prizeshuffle[player] in ['none', 'dungeon']:
            state.append_door_to_list(portal_door, state.prize_doors)
            state.prize_door_set[portal_door] = dungeon_entrance
            # key_layout.prize_relevant = prize_relevant_flag
        else:
            state.visit_region(region, key_checks=True)
            state.add_all_doors_check_big_keys(region, flat_proposal, world, player)
    expand_big_key_state(state, flat_proposal, world, player)
    if bk_special:
        for loc in state.found_locations:
            if loc.forced_big_key():
                return True
    else:
        return state.count_locations_exclude_specials(world, player) > 0
    return False


# Soft lock stuff
def validate_key_layout(key_layout, world, player):
    # universal key is all good - except for hyrule castle in standard mode
    if world.logic[player] == 'nologic' or (world.keyshuffle[player] == 'universal' and
       (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')):
        return True
    flat_proposal = key_layout.flat_prop
    state = ExplorationState(dungeon=key_layout.sector.name)
    state.init_zelda_event_doors(key_layout.event_starts, player)
    state.key_locations = key_layout.max_chests
    state.big_key_special = check_bk_special(key_layout.sector.regions, world, player)
    for region in key_layout.start_regions:
        dungeon_entrance, portal_door = find_outside_connection(region)
        prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance, world.is_atgt_swapped(player))
        if prize_relevant_flag and world.prizeshuffle[player] in ['none', 'dungeon']:
            state.append_door_to_list(portal_door, state.prize_doors)
            state.prize_door_set[portal_door] = dungeon_entrance
            key_layout.prize_relevant = prize_relevant_flag
        else:
            state.visit_region(region, key_checks=True)
            state.add_all_doors_check_keys(region, flat_proposal, world, player)
    return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, None, 0, world, player)


def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposal, prev_state, prev_avail, world, player):
    expand_key_state(state, flat_proposal, world, player)
    smalls_avail = len(state.small_doors) > 0   # de-dup crystal repeats
    num_bigs = 1 if len(state.big_doors) > 0 else 0  # all or nothing
    if not smalls_avail and num_bigs == 0:
        return True   # I think that's the end
    # todo: fix state to separate out these types
    if state.big_key_opened:
        ttl_locations = count_free_locations(state, world, player)
    else:
        ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player)
    ttl_small_key_only = count_small_key_only_locations(state)
    available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state,
                                                          key_layout, world, player)
    available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
    if invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player):
        return False
    # todo: allow more key shuffles - refine placement rules
    # if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0):
    found_forced_bk = state.found_forced_bk()
    smalls_done = not smalls_avail or available_small_locations == 0
    # or not enough_small_locations(state, available_small_locations)
    bk_done = state.big_key_opened or num_bigs == 0 or (available_big_locations == 0 and not found_forced_bk)
    # prize door should not be opened if the boss is reachable - but not reached yet
    allow_for_prize_lock = (key_layout.prize_can_lock and
                            not any(x for x in state.found_locations if x.prize))
    prize_done = not key_layout.prize_relevant or state.prize_doors_opened or allow_for_prize_lock
    if smalls_done and bk_done and prize_done:
        return False
    else:
        # todo: pretty sure you should OR these paths together, maybe when there's one location and it can
        # either be small or big key
        if smalls_avail and available_small_locations > 0:
            for exp_door in state.small_doors:
                state_copy = state.copy()
                open_a_door(exp_door.door, state_copy, flat_proposal, world, player)
                state_copy.used_smalls += 1
                if state_copy.used_smalls > ttl_small_key_only:
                    state_copy.used_locations += 1
                code = validate_id(state_copy, flat_proposal)
                if code not in checked_states.keys():
                    valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
                                                         state, available_small_locations, world, player)
                    checked_states[code] = valid
                else:
                    valid = checked_states[code]
                if not valid:
                    return False
        if not state.big_key_opened and (available_big_locations >= num_bigs > 0 or (found_forced_bk and num_bigs > 0)):
            state_copy = state.copy()
            open_a_door(state.big_doors[0].door, state_copy, flat_proposal, world, player)
            if not found_forced_bk:
                state_copy.used_locations += 1
            code = validate_id(state_copy, flat_proposal)
            if code not in checked_states.keys():
                valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
                                                     state, available_small_locations, world, player)
                checked_states[code] = valid
            else:
                valid = checked_states[code]
            if not valid:
                return False
        # todo: feel like you only open these if the boss is available???
        # todo: or if a crystal isn't valid placement on this boss
        if not state.prize_doors_opened and key_layout.prize_relevant:
            state_copy = state.copy()
            open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player)
            code = validate_id(state_copy, flat_proposal)
            if code not in checked_states.keys():
                valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
                                                     state, available_small_locations, world, player)
                checked_states[code] = valid
            else:
                valid = checked_states[code]
            if not valid:
                return False
    return True


def invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player):
    if prev_state is None or state.used_smalls == prev_state.used_smalls:
        return False
    if state.found_forced_bk() and not prev_state.found_forced_bk():
        return False
    if state.big_key_opened:
        new_bk_doors = set(state.big_doors).difference(set(prev_state.big_doors))
        state_copy = state.copy()
        while len(new_bk_doors) > 0:
            for door in new_bk_doors:
                open_a_door(door.door, state_copy, key_layout.flat_prop, world, player)
            new_bk_doors = set(state_copy.big_doors).difference(set(prev_state.big_doors))
        expand_key_state(state_copy, key_layout.flat_prop, world, player)
    else:
        state_copy = state
    new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations))
    important_found = False
    for loc in new_locations:
        important_found |= important_location(loc, world, player)
    if not important_found:
        return False
    new_small_doors = set(state.small_doors).difference(set(prev_state.small_doors))
    if len(new_small_doors) > 0:
        return False
    return prev_avail - 1 == 0


def enough_small_locations(state, avail_small_loc):
    unique_d_set = set()
    for exp_door in state.small_doors:
        door = exp_door.door
        if door not in unique_d_set and door.dest not in unique_d_set:
            unique_d_set.add(door)
    return avail_small_loc >= len(unique_d_set)


def determine_prize_lock(key_layout, world, player):
    if world.logic[player] == 'nologic' or (world.keyshuffle[player] == 'universal' and
       (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')):
        return  # done, doesn't matter what
    flat_proposal = key_layout.flat_prop
    state = ExplorationState(dungeon=key_layout.sector.name)
    state.key_locations = key_layout.max_chests
    state.big_key_special = check_bk_special(key_layout.sector.regions, world, player)
    prize_lock_possible = False
    for region in key_layout.start_regions:
        dungeon_entrance, portal_door = find_outside_connection(region)
        prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance, world.is_atgt_swapped(player))
        if prize_relevant_flag and world.prizeshuffle[player] in ['none', 'dungeon']:
            state.append_door_to_list(portal_door, state.prize_doors)
            state.prize_door_set[portal_door] = dungeon_entrance
            key_layout.prize_relevant = prize_relevant_flag
            prize_lock_possible = True
        else:
            state.visit_region(region, key_checks=True)
            state.add_all_doors_check_keys(region, flat_proposal, world, player)
    if not prize_lock_possible:
        return  # done, no prize entrances to worry about
    expand_key_state(state, flat_proposal, world, player)
    while len(state.small_doors) > 0 or len(state.big_doors) > 0:
        if len(state.big_doors) > 0:
            open_a_door(state.big_doors[0].door, state, flat_proposal, world, player)
        elif len(state.small_doors) > 0:
            open_a_door(state.small_doors[0].door, state, flat_proposal, world, player)
        expand_key_state(state, flat_proposal, world, player)
    if any(x for x in state.found_locations if x.prize):
        key_layout.prize_can_lock = True


def cnt_avail_small_locations(free_locations, key_only, state, key_layout, world, player):
    std_flag = world.mode[player] == 'standard' and key_layout.sector.name == 'Hyrule Castle'
    if world.keyshuffle[player] == 'none' or std_flag:
        bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0
        # this is the secret passage, could expand to Uncle/Links House with appropriate logic
        std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0
        avail_chest_keys = min(free_locations + std_adj - bk_adj, state.key_locations - key_only)
        return max(0, avail_chest_keys + key_only - state.used_smalls)
    return state.key_locations - state.used_smalls


def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player):
    std_flag = world.mode[player] == 'standard' and layout.sector.name == 'Hyrule Castle'
    if world.keyshuffle[player] == 'none' or std_flag:
        bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0
        # this is the secret passage, could expand to Uncle/Links House with appropriate logic
        std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0
        avail_chest_keys = min(free_locations + std_adj - bk_adj, layout.max_chests)
        return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys)
    return layout.max_chests + len(counter.key_only_locations) - counter.used_keys


def cnt_avail_big_locations(ttl_locations, state, world, player):
    if world.bigkeyshuffle[player] == 'none':
        return max(0, ttl_locations - state.used_locations) if not state.big_key_special else 0
    return 1 if not state.big_key_special else 0


def cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player):
    if world.bigkeyshuffle[player] == 'none':
        bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0
        used_locations = max(0, counter.used_keys - len(counter.key_only_locations)) + bk_adj
        return max(0, ttl_locations - used_locations) if not layout.big_key_special else 0
    return 1 if not layout.big_key_special else 0


def create_key_counters(key_layout, world, player):
    key_counters = {}
    key_layout.found_doors.clear()
    flat_proposal = key_layout.flat_prop
    state = ExplorationState(dungeon=key_layout.sector.name)
    state.init_zelda_event_doors(key_layout.event_starts, player)
    if world.doorShuffle[player] == 'vanilla':
        builder = world.dungeon_layouts[player][key_layout.sector.name]
        state.key_locations = len(builder.key_door_proposal) - builder.key_drop_cnt
    else:
        builder = world.dungeon_layouts[player][key_layout.sector.name]
        state.key_locations = max(0, builder.total_keys - builder.key_drop_cnt)
    state.big_key_special = False
    for region in key_layout.sector.regions:
        for location in region.locations:
            if location.forced_big_key():
                state.big_key_special = True
    for region in key_layout.start_regions:
        dungeon_entrance, portal_door = find_outside_connection(region)
        prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance, world.is_atgt_swapped(player))
        if prize_relevant_flag and world.prizeshuffle[player] in ['none', 'dungeon']:
            state.append_door_to_list(portal_door, state.prize_doors)
            state.prize_door_set[portal_door] = dungeon_entrance
            key_layout.prize_relevant = prize_relevant_flag
        else:
            state.visit_region(region, key_checks=True)
            state.add_all_doors_check_keys(region, flat_proposal, world, player)
    expand_key_state(state, flat_proposal, world, player)
    code = state_id(state, key_layout.flat_prop)
    key_counters[code] = create_key_counter(state, key_layout, world, player)
    queue = deque([(key_counters[code], state)])
    while len(queue) > 0:
        next_key_counter, parent_state = queue.popleft()
        for door in next_key_counter.child_doors:
            key_layout.found_doors.add(door)
            if door.dest in flat_proposal and door.type != DoorType.SpiralStairs:
                key_layout.found_doors.add(door.dest)
            child_state = parent_state.copy()
            if door.bigKey or door.name in get_special_big_key_doors(world, player):
                key_layout.key_logic.bk_doors.add(door)
            # open the door, if possible
            if can_open_door(door, child_state, key_layout, world, player):
                open_a_door(door, child_state, flat_proposal, world, player)
                expand_key_state(child_state, flat_proposal, world, player)
                code = state_id(child_state, key_layout.flat_prop)
                if code not in key_counters.keys():
                    child_kr = create_key_counter(child_state, key_layout, world, player)
                    key_counters[code] = child_kr
                    queue.append((child_kr, child_state))
    return key_counters


def find_outside_connection(region):
    portal = next((x for x in region.entrances if ' Portal' in x.parent_region.name), None)
    if portal:
        dungeon_entrance = next(x for x in portal.parent_region.entrances
                                if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
        portal_entrance = next(x for x in portal.parent_region.entrances if x.parent_region == region)
        return dungeon_entrance, portal_entrance.door
    return None, None


def can_open_door(door, state, key_layout, world, player):
    if state.big_key_opened:
        ttl_locations = count_free_locations(state, world, player)
    else:
        ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player)
    if door.smallKey:
        ttl_small_key_only = count_small_key_only_locations(state)
        available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state,
                                                              key_layout, world, player)
        return available_small_locations > 0
    elif door.bigKey:
        available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
        found_forced_bk = state.found_forced_bk()
        return not state.big_key_opened and (available_big_locations > 0 or found_forced_bk)
    else:
        return True


def can_open_door_by_counter(door, counter: KeyCounter, layout, world, player):
    if counter.big_key_opened:
        ttl_locations = len(counter.free_locations)
    else:
        ttl_locations = len([x for x in counter.free_locations if '- Big Chest' not in x.name])

    if door.smallKey:
        # ttl_small_key_only = len(counter.key_only_locations)
        return cnt_avail_small_locations_by_ctr(ttl_locations, counter, layout, world, player) > 0
    elif door.bigKey:
        if counter.big_key_opened:
            return False
        if layout.big_key_special:
            return any(x for x in counter.other_locations.keys() if x.forced_item and x.forced_item.bigkey)
        else:
            available_big_locations = cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player)
            return available_big_locations > 0
    else:
        return True


def create_key_counter(state, key_layout, world, player):
    key_counter = KeyCounter(key_layout.max_chests)
    key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors+state.prize_doors)))
    for loc in state.found_locations:
        if important_location(loc, world, player):
            key_counter.important_location = True
            key_counter.other_locations[loc] = None
            key_counter.important_locations[loc] = None
        elif loc.forced_item and loc.item.name == key_layout.key_logic.small_key_name:
            key_counter.key_only_locations[loc] = None
        elif loc.forced_item and loc.item.name == key_layout.key_logic.bk_name:
            key_counter.other_locations[loc] = None
        elif loc.name not in location_events:
            key_counter.free_locations[loc] = None
        else:
            key_counter.other_locations[loc] = None
    key_counter.open_doors.update(dict.fromkeys(state.opened_doors))
    key_counter.used_keys = count_unique_sm_doors(state.opened_doors)
    key_counter.big_key_opened = state.big_key_opened
    if len(state.prize_door_set) > 0 and state.prize_doors_opened:
        key_counter.prize_doors_opened = True
    if any(x for x in key_counter.important_locations if x.prize):
        key_counter.prize_received = True
    return key_counter


imp_locations = None


def imp_locations_factory(world, player):
    global imp_locations
    if imp_locations:
        return imp_locations
    imp_locations = ['Agahnim 1', 'Agahnim 2', 'Attic Cracked Floor', 'Suspicious Maiden']
    if world.mode[player] == 'standard':
        if world.default_zelda_region[player] == 'Thieves Blinds\' Cell':
            imp_locations.append('Suspicious Maiden')
        else:
            imp_locations.append('Zelda Pickup')
        imp_locations.append('Zelda Drop Off')
    return imp_locations


def important_location(loc, world, player):
    return loc.prize or loc.name in imp_locations_factory(world, player) or (loc.forced_big_key())


def create_odd_key_counter(door, parent_counter, key_layout, world, player):
    odd_counter = KeyCounter(key_layout.max_chests)
    next_counter = find_next_counter(door, parent_counter, key_layout)
    odd_counter.free_locations = dict_difference(next_counter.free_locations, parent_counter.free_locations)
    odd_counter.key_only_locations = dict_difference(next_counter.key_only_locations, parent_counter.key_only_locations)
    odd_counter.child_doors = {}
    for d in next_counter.child_doors:
        if d not in parent_counter.child_doors and (d.type == DoorType.SpiralStairs or d.dest not in parent_counter.child_doors):
            odd_counter.child_doors[d] = None
    odd_counter.other_locations = dict_difference(next_counter.other_locations, parent_counter.other_locations)
    odd_counter.important_locations = dict_difference(next_counter.important_locations, parent_counter.important_locations)
    for loc in odd_counter.other_locations:
        if important_location(loc, world, player):
            odd_counter.important_location = True
    return odd_counter


def dict_difference(dict_a, dict_b):
    return dict.fromkeys([x for x in dict_a.keys() if x not in dict_b.keys()])


def dict_intersection(dict_a, dict_b):
    return dict.fromkeys([x for x in dict_a.keys() if x in dict_b.keys()])


def state_id(state, flat_proposal):
    s_id = '1' if state.big_key_opened else '0'
    for d in flat_proposal:
        s_id += '1' if d in state.opened_doors else '0'
    if len(state.prize_door_set) > 0:
        s_id += '1' if state.prize_doors_opened else '0'
    return s_id


def validate_id(state, flat_proposal):
    s_id = '1' if state.big_key_opened else '0'
    for d in flat_proposal:
        s_id += '1' if d in state.opened_doors else '0'
    if len(state.prize_door_set) > 0:
        s_id += '1' if state.prize_doors_opened else '0'
    s_id += str(state.used_locations)
    return s_id


def find_counter(opened_doors, bk_hint, key_layout, prize_flag, raise_on_error=True):
    counter = find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag)
    if counter is not None:
        return counter
    more_doors = []
    for door in opened_doors.keys():
        more_doors.append(door)
        if door.dest not in opened_doors.keys():
            more_doors.append(door.dest)
    if len(more_doors) > len(opened_doors.keys()):
        counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout, prize_flag)
        if counter is not None:
            return counter
    if raise_on_error:
        cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag)
        raise Exception(f'Unable to find door permutation. Init CID: {cid}')
    return None


def find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag):
    cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag)
    if cid in key_layout.key_counters.keys():
        return key_layout.key_counters[cid]
    if not bk_hint:
        cid = counter_id(opened_doors, True, key_layout.flat_prop, key_layout.prize_relevant, prize_flag)
        if cid in key_layout.key_counters.keys():
            return key_layout.key_counters[cid]
    return None


def find_max_counter(key_layout):
    max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout, True)
    if max_counter is None:
        raise Exception("Max Counter is none - something is amiss")
    if len(max_counter.child_doors) > 0:
        max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), True, key_layout, True)
    return max_counter


def counter_id(opened_doors, bk_unlocked, flat_proposal, prize_relevant, prize_flag):
    s_id = '1' if bk_unlocked else '0'
    for d in flat_proposal:
        s_id += '1' if d in opened_doors.keys() else '0'
    if prize_relevant:
        s_id += '1' if prize_flag else '0'
    return s_id


def cid(counter, key_layout):
    return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop,
                      key_layout.prize_relevant, counter.prize_doors_opened)


# class SoftLockException(Exception):
#     pass


# vanilla validation code
def validate_vanilla_key_logic(world, player):
    validators = {
        'Hyrule Castle': val_hyrule,
        'Eastern Palace': val_eastern,
        'Desert Palace': val_desert,
        'Tower of Hera': val_hera,
        'Agahnims Tower': val_tower,
        'Palace of Darkness': val_pod,
        'Swamp Palace': val_swamp,
        'Skull Woods': val_skull,
        'Thieves Town': val_thieves,
        'Ice Palace': val_ice,
        'Misery Mire': val_mire,
        'Turtle Rock': val_turtle,
        'Ganons Tower': val_ganons
    }
    key_logic_dict = world.key_logic[player]
    for key, key_logic in key_logic_dict.items():
        validators[key](key_logic, world, player)


def val_hyrule(key_logic, world, player):
    if world.mode[player] == 'standard':
        val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 1)
        val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 2)
        val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 3)
        val_rule(key_logic.door_rules['Sewers Key Rat NE'], 4)
    else:
        val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 2)
        val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 2)
        val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 2)
        val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 4)


def val_eastern(key_logic, world, player):
    val_rule(key_logic.door_rules['Eastern Dark Square Key Door WN'], 2, True, 'Eastern Palace - Big Key Chest', 1, {'Eastern Palace - Big Key Chest'})
    val_rule(key_logic.door_rules['Eastern Darkness Up Stairs'], 2)
    assert world.get_location('Eastern Palace - Big Chest', player) in key_logic.bk_restricted
    assert world.get_location('Eastern Palace - Boss', player) in key_logic.bk_restricted
    assert len(key_logic.bk_restricted) == 2


def val_desert(key_logic, world, player):
    val_rule(key_logic.door_rules['Desert East Wing Key Door EN'], 4)
    val_rule(key_logic.door_rules['Desert Tiles 1 Up Stairs'], 2)
    val_rule(key_logic.door_rules['Desert Beamos Hall NE'], 3)
    val_rule(key_logic.door_rules['Desert Tiles 2 NE'], 4)
    assert world.get_location('Desert Palace - Big Chest', player) in key_logic.bk_restricted
    assert world.get_location('Desert Palace - Boss', player) in key_logic.bk_restricted
    assert len(key_logic.bk_restricted) == 2


def val_hera(key_logic, world, player):
    val_rule(key_logic.door_rules['Hera Lobby Key Stairs'], 1, True, 'Tower of Hera - Big Key Chest')
    assert world.get_location('Tower of Hera - Big Chest', player) in key_logic.bk_restricted
    assert world.get_location('Tower of Hera - Compass Chest', player) in key_logic.bk_restricted
    assert world.get_location('Tower of Hera - Boss', player) in key_logic.bk_restricted
    assert len(key_logic.bk_restricted) == 3


def val_tower(key_logic, world, player):
    val_rule(key_logic.door_rules['Tower Room 03 Up Stairs'], 1)
    val_rule(key_logic.door_rules['Tower Dark Maze ES'], 2)
    val_rule(key_logic.door_rules['Tower Dark Archers Up Stairs'], 3)
    val_rule(key_logic.door_rules['Tower Circle of Pots ES'], 4)


def val_pod(key_logic, world, player):
    val_rule(key_logic.door_rules['PoD Arena Main NW'], 4)
    val_rule(key_logic.door_rules['PoD Basement Ledge Up Stairs'], 6, True, 'Palace of Darkness - Big Key Chest')
    val_rule(key_logic.door_rules['PoD Compass Room SE'], 6, True, 'Palace of Darkness - Harmless Hellway')
    val_rule(key_logic.door_rules['PoD Falling Bridge WN'], 6)
    val_rule(key_logic.door_rules['PoD Dark Pegs WN'], 6)
    assert world.get_location('Palace of Darkness - Big Chest', player) in key_logic.bk_restricted
    assert world.get_location('Palace of Darkness - Boss', player) in key_logic.bk_restricted
    assert len(key_logic.bk_restricted) == 2


def val_swamp(key_logic, world, player):
    val_rule(key_logic.door_rules['Swamp Entrance Down Stairs'], 1)
    val_rule(key_logic.door_rules['Swamp Pot Row WS'], 2)
    val_rule(key_logic.door_rules['Swamp Trench 1 Key Ledge NW'], 3)
    val_rule(key_logic.door_rules['Swamp Hub North Ledge N'], 5)
    val_rule(key_logic.door_rules['Swamp Hub WN'], 6)
    val_rule(key_logic.door_rules['Swamp Waterway NW'], 6)
    assert world.get_location('Swamp Palace - Entrance', player) in key_logic.bk_restricted
    assert len(key_logic.bk_restricted) == 1


def val_skull(key_logic, world, player):
    val_rule(key_logic.door_rules['Skull 3 Lobby NW'], 4)
    val_rule(key_logic.door_rules['Skull Spike Corner ES'], 5)


def val_thieves(key_logic, world, player):
    val_rule(key_logic.door_rules['Thieves Hallway WS'], 1)
    val_rule(key_logic.door_rules['Thieves Spike Switch Up Stairs'], 3)
    val_rule(key_logic.door_rules['Thieves Conveyor Bridge WS'], 3, True, 'Thieves\' Town - Big Chest')
    assert world.get_location('Thieves\' Town - Attic', player) in key_logic.bk_restricted
    assert world.get_location('Thieves\' Town - Boss', player) in key_logic.bk_restricted
    assert world.get_location('Thieves\' Town - Blind\'s Cell', player) in key_logic.bk_restricted
    assert world.get_location('Thieves\' Town - Big Chest', player) in key_logic.bk_restricted
    assert len(key_logic.bk_restricted) == 4


def val_ice(key_logic, world, player):
    val_rule(key_logic.door_rules['Ice Jelly Key Down Stairs'], 1)
    val_rule(key_logic.door_rules['Ice Conveyor SW'], 2)
    val_rule(key_logic.door_rules['Ice Backwards Room Down Stairs'], 5)
    assert world.get_location('Ice Palace - Boss', player) in key_logic.bk_restricted
    assert world.get_location('Ice Palace - Big Chest', player) in key_logic.bk_restricted
    assert len(key_logic.bk_restricted) == 2


def val_mire(key_logic, world, player):
    mire_west_wing = {'Misery Mire - Big Key Chest', 'Misery Mire - Compass Chest'}
    val_rule(key_logic.door_rules['Mire Spikes NW'], 3)  # todo: is sometimes 3 or 5? best_counter order matters
    # val_rule(key_logic.door_rules['Mire Spike Barrier NE'], 4)  # kind of a waste mostly
    val_rule(key_logic.door_rules['Mire Hub WS'], 5, False, None, 3, mire_west_wing)
    val_rule(key_logic.door_rules['Mire Conveyor Crystal WS'], 6, False, None, 4, mire_west_wing)
    assert world.get_location('Misery Mire - Boss', player) in key_logic.bk_restricted
    assert world.get_location('Misery Mire - Big Chest', player) in key_logic.bk_restricted
    assert len(key_logic.bk_restricted) == 2


def val_turtle(key_logic, world, player):
    # todo: check vanilla key logic when TR back doors are accessible
    if world.shuffle[player] == 'vanilla' and (not world.is_tile_swapped(0x05, player)) and world.logic[player] in ('noglitches', 'minorglitches'):
        val_rule(key_logic.door_rules['TR Hub NW'], 1)
        val_rule(key_logic.door_rules['TR Pokey 1 NW'], 2)
        val_rule(key_logic.door_rules['TR Chain Chomps Down Stairs'], 3)
        val_rule(key_logic.door_rules['TR Pokey 2 ES'], 6, True, 'Turtle Rock - Big Key Chest', 4, {'Turtle Rock - Big Key Chest'})
        val_rule(key_logic.door_rules['TR Crystaroller Down Stairs'], 5)
        val_rule(key_logic.door_rules['TR Dash Bridge WS'], 6)
        assert world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player) in key_logic.bk_restricted
        assert world.get_location('Turtle Rock - Eye Bridge - Top Left', player) in key_logic.bk_restricted
        assert world.get_location('Turtle Rock - Eye Bridge - Top Right', player) in key_logic.bk_restricted
        assert world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player) in key_logic.bk_restricted
        assert world.get_location('Turtle Rock - Boss', player) in key_logic.bk_restricted
        assert world.get_location('Turtle Rock - Crystaroller Room', player) in key_logic.bk_restricted
        assert world.get_location('Turtle Rock - Big Chest', player) in key_logic.bk_restricted
        assert len(key_logic.bk_restricted) == 7


def val_ganons(key_logic, world, player):
    rando_room = {'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'}
    compass_room = {'Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right'}
    gt_middle = {'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Chest', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest'}
    val_rule(key_logic.door_rules['GT Double Switch EN'], 6, False, None, 4, rando_room.union({'Ganons Tower - Firesnake Room'}))
    val_rule(key_logic.door_rules['GT Hookshot ES'], 7, False, 'Ganons Tower - Map Chest', 5, {'Ganons Tower - Map Chest'})
    val_rule(key_logic.door_rules['GT Tile Room EN'], 6, False, None, 5, compass_room)
    val_rule(key_logic.door_rules['GT Firesnake Room SW'], 7, False, None, 5, rando_room)
    val_rule(key_logic.door_rules['GT Conveyor Star Pits EN'], 6, False, None, 5, gt_middle)  # should be 7?
    val_rule(key_logic.door_rules['GT Mini Helmasaur Room WN'], 6)  # not sure about this 6...
    val_rule(key_logic.door_rules['GT Crystal Circles SW'], 8)
    assert world.get_location('Ganons Tower - Mini Helmasaur Room - Left', player) in key_logic.bk_restricted
    assert world.get_location('Ganons Tower - Mini Helmasaur Room - Right', player) in key_logic.bk_restricted
    assert world.get_location('Ganons Tower - Big Chest', player) in key_logic.bk_restricted
    assert world.get_location('Ganons Tower - Pre-Moldorm Chest', player) in key_logic.bk_restricted
    assert world.get_location('Ganons Tower - Validation Chest', player) in key_logic.bk_restricted
    assert len(key_logic.bk_restricted) == 5


def val_rule(rule, skn, allow=False, loc=None, askn=None, setCheck=None):
    if setCheck is None:
        setCheck = set()
    assert rule.small_key_num == skn
    assert rule.allow_small == allow
    assert rule.small_location == loc or rule.small_location.name == loc
    assert rule.alternate_small_key == askn
    assert len(setCheck) == len(rule.alternate_big_key_loc)
    for loc in rule.alternate_big_key_loc:
        assert loc.name in setCheck


# Soft lock stuff
def validate_key_placement(key_layout, world, player):
    if world.keyshuffle[player] == 'universal' or world.accessibility[player] == 'none':
        return True  # Can't keylock in retro.  Expected if beatable only.
    max_counter = find_max_counter(key_layout)
    keys_outside = 0
    big_key_outside = False
    smallkey_name = dungeon_keys[key_layout.sector.name]
    bigkey_name = dungeon_bigs[key_layout.sector.name]
    if world.keyshuffle[player] != 'none':
        keys_outside = key_layout.max_chests - sum(1 for i in max_counter.free_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player)
    if world.bigkeyshuffle[player] != 'none':
        max_counter = find_max_counter(key_layout)
        big_key_outside = bigkey_name not in (l.item.name for l in max_counter.free_locations if l.item)
    for i in world.precollected_items:
        if i.player == player and i.name == bigkey_name:
            big_key_outside = True
            break
        if i.player == player and i.name == smallkey_name:
            keys_outside += 1

    if world.logic[player] == 'hybridglitches':
        # Swamp keylogic
        if smallkey_name.endswith('(Swamp Palace)'):
            swamp_entrance = world.get_location('Swamp Palace - Entrance', player)
            # Swamp small not vanilla
            if swamp_entrance.item is None or (swamp_entrance.item.name != smallkey_name or swamp_entrance.item.player != player):
                mire_keylayout = world.key_layout[player]['Misery Mire']
                mire_smallkey_name = dungeon_keys[mire_keylayout.sector.name]
                # Check if any mire keys are in swamp (excluding entrance), if none then add one to keys_outside
                mire_keys_in_swamp = sum([1 if x.item.name == mire_smallkey_name else 0 for x in key_layout.item_locations if x.item is not None and x != swamp_entrance])
                if mire_keys_in_swamp == 0:
                    keys_outside +=1 
        # Mire keylogic
        if smallkey_name.endswith('(Tower of Hera)'):
            # TODO: Make sure that mire medallion isn't in hera basement, or if it is, the small key is available downstairs
            big_key_outside = True
    for code, counter in key_layout.key_counters.items():
        if len(counter.child_doors) == 0:
            continue
        if key_layout.big_key_special:
            big_found = any(i.forced_item is not None and i.item.bigkey for i in counter.other_locations) or big_key_outside
        else:
            big_found = any(i.item is not None and i.item.name == bigkey_name for i in counter.free_locations if "- Big Chest" not in i.name) or big_key_outside
        if counter.big_key_opened and not big_found:
            continue  # Can't get to this state
        found_locations = set(i for i in counter.free_locations if big_found or "- Big Chest" not in i.name)
        found_keys = sum(1 for i in found_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \
                     len(counter.key_only_locations) + keys_outside
        if key_layout.prize_relevant:
            found_prize = any(x for x in counter.important_locations if x.prize)
            if not found_prize and dungeon_table[key_layout.sector.name].prize:
                prize_dungeon = [d for d in world.dungeons if d.name == key_layout.sector.name][0]
                if key_layout.prize_relevant == 'BigBomb':
                    found_prize = prize_dungeon.prize.name not in ['Crystal 5', 'Crystal 6']
                elif key_layout.prize_relevant == 'GT':
                    found_prize = 'Crystal' not in prize_dungeon.prize.name or world.crystals_needed_for_gt[player] < 7
        else:
            found_prize = False
        can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \
            found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) or \
            self_locked_child_door(key_layout, counter) or \
            (key_layout.prize_relevant and not counter.prize_doors_opened and found_prize)
        if not can_progress:
            missing_locations = set(max_counter.free_locations.keys()).difference(found_locations)
            missing_items = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item.name != bigkey_name) or "- Boss" in l.name]
            # missing_key_only = set(max_counter.key_only_locations.keys()).difference(counter.key_only_locations.keys()) # do freestanding keys matter for locations?
            if len(missing_items) > 0:  # world.accessibility[player]=='locations' and (len(missing_locations)>0 or len(missing_key_only) > 0):
                logging.getLogger('').error("Keylock - can't open locations: ")
                logging.getLogger('').error("code: " + code)
                for i in missing_locations:
                    logging.getLogger('').error(i)
                return False

    return True


def self_locked_child_door(key_layout, counter):
    if len(counter.child_doors) == 1:
        door = next(iter(counter.child_doors.keys()))
        return door.smallKey and key_layout.key_logic.door_rules[door.name].allow_small
    return False


