import collections
import logging
import time

import RaceRandom as random
from BaseClasses import (
    CrystalBarrier,
    DoorType,
    Hook,
    RegionType,
    Sector,
    flooded_keys,
    hook_from_door,
)
from Regions import flooded_keys_reverse, location_events


def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
    pass
    # todo: determine the part of check_valid that are necessary here


def generate_dungeon(builder, entrance_region_names, split_dungeon, world, player):
    if builder.valid_proposal:  # we made this earlier in gen, just use it
        proposed_map = builder.valid_proposal
    else:
        proposed_map = generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player)
        builder.valid_proposal = proposed_map
    queue = collections.deque(proposed_map.items())
    while len(queue) > 0:
        a, b = queue.popleft()
        if a == b or world.decoupledoors[player]:
            connect_doors_one_way(a, b)
        else:
            connect_doors(a, b)
            queue.remove((b, a))
    if len(builder.sectors) == 0:
        return Sector()
    available_sectors = list(builder.sectors)
    master_sector = available_sectors.pop()
    for sub_sector in available_sectors:
        master_sector.regions.extend(sub_sector.regions)
    master_sector.outstanding_doors.clear()
    master_sector.r_name_set = None
    return master_sector


def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player):
    logger = logging.getLogger('')
    name = builder.name
    logger.debug(f'Generating Dungeon: {name}')
    entrance_regions = convert_regions(entrance_region_names, world, player)
    excluded = {}
    for region in entrance_regions:
        portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
        if portal:
            if portal.destination:
                excluded[region] = None
            elif len(entrance_regions) > 1:
                p_region = portal.door.entrance.connected_region
                access_region = next(x.parent_region for x in p_region.entrances
                                     if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
                if (access_region.name in world.inaccessible_regions[player] and
                     region.name not in world.enabled_entrances[player]):
                    excluded[region] = None
        elif split_dungeon and builder.sewers_access and builder.sewers_access.entrance.parent_region == region:
            continue
        drop_region = next((x.parent_region for x in region.entrances
                           if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
                           or x.parent_region.name == 'Sewer Drop'), None)
        if drop_region:  # for holes
            if drop_region.name == 'Sewer Drop':
                drop_region = next(x.parent_region for x in drop_region.entrances)
            if (drop_region.name in world.inaccessible_regions[player] and
               region.name not in world.enabled_entrances[player]):
                excluded[region] = None
            elif region in excluded:
                del excluded[region]
    entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
    doors_to_connect, idx = {}, 0
    all_regions = set()
    bk_special = False
    for sector in builder.sectors:
        for door in sector.outstanding_doors:
            doors_to_connect[door.name] = door, idx
            idx += 1
        all_regions.update(sector.regions)
        bk_special |= check_for_special(sector.regions)
    finished = False
    # flag if standard and this is hyrule castle
    paths = determine_paths_for_dungeon(world, player, all_regions, name)
    proposed_map = create_random_proposal(doors_to_connect, world, player)
    itr = 0
    hash_code = proposal_hash(doors_to_connect, proposed_map)
    hash_code_set = set()
    start = time.time()
    while not finished:
        if itr > 1000:
            elasped = time.time() - start
            raise GenerationException(f'Generation taking too long. {elasped}. Ref {name}')
        if hash_code in hash_code_set:
            proposed_map = create_random_proposal(doors_to_connect, world, player)
            hash_code = proposal_hash(doors_to_connect, proposed_map)
        if hash_code not in hash_code_set:
            hash_code_set.add(hash_code)
            explored_state = explore_proposal(name, entrance_regions, all_regions, proposed_map, doors_to_connect,
                                              bk_special, world, player)
            if check_valid(name, explored_state, proposed_map, doors_to_connect, all_regions,
                           paths, entrance_regions, bk_special, world, player):
                finished = True
            else:
                proposed_map, hash_code = modify_proposal(proposed_map, explored_state, doors_to_connect,
                                                          hash_code_set, world, player)
        itr += 1
    return proposed_map


def create_random_proposal(doors_to_connect, world, player):
    logger = logging.getLogger('')
    hooks = [Hook.North, Hook.South, Hook.East, Hook.West, Hook.Stairs]
    primary_bucket = collections.defaultdict(list)
    secondary_bucket = collections.defaultdict(list)
    for name, door in doors_to_connect.items():
        door, idx = door
        primary_bucket[hook_from_door(door)].append(door)
        secondary_bucket[hook_from_door(door)].append(door)
    proposal = {}
    while True:
        hooks_left, left = [], 0
        for hook in hooks:
            hook_len = len(primary_bucket[hook])
            if hook_len > 0:
                hooks_left.append(hook)
                left += hook_len
        if left == 0:
            return proposal
        next_hook = random.choice(hooks_left)
        primary_door = random.choice(primary_bucket[next_hook])
        opp_hook, secondary_door = type_map[next_hook], None
        while (secondary_door is None or (secondary_door == primary_door and not world.door_self_loops[player])
               or decouple_check(primary_bucket[next_hook], secondary_bucket[opp_hook],
                                 primary_door, secondary_door, world, player)):
            secondary_door = random.choice(secondary_bucket[opp_hook])
        proposal[primary_door] = secondary_door
        primary_bucket[next_hook].remove(primary_door)
        secondary_bucket[opp_hook].remove(secondary_door)
        if primary_door != secondary_door and not world.decoupledoors[player]:
            proposal[secondary_door] = primary_door
            primary_bucket[opp_hook].remove(secondary_door)
            secondary_bucket[next_hook].remove(primary_door)
            logger.debug(f'  Linking {primary_door.name} <-> {secondary_door.name}')
        else:
            logger.debug(f'  Linking {primary_door.name} -> {secondary_door.name}')


def decouple_check(primary_list, secondary_list, primary_door, secondary_door, world, player):
    if world.decoupledoors[player] and len(primary_list) == 2 and len(secondary_list) == 2:
        primary_alone = next(d for d in primary_list if d != primary_door)
        secondary_alone = next(d for d in secondary_list if d != secondary_door)
        return primary_alone == secondary_alone
    return False


def proposal_hash(doors_to_connect, proposed_map):
    hash_code = ''
    for name, door_pair in doors_to_connect.items():
        door, idx = door_pair
        hash_code += str(idx) + str(doors_to_connect[proposed_map[door].name][1])
    return hash_code


def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_set, world, player):
    logger = logging.getLogger('')
    hash_code, itr = None, 0
    while hash_code is None or hash_code in hash_code_set:
        if itr > 10:
            proposed_map = create_random_proposal(doors_to_connect, world, player)
            hash_code = proposal_hash(doors_to_connect, proposed_map)
            return proposed_map, hash_code
        visited_bucket = collections.defaultdict(list)
        unvisted_bucket = collections.defaultdict(list)
        visited_choices = []
        unvisted_count = 0
        for door_one, door_two in proposed_map.items():
            if door_one in explored_state.visited_doors:
                visited_bucket[hook_from_door(door_one)].append(door_one)
                visited_choices.append(door_one)
            else:
                unvisted_bucket[hook_from_door(door_one)].append(door_one)
                unvisted_count += 1
        if unvisted_count == 0:
            # something is wrong beyond connectedness, crystal switch puzzle or bk layout - reshuffle
            proposed_map = create_random_proposal(doors_to_connect, world, player)
            hash_code = proposal_hash(doors_to_connect, proposed_map)
            return proposed_map, hash_code

        attempt, opp_hook = None, None
        opp_hook_len, possible_swaps = 0, list(visited_choices)
        while opp_hook_len == 0:
            if len(possible_swaps) == 0:
                break
            attempt = random.choice(possible_swaps)
            possible_swaps.remove(attempt)
            opp_hook = type_map[hook_from_door(attempt)]
            opp_hook_len = len(unvisted_bucket[opp_hook])
        if opp_hook_len == 0:
            itr += 1
            continue
        unvisted_bucket[opp_hook].sort(key=lambda d: d.name)
        new_door = random.choice(unvisted_bucket[opp_hook])
        old_target = proposed_map[attempt]
        if not world.decoupledoors[player]:
            old_attempt = proposed_map[new_door]
        else:
            old_attempt = next(x for x in proposed_map if proposed_map[x] == new_door)
        # ensure nothing gets messed up when something loops with itself
        if attempt == old_target and old_attempt == new_door:
            old_attempt = new_door
            old_target = attempt
        elif attempt == old_target:
            old_target = old_attempt
        elif old_attempt == new_door:
            old_attempt = old_target
        proposed_map[attempt] = new_door
        proposed_map[old_attempt] = old_target
        if not world.decoupledoors[player]:
            proposed_map[old_target] = old_attempt
            proposed_map[new_door] = attempt
        hash_code = proposal_hash(doors_to_connect, proposed_map)
        itr += 1

    if not world.decoupledoors[player]:
        logger.debug(f'   Re-linking {attempt.name} <-> {new_door.name}')
        logger.debug(f'   Re-linking {old_attempt.name} <-> {old_target.name}')
    else:
        logger.debug(f'   Re-Linking {attempt.name} -> {new_door.name}')
        logger.debug(f'   Re-Linking {old_attempt.name} -> {old_target.name}')
    return proposed_map, hash_code


def explore_proposal(name, entrance_regions, all_regions, proposed_map, valid_doors, bk_special, world, player):
    start = ExplorationState(dungeon=name)
    bk_relevant = (world.door_type_mode[player] == 'original' and world.bigkeyshuffle[player] == 'none') or bk_special
    start.big_key_special = bk_special
    original_state = extend_reachable_state_lenient(entrance_regions, start, proposed_map,
                                                    all_regions, valid_doors, bk_relevant, world, player)
    return original_state


def check_valid(name, exploration_state, proposed_map, doors_to_connect, all_regions,
                paths, entrance_regions, bk_special, world, player):
    all_visited = set()
    all_visited.update(exploration_state.visited_blue)
    all_visited.update(exploration_state.visited_orange)
    if len(all_regions.difference(all_visited)) > 0:
        return False
    if not valid_paths(name, paths, entrance_regions, doors_to_connect, all_regions, proposed_map,
                       bk_special, world, player):
        return False
    return True


def determine_if_bk_needed(sector, split_dungeon, bk_special, world, player):
    if not split_dungeon or bk_special:
        for region in sector.regions:
            for ext in region.exits:
                door = world.check_for_door(ext.name, player)
                if door is not None and door.bigKey:
                    return True
    return False


def check_for_special(regions):
    for region in regions:
        for loc in region.locations:
            if loc.forced_big_key():
                return True
    return False


def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, proposed_map, bk_special, world, player):
    for path in paths:
        if type(path) is tuple:
            target = path[1]
            start_regions = []
            for region in all_regions:
                if path[0] == region.name:
                    start_regions.append(region)
                    break
        else:
            target = path
            start_regions = entrance_regions
        if not valid_path(name, start_regions, target, valid_doors, proposed_map, all_regions,
                          bk_special, world, player):
            return False
    return True


def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_regions, bk_special, world, player):
    target_regions = set()
    if type(target) is not list:
        for region in all_regions:
            if target == region.name:
                target_regions.add(region)
                break
    else:
        for region in all_regions:
            if region.name in target:
                target_regions.add(region)

    start = ExplorationState(dungeon=name)
    bk_relevant = (world.door_type_mode[player] == 'original' and world.bigkeyshuffle[player] == 'none') or bk_special
    start.big_key_special = bk_special
    original_state = extend_reachable_state_lenient(starting_regions, start, proposed_map, all_regions,
                                                    valid_doors, bk_relevant, world, player)

    for exp_door in original_state.unattached_doors:
        if not exp_door.door.blocked or exp_door.door.trapFlag != 0:
            return True  # outstanding connection possible
    for target in target_regions:
        if original_state.visited_at_all(target):
            return True
    return False  # couldn't find an outstanding door or the target


boss_path_checks = ['Eastern Boss', 'Desert Boss', 'Hera Boss', 'Tower Agahnim 1', 'PoD Boss', 'Swamp Boss',
                    'Skull Boss', 'Ice Boss', 'Mire Boss', 'TR Boss', 'GT Agahnim 2']

# pinball is allowed to orphan you
drop_path_checks = ['Skull Pot Circle', 'Skull Left Drop', 'Skull Back Drop', 'Sewers Rat Path']


def determine_paths_for_dungeon(world, player, all_regions, name):
    all_r_names = set(x.name for x in all_regions)
    paths = []
    non_hole_portals = []
    for portal in world.dungeon_portals[player]:
        if portal.door.entrance.parent_region in all_regions:
            non_hole_portals.append(portal.door.entrance.parent_region.name)
            if portal.destination:
                paths.append(portal.door.entrance.parent_region.name)
    if world.mode[player] == 'standard' and name == 'Hyrule Castle Dungeon':
        paths.append(world.default_zelda_region[player])
        paths.append((world.default_zelda_region[player], 'Hyrule Castle Throne Room'))
        entrance = next(x for x in world.dungeon_portals[player] if x.name == 'Hyrule Castle South')
        # todo: in non-er, we can use the other portals too
        paths.append((world.default_zelda_region[player], entrance.door.entrance.parent_region.name))
        paths.append(('Hyrule Castle Throne Room', [entrance.door.entrance.parent_region.name,
                                                    world.default_zelda_region[player]]))
    if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town':
        paths.append('Thieves Attic Window')
    elif 'Thieves Attic Window' in all_r_names:
        paths.append('Thieves Attic Window')
    for boss in boss_path_checks:
        if boss in all_r_names:
            paths.append(boss)
    if 'Thieves Boss' in all_r_names:
        paths.append('Thieves Boss')
        if world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind' \
                and not world.shuffle_followers[player]:
            paths.append(('Thieves Blind\'s Cell', 'Thieves Boss'))
    for drop_check in drop_path_checks:
        if drop_check in all_r_names:
            paths.append((drop_check, non_hole_portals))
    return paths


def convert_regions(region_names, world, player):
    region_list = []
    for name in region_names:
        region_list.append(world.get_region(name, player))
    return region_list


type_map = {
    Hook.Stairs: Hook.Stairs,
    Hook.North: Hook.South,
    Hook.South: Hook.North,
    Hook.West: Hook.East,
    Hook.East: Hook.West
}


def connect_doors(a, b):
    # Return on unsupported types.
    if a.type in [DoorType.Hole, DoorType.Warp, DoorType.Interior, DoorType.Logical]:
        return
    # Connect supported types
    if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
        connect_two_way(a.entrance, b.entrance)
        dep_doors, target = [], None
        if len(a.dependents) > 0:
            dep_doors, target = a.dependents, b
        elif len(b.dependents) > 0:
            dep_doors, target = b.dependents, a
        if target is not None:
            target_region = target.entrance.parent_region
            for dep in dep_doors:
                connect_simple_door(dep, target_region)
        return
    # If we failed to account for a type, panic
    raise RuntimeError('Unknown door type ' + a.type.name)


def connect_doors_one_way(a, b):
    # Return on unsupported types.
    if a.type in [DoorType.Hole, DoorType.Warp, DoorType.Interior, DoorType.Logical]:
        return
    # Connect supported types
    if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
        connect_one_way(a.entrance, b.entrance)
        dep_doors, target = [], None
        if len(a.dependents) > 0:
            dep_doors, target = a.dependents, b
        if target is not None:
            target_region = target.entrance.parent_region
            for dep in dep_doors:
                connect_simple_door(dep, target_region)
        return
    # If we failed to account for a type, panic
    raise RuntimeError('Unknown door type ' + a.type.name)


def connect_two_way(entrance, ext):

    # if these were already connected somewhere, remove the backreference
    if entrance.connected_region is not None:
        entrance.connected_region.entrances.remove(entrance)
    if ext.connected_region is not None:
        ext.connected_region.entrances.remove(ext)

    entrance.connect(ext.parent_region)
    ext.connect(entrance.parent_region)
    if entrance.parent_region.dungeon:
        ext.parent_region.dungeon = entrance.parent_region.dungeon
    x = entrance.door
    y = ext.door
    if x is not None:
        x.dest = y
    if y is not None:
        y.dest = x


def connect_one_way(entrance, ext):

    # if these were already connected somewhere, remove the backreference
    if entrance.connected_region is not None:
        entrance.connected_region.entrances.remove(entrance)

    entrance.connect(ext.parent_region)
    if entrance.parent_region.dungeon:
        ext.parent_region.dungeon = entrance.parent_region.dungeon
    x = entrance.door
    if x is not None:
        x.dest = ext.door


def connect_simple_door(exit_door, region):
    exit_door.entrance.connect(region)
    exit_door.dest = region


special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"]
std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors


def get_special_big_key_doors(world, player):
    if world.mode[player] == 'standard':
        return std_special_big_key_doors
    return special_big_key_doors


class ExplorationState(object):

    def __init__(self, init_crystal=CrystalBarrier.Orange, dungeon=None):

        self.unattached_doors = []
        self.avail_doors = []
        self.event_doors = []

        self.visited_orange = []
        self.visited_blue = []
        self.visited_doors = set()
        self.events = set()
        self.crystal = init_crystal

        # key region stuff
        self.door_krs = {}

        # key validation stuff
        self.small_doors = []
        self.big_doors = []
        self.opened_doors = []
        self.big_key_opened = False
        self.big_key_special = False

        self.found_locations = []
        self.ttl_locations = 0
        self.used_locations = 0
        self.key_locations = 0
        self.used_smalls = 0
        self.bk_found = set()

        self.non_door_entrances = []
        self.dungeon = dungeon
        self.pinball_used = False

        self.prize_door_set = {}
        self.prize_doors = []
        self.prize_doors_opened = False

    def copy(self):
        ret = ExplorationState(dungeon=self.dungeon)
        ret.unattached_doors = list(self.unattached_doors)
        ret.avail_doors = list(self.avail_doors)
        ret.event_doors = list(self.event_doors)
        ret.visited_orange = list(self.visited_orange)
        ret.visited_blue = list(self.visited_blue)
        ret.events = set(self.events)
        ret.crystal = self.crystal
        ret.door_krs = self.door_krs.copy()

        ret.small_doors = list(self.small_doors)
        ret.big_doors = list(self.big_doors)
        ret.opened_doors = list(self.opened_doors)
        ret.big_key_opened = self.big_key_opened
        ret.big_key_special = self.big_key_special
        ret.ttl_locations = self.ttl_locations
        ret.key_locations = self.key_locations
        ret.used_locations = self.used_locations
        ret.used_smalls = self.used_smalls
        ret.found_locations = list(self.found_locations)
        ret.bk_found = set(self.bk_found)

        ret.non_door_entrances = list(self.non_door_entrances)
        ret.dungeon = self.dungeon
        ret.pinball_used = self.pinball_used

        ret.prize_door_set = dict(self.prize_door_set)
        ret.prize_doors = list(self.prize_doors)
        ret.prize_doors_opened = self.prize_doors_opened
        return ret

    def next_avail_door(self):
        self.avail_doors.sort(key=lambda x: 0 if x.flag else 1 if x.door.bigKey else 2)
        exp_door = self.avail_doors.pop()
        self.crystal = exp_door.crystal
        return exp_door

    def visit_region(self, region, key_region=None, key_checks=False, bk_relevant=False):
        if region.type != RegionType.Dungeon:
            self.crystal = CrystalBarrier.Orange
        if self.crystal == CrystalBarrier.Either:
            if region not in self.visited_blue:
                self.visited_blue.append(region)
            if region not in self.visited_orange:
                self.visited_orange.append(region)
        elif self.crystal == CrystalBarrier.Orange:
            self.visited_orange.append(region)
        elif self.crystal == CrystalBarrier.Blue:
            self.visited_blue.append(region)
        if region.type == RegionType.Dungeon:
            for location in region.locations:
                if key_checks and location not in self.found_locations:
                    if location.forced_item and 'Small Key' in location.item.name:
                        self.key_locations += 1
                    if location.name not in location_events and not ('- Prize' in location.name and location.prize) and location.name not in ['Agahnim 1', 'Agahnim 2']:
                        self.ttl_locations += 1
                if location not in self.found_locations:
                    self.found_locations.append(location)
                    if bk_relevant:
                        if self.big_key_special:
                            if special_big_key_found(self):
                                self.bk_found.add(location)
                                self.re_add_big_key_doors()
                        else:
                            self.bk_found.add(location)
                            self.re_add_big_key_doors()
                if location.name in location_events and location.name not in self.events:
                    if self.flooded_key_check(location):
                        self.perform_event(location.name, key_region)
                if location.name in flooded_keys_reverse.keys() and self.location_found(
                     flooded_keys_reverse[location.name]):
                    self.perform_event(flooded_keys_reverse[location.name], key_region)
                if location.prize:
                    self.prize_received = True

    def flooded_key_check(self, location):
        if location.name not in flooded_keys.keys():
            return True
        return flooded_keys[location.name] in [x.name for x in self.found_locations]

    def location_found(self, location_name):
        for l in self.found_locations:
            if l.name == location_name:
                return True
        return False

    def re_add_big_key_doors(self):
        self.big_key_opened = True
        queue = collections.deque(self.big_doors)
        while len(queue) > 0:
            exp_door = queue.popleft()
            self.avail_doors.append(exp_door)
            self.big_doors.remove(exp_door)

    def perform_event(self, location_name, key_region):
        self.events.add(location_name)
        queue = collections.deque(self.event_doors)
        while len(queue) > 0:
            exp_door = queue.popleft()
            if exp_door.door.req_event == location_name:
                self.avail_doors.append(exp_door)
                self.event_doors.remove(exp_door)
                if key_region is not None:
                    d_name = exp_door.door.name
                    if d_name not in self.door_krs.keys():
                        self.door_krs[d_name] = key_region

    def add_all_entrance_doors_check_unattached(self, region, world, player):
        door_list = [x for x in get_doors(world, region, player) if x.type in [DoorType.Normal, DoorType.SpiralStairs]]
        door_list.extend(get_entrance_doors(world, region, player))
        for door in door_list:
            if self.can_traverse(door):
                if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
                    self.append_door_to_list(door, self.unattached_doors)
                elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
                                                                                                                self.event_doors):
                    self.append_door_to_list(door, self.event_doors)
                elif not self.in_door_list(door, self.avail_doors):
                    self.append_door_to_list(door, self.avail_doors)
        for entrance in region.entrances:
            door = world.check_for_door(entrance.name, player)
            if door is None:
                self.non_door_entrances.append(entrance)

    def add_all_doors_check_unattached(self, region, world, player):
        for door in get_doors(world, region, player):
            if self.can_traverse(door):
                if door.controller is not None:
                    door = door.controller
                if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
                    self.append_door_to_list(door, self.unattached_doors)
                elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
                                                                                                                self.event_doors):
                    self.append_door_to_list(door, self.event_doors)
                elif not self.in_door_list(door, self.avail_doors):
                    self.append_door_to_list(door, self.avail_doors)

    def add_all_doors_check_proposed(self, region, proposed_map, valid_doors, flag, world, player, exception):
        for door in get_doors(world, region, player):
            if door in proposed_map and door.name in valid_doors:
                self.visited_doors.add(door)
            if door.blocked and exception(door):
                self.pinball_used = True
            if self.can_traverse(door, exception):
                if door.controller is not None:
                    door = door.controller
                if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors:
                    if not self.in_door_list_ic(door, self.unattached_doors):
                        self.append_door_to_list(door, self.unattached_doors, flag)
                    else:
                        other = self.find_door_in_list(door, self.unattached_doors)
                        if self.crystal != other.crystal:
                            other.crystal = CrystalBarrier.Either
                elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
                                                                                                                self.event_doors):
                    self.append_door_to_list(door, self.event_doors, flag)
                elif not self.in_door_list(door, self.avail_doors):
                    self.append_door_to_list(door, self.avail_doors, flag)

    # same as above but traps are ignored, and flag is not used
    def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, bk_relevant, world, player):
        for door in get_doors(world, region, player):
            if door in proposed_map and door.name in valid_doors:
                self.visited_doors.add(door)
            if self.can_traverse_ignore_traps(door):
                if door.controller is not None:
                    door = door.controller
                if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors:
                    if not self.in_door_list_ic(door, self.unattached_doors):
                        self.append_door_to_list(door, self.unattached_doors)
                    else:
                        other = self.find_door_in_list(door, self.unattached_doors)
                        if self.crystal != other.crystal:
                            other.crystal = CrystalBarrier.Either
                elif (door.req_event is not None and door.req_event not in self.events
                      and not self.in_door_list(door, self.event_doors)):
                    self.append_door_to_list(door, self.event_doors)
                elif (bk_relevant and (door.bigKey or door.name in get_special_big_key_doors(world, player))
                                       and not self.big_key_opened):
                    if not self.in_door_list(door, self.big_doors):
                        self.append_door_to_list(door, self.big_doors)
                elif not self.in_door_list(door, self.avail_doors):
                    self.append_door_to_list(door, self.avail_doors)

    # same as above but traps are checked for
    def add_all_doors_check_proposed_3(self, region, proposed_map, valid_doors, bk_relevant, world, player):
        for door in get_doors(world, region, player):
            if door in proposed_map and door.name in valid_doors:
                self.visited_doors.add(door)
            if self.can_traverse(door):
                if door.controller is not None:
                    door = door.controller
                if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors:
                    if not self.in_door_list_ic(door, self.unattached_doors):
                        self.append_door_to_list(door, self.unattached_doors)
                    else:
                        other = self.find_door_in_list(door, self.unattached_doors)
                        if self.crystal != other.crystal:
                            other.crystal = CrystalBarrier.Either
                elif (door.req_event is not None and door.req_event not in self.events
                      and not self.in_door_list(door, self.event_doors)):
                    self.append_door_to_list(door, self.event_doors)
                elif (bk_relevant and (door.bigKey or door.name in get_special_big_key_doors(world, player))
                      and not self.big_key_opened):
                    if not self.in_door_list(door, self.big_doors):
                        self.append_door_to_list(door, self.big_doors)
                elif not self.in_door_list(door, self.avail_doors):
                    self.append_door_to_list(door, self.avail_doors)

    def add_all_doors_check_proposed_traps(self, region, proposed_traps, world, player):
        for door in get_doors(world, region, player):
            if self.can_traverse_ignore_traps(door) and door not in proposed_traps:
                if door.controller is not None:
                    door = door.controller
                if door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
                                                                                                                self.event_doors):
                    self.append_door_to_list(door, self.event_doors, False)
                elif not self.in_door_list(door, self.avail_doors):
                    self.append_door_to_list(door, self.avail_doors, False)

    def add_all_doors_check_key_region(self, region, key_region, world, player):
        for door in get_doors(world, region, player):
            if self.can_traverse(door):
                if door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
                                                                                                              self.event_doors):
                    self.append_door_to_list(door, self.event_doors)
                elif not self.in_door_list(door, self.avail_doors):
                    self.append_door_to_list(door, self.avail_doors)
                    if door.name not in self.door_krs.keys():
                        self.door_krs[door.name] = key_region
            else:
                if door.name not in self.door_krs.keys():
                    self.door_krs[door.name] = key_region

    def add_all_doors_check_keys(self, region, key_door_proposal, world, player):
        for door in get_doors(world, region, player):
            if self.can_traverse(door):
                if door.controller:
                    door = door.controller
                if door in key_door_proposal and door not in self.opened_doors:
                    if not self.in_door_list(door, self.small_doors):
                        self.append_door_to_list(door, self.small_doors)
                elif (door.bigKey or door.name in get_special_big_key_doors(world, player)) and not self.big_key_opened:
                    if not self.in_door_list(door, self.big_doors):
                        self.append_door_to_list(door, self.big_doors)
                elif door.req_event is not None and door.req_event not in self.events:
                    if not self.in_door_list(door, self.event_doors):
                        self.append_door_to_list(door, self.event_doors)
                elif not self.in_door_list(door, self.avail_doors):
                    self.append_door_to_list(door, self.avail_doors)

    def visited(self, region):
        if self.crystal == CrystalBarrier.Either:
            return region in self.visited_blue and region in self.visited_orange
        elif self.crystal == CrystalBarrier.Orange:
            return region in self.visited_orange
        elif self.crystal == CrystalBarrier.Blue:
            return region in self.visited_blue
        return False

    def visited_at_all(self, region):
        return region in self.visited_blue or region in self.visited_orange

    def found_forced_bk(self):
        for location in self.found_locations:
            if location.forced_big_key():
                return True
        return False

    def can_traverse(self, door, exception=None):
        if door.blocked:
            return exception(door) if exception else False
        if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]:
            return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal
        return True

    def can_traverse_ignore_traps(self, door):
        if door.blocked and door.trapFlag == 0:
            return False
        if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]:
            return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal
        return True

    def count_locations_exclude_specials(self, world, player):
        return count_locations_exclude_big_chest(self.found_locations, world, player)

    def validate(self, door, region, world, player):
        return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon,
                                                                                                world, player)

    def in_door_list(self, door, door_list):
        for d in door_list:
            if d.door == door and d.crystal == self.crystal:
                return True
        return False

    @staticmethod
    def in_door_list_ic(door, door_list):
        for d in door_list:
            if d.door == door:
                return True
        return False

    @staticmethod
    def find_door_in_list(door, door_list):
        for d in door_list:
            if d.door == door:
                return d
        return None

    def append_door_to_list(self, door, door_list, flag=False):
        if door.crystal == CrystalBarrier.Null:
            door_list.append(ExplorableDoor(door, self.crystal, flag))
        else:
            door_list.append(ExplorableDoor(door, door.crystal, flag))

    def key_door_sort(self, d):
        if d.door.smallKey:
            if d.door in self.opened_doors:
                return 1
            else:
                return 0
        return 2


def count_locations_exclude_big_chest(locations, world, player):
    cnt = 0
    for loc in locations:
        if ('- Big Chest' not in loc.name and not loc.forced_item and not reserved_location(loc, world, player)
             and not prize_or_event(loc) and not blind_boss_unavail(loc, locations, world, player)):
            cnt += 1
    return cnt


def prize_or_event(loc):
    return loc.name in location_events or loc.prize


def reserved_location(loc, world, player):
    return hasattr(world, 'item_pool_config') and loc.name in world.item_pool_config.reserved_locations[player]


def blind_boss_unavail(loc, locations, world, player):
    if loc.name == "Thieves' Town - Boss":
        return (loc.parent_region.dungeon.boss.name == 'Blind' and
                (not any(x for x in 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 locations if x.name == 'Attic Cracked Floor'))))
    return False


class ExplorableDoor(object):

    def __init__(self, door, crystal, flag):
        self.door = door
        self.crystal = crystal
        self.flag = flag

    def __str__(self):
        return str(self.__unicode__())

    def __unicode__(self):
        return '%s (%s)' % (self.door.name, self.crystal.name)


def extend_reachable_state_improved(search_regions, state, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception):
    local_state = state.copy()
    for region in search_regions:
        local_state.visit_region(region)
        local_state.add_all_doors_check_proposed(region, proposed_map, valid_doors, False, world, player, exception)
    while len(local_state.avail_doors) > 0:
        explorable_door = local_state.next_avail_door()
        if explorable_door.door.bigKey:
            if bk_flag:
                big_not_found = (not special_big_key_found(local_state) if local_state.big_key_special
                                 else local_state.count_locations_exclude_specials(world, player) == 0)
                if big_not_found:
                    continue  # we can't open this door
        if explorable_door.door in proposed_map:
            connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region
        else:
            connect_region = world.get_entrance(explorable_door.door.name, player).connected_region
        if connect_region is not None:
            if valid_region_to_explore_in_regions(connect_region, all_regions, world, player) and not local_state.visited(
                 connect_region):
                flag = explorable_door.flag or explorable_door.door.bigKey
                local_state.visit_region(connect_region, bk_flag=flag)
                local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, flag, world, player, exception)
    return local_state


# bk_relevant means the big key doors need to be checks
def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regions, valid_doors, bk_relevant,
                                   world, player):
    local_state = state.copy()
    for region in search_regions:
        local_state.visit_region(region, bk_relevant=bk_relevant)
        if world.trap_door_mode[player] == 'vanilla':
            local_state.add_all_doors_check_proposed_3(region, proposed_map, valid_doors, bk_relevant, world, player)
        else:
            local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, bk_relevant, world, player)
    while len(local_state.avail_doors) > 0:
        explorable_door = local_state.next_avail_door()
        if explorable_door.door.bigKey:
            if bk_relevant and (not special_big_key_found(local_state) if local_state.big_key_special
                                else local_state.count_locations_exclude_specials(world, player) == 0):
                continue
        if explorable_door.door in proposed_map:
            connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region
        else:
            connect_region = world.get_entrance(explorable_door.door.name, player).connected_region
        if connect_region is not None:
            if (valid_region_to_explore_in_regions(connect_region, all_regions, world, player)
               and not local_state.visited(connect_region)):
                local_state.visit_region(connect_region, bk_relevant=bk_relevant)
                if world.trap_door_mode[player] == 'vanilla':
                    local_state.add_all_doors_check_proposed_3(connect_region, proposed_map, valid_doors,
                                                               bk_relevant, world, player)
                else:
                    local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors,
                                                               bk_relevant, world, player)
    return local_state


def special_big_key_found(state):
    for location in state.found_locations:
        if location.forced_item and location.forced_item.bigkey:
            return True
    return False


def valid_region_to_explore_in_regions(region, all_regions, world, player):
    if region is None:
        return False
    return ((region.type == RegionType.Dungeon and region in all_regions)
            or region.name in world.inaccessible_regions[player]
            or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))


def valid_region_to_explore(region, name, world, player):
    if region is None:
        return False
    return ((region.type == RegionType.Dungeon and region.dungeon and region.dungeon.name in name)
            or region.name in world.inaccessible_regions[player]
            or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))


def get_doors(world, region, player):
    res = []
    for ext in region.exits:
        door = world.check_for_door(ext.name, player)
        if door is not None:
            res.append(door)
    return res


def get_entrance_doors(world, region, player):
    res = []
    for ext in region.entrances:
        door = world.check_for_door(ext.name, player)
        if door is not None:
            res.append(door)
    return res


class GenerationException(Exception):
    pass


