import logging
from collections import defaultdict

import RaceRandom as random
from source.dungeon.EnemyList import (
    EnemySprite,
    SpriteType,
    enemy_names,
    overlord_names,
    sprite_translation,
)
from source.dungeon.RoomConstants import *


class SpriteRequirement:
    def __init__(self, sprite, overlord=0):
        self.sprite = sprite
        self.overlord = overlord

        self.boss = False
        self.static = False  # npcs and do not randomize
        self.killable = True
        self.can_drop = True
        self.water_only = False
        self.dont_use = False
        self.ow_valid = True
        self.uw_valid = True
        self.can_randomize = True
        self.water_phobic = False
        self.bush_valid = True

        self.groups = []
        self.sub_groups = defaultdict(list)

        self.excluded_rooms = set()
        self.allowed_rooms = set()

    def can_spawn_in_room(self, room_id):
        return room_id not in self.excluded_rooms and (self.sprite != EnemySprite.Wallmaster or room_id < 0x100)

    def no_drop(self):
        self.can_drop = False
        return self

    def sub_group(self, key, subs):
        if isinstance(subs, list):
            self.sub_groups[key].extend(subs)
        else:
            self.sub_groups[key].append(subs)
        return self

    def group(self, group_id):
        self.groups.append(group_id)
        return self

    def exclude(self, exclusions):
        self.excluded_rooms.update(exclusions)
        return self

    def allow(self, allowed):
        self.allowed_rooms.update(allowed)
        return self

    def affix(self):
        self.static = True
        self.killable = False
        self.can_drop = False
        return self

    def stasis(self):
        self.can_randomize = False
        return self

    def exalt(self):
        self.boss = True
        self.static = True  # not randomized by sprite sheet
        return self

    def immune(self):
        self.killable = False
        self.can_drop = False
        return self

    def immerse(self):
        self.water_only = True
        return self

    def aquaphobia(self):
        self.water_phobic = True
        return self

    def skip(self):
        self.dont_use = True
        return self

    def ow_skip(self):
        self.ow_valid = False
        return self

    def uw_skip(self):
        self.uw_valid = False
        return self

    def no_bush(self):
        self.bush_valid = False
        return self

    def good_for_uw_water(self):
        return self.water_only and not self.static and not self.dont_use and self.uw_valid

    def good_for_shutter(self, forbidden):
        if self.sprite in forbidden:
            return False
        return self.killable and not self.static and not self.dont_use and self.uw_valid

    def good_for_key_drop(self, forbidden):
        return self.good_for_shutter(forbidden) and self.can_drop

    def __str__(self):
        return f'Req for {enemy_names[self.sprite] if self.overlord != 0x7 else overlord_names[self.sprite]}'


NoFlyingRooms = {0xd2, 0x10c}  # Mire 2, Mimic Cave
NoBeamosOrTrapRooms = {0xb, 0x16, 0x19, 0x1e, 0x26, 0x27, 0x36, 0x3f, 0x40, 0x42, 0x46, 0x49, 0x4b, 0x4e, 0x55, 0x57,
                       0x5f, 0x65, 0x6a, 0x74, 0x76, 0x7d, 0x7f, 0x83, 0x84, 0x85, 0x8c, 0x8d, 0x92, 0x95, 0x98, 0x9b,
                       0x9c, 0x9d, 0x9e, 0xa0, 0xaa, 0xaf, 0xb3, 0xba, 0xbb, 0xbc, 0xc6, 0xcb, 0xce, 0xd0, 0xd2, 0xd5,
                       0xd8, 0xdc, 0xdf, 0xe4, 0xe7, 0xee, 0xf9, 0xfd, 0x10c}
LenientTrapsForTesting = {0x16, 0x26, 0x3f, 0x40, 0x42, 0x46, 0x49, 0x4e, 0x57,
                          0x65, 0x6a, 0x74, 0x76, 0x7d, 0x98,
                          0x9e, 0xaf, 0xba, 0xc6, 0xcb, 0xce, 0xd2, 0xd5,
                          0xd8, 0xdf, 0xe4, 0xe7, 0xee, 0xfd, 0x10c}
PitRooms = {0x17, 0x1a, 0x2a, 0x31, 0x3c, 0x3d, 0x40, 0x44, 0x49, 0x4e, 0x56, 0x58, 0x5c, 0x67, 0x72,
            0x7b, 0x7c, 0x7d, 0x7f, 0x82, 0x8b, 0x8d, 0x95, 0x96, 0x9b, 0x9c, 0x9d, 0x9e, 0xa0, 0xa5,
            0xaf, 0xbc, 0xc0, 0xc5, 0xc6, 0xd1, 0xd5, 0xe7, 0xe8, 0xee, 0xf0, 0xf1, 0xfb, 0x123}

# wallmasters must not be on tiles near spiral staircases. Unknown if other stairs have issues
WallmasterInvalidRooms = {
    HC_NorthCorridor, HC_SwitchRoom, TR_CrystalRollerRoom,
    PalaceofDarkness0x09, PoD_StalfosTrapRoom, GT_EntranceRoom, Ice_EntranceRoom,
    HC_BombableStockRoom, TurtleRock0x15,
    Swamp_SwimmingTreadmill, Hera_MoldormFallRoom, PoD_BigChestRoom,
    GT_IceArmos, GT_FinalHallway, Ice_BombFloor_BariRoom,
    Swamp_StatueRoom, Hera_BigChest,
    Swamp_EntranceRoom, Hera_HardhatBeetlesRoom,
    Swamp_PushBlockPuzzle_Pre_BigKeyRoom,
    Swamp_KeyPotRoom,
    PoD_BombableFloorRoom,
    Ice_MapChestRoom, Tower_FinalBridgeRoom, HC_FirstDarkRoom, HC_6RopesRoom,
    TT_JailCellsRoom, PoD_EntranceRoom,
    GT_Mini_HelmasaurConveyorRoom, GT_MoldormRoom, Ice_Bomb_JumpRoom,
    Desert_Popos2_BeamosHellwayRoom,
    GT_Ganon_BallZ,
    GT_Gauntlet1_2_3, Ice_HiddenChest_SpikeFloorRoom,
    Desert_FinalSectionEntranceRoom, TT_WestAtticRoom,
    Swamp_HiddenChest_HiddenDoorRoom, PoD_RupeeRoom, GT_MimicsRooms,
    GT_LanmolasRoom, Ice_PengatorsRoom, HC_SmallCorridortoJailCells, HC_BoomerangChestRoom,
    HC_MapChestRoom, Swamp_WaterDrainRoom,
    Hera_EntranceRoom,
    Ice_BigSpikeTrapsRoom, HC_JailCellRoom,
    Hera_TileRoom,
    GT_EastandWestDownstairs_BigChestRoom,
    IcePalace0x8E, Mire_FinalSwitchRoom,
    Mire_DarkCaneFloorSwitchPuzzleRoom, Mire_TorchPuzzle_MovingWallRoom,
    Mire_EntranceRoom, Eastern_EyegoreKeyRoom,
    Ice_BigChestRoom, Mire_Pre_VitreousRoom,
    Mire_BridgeKeyChestRoom, GT_WizzrobesRooms, GT_MoldormFallRoom,
    TT_MovingSpikes_KeyPotRoom,
    IcePalace0xAE, Tower_CircleofPots,
    TR_DarkMaze, TR_ChainChompsRoom,
    TT_ConveyorToilet, Ice_BlockPuzzleRoom,
    Tower_DarkBridgeRoom,
    UnknownRoom,
    Tower_DarkMaze, Mire_ConveyorSlug_BigKeyRoom, Mire_Mire02_WizzrobesRoom,
    EasternPalace,
    Tower_EntranceRoom, Cave_BackwardsDeathMountainTopFloor, Cave0xE8, Cave_SpectacleRockHP, Cave0xEB, Cave0xED,
    Cave_CrystalSwitch_5ChestsRoom, Cave0xF8, Cave0xFA, Cave0xFB, Cave0xFD, Cave0xFF
}


def init_sprite_requirements():
    reqs = [
        SpriteRequirement(EnemySprite.Raven).no_drop().sub_group(3, [0x11, 0x19]).exclude(NoFlyingRooms),
        SpriteRequirement(EnemySprite.Vulture).no_drop().sub_group(2, 0x12).exclude(NoFlyingRooms),
        SpriteRequirement(EnemySprite.CorrectPullSwitch).affix().sub_group(3, [0x52, 0x53]),
        SpriteRequirement(EnemySprite.WrongPullSwitch).affix().sub_group(3, [0x52, 0x53]),
        SpriteRequirement(EnemySprite.Octorok).sub_group(2, [0xc, 0x18]),
        SpriteRequirement(EnemySprite.Moldorm).exalt().sub_group(2, 0x30),
        SpriteRequirement(EnemySprite.Octorok4Way).sub_group(2, 0xc),
        SpriteRequirement(EnemySprite.Cucco).immune().sub_group(3, [0x15, 0x50]).exclude(NoFlyingRooms),
        SpriteRequirement(EnemySprite.Buzzblob).sub_group(3, 0x11),
        SpriteRequirement(EnemySprite.Snapdragon).sub_group(0, 0x16).sub_group(2, 0x17),
        SpriteRequirement(EnemySprite.Octoballoon).no_drop().sub_group(2, 0xc).exclude(NoFlyingRooms),
        SpriteRequirement(EnemySprite.Hinox).sub_group(0, 0x16),
        SpriteRequirement(EnemySprite.Moblin).sub_group(2, 0x17),
        SpriteRequirement(EnemySprite.MiniHelmasaur).aquaphobia().sub_group(1, 0x1e),
        SpriteRequirement(EnemySprite.AntiFairy).no_drop().sub_group(3, [0x52, 0x53])
        .exclude(NoFlyingRooms).exclude({0x40}),  # no anti-fairies in aga tower bridge room
        SpriteRequirement(EnemySprite.Wiseman).affix().sub_group(2, 0x4c),
        SpriteRequirement(EnemySprite.Hoarder).sub_group(3, 0x11).exclude({0x10c}),
        SpriteRequirement(EnemySprite.MiniMoldorm).sub_group(1, 0x1e),
        SpriteRequirement(EnemySprite.Poe).no_drop().sub_group(3, 0x15).exclude(NoFlyingRooms),
        SpriteRequirement(EnemySprite.Smithy).affix().sub_group(1, 0x1d).sub_group(3, 0x15),
        SpriteRequirement(EnemySprite.Statue).stasis().immune().sub_group(3, [0x52, 0x53]),
        SpriteRequirement(EnemySprite.CrystalSwitch).affix().sub_group(3, [0x52, 0x53]),
        SpriteRequirement(EnemySprite.SickKid).affix().sub_group(0, 0x51),
        SpriteRequirement(EnemySprite.Sluggula).sub_group(2, 0x25),
        SpriteRequirement(EnemySprite.WaterSwitch).affix().sub_group(3, 0x53),
        SpriteRequirement(EnemySprite.Ropa).sub_group(0, 0x16),
        SpriteRequirement(EnemySprite.RedBari).sub_group(0, 0x1f),
        SpriteRequirement(EnemySprite.BlueBari).sub_group(0, 0x1f),
        SpriteRequirement(EnemySprite.TalkingTree).affix().sub_group(3, [0x15, 0x1B]),
        SpriteRequirement(EnemySprite.HardhatBeetle).sub_group(1, 0x1e),
        SpriteRequirement(EnemySprite.Deadrock).sub_group(3, 0x10).exclude({0x7f, 0x10c}),
        SpriteRequirement(EnemySprite.DarkWorldHintNpc).affix(),   # no groups?
        SpriteRequirement(EnemySprite.AdultNpc).affix().sub_group(0, [0xe, 0x4f]),
        SpriteRequirement(EnemySprite.SweepingLady).affix().group(6),  # no sub groups?
        SpriteRequirement(EnemySprite.Lumberjacks).affix().sub_group(2, 0x4a),
        SpriteRequirement(EnemySprite.RaceGameLady).affix().group(6),
        SpriteRequirement(EnemySprite.FortuneTeller).affix().sub_group(0, 0x4b),
        SpriteRequirement(EnemySprite.ArgueBros).affix().sub_group(0, 0x4f),
        SpriteRequirement(EnemySprite.RupeePull).affix(),
        SpriteRequirement(EnemySprite.YoungSnitch).affix().group(6),
        SpriteRequirement(EnemySprite.Innkeeper).affix(),   # no groups?
        SpriteRequirement(EnemySprite.Witch).affix().sub_group(2, 0x7c),
        SpriteRequirement(EnemySprite.Waterfall).affix(),
        SpriteRequirement(EnemySprite.EyeStatue).affix(),
        SpriteRequirement(EnemySprite.Locksmith).affix().sub_group(3, 0x11),
        SpriteRequirement(EnemySprite.MagicBat).affix().sub_group(3, 0x1d),
        SpriteRequirement(EnemySprite.KidInKak).affix().group(6),
        SpriteRequirement(EnemySprite.OldSnitch).affix().group(6),
        SpriteRequirement(EnemySprite.Hoarder2).sub_group(3, 0x11).exclude({0x10c}),
        SpriteRequirement(EnemySprite.TutorialGuard).affix(),
        SpriteRequirement(EnemySprite.LightningGate).affix().sub_group(3, 0x3f),
        SpriteRequirement(EnemySprite.BlueGuard).sub_group(1, [0xd, 0x49]).exclude(PitRooms),
        SpriteRequirement(EnemySprite.BlueGuard).sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]),
        SpriteRequirement(EnemySprite.GreenGuard).sub_group(1, 0x49).exclude(PitRooms),
        SpriteRequirement(EnemySprite.GreenGuard).sub_group(1, 0x49).sub_group(2, 0x13),
        SpriteRequirement(EnemySprite.RedSpearGuard).sub_group(1, [0xd, 0x49]).exclude(PitRooms),
        SpriteRequirement(EnemySprite.RedSpearGuard).sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]),
        SpriteRequirement(EnemySprite.BluesainBolt).aquaphobia().sub_group(0, 0x46).sub_group(1, [0xd, 0x49]),
        SpriteRequirement(EnemySprite.UsainBolt).aquaphobia().sub_group(1, [0xd, 0x49]),
        SpriteRequirement(EnemySprite.BlueArcher).sub_group(0, 0x48).sub_group(1, 0x49),
        SpriteRequirement(EnemySprite.GreenBushGuard).sub_group(0, 0x48).sub_group(1, 0x49),
        SpriteRequirement(EnemySprite.RedJavelinGuard).sub_group(0, 0x46).sub_group(1, 0x49),
        SpriteRequirement(EnemySprite.RedBushGuard).sub_group(0, 0x46).sub_group(1, 0x49),
        SpriteRequirement(EnemySprite.BombGuard).sub_group(0, 0x46).sub_group(1, 0x49),
        SpriteRequirement(EnemySprite.GreenKnifeGuard).sub_group(1, 0x49).sub_group(2, 0x13),
        SpriteRequirement(EnemySprite.Geldman).sub_group(2, 0x12).exclude({0x10c}),
        SpriteRequirement(EnemySprite.Toppo).immune().sub_group(3, 0x11),
        SpriteRequirement(EnemySprite.Popo).sub_group(1, 0x2c),
        SpriteRequirement(EnemySprite.Popo2).sub_group(1, 0x2c),
        SpriteRequirement(EnemySprite.ArmosStatue).sub_group(3, 0x10).exclude({0x10c}),
        SpriteRequirement(EnemySprite.KingZora).affix().sub_group(3, 0x44),
        SpriteRequirement(EnemySprite.ArmosKnight).exalt().sub_group(3, 0x1d),
        SpriteRequirement(EnemySprite.Lanmolas).exalt().sub_group(3, 0x31),
        SpriteRequirement(EnemySprite.FireballZora).immerse().no_drop().sub_group(2, [0xc, 0x18]),  # .uw_skip() test
        SpriteRequirement(EnemySprite.Zora).sub_group(2, 0xc).sub_group(3, 0x44).uw_skip(),
        SpriteRequirement(EnemySprite.DesertStatue).affix().sub_group(2, 0x12),
        SpriteRequirement(EnemySprite.Crab).sub_group(2, 0xc),
        SpriteRequirement(EnemySprite.LostWoodsBird).affix().sub_group(2, 0x37).sub_group(3, 0x36),
        SpriteRequirement(EnemySprite.LostWoodsSquirrel).affix().sub_group(2, 0x37).sub_group(3, 0x36),
        SpriteRequirement(EnemySprite.SparkCW).immune().sub_group(0, 0x1f),
        SpriteRequirement(EnemySprite.SparkCCW).immune().sub_group(0, 0x1f),
        SpriteRequirement(EnemySprite.RollerVerticalUp).immune().sub_group(2, 0x27).exclude(NoBeamosOrTrapRooms),
        SpriteRequirement(EnemySprite.RollerVerticalDown).immune().sub_group(2, 0x27).exclude(NoBeamosOrTrapRooms),
        SpriteRequirement(EnemySprite.RollerHorizontalLeft).immune().sub_group(2, 0x27).exclude(NoBeamosOrTrapRooms),
        SpriteRequirement(EnemySprite.RollerHorizontalRight).immune().sub_group(2, 0x27).exclude(NoBeamosOrTrapRooms),
        SpriteRequirement(EnemySprite.Beamos).no_drop().sub_group(1, 0x2c).exclude(NoBeamosOrTrapRooms),
        SpriteRequirement(EnemySprite.MasterSword).affix().sub_group(2, 0x37).sub_group(3, 0x36),

        SpriteRequirement(EnemySprite.DebirandoPit).sub_group(0, 0x2f),  # skip
        SpriteRequirement(EnemySprite.Debirando).sub_group(0, 0x2f),     # skip
        SpriteRequirement(EnemySprite.ArcheryNpc).affix().sub_group(0, 0x4b),
        SpriteRequirement(EnemySprite.WallCannonVertLeft).affix().sub_group(0, 0x2f),
        SpriteRequirement(EnemySprite.WallCannonVertRight).affix().sub_group(0, 0x2f),
        SpriteRequirement(EnemySprite.WallCannonHorzTop).affix().sub_group(0, 0x2f),
        SpriteRequirement(EnemySprite.WallCannonHorzBottom).affix().sub_group(0, 0x2f),
        SpriteRequirement(EnemySprite.BallNChain).sub_group(0, 0x46).sub_group(1, 0x49),
        SpriteRequirement(EnemySprite.CannonTrooper).sub_group(0, 0x46).sub_group(1, 0x49),
        SpriteRequirement(EnemySprite.CricketRat).sub_group(2, [0x1c, 0x24]),
        SpriteRequirement(EnemySprite.Snake).sub_group(2, [0x1c, 0x24]),
        SpriteRequirement(EnemySprite.Keese).no_drop().sub_group(2, [0x1c, 0x24]),
        SpriteRequirement(EnemySprite.Leever).sub_group(0, 0x2f),
        SpriteRequirement(EnemySprite.FairyPondTrigger).affix().sub_group(3, 0x36),
        SpriteRequirement(EnemySprite.UnclePriest).affix().sub_group(0, [0x47, 0x51]),
        SpriteRequirement(EnemySprite.RunningNpc).affix().group(6),
        SpriteRequirement(EnemySprite.BottleMerchant).affix().group(6),
        SpriteRequirement(EnemySprite.Zelda).affix(),
        SpriteRequirement(EnemySprite.Grandma).affix().sub_group(0, 0x4b).sub_group(1, 0x4d).sub_group(2, 0x4a),
        SpriteRequirement(EnemySprite.Agahnim).exalt().sub_group(0, 0x55).sub_group(1, [0x1a, 0x3d]).sub_group(2, 0x42)
        .sub_group(3, 0x43),
        SpriteRequirement(EnemySprite.FloatingSkull).sub_group(0, 0x1f).exclude(NoFlyingRooms),
        SpriteRequirement(EnemySprite.BigSpike).sub_group(3, [0x52, 0x53]).no_drop(),
        SpriteRequirement(EnemySprite.FirebarCW).immune().sub_group(0, 0x1f),
        SpriteRequirement(EnemySprite.FirebarCCW).immune().sub_group(0, 0x1f),
        SpriteRequirement(EnemySprite.Firesnake).no_drop().sub_group(0, 0x1f),
        SpriteRequirement(EnemySprite.Hover).sub_group(2, 0x22),  # .exclude(NoFlyingRooms), might be okay now
        SpriteRequirement(EnemySprite.AntiFairyCircle).no_drop().sub_group(3, [0x52, 0x53]),
        SpriteRequirement(EnemySprite.GreenEyegoreMimic).sub_group(2, 0x2e),
        SpriteRequirement(EnemySprite.RedEyegoreMimic).sub_group(2, 0x2e),

        SpriteRequirement(EnemySprite.Kodongo).sub_group(2, 0x2a),
        # SpriteRequirement(EnemySprite.YellowStalfos).sub_group(0, 0x1f),  # doesn't spawn
        SpriteRequirement(EnemySprite.Mothula).exalt().sub_group(2, 0x38).sub_group(3, 0x52),
        SpriteRequirement(EnemySprite.SpikeBlock).immune().sub_group(3, [0x52, 0x53]).exclude(NoBeamosOrTrapRooms)
        .exclude({0x28}),  # why exclude sp entrance?
        SpriteRequirement(EnemySprite.Gibdo).sub_group(2, 0x23),
        SpriteRequirement(EnemySprite.Arrghus).exalt().sub_group(2, 0x39),
        SpriteRequirement(EnemySprite.Arrghi).exalt().sub_group(2, 0x39),
        SpriteRequirement(EnemySprite.Terrorpin).sub_group(2, 0x2a).exclude({0x10c}),  # probably fine in mimic now
        SpriteRequirement(EnemySprite.Blob).sub_group(1, 0x20),
        SpriteRequirement(EnemySprite.Wallmaster).immune().ow_skip().sub_group(2, 0x23)
        .exclude(WallmasterInvalidRooms),
        SpriteRequirement(EnemySprite.StalfosKnight).sub_group(1, 0x20).exclude({0x10c}),
        SpriteRequirement(EnemySprite.HelmasaurKing).exalt().sub_group(2, 0x3a).sub_group(3, 0x3e),
        SpriteRequirement(EnemySprite.Bumper).immune().aquaphobia().sub_group(3, [0x52, 0x53]),
        SpriteRequirement(EnemySprite.LaserEyeLeft).affix().sub_group(3, [0x52, 0x53]),
        SpriteRequirement(EnemySprite.LaserEyeRight).affix().sub_group(3, [0x52, 0x53]),
        SpriteRequirement(EnemySprite.LaserEyeTop).affix().sub_group(3, [0x52, 0x53]),
        SpriteRequirement(EnemySprite.LaserEyeBottom).affix().sub_group(3, [0x52, 0x53]),
        SpriteRequirement(EnemySprite.Pengator).sub_group(2, 0x26),
        SpriteRequirement(EnemySprite.Kyameron).no_drop().immerse().sub_group(2, 0x22),
        SpriteRequirement(EnemySprite.Wizzrobe).sub_group(2, [0x25, 0x29]),
        SpriteRequirement(EnemySprite.Zoro).sub_group(1, 0x20),
        SpriteRequirement(EnemySprite.Babasu).sub_group(1, 0x20),
        SpriteRequirement(EnemySprite.GroveOstritch).affix().sub_group(2, 0x4e),
        SpriteRequirement(EnemySprite.GroveRabbit).affix(),
        SpriteRequirement(EnemySprite.GroveBird).affix().sub_group(2, 0x4e),
        SpriteRequirement(EnemySprite.Freezor).stasis().skip().sub_group(2, 0x26),
        SpriteRequirement(EnemySprite.Kholdstare).exalt().sub_group(2, 0x3c),
        SpriteRequirement(EnemySprite.KholdstareShell).exalt(),
        SpriteRequirement(EnemySprite.FallingIce).exalt().sub_group(2, 0x3c),
        SpriteRequirement(EnemySprite.BlueZazak).sub_group(2, 0x28),
        SpriteRequirement(EnemySprite.RedZazak).sub_group(2, 0x28),
        SpriteRequirement(EnemySprite.Stalfos).sub_group(0, 0x1f),
        SpriteRequirement(EnemySprite.GreenZirro).no_drop().sub_group(3, 0x1b).exclude(NoFlyingRooms),
        SpriteRequirement(EnemySprite.BlueZirro).no_drop().sub_group(3, 0x1b).exclude(NoFlyingRooms),
        SpriteRequirement(EnemySprite.Pikit).no_drop().sub_group(3, 0x1b),
        SpriteRequirement(EnemySprite.CrystalMaiden).affix(),
        SpriteRequirement(EnemySprite.OldMan).affix().sub_group(2, 0x1c),
        SpriteRequirement(EnemySprite.PipeDown).affix(),
        SpriteRequirement(EnemySprite.PipeUp).affix(),
        SpriteRequirement(EnemySprite.PipeRight).affix(),
        SpriteRequirement(EnemySprite.PipeLeft).affix(),
        SpriteRequirement(EnemySprite.GoodBee).affix().sub_group(0, 0x1f),
        SpriteRequirement(EnemySprite.PurpleChest).affix().sub_group(3, 0x15),
        SpriteRequirement(EnemySprite.BombShopGuy).affix().sub_group(1, 0x4d),
        SpriteRequirement(EnemySprite.Kiki).affix().sub_group(3, 0x19),
        SpriteRequirement(EnemySprite.BlindMaiden).affix(),
        #     dialog tester.sub_group(1, 0x2c)
        SpriteRequirement(EnemySprite.BullyPinkBall).affix().sub_group(3, 0x14),
        # shop keepers in complex thing below
        SpriteRequirement(EnemySprite.Drunkard).affix().sub_group(0, 0x4f).sub_group(1, 0x4d).sub_group(2, 0x4a).
        sub_group(3, 0x50),
        SpriteRequirement(EnemySprite.Vitreous).exalt().sub_group(3, 0x3d),
        SpriteRequirement(EnemySprite.Catfish).affix().sub_group(2, 0x18),
        SpriteRequirement(EnemySprite.CutsceneAgahnim).affix().sub_group(0, 0x55).sub_group(1, 0x3d)
        .sub_group(2, 0x42).sub_group(3, 0x43),
        SpriteRequirement(EnemySprite.Boulder).affix().sub_group(3, 0x10),
        SpriteRequirement(EnemySprite.Gibo).sub_group(2, 0x28),
        SpriteRequirement(EnemySprite.Thief).immune().uw_skip().sub_group(0, [0xe, 0x15]),
        SpriteRequirement(EnemySprite.Medusa).affix(),
        SpriteRequirement(EnemySprite.FourWayShooter).affix(),
        SpriteRequirement(EnemySprite.Pokey).sub_group(2, 0x27),
        SpriteRequirement(EnemySprite.BigFairy).affix().sub_group(2, 0x39).sub_group(3, 0x36),
        SpriteRequirement(EnemySprite.Tektite).sub_group(3, 0x10),
        SpriteRequirement(EnemySprite.Chainchomp).immune().sub_group(2, 0x27),
        SpriteRequirement(EnemySprite.TrinexxRockHead).exalt().sub_group(0, 0x40).sub_group(3, 0x3f),
        SpriteRequirement(EnemySprite.TrinexxFireHead).exalt().sub_group(0, 0x40).sub_group(3, 0x3f),
        SpriteRequirement(EnemySprite.TrinexxIceHead).exalt().sub_group(0, 0x40).sub_group(3, 0x3f),
        SpriteRequirement(EnemySprite.Blind).exalt().sub_group(1, 0x2c).sub_group(2, 0x3b),
        SpriteRequirement(EnemySprite.Swamola).skip().no_drop().sub_group(3, 0x19),
        SpriteRequirement(EnemySprite.Lynel).sub_group(3, 0x14),
        SpriteRequirement(EnemySprite.BunnyBeam).no_drop().ow_skip(),
        SpriteRequirement(EnemySprite.FloppingFish).uw_skip().immune(),
        SpriteRequirement(EnemySprite.Stal),
        SpriteRequirement(EnemySprite.Landmine).skip(),
        SpriteRequirement(EnemySprite.DiggingGameNPC).affix().sub_group(1, 0x2a),
        SpriteRequirement(EnemySprite.Ganon).exalt().sub_group(0, 0x21).sub_group(1, 0x41)
        .sub_group(2, 0x45).sub_group(3, 0x33),
        SpriteRequirement(EnemySprite.Faerie).ow_skip().immune(),
        SpriteRequirement(EnemySprite.FakeMasterSword).immune().sub_group(3, 0x11),
        SpriteRequirement(EnemySprite.MagicShopAssistant).affix().sub_group(0, 0x4b).sub_group(3, 0x5a),
        SpriteRequirement(EnemySprite.SomariaPlatform).affix().sub_group(2, 0x27),
        SpriteRequirement(EnemySprite.CastleMantle).affix().sub_group(0, 0x5d),
        SpriteRequirement(EnemySprite.GreenMimic).sub_group(1, 0x2c).no_bush(),
        SpriteRequirement(EnemySprite.RedMimic).sub_group(1, 0x2c).no_bush(),
        SpriteRequirement(EnemySprite.MedallionTablet).affix().sub_group(2, 0x12),

        # overlord requirements - encapsulated mostly in the required sheets
        SpriteRequirement(2, 7).affix().sub_group(2, 46),
        SpriteRequirement(3, 7).affix().sub_group(2, 46),
        SpriteRequirement(5, 7).affix().sub_group(0, 31),
        SpriteRequirement(6, 7).affix().sub_group(2, [28, 36]),
        SpriteRequirement(7, 7).affix(),
        SpriteRequirement(8, 7).affix().sub_group(1, 32),
        SpriteRequirement(9, 7).affix().sub_group(2, 35),
        SpriteRequirement(0xa, 7).affix().sub_group(3, 82),
        SpriteRequirement(0xb, 7).affix().sub_group(3, 82),
        SpriteRequirement(0x10, 7).affix().sub_group(2, 34),
        SpriteRequirement(0x11, 7).affix().sub_group(2, 34),
        SpriteRequirement(0x12, 7).affix().sub_group(2, 34),
        SpriteRequirement(0x13, 7).affix().sub_group(2, 34),
        SpriteRequirement(0x14, 7).affix(),
        SpriteRequirement(0x15, 7).affix().sub_group(2, [37, 41]),
        SpriteRequirement(0x16, 7).affix().sub_group(1, 32),
        SpriteRequirement(0x17, 7).affix().sub_group(0, 31),
        SpriteRequirement(0x18, 7).affix().sub_group(0, 31),
        SpriteRequirement(0x19, 7).affix(),
        SpriteRequirement(0x1a, 7).affix(),
    ]
    simple = {(r.sprite, r.overlord): r for r in reqs}
    shopkeeper = [
        SpriteRequirement(EnemySprite.Shopkeeper).affix().sub_group(0, 75).sub_group(2, 74).sub_group(3, 90)
        .allow({0xff, 0x112, 0x11f}),
        SpriteRequirement(EnemySprite.Shopkeeper).affix().sub_group(0, 75).sub_group(1, 77).sub_group(2, 74)
        .sub_group(3, 90).allow({0x10f, 0x110, 0x11f}),
        SpriteRequirement(EnemySprite.Shopkeeper).affix().sub_group(0, 79).sub_group(2, 74).sub_group(3, 90)
        .allow({0x118}),
        SpriteRequirement(EnemySprite.Shopkeeper).affix().sub_group(0, 14).sub_group(2, 74).sub_group(3, 90)
        .allow({0x123, 0x124}),
        SpriteRequirement(EnemySprite.Shopkeeper).affix().sub_group(0, 14).sub_group(2, 74).sub_group(3, 80)
        .allow({0x125, 0x100}),
        SpriteRequirement(EnemySprite.Shopkeeper).affix().sub_group(0, 21).allow({0x11e}),
    ]
    complex_r = {}
    for req in shopkeeper:
        for r in req.allowed_rooms:
            complex_r[r] = req
    simple[(EnemySprite.Shopkeeper, 0)] = complex_r
    return simple


# sheet 1 and 1c have group 4 modified from vanilla for murahdahla
vanilla_sheets = [
    (0x00, 0x49, 0x00, 0x00), (0x46, 0x49, 0x0C, 0x3F), (0x48, 0x49, 0x13, 0x3F), (0x46, 0x49, 0x13, 0x0E),
    (0x48, 0x49, 0x0C, 0x11), (0x48, 0x49, 0x0C, 0x10), (0x4F, 0x49, 0x4A, 0x50), (0x0E, 0x49, 0x4A, 0x11),
    (0x46, 0x49, 0x12, 0x00), (0x00, 0x49, 0x00, 0x50), (0x00, 0x49, 0x00, 0x11), (0x48, 0x49, 0x0C, 0x00),
    (0x00, 0x00, 0x37, 0x36), (0x48, 0x49, 0x4C, 0x11), (0x5D, 0x2C, 0x0C, 0x44), (0x00, 0x00, 0x4E, 0x00),

    (0x0F, 0x00, 0x12, 0x10), (0x00, 0x00, 0x00, 0x4C), (0x00, 0x0D, 0x17, 0x00), (0x16, 0x0D, 0x17, 0x1B),
    (0x16, 0x0D, 0x17, 0x14), (0x15, 0x0D, 0x17, 0x15), (0x16, 0x0D, 0x18, 0x19), (0x16, 0x0D, 0x17, 0x19),
    (0x16, 0x0D, 0x00, 0x00), (0x16, 0x0D, 0x18, 0x1B), (0x0F, 0x49, 0x4A, 0x11), (0x4B, 0x2A, 0x5C, 0x15),
    (0x16, 0x49, 0x17, 0x3F), (0x00, 0x00, 0x00, 0x15), (0x16, 0x0D, 0x17, 0x10), (0x16, 0x49, 0x12, 0x00),

    (0x16, 0x49, 0x0C, 0x11), (0x00, 0x00, 0x12, 0x10), (0x16, 0x0D, 0x00, 0x11), (0x16, 0x49, 0x0C, 0x00),
    (0x16, 0x0D, 0x4C, 0x11), (0x0E, 0x0D, 0x4A, 0x11), (0x16, 0x1A, 0x17, 0x1B), (0x4F, 0x34, 0x4A, 0x50),
    (0x35, 0x4D, 0x65, 0x36), (0x4A, 0x34, 0x4E, 0x00), (0x0E, 0x34, 0x4A, 0x11), (0x51, 0x34, 0x5D, 0x59),
    (0x4B, 0x49, 0x4C, 0x11), (0x2D, 0x00, 0x00, 0x00), (0x5D, 0x00, 0x12, 0x59), (0x00, 0x00, 0x00, 0x00),

    (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00),
    (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00),
    (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00),
    (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00),

    (0x47, 0x49, 0x2B, 0x2D), (0x46, 0x49, 0x1C, 0x52), (0x00, 0x49, 0x1C, 0x52), (0x5D, 0x49, 0x00, 0x52),
    (0x46, 0x49, 0x13, 0x52), (0x4B, 0x4D, 0x4A, 0x5A), (0x47, 0x49, 0x1C, 0x52), (0x4B, 0x4D, 0x39, 0x36),
    (0x1F, 0x2C, 0x2E, 0x52), (0x1F, 0x2C, 0x2E, 0x1D), (0x2F, 0x2C, 0x2E, 0x52), (0x2F, 0x2C, 0x2E, 0x31),
    (0x1F, 0x1E, 0x30, 0x52), (0x51, 0x49, 0x13, 0x00), (0x4F, 0x49, 0x13, 0x50), (0x4F, 0x4D, 0x4A, 0x50),

    (0x4B, 0x49, 0x4C, 0x2B), (0x1F, 0x20, 0x22, 0x53), (0x55, 0x3D, 0x42, 0x43), (0x1F, 0x1E, 0x23, 0x52),
    (0x1F, 0x1E, 0x39, 0x3A), (0x1F, 0x1E, 0x3A, 0x3E), (0x1F, 0x1E, 0x3C, 0x3D), (0x40, 0x1E, 0x27, 0x3F),
    (0x55, 0x1A, 0x42, 0x43), (0x1F, 0x1E, 0x2A, 0x52), (0x1F, 0x1E, 0x38, 0x52), (0x1F, 0x20, 0x28, 0x52),
    (0x1F, 0x20, 0x26, 0x52), (0x1F, 0x2C, 0x25, 0x52), (0x1F, 0x20, 0x27, 0x52), (0x1F, 0x1E, 0x29, 0x52),

    (0x1F, 0x2C, 0x3B, 0x52), (0x46, 0x49, 0x24, 0x52), (0x21, 0x41, 0x45, 0x33), (0x1F, 0x2C, 0x28, 0x31),
    (0x1F, 0x0D, 0x29, 0x52), (0x1F, 0x1E, 0x27, 0x52), (0x1F, 0x20, 0x27, 0x53), (0x48, 0x49, 0x13, 0x52),
    (0x0E, 0x1E, 0x4A, 0x50), (0x1F, 0x20, 0x26, 0x53), (0x15, 0x00, 0x00, 0x00), (0x1F, 0x00, 0x2A, 0x52),
    (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00),
    (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00),
    (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00),
    (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00),
    (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x08), (0x5D, 0x49, 0x00, 0x52), (0x55, 0x49, 0x42, 0x43),
]

required_boss_sheets = {EnemySprite.ArmosKnight: 9, EnemySprite.Lanmolas: 11, EnemySprite.Moldorm: 12,
                        EnemySprite.HelmasaurKing: 21, EnemySprite.Arrghus: 20, EnemySprite.Mothula: 26,
                        EnemySprite.Blind: 32, EnemySprite.Kholdstare: 22, EnemySprite.Vitreous: 22,
                        EnemySprite.TrinexxRockHead: 23}

sheets_with_free_gfx = {
    # intended for identifying gfx slots on each sheet that are unused during general enemization
    # (ie. Catfish gfx unused when used elsewhere other than the usual Catfish screen)
    # TODO: Could also provide sprite ID/s of replaced gfx slots indicated to be used as verification
    0x0E: [0x08, 0x0C],
    0x10: [0xCC, 0xCE, 0xEC, 0xEE],
    0x11: [0xEA, 0xEC, 0xEE],
    0x12: [0x88, 0x8A, 0xAA, 0x8C, 0xAC, 0x8E, 0xAE],
    0x13: [0xA2, 0xA4],
    0x14: [0xC0, 0xC2, 0xC4, 0xE0, 0xE2],
    0x15: [0xC8, 0xEE],
    0x18: [0x86, 0x8C, 0x8E],
    0x19: [0xCE, 0xEC, 0xEE],
    0x1C: [0xA0, 0xAC, 0xAE],
    0x22: [0x8C, 0x8E, 0xAA, 0xAC, 0xAE],
    0x24: [0xAC, 0xAE],
    0x26: [0xA6, 0xA8, 0xAA, 0xAC, 0xAE],
    0x27: [0x84, 0xA4],
    0x29: [0x82, 0x84],
    0x2A: [0x80, 0x82, 0x84, 0x86, 0x88],
    0x2E: [0x80, 0x82, 0x84, 0x86, 0x88],
    0x2F: [0x2C, 0x0A, 0x0C, 0x0E, 0x2E, 0x24],
    0x36: [0xE7, 0xE9, 0xEB, 0xED, 0xC7, 0xC9, 0xCB, 0xCD],
    0x48: [0x2B, 0x2D],
    0x52: [0xE8, 0xC6, 0xC8, 0xCE, 0xEE, 0xCA, 0xCC, 0xEA],
    0x53: [0xE8, 0xEA, 0xCA, 0xCC, 0xC6, 0xC8]
}


class SpriteSheet:
    def __init__(self, id, default_sub_groups):
        self.id = id
        self.sub_groups = list(default_sub_groups)
        self.locked = [False] * 4
        self.room_set = set()

    def dungeon_id(self):
        return self.id + 0x40

    def valid_sprite(self, requirement):
        if requirement.groups and self.id not in requirement.groups:
            return False
        for idx, sub in enumerate(self.sub_groups):
            if requirement.sub_groups[idx] and sub not in requirement.sub_groups[idx]:
                return False
        return True

    def lock_sprite_in(self, sprite):
        for i, options in sprite.sub_groups.items():
            self.locked[i] = len(options) > 0

    def add_sprite_to_sheet(self, groups, rooms=None):
        for idx, g in enumerate(groups):
            if g is not None:
                self.sub_groups[idx] = g
                self.locked[idx] = True
        if rooms is not None:
            self.room_set.update(rooms)

    def write_to_rom(self, rom, base_address):
        rom.write_bytes(base_address + self.id * 4, self.sub_groups)

    def __str__(self):
        return f'{self.id} => [{", ".join([str(x) for x in self.sub_groups])}]'


# convert to dungeon id
def did(n):
    return n + 0x40


def init_sprite_sheets(requirements):
    sheets = {id: SpriteSheet(id, def_sheet) for id, def_sheet in enumerate(vanilla_sheets)}
    # wait until bosses are randomized to determine which are randomized
    for sprite, sheet_num in required_boss_sheets.items():
        sheet = sheets[did(sheet_num)]  # convert to dungeon sheet id
        boss_sprite = requirements[(sprite, 0)]
        sheet.lock_sprite_in(boss_sprite)
    return sheets


def setup_required_dungeon_groups(sheets, data_tables):

    sheets[did(1)].add_sprite_to_sheet([None, None, 28, None], {0xe4, 0xf0})  # old man
    # various npcs
    sheets[did(5)].add_sprite_to_sheet([75, 77, 74, 90], {0xf3, 0xff, 0x109, 0x10e, 0x10f, 0x110, 0x111, 0x112,
                                                          0x11a, 0x11c, 0x11f, 0x122})
    sheets[did(7)].add_sprite_to_sheet([75, 77, 57, 54], {0x8, 0x2c, 0x114, 0x115, 0x116})  # big fairies
    sheets[did(13)].add_sprite_to_sheet([81, None, None, None], {0x55, 0x102, 0x104})  # uncle, sick kid
    sheets[did(14)].add_sprite_to_sheet([71, 73, 76, 80], {0x12, 0x105, 0x10a})  # wisemen
    sheets[did(15)].add_sprite_to_sheet([79, 77, 74, 80], {0xf4, 0xf5, 0x101, 0x103, 0x106, 0x118, 0x119})  # more npcs
    sheets[did(18)].add_sprite_to_sheet([85, 61, 66, 67], {0x20, 0x30})  # aga alter, aga1
    sheets[did(24)].add_sprite_to_sheet([85, 26, 66, 67], {0xd})  # aga2
    sheets[did(34)].add_sprite_to_sheet([33, 65, 69, 51], {0})  # ganon
    sheets[did(40)].add_sprite_to_sheet([14, None, 74, 80], {0x124, 0x125, 0x126})  # fairy + rupee npcs
    sheets[did(9)].add_sprite_to_sheet([None, None, None, 29], {0xe3})  # magic bat
    sheets[did(28)].add_sprite_to_sheet([None, None, 38, 82], {0xe, 0x7e, 0x8e, 0x9e})  # freezors
    sheets[did(3)].add_sprite_to_sheet([93, None, None, None], {0x51})  # mantle
    sheets[did(42)].add_sprite_to_sheet([21, None, None, None], {0x11e})  # hype cave
    sheets[did(10)].add_sprite_to_sheet([47, None, 46, None], {0x5c, 0x75, 0xb9, 0xd9})  # cannonballs
    sheets[did(37)].add_sprite_to_sheet([None, None, 39, 82], {0x24, 0xb4, 0xb5, 0xc6, 0xc7, 0xd6})  # somaria platforms

    free_sheet_reqs = [
        ([None, 77, None, 21], [0x121]),  # smithy
        ([None, None, None, 80], [0x108]),  # chicken house
        ([14, 30, None, None], [0x123]),  # mini moldorm (shutter door)
        ([None, None, 34, None], [0x36, 0x46, 0x66]),  # pirogusu spawners
        ([None, 32, None, None], [0x9f]),  # babasu spawners
        ([31, None, None, None], [0x7f]),  # force baris
        ([None, None, 35, None], [0x39, 0x49]),  # wallmasters
        # bumpers - why the split - because of other requirements -
        ([None, None, None, (82, 83)], [0x17, 0x2a, 0x4c, 0x59, 0x67, 0x7e, 0x8b, 0xeb, 0xfb]),
        # crystal switches - split for some reason
        ([None, None, None, (82, 83)], [0xb, 0x13, 0x1b, 0x1e, 0x2a, 0x2b, 0x31, 0x5b, 0x6b, 0x77, 0x8b,
                                        0x91, 0x92, 0x9b, 0x9d, 0xa1, 0xab, 0xbf, 0xc4, 0xef]),
        # laser eyes - split for some reason
        ([None, None, None, (82, 83)], [0x13, 0x23, 0x96, 0xa5, 0xc5, 0xd5]),
        # statues - split for some reason
        ([None, None, None, (82, 83)], [0x26, 0x2b, 0x40, 0x4a, 0x6b, 0x7b]),
        ([None, None, None, 83], [0x43, 0x63, 0x87]),  # tile rooms

        # non-optional
        ([None, None, None, 82], [0x58, 0x8c, 0x10b]),  # pull switches
        ([None, None, (28, 36), 82], [0x2, 0x64]),  # pull switches (snakes)
        ([None, None, None, 82], [0x1a, 0x3d, 0x44, 0x5e, 0x7c, 0x95, 0xc3]),  # collapsing bridges
        ([None, None, None, 83], [0x3f, 0xce]),  # pull tongue
        ([None, None, None, 83], [0x35, 0x37]),  # swamp drains
        ([None, None, 34, None], [0x28]),  # tektike forced? - spawn chest
        ([None, None, 37, None], [0x97]),  # wizzrobe spawner - in middle of room...

        # combined
        ([None, 32, None, (82, 83)], [0x3e]),  # babasu spawners + crystal switch
        ([None, 32, None, 83], [0x4]),  # zoro spawners + crystal switch + pull switch
        ([None, None, 35, 82], [0x56]),  # wallmaster + collasping bridge
        ([None, None, 35, (82, 83)], [0x57, 0x68]),  # wallmaster + statue and wallmaster + bumpers
        ([None, None, 34, 83], [0x76]),  # swamp drain + pirogusu spawners
        ([None, None, 35, 83], [0x8d]),  # wallmaster + tile room
        ([None, None, None, 83], [0xb6, 0xc1]),  # tile room + crystal switch

        # allow some sprites / increase odds:
        ([72, 73, None, None], []),  # allow for blue archer + greenbush
        ([None, 73, 19, None], []),  # allow for green knife guard
        ([None, None, 12, 68], []),  # increase odds for zora
        ([22, None, 23, None], []),  # increase odds for snapdragon
    ]

    data_tables.room_requirements = {}
    # find home for the free_sheet_reqs
    for pair in free_sheet_reqs:
        groups, room_list = pair
        for room in room_list:
            data_tables.room_requirements[room] = groups
        find_matching_sheet(groups, sheets, range(65, 124), room_list)


# RandomizeRooms(optionFlags);
    # roomCollection.LoadRooms()
    # roomCollection.RandomizeRoomSpriteGroups(spriteGroupCollection, optionFlags);
    # more stuff
sub_group_choices = {
    0: [22, 31, 47, 14],
    1: [44, 30, 32],  # 73, 13
    2: [12, 18, 23, 24, 28, 46, 34, 35, 39, 40, 38, 41, 36, 37, 42],
    3: [17, 16, 27, 20, 82, 83, 25]  # 25 for Swamola
}
# 70, 72 for guards
# 0: 72 specifically for BlueArcher/GreenBush (but needs combination)
# 0: 70 for guards but needs combination
# 2: 19 for green knife guard, but needs combination
# 3: 68 for Zora, but needs combination


def combine_req(sub_groups, requirement):
    for i in range(0, 4):
        if requirement.sub_groups[i]:
            if len(sub_groups[i]) == 0:
                sub_groups[i].update(requirement.sub_groups[i])
            else:
                if len(sub_groups[i].intersection(requirement.sub_groups[i])) == 0:
                    raise IncompatibleEnemyException
                sub_groups[i].intersection_update(requirement.sub_groups[i])


def setup_custom_enemy_sheets(custom_enemies, sheets, data_tables, sheet_range, uw=True):
    requirements = data_tables.sprite_requirements
    for room_id, enemy_map in custom_enemies.items():
        if uw:
            original_list = data_tables.uw_enemy_table.room_map[room_id]
        else:
            original_list = data_tables.ow_enemy_table[room_id]
        sub_groups_choices = [set(), set(), set(), set()]
        for idx, sprite in enumerate(original_list):
            if idx in enemy_map:
                key = (sprite_translation[enemy_map[idx]], 0)
                if key not in requirements:
                    continue
                req = requirements[key]
                try:
                    combine_req(sub_groups_choices, req)
                except IncompatibleEnemyException:
                    logging.getLogger('').warning(f'Incompatible enemy: {hex(room_id)}:{idx} {enemy_map[idx]}')
            else:
                sprite_secondary = 0 if sprite.sub_type != SpriteType.Overlord else sprite.sub_type
                key = (sprite.kind, sprite_secondary)
                if key not in requirements:
                    continue
                req = requirements[key]
                if isinstance(req, dict) and room_id in req:
                    req = req[room_id]
                else:
                    req = None
                if req and (req.static or not req.can_randomize):
                    try:
                        combine_req(sub_groups_choices, req)
                    except IncompatibleEnemyException:
                        raise IncompatibleEnemyException(f'Incompatible enemy: {hex(room_id)}:{idx} {str(req)}')
        sheet_req = [None if not x else tuple(x) for x in sub_groups_choices]
        find_matching_sheet(sheet_req, sheets, sheet_range, [room_id], True)


def randomize_underworld_sprite_sheets(sheets, data_tables, custom_enemies):
    setup_required_dungeon_groups(sheets, data_tables)

    setup_custom_enemy_sheets(custom_enemies, sheets, data_tables, range(65, 124), True)

    for num in range(65, 124):  # sheets 0x41 to 0x7B inclusive
        sheet = sheets[num]
        # if not sheet.locked[1] and num in [65, 66, 67, 68]:  # guard stuff, kind of
        #     sheet.locked[1] = True
        #     sheet.sub_groups[1] = random.choice([13, 73])

        free_slots = [idx for idx in range(0, 4) if not sheet.locked[idx]]
        while free_slots:
            choices = [c for c in data_tables.sheet_choices if all(slot in free_slots for slot in c.slots)]
            weights = [c.weight for c in choices]
            choice = random.choices(choices, weights, k=1)[0]
            for idx, val in choice.assignments.items():
                v = random.choice(val) if isinstance(val, list) else val
                sheet.sub_groups[idx] = v
                sheet.locked[idx] = True
            free_slots = [idx for idx in range(0, 4) if not sheet.locked[idx]]


def setup_required_overworld_groups(sheets):
    sheets[7].add_sprite_to_sheet([None, None, 74, None], {0x2})  # lumberjacks
    sheets[16].add_sprite_to_sheet([None, None, 18, 16], {0x3, 0x93})  # WDM (pre/post-Aga)
    sheets[7].add_sprite_to_sheet([None, None, None, 17], {0xA, 0x9A})  # DM Foothills? (pre/post-Aga)
    sheets[4].add_sprite_to_sheet([None, None, None, None], {0xF, 0x9F})  # Waterfall of wishing (pre/post-Aga)
    sheets[3].add_sprite_to_sheet([None, None, None, 14], {0x14, 0xA4})  # Graveyard (pre/post-Aga)
    sheets[1].add_sprite_to_sheet([None, None, 76, 0x3F], {0x1B, 0xAB})  # Hyrule Castle (pre/post-Aga)
    ## group 0 set to 0x48 for tutortial guards
    ## group 1 & 2 set for green knife guards (and probably normal green guard)
    ## group 3 set for lightning gate
    sheets[2].add_sprite_to_sheet([0x48, 0x49, 0x13, 0x3F], {})  # Hyrule Castle - rain state

    # Smithy/Race/Kak (pre/post-Aga)
    sheets[6].add_sprite_to_sheet([0x4F, 0x49, 0x4A, 0x50], {0x18, 0x22, 0x28, 0xA8, 0xB2, 0xB8})
    sheets[8].add_sprite_to_sheet([None, None, 18, None], {0x30, 0xC0})  # Desert (pre/post-Aga)
    sheets[10].add_sprite_to_sheet([None, None, None, 17], {0x3A, 0xCA})  # M-rock (pre/post-Aga)
    sheets[22].add_sprite_to_sheet([None, None, 24, None], {0x4F})  # Catfish
    sheets[21].add_sprite_to_sheet([None, None, None, 21], {0x62, 0x69})  # Smith DW/VoO South
    sheets[27].add_sprite_to_sheet([None, 42, None, None], {0x68})  # Dig Game
    sheets[13].add_sprite_to_sheet([None, None, 76, None], {0x16, 0xA6})  # Witch hut (pre/post-Aga)
    #sheets[29].add_sprite_to_sheet([None, 77, None, 21], {0x69})  # VoO South
    sheets[15].add_sprite_to_sheet([None, None, 78, None], {0x2A, 0xBA})  # Haunted Grove (pre/post-Aga)
    sheets[17].add_sprite_to_sheet([None, None, None, 76], {0x6A})  # Stumpy
    sheets[12].add_sprite_to_sheet([None, None, 55, 54], {0x80})  # Specials
    sheets[14].add_sprite_to_sheet([None, None, 12, 68], {0x81})  # Zora's Domain
    sheets[26].add_sprite_to_sheet([15, None, None, None], {0x92})  # Lumberjacks post-Aga
    sheets[23].add_sprite_to_sheet([None, None, None, 25], {0x5E})  # PoD
    sheets[19].add_sprite_to_sheet([None, 26, None, None], {0x5B})  # Pyramid post-Aga2 bat crash

    free_sheet_reqs = [
        [None, None, None, 0x14],  # bully+pink ball needs this
        [72, 73, None, None],  # allow for blue archer + green bush
        [None, 73, 19, None],  # allow for green knife guard
        [22, None, 23, None],  # increase odds for snapdragon
        [70, 73, None, None],  # guards group (ballnchain, redbush, redjav, cannon, bomb, bluesain
        [None, None, None, 0x15],  # an option for talking trees
        [None, None, None, 0x1B],  # an option for talking trees
    ]

    for group in free_sheet_reqs:
        find_matching_sheet(group, sheets, range(1, 64))


class NoMatchingSheetException(Exception):
    pass


class IncompatibleEnemyException(Exception):
    pass


def find_matching_sheet(groups, sheets, search_sheets, room_list=None, lock_match=False):
    possible_sheets = []
    found_match = False
    for num in search_sheets:
        if num in {6, 65, 69, 71, 78, 79, 82, 88, 98}:  # these are not useful sheets for randomization
            continue
        sheet = sheets[num]
        valid = True
        match = True
        for idx, value in enumerate(groups):
            if value is not None and sheet.locked[idx]:
                valid = False
                if (sheet.sub_groups[idx] not in value if isinstance(value, tuple)
                   else value != sheet.sub_groups[idx]):
                    match = False
            elif value is not None:
                match = False
        if match:
            found_match = True
            if lock_match and room_list is not None:
                sheet.room_set.update(room_list)
            break
        if valid:
            possible_sheets.append(sheet)
    if not found_match:
        if len(possible_sheets) == 0:
            raise NoMatchingSheetException
        chosen_sheet = random.choice(possible_sheets)
        chosen_groups = [(random.choice(g) if isinstance(g, tuple) else g) for g in groups]
        chosen_sheet.add_sprite_to_sheet(chosen_groups, room_list)


def randomize_overworld_sprite_sheets(sheets, data_tables, custom_enemies):
    setup_required_overworld_groups(sheets)

    setup_custom_enemy_sheets(custom_enemies, sheets, data_tables, range(1, 64), False)

    for num in range(1, 64):  # sheets 0x1 to 0x3F inclusive
        sheet = sheets[num]
        if num == 6:  # skip this group - it is locked for kakariko
            continue

        free_slots = [idx for idx in range(0, 4) if not sheet.locked[idx]]
        while free_slots:
            choices = [c for c in data_tables.sheet_choices if all(slot in free_slots for slot in c.slots)]
            weights = [c.weight for c in choices]
            choice = random.choices(choices, weights, k=1)[0]
            for idx, val in choice.assignments.items():
                v = random.choice(val) if isinstance(val, list) else val
                sheet.sub_groups[idx] = v
                sheet.locked[idx] = True
            free_slots = [idx for idx in range(0, 4) if not sheet.locked[idx]]


class SheetChoice:

    def __init__(self, slots, assignments, weight):
        self.slots = slots
        self.assignments = assignments
        self.weight = weight
