from pathlib import Path
import random
from collections import OrderedDict, defaultdict
from pprint import pprint

import yaml
import json
from io import BytesIO
from enum import IntEnum
from typing import Optional
import re

import nlzss11
from sslib import AllPatcher, U8File
from sslib.msb import process_control_sequences
from sslib.utils import write_bytes_create_dirs, encodeBytes
from sslib.fs_helpers import write_str, write_u16, write_float
from sslib.dol import DOL
from sslib.rel import REL
from paths import RANDO_ROOT_PATH
from tboxSubtypes import tboxSubtypes

from logic.logic import Logic

from asm.patcher import apply_dol_patch, apply_rel_patch

TOTAL_STAGE_FILES = 369
TOTAL_EVENT_FILES = 6

# arc cache, main.dol, rels, objectpack
GAMEPATCH_TOTAL_STEP_COUNT = TOTAL_EVENT_FILES + TOTAL_STAGE_FILES + 4

DEFAULT_SOBJ = OrderedDict(
    params1 = 0,
    params2 = 0,
    posx = 0,
    posy = 0,
    posz = 0,
    sizex = 0,
    sizey = 0,
    sizez = 0,
    anglex = 0,
    angley = 0,
    anglez = 0,
    id = 0,
    name = "",
)

DEFAULT_OBJ = OrderedDict(
    params1 = 0,
    params2 = 0,
    posx = 0,
    posy = 0,
    posz = 0,
    anglex = 0,
    angley = 0,
    anglez = 0,
    id = 0,
    name = "",
)

DEFAULT_SCEN = OrderedDict(
    name = "",
    room = 0,
    layer = 0,
    entrance = 0,
    byte4 = 0,
    byte5 = 0,
    flag6 = 0,
    zero = 0,
    flag8 = 0
)

DEFAULT_AREA = OrderedDict(
    posx = 0,
    posy  = 0,
    posz = 0,
    sizex = 0,
    sizey = 0,
    sizez = 0,
    angle = 0,
    area_link = -1,
    unk3 = 0,
    dummy = b'\xFF\xFF\xFF',
)

# cutscenes to use to set storyflags, sceneflags and itemflags
START_CUTSCENES = [
    # stage, room, eventindex
    ('F000',0,22),
    ('F000',0,23),
    ('F001r',1,2),
    ('F405',0,0),
]

START_ITEM_STORYFLAGS = {
    "Emerald Tablet": 46,
    "Ruby Tablet": 47,
    "Amber Tablet": 48,
}

# The stage name of each dungeon
DUNGEON_STAGES = {
    'Skyview': 'D100',
    'Ancient Cistern': 'D101',
    'Earth Temple': 'D200',
    'Fire Sanctuary': 'D201',
    'Lanayru Mining Facility': 'D300',
    'Sandship': 'D301',
    'Skykeep': 'D003_7'
}

# The stage for each map where there are dungeon entrances
DUNGEON_ENTRANCE_STAGES = {
    # stage, room, scen
    "Dungeon Entrance In Deep Woods": ("F101", 0, 1),
    "Dungeon Entrance In Lake Floria": ('F102_1', 0, 1),
    "Dungeon Entrance In Eldin Volcano": ("F200", 4, 0),
    "Dungeon Entrance In Volcano Summit": ('F201_3', 0, 1),
    "Dungeon Entrance In Lanayru Desert": ('F300', 0, 5),
    "Dungeon Entrance In Sand Sea": ('F301_1', 0, 1),
    "Dungeon Entrance On Skyloft": ('F000', 0, 48),
}

DUNGEON_EXITS = {
    # stage, layer, room, entrance
    "Dungeon Entrance In Deep Woods": ('F101', 0, 0, 1),
    "Dungeon Entrance In Lake Floria": ('F102_1', 0, 0, 1),
    "Dungeon Entrance In Eldin Volcano": ('F200', 0, 4, 1),
    "Dungeon Entrance In Volcano Summit": ('F201_3', 0, 0, 1),
    "Dungeon Entrance In Lanayru Desert": ('F300', 0, 0, 5),
    "Dungeon Entrance In Sand Sea": ('F301_1', 0, 0, 4),
    "Dungeon Entrance On Skyloft": ('F000', 0, 0, 53),
}

DUNGEON_FINISH_EXITS = {
    # stage, layer, room, entrance
    "Dungeon Entrance In Deep Woods": ('F101', 0, 0, 1),
    "Dungeon Entrance In Lake Floria": ('F102_1', 0, 0, 6),
    "Dungeon Entrance In Eldin Volcano": ('F200', 0, 4, 1),
    "Dungeon Entrance In Volcano Summit": ('F201_3', 0, 0, 1),
    "Dungeon Entrance In Lanayru Desert": ('F300_4', 2, 0, 2),
    "Dungeon Entrance In Sand Sea": ('F301', 0, 0, 3),
    "Dungeon Entrance On Skyloft": ('F000', 0, 0, 52),
}

DUNGEON_ENTRANCES = {
    # stage, layer, room, entrance
    'Skyview': ('D100', 0, 0, 0),
    'Earth Temple': ('D200', 0, 1, 0),
    'Lanayru Mining Facility': ('D300', 0, 0, 0),
    'Ancient Cistern': ('D101', 0, 0, 0),
    'Sandship': ('D301', 1, 0, 0),
    'Fire Sanctuary': ('D201', 0, 0, 0),
    'Skykeep': ('D003_7', 0, 0, 4),
}

DUNGEON_FINISH_EXIT_SCEN = {
    # stage, room, index
    'Skyview': ('B100_1', 0, 1),
    'Earth Temple': ('B210', 0, 0),
    'Lanayru Mining Facility': ('F300_4', 0, 3),
    'Ancient Cistern': ('B101_1', 0, 3),
    'Sandship': ('B301', 0, 4),
    'Fire Sanctuary': ('B201_1', 0, 2),
    'Skykeep': ('F407', 0, 1),
}

DUNGEON_EXIT_SCENS = {
    # stage, room, index
    'Skyview': [('D100', 0, 0), ('D100', 0, 2), ('D100', 2, 0), ('D100', 5, 0), ('D100', 9, 0),
                ('B100_1', 0, 4)],
    'Earth Temple': [('D200', 1, 0), ('D200', 1, 1), ('D200', 2, 0), ('D200', 4, 2)],
    'Lanayru Mining Facility': [('D300', 0, 0), ('D300', 0, 1), ('D300', 5, 4),
                                ('D300_1', 0, 0), ('D300_1', 0, 1), ('D300_1', 5, 4),
                                ('B300', 0, 1),
                                ('F300_5', 0, 3), # extra bird statue
                                ],
    'Ancient Cistern': [('D101', 0, 2), ('D101', 0, 3), ('D101', 3, 1), ('D101', 4, 2), ('D101', 5, 0),
                        # ('B101_1', 0, 1)
                        ],
    'Sandship': [('D301', 0, 0), ('D301', 0, 1), ('D301', 1, 2), ('D301', 2, 0), ('D301', 6, 0), ('D301', 9, 1),
                 ('D301', 12, 0), ('D301', 13, 0),
                 ('B301', 0, 1)],
    'Fire Sanctuary': [('D201', 0, 1), ('D201', 3, 2), ('D201', 10, 2),
                       ('D201_1', 1, 0) , ('D201_1', 7, 0), ('D201_1', 5, 3), ('D201_1', 6, 2)],
    'Skykeep': [('D003_0', 0, 3), # most of them not needed
                ('D003_1', 0, 2),
                ('D003_2', 0, 3),
                ('D003_3', 0, 3),
                ('D003_4', 0, 2),
                ('D003_5', 0, 2),
                ('D003_6', 0, 2),
                ('D003_7', 0, 2),
                ('D003_0', 0, 1), # most of them not needed
                ('D003_1', 0, 1),
                ('D003_2', 0, 1),
                ('D003_3', 0, 1),
                ('D003_4', 0, 1),
                ('D003_5', 0, 1),
                ('D003_6', 0, 1),
                ('D003_7', 0, 1),
                ],
}

#fixes for entrance rando, ET, SSH and SK don't need to set storyflags at all
#for LMF it's the ToT layer change
POST_DUNGEON_STORYFLAGS = {
    "Dungeon Entrance In Deep Woods":  5,
    "Dungeon Entrance In Lake Floria": 900,
    "Dungeon Entrance In Eldin Volcano": -1,
    "Dungeon Entrance In Volcano Summit": 901,
    "Dungeon Entrance In Lanayru Desert": 914,
    "Dungeon Entrance In Sand Sea": -1,
    "Dungeon Entrance On Skyloft": -1,
}

POST_DUNGEON_CUTSCENE = {
    # (stage, room, EVNT)
    'Skyview': ('B100_1', 0, 1),
    'Earth Temple': ('B210',   0, 2),
    'Lanayru Mining Facility': ('F300_4', 0, 20),
    'Ancient Cistern': ('B101_1', 0, 1),
    'Sandship': ('B301', 0, 1),
    'Fire Sanctuary': ('B201_1', 0, 0),
    'Skykeep': ('F407', 0, 2),
}

BEEDLE_TEXT_PATCHES = {  # (undiscounted, discounted, normal price, discounted price)
    'Skyloft - Beedle 50 Rupee Item': (25, 26, 50, 25),
    'Skyloft - Beedle First 100 Rupee Item': (23, 24, 100, 50),
    'Skyloft - Beedle Second 100 Rupee Item': ("Second 100R undiscounted Text", "Second 100R discounted Text", 100, 50),
    'Skyloft - Beedle Third 100 Rupee Item': ("Third 100R undiscounted Text", "Third 100R discounted Text", 100, 50),
    'Skyloft - Beedle 300 Rupee Item': (19, 20, 300, 150),
    'Skyloft - Beedle 600 Rupee Item': (29, 30, 600, 300),
    'Skyloft - Beedle 800 Rupee Item': (27, 28, 800, 400),
    'Skyloft - Beedle 1000 Rupee Item': (33, 34, 1000, 500),
    'Skyloft - Beedle 1200 Rupee Item': (31, 32, 1200, 600),
    'Skyloft - Beedle 1600 Rupee Item': (21, 22, 1600, 800),
}

BEEDLE_BUY_SWTICH = '[1]I\'ll buy it![2-]No, thanks.'

PROGRESSIVE_SWORD_STORYFLAGS = [906, 907, 908, 909, 910, 911]
PROGRESSIVE_SWORD_ITEMIDS = [10, 11, 12, 9, 13, 14]

class FlagEventTypes(IntEnum):
    SET_STORYFLAG = 0,
    UNSET_STORYFLAG = 1,
    SET_SCENEFLAG = 2,
    UNSET_SCENEFLAG = 3,
    SET_ZONEFLAG = 4,
    UNSET_ZONEFLAG = 5,
    SET_TEMPFLAG = 28,
    UNSET_TEMPFLAG = 29,

class FlagSwitchTypes(IntEnum):
    CHOICE = 0,
    STORYFLAG = 3,
    ZONEFLAG = 5,
    SCENEFLAG = 6,
    TEMPFLAG = 9,

def entrypoint_hash(name: str, entries: int) -> int:
    hash = 0
    for char in name:
        hash = (hash * 0x492 + ord(char)) & 0xFFFFFFFF
    return hash % entries

def make_switch(subtype: FlagSwitchTypes, arg: int):
    if subtype == FlagSwitchTypes.CHOICE:
        p2 = 0
        p3 = arg # number of choices
    else:
        p2 = arg
        p3 = subtype.value
    return OrderedDict(
        type = "switch",
        subType = 6,
        param1 = 0,
        param2 = p2,
        next = -1,
        param3 = p3,
        param4 = -1,
        param5 = -1,
    )

def make_give_item_event(item):
    return OrderedDict(
        type = "type3",
        subType = 0,
        param1 = 0,
        param2 = item,
        next = -1,
        param3 = 9,
        param4 = 0,
        param5 = 0,
    )

def make_flag_event(subtype: FlagEventTypes, flag):
    if subtype == FlagEventTypes.SET_STORYFLAG or subtype == FlagEventTypes.UNSET_STORYFLAG:
        st = 0
        p1 = 0
        p2 = flag
    else:
        st = 1
        p1 = flag
        p2 = 0
    return OrderedDict(
        type = "type3",
        subType = st,
        param1 = p1,
        param2 = p2,
        next = -1,
        param3 = subtype.value,
        param4 = 0,
        param5 = 0,
    )

def add_msbf_branch(msbf, switch, branchpoints):
    branch_index = len(msbf['FLW3']['branch_points'])
    msbf['FLW3']['branch_points'].extend(branchpoints)
    switch['param4'] = len(branchpoints)
    switch['param5'] = branch_index
    msbf['FLW3']['flow'].append(switch)

def make_progressive_item(msbf, base_item_start, item_text_indexes, item_ids, storyflags):
    if len(item_text_indexes) != len(storyflags) or len(item_text_indexes) != len(item_ids):
        raise Exception("item_text_indexes should be the same length as storyflags!")
    flow_idx = len(msbf['FLW3']['flow'])
    msbf['FLW3']['flow'][base_item_start]['next'] = flow_idx
    index = len(item_text_indexes) - 1 # start from the highest upgrade
    # first, check if the storyflag of the previous upgrade is set
    # if yes, set the storyflag for this upgrade, give the upgrade and jump to that upgrade's text
    # otherwise check the next upgrade storyflag. If no storyflag is set, set the lowest upgrades storyflag
    # but no need to give that item since it's that items event that is hijacked
    for index in range(len(item_text_indexes)-1, 0, -1):
        branch = make_switch(FlagSwitchTypes.STORYFLAG, storyflags[index-1])
        add_msbf_branch(msbf, branch, [flow_idx+1, flow_idx+3])
        event = make_give_item_event(item_ids[index])
        event['next'] = flow_idx + 2
        msbf['FLW3']['flow'].append(event)
        event = make_flag_event(FlagEventTypes.SET_STORYFLAG, storyflags[index])
        event['next'] = item_text_indexes[index]
        msbf['FLW3']['flow'].append(event)
        flow_idx += 3
    event = make_flag_event(FlagEventTypes.SET_STORYFLAG, storyflags[0])
    event['next'] = item_text_indexes[0]
    msbf['FLW3']['flow'].append(event)

# check highest
def highest_objid(bzs):
    max_id = 0
    for layer in bzs.get('LAY ',{}).values():
        if len(layer) == 0:
            continue
        for objtype in ['OBJS','OBJ ','SOBS','SOBJ','STAS','STAG','SNDT','DOOR']:
            if objtype in layer:
                id = layer[objtype][-1]['id'] & 0x3FF
                if id != 0x3FF: # aparently some objects have the max id?
                    max_id = max(max_id, id)
    return max_id

def mask_shift_set(value, mask, shift, new_value):
    """
    Replace new_value in value, by applying the mask after the shift
    """
    new_value = new_value & mask
    return (value & ~(mask << shift)) | (new_value << shift)

def try_patch_obj(obj, key, value):
    if obj['name'].startswith('Npc'):
        if key == 'trigstoryfid':
            obj['params1'] = mask_shift_set(obj['params1'], 0x7FF, 10, value)
        elif key == 'untrigstoryfid':
            obj['params1'] = mask_shift_set(obj['params1'], 0x7FF, 21, value)
        elif key == 'talk_behaviour':
            obj['anglez'] = value
        elif obj['name'] == 'NpcTke':
            if key == 'trigscenefid':
                obj['anglex'] = mask_shift_set(obj['anglex'], 0xFF, 0, value)
            elif key == 'untrigscenefid':
                obj['anglex'] = mask_shift_set(obj['anglex'], 0xFF, 8, value)
            elif key == 'subtype':
                obj['params1'] = mask_shift_set(obj['params1'], 0xFF, 0, value)
            else:
                print(f'ERROR: unsupported key "{key}" to patch for object {obj}')
        else:
            print(f'ERROR: unsupported key "{key}" to patch for object {obj}')
    elif obj['name'] == 'TBox':
        if key == 'spawnscenefid':
            obj['params1'] = mask_shift_set(obj['params1'], 0xFF, 20, value)
        elif key == 'setscenefid':
            obj['anglex'] = mask_shift_set(obj['anglex'], 0xFF, 0, value)
        elif key == 'itemid':
            obj['anglez'] = mask_shift_set(obj['anglez'], 0x1FF, 0, value)
        else:
            print(f'ERROR: unsupported key "{key}" to patch for object {obj}')
    elif obj['name'] == 'EvntTag':
        if key == 'trigscenefid':
            obj['params1'] = mask_shift_set(obj['params1'], 0xFF, 16, value)
        elif key == 'setscenefid':
            obj['params1'] = mask_shift_set(obj['params1'], 0xFF, 8, value)
        elif key == 'event':
            obj['params1'] = mask_shift_set(obj['params1'], 0xFF, 0, value)
        else:
            print(f'ERROR: unsupported key "{key}" to patch for object {obj}')
    elif obj['name'] == 'EvfTag':
        if key == 'trigstoryfid':
            obj['params1'] = mask_shift_set(obj['params1'], 0x7FF, 19, value)
        elif key == 'setstoryfid':
            obj['params1'] = mask_shift_set(obj['params1'], 0x7FF, 8, value)
        elif key == 'event':
            obj['params1'] = mask_shift_set(obj['params1'], 0xFF, 0, value)
        else:
            print(f'ERROR: unsupported key "{key}" to patch for object {obj}')
    elif obj['name'] == 'ScChang':
        if key == 'trigstoryfid':
            obj['anglex'] = mask_shift_set(obj['anglex'], 0x7FF, 0, value)
        elif key == 'untrigstoryfid':
            obj['anglez'] = mask_shift_set(obj['anglez'], 0x7FF, 0, value)
        elif key == 'scen_link':
            obj['params1'] = mask_shift_set(obj['params1'], 0xFF, 0, value)
        elif key == 'trigscenefid':
            obj['params1'] = mask_shift_set(obj['params1'], 0xFF, 24, value)
        else:
            print(f'ERROR: unsupported key "{key}" to patch for object {obj}')
    elif obj['name'] == 'SwAreaT':
        if key == 'setstoryfid':
            obj['anglex'] = mask_shift_set(obj['anglex'], 0x7FF, 0, value)
        elif key == 'unsetstoryfid':
            obj['anglez'] = mask_shift_set(obj['anglez'], 0x7FF, 0, value)
        elif key == 'setscenefid':
            obj['params1'] = mask_shift_set(obj['params1'], 0xFF, 0, value)
        elif key == 'unsetscenefid':
            obj['params1'] = mask_shift_set(obj['params1'], 0xFF, 8, value)
        else:
            print(f'ERROR: unsupported key "{key}" to patch for object {obj}')
    else:
        print(f'ERROR: unsupported object to patch {obj}')

def patch_tbox_item(tbox: OrderedDict, itemid: int):
    origitemid = tbox['anglez'] & 0x1FF
    boxtype = tboxSubtypes[origitemid]
    tbox['anglez'] = mask_shift_set(tbox['anglez'], 0x1FF, 0, itemid)
    # code has been patched, to interpret this part of params1 as boxtype
    tbox['params1'] = mask_shift_set(tbox['params1'], 0x3, 4, boxtype)

def patch_item_item(itemobj: OrderedDict, itemid: int):
    itemobj['params1'] = mask_shift_set(itemobj['params1'], 0xFF, 0, itemid)
    # subtype 9, this acts like hearpieces and force being collected with a textbox
    itemobj['params1'] = mask_shift_set(itemobj['params1'], 0xF, 0x14, 9)

# these are not treasure chests, but instead only used for the hp in zeldas room
def patch_chest_item(chest: OrderedDict, itemid: int):
    chest['params1'] = mask_shift_set(chest['params1'], 0xFF, 8, itemid)

# code has been patched to use this part of params1 as itemid
def patch_heart_co(heart_co: OrderedDict, itemid: int):
    heart_co['params1'] = mask_shift_set(heart_co['params1'], 0xFF, 16, itemid)

# code has been patched to use this part of params1 as itemid
def patch_chandelier_item(chandel: OrderedDict, itemid: int):
    chandel['params1'] = mask_shift_set(chandel['params1'], 0xFF, 8, itemid)

def patch_soil_item(soil: OrderedDict, itemid: int):
    # match key piece soils in all ways but keep sceneflag
    soil['params1'] = (soil['params1'] & 0xFF0) | 0xFF0B1004
    # code has been patched to use the first byte of params2 as itemid, but only
    # if it would have been a key piece otherwise
    soil['params2'] = mask_shift_set(soil['params2'], 0xFF, 0x18, itemid)

def patch_trial_item(trial: OrderedDict, itemid: int):
    trial['params1'] = mask_shift_set(trial['params1'], 0xFF, 0x18, itemid)

def patch_key_bokoblin_item(boko: OrderedDict, itemid: int):
    boko['params2'] = mask_shift_set(boko['params2'], 0xFF, 0x0, itemid)

# not treasure chest, wardrobes you can open, used for zelda room HP
def rando_patch_chest(bzs: OrderedDict, itemid: int, id: str):
    id = int(id)
    chest = next(filter(lambda x: x['name'] == 'chest' and (x['params1'] & 0xFF) == id, bzs['OBJ ']))
    patch_chest_item(chest, itemid)

def rando_patch_heartco(bzs: OrderedDict, itemid: int, id: str):
    obj = next(filter(lambda x: x['name'] == 'HeartCo', bzs['OBJ '])) # there is only one heart container at a time
    patch_heart_co(obj, itemid)

def rando_patch_warpobj(bzs: OrderedDict, itemid: int, id: str):
    obj = next(filter(lambda x: x['name'] == 'WarpObj', bzs['OBJ '])) # there is only one trial exit at a time
    patch_trial_item(obj, itemid)

def rando_patch_tbox(bzs: OrderedDict, itemid: int, id: str):
    id = int(id)
    tboxs = list(filter(lambda x: x['name'] == 'TBox' and (x['anglez']>>9) == id, bzs['OBJS']))
    if len(tboxs) == 0:
        print(tboxs)
    obj = tboxs[0] # anglez >> 9 is chest id
    patch_tbox_item(obj, itemid)

def rando_patch_item(bzs: OrderedDict, itemid: int, id: str):
    id = int(id)
    obj = next(filter(lambda x: x['name'] == 'Item' and ((x['params1'] >> 10) & 0xFF) == id, bzs['OBJ '])) # (params1 >> 10) & 0xFF is sceneflag
    patch_item_item(obj, itemid)

def rando_patch_chandelier(bzs: OrderedDict, itemid: int, id: str):
    obj = next(filter(lambda x: x['name'] == 'Chandel', bzs['OBJ ']))
    patch_chandelier_item(obj, itemid)

def rando_patch_soil(bzs: OrderedDict, itemid: int, id: str):
    id = int(id)
    obj = next(filter(lambda x: x['name'] == 'Soil' and ((x['params1'] >> 4) & 0xFF) == id, bzs['OBJ '])) # (params1 >> 4) & 0xFF is sceneflag
    patch_soil_item(obj, itemid)

def rando_patch_bokoblin(bzs: OrderedDict, itemid: int, id: str):
    id = int(id, 0)
    obj = next(filter(lambda x: x['name'] == 'EBc' and x['id']== id, bzs['OBJ ']))
    patch_key_bokoblin_item(obj, itemid)

def rando_patch_goddess_crest(bzs: OrderedDict, itemid: int, index: str):
    obj = next(filter(lambda x: x['name'] == 'SwSB', bzs['OBJ ']))
    # we need to patch 3 item ids into this object:
    # 1 is params1 FF 00 00 00, 2 is params1 00 FF 00 00
    # 3 is params2 FF 00 00 00
    if index == "0":
        obj['params1'] = mask_shift_set(obj['params1'], 0xFF, 0x18, itemid)
    elif index == "1":
        obj['params1'] = mask_shift_set(obj['params1'], 0xFF, 0x10, itemid)
    elif index == "2":
        obj['params2'] = mask_shift_set(obj['params2'], 0xFF, 0x18, itemid)

# functions, that patch the object, they take: the bzs of that layer, the item id and optionally an id, then patches the object in place
RANDO_PATCH_FUNCS = {
    'chest': rando_patch_chest,
    'HeartCo': rando_patch_heartco,
    'WarpObj': rando_patch_warpobj,
    'TBox': rando_patch_tbox,
    'Item': rando_patch_item,
    'Chandel': rando_patch_chandelier,
    'Soil': rando_patch_soil,
    'EBc': rando_patch_bokoblin,
    'Tbox': rando_patch_tbox,
    'SwSB': rando_patch_goddess_crest,
}

def get_patches_from_location_item_list(all_checks, filled_checks):
    with (RANDO_ROOT_PATH / 'items.yaml').open() as f:
        items = yaml.safe_load(f)
    by_item_name=dict((x['name'],x) for x in items)

    # make sure dungeon items exist
    DUNGEONS = ['SW', 'ET', 'LMF', 'AC', 'SS', 'FS', 'SK', 'LanayruCaves'] # caves has a key, no spaces because the randomizer splits by spaces
    for dungeon in DUNGEONS:
        by_item_name[f'{dungeon} Small Key'] = by_item_name['Small Key']
        by_item_name[f'{dungeon} Map'] = by_item_name['Map']
    # (stage, room) -> (object name, layer, id?, itemid)
    stagepatchv2 = defaultdict(list)
    # (stage, layer) -> oarc
    stageoarcs = defaultdict(set)
    # # eventfile: (line, itemid)
    eventpatches = defaultdict(list)
    # shopindex: (itemid, arcname, modelname)
    shoppatches = {}

    stage_re = re.compile(r'stage/(?P<stage>[^/]+)/r(?P<room>[0-9]+)/l(?P<layer>[0-9]+)/(?P<objname>[a-zA-Z]+)(/(?P<objid>[^/]+))?')
    event_re = re.compile(r'event/(?P<eventfile>[^/]+)/(?P<eventid>[^/]+)')
    oarc_re = re.compile(r'oarc/(?P<stage>[^/]+)/l(?P<layer>[^/]+)')
    shop_smpl_re = re.compile(r'ShpSmpl/(?P<index>[0-9]+)')

    for checkname, itemname in filled_checks.items():
        # single gratitude crystals aren't randomized
        if itemname == 'Gratitude Crystal':
            continue
        check = all_checks[checkname]
        item = by_item_name[itemname]
        for path in check['Paths']:
            stage_match = stage_re.match(path)
            event_match = event_re.match(path)
            oarc_match = oarc_re.match(path)
            shop_smpl_match = shop_smpl_re.match(path)
            if stage_match:
                stage = stage_match.group('stage')
                room = int(stage_match.group('room'))
                layer = int(stage_match.group('layer'))
                objname = stage_match.group('objname')
                objid = stage_match.group('objid')
                oarc = item['oarc']
                if oarc:
                    if isinstance(oarc, list):
                        for o in oarc:
                            stageoarcs[(stage, layer)].add(o)
                    else:
                        stageoarcs[(stage, layer)].add(oarc)
                stagepatchv2[(stage, room)].append((objname, layer, objid, item['id']))
            elif event_match:
                eventfile = event_match.group('eventfile')
                eventid = event_match.group('eventid')
                eventpatches[eventfile].append((eventid, item['id']))
            elif oarc_match:
                stage = oarc_match.group('stage')
                layer = int(oarc_match.group('layer'))
                oarc = item['oarc']
                if oarc:
                    if isinstance(oarc, list):
                        for o in oarc:
                            stageoarcs[(stage, layer)].add(o)
                    else:
                        stageoarcs[(stage, layer)].add(oarc)
            elif shop_smpl_match:
                index = int(shop_smpl_match.group('index'))
                # TODO: super fix this, add all models/arcs to items.yaml
                arcname = item.get('getarcname', None)
                modelname = item.get('getmodelname', None)
                oarc = item['oarc']
                if oarc:
                    if isinstance(oarc, list):
                        for o in oarc:
                            stageoarcs[("F002r", 1)].add(o)
                    else:
                        stageoarcs[("F002r", 1)].add(oarc)
                if modelname is None or arcname is None:
                    raise Exception(f'no modelnames for {item}')
                shoppatches[index] = (item['id'], arcname, modelname)
            else:
                print(f'ERROR: {path} didn\'t match any regex!')
    return stagepatchv2, stageoarcs, eventpatches, shoppatches

def get_entry_from_bzs(bzs: OrderedDict, objdef: dict, remove: bool=False) -> Optional[OrderedDict]:
    id = objdef.get('id',None)
    index = objdef.get('index',None)
    layer = objdef.get('layer', None)
    objtype = objdef['objtype'].ljust(4) # OBJ has an whitespace but thats was too error prone for the yaml, so just pad it here
    if layer is None:
        objlist = bzs[objtype]
    else:
        objlist = bzs['LAY '][f'l{layer}'][objtype]
    if not id is None:
        objs = [x for x in objlist if x['id'] == id]
        if len(objs) != 1:
            print(f'Error finding object: {json.dumps(objdef)}')
            return None
        obj = objs[0]
        if remove:
            objlist.remove(obj)
    elif not index is None:
        if index >= len(objlist):
            print(f'Error lisError list index out of range: {json.dumps(objdef)}')
            return None
        if remove:
            obj = objlist.pop(index)
        else:
            obj = objlist[index]
    else:
        print(f'ERROR: neither id nor index given for object {json.dumps(objdef)}')
        return None
    return obj
class GamePatcher:
    def __init__(self, rando):
        self.rando = rando
        self.patcher = AllPatcher(
            actual_extract_path=rando.actual_extract_path,
            modified_extract_path=rando.modified_extract_path,
            oarc_cache_path=rando.oarc_cache_path,
            copy_unmodified=False)
        self.text_labels = {}

    def do_all_gamepatches(self):
        self.load_base_patches()
        self.add_entrance_rando_patches()
        if self.rando.options['shop-mode'] != 'Vanilla':
            self.shopsanity_patches()
        self.do_build_arc_cache()
        self.add_startitem_patches()
        self.add_required_dungeon_patches()
        self.add_startstoryflags_to_patches()
        self.add_trial_hint_patches()
        self.handle_oarc_add_remove()
        self.add_rando_hash()

        self.patcher.set_bzs_patch(self.bzs_patch_func)
        self.patcher.set_event_patch(self.flow_patch)
        self.patcher.set_event_text_patch(self.text_patch)
        self.patcher.progress_callback = self.rando.progress_callback
        self.patcher.do_patch()

        self.do_dol_patch()
        self.do_rel_patch()
        self.do_patch_object_pack()
        self.do_patch_title_screen_logo()

    def filter_option_requirement(self, entry):
        return not (isinstance(entry, dict) and 'onlyif' in entry \
            and not self.rando.logic.check_logical_expression_string_req(entry['onlyif']))

    def add_patch_to_stage(self, stage, stagepatch):
        if stage not in self.patches:
            self.patches[stage] = []
        self.patches[stage].append(stagepatch)

    def load_base_patches(self):
        with (RANDO_ROOT_PATH / "patches.yaml").open() as f:
            self.patches = yaml.safe_load(f)
        with (RANDO_ROOT_PATH / "eventpatches.yaml").open() as f:
            self.eventpatches = yaml.safe_load(f)

        filtered_storyflags = []
        for storyflag in self.patches['global']['startstoryflags']:
            # conditionals are an object
            if not isinstance(storyflag, int):
                if self.filter_option_requirement(storyflag):
                    storyflag = storyflag['storyflag']
                else:
                    continue
            filtered_storyflags.append(storyflag)
        self.patches['global']['startstoryflags'] = filtered_storyflags

        # patches from randomizing items
        self.rando_stagepatches, self.stageoarcs, self.rando_eventpatches, self.shoppatches = \
            get_patches_from_location_item_list(self.rando.logic.item_locations, self.rando.logic.done_item_locations)

        # assembly patches
        self.all_asm_patches = defaultdict(OrderedDict)
        self.add_asm_patch('custom_funcs')
        self.add_asm_patch('ss_necessary')
        if self.rando.options['shop-mode'] != 'Vanilla':
            self.add_asm_patch('shopsanity')

        # for asm, custom symbols
        with (RANDO_ROOT_PATH / 'asm' / 'custom_symbols.txt').open('r') as f:
            self.custom_symbols = yaml.safe_load(f)
        self.main_custom_symbols = self.custom_symbols.get('main.dol',{})
        with (RANDO_ROOT_PATH / 'asm' / 'original_symbols.txt').open('r') as f:
            self.original_symbols = yaml.safe_load(f)
        self.main_original_symbols = self.original_symbols.get('main.dol',{})

        # for asm, free space start offset
        with (RANDO_ROOT_PATH / 'asm' / 'free_space_start_offsets.txt').open('r') as f:
            self.free_space_start_offsets = yaml.safe_load(f)

    def add_asm_patch(self, name):
        with (RANDO_ROOT_PATH / 'asm' / 'patch_diffs' / f'{name}_diff.txt').open('r') as f:
            asm_patch_file_data = yaml.safe_load(f)
        for exec_file, patches in asm_patch_file_data.items():
            self.all_asm_patches[exec_file].update(patches)

    def add_entrance_rando_patches(self):
        for entrance, dungeon in self.rando.entrance_connections.items():
            entrance_stage, entrance_room, entrance_scen = DUNGEON_ENTRANCE_STAGES[entrance]
            dungeon_stage, layer, room, entrance_index = DUNGEON_ENTRANCES[dungeon]
            # patch dungeon entrance
            self.add_patch_to_stage(entrance_stage, {
                'name': f'Dungeon entrance patch - {entrance} to {dungeon}',
                'type': 'objpatch',
                'index': entrance_scen,
                'room': entrance_room,
                'objtype': 'SCEN',
                'object': {
                    'name': dungeon_stage,
                    'layer': layer,
                    'room': room,
                    'entrance': entrance_index
                }
            })

            # handle the extra loading zone to the dungeon in Sand Sea from Ancient Harbor
            # yes I know there was probably a better way to do this but it's a one off special case
            if entrance == 'Dungeon Entrance In Sand Sea':
                self.add_patch_to_stage('F301', {
                    'name': f'Dungeon entrance patch - Ancient Harbor to {dungeon}',
                    'type': 'objpatch',
                    'index': 0,
                    'room': 0,
                    'objtype': 'SCEN',
                    'object': {
                        'name': dungeon_stage,
                        'layer': layer,
                        'room': room,
                        'entrance': entrance_index
                    }
                })
                self.add_patch_to_stage('F301', {
                    'name': f'Dungeon entrance patch - Ancient Harbor to {dungeon}',
                    'type': 'objpatch',
                    'index': 4,
                    'room': 0,
                    'objtype': 'SCEN',
                    'object': {
                        'name': dungeon_stage,
                        'layer': layer,
                        'room': room,
                        'entrance': entrance_index
                    }
                })

            # most dungeons only have a single exit, exception being LMF, which is handled seperately
            exit_stage, exit_layer, exit_room, exit_entrance = DUNGEON_EXITS[entrance]
            # the exit out of the back of LMF is special, because it's the only dungeon finish that can be
            # taken multiple times. The first time it should show a save prompt and subsequent times
            # it should not and they don't need to be touched if the LMF entrance is vanilla
            # the first time exit is taken care of by the DUNGEON_FINISH_EXIT_SCEN stuff
            # patch the secondary exit if it's not vanilla
            if dungeon == 'Lanayru Mining Facility' and not entrance == 'Dungeon Entrance In Lanayru Desert':
                self.add_patch_to_stage('F300_5', {
                    'name': f'Dungeon exit patch - second LMF finish to {entrance}',
                    'type': 'objpatch',
                    'index': 1,
                    'room': 0,
                    'objtype': 'SCEN',
                    'object': {
                        'name': exit_stage,
                        'layer': exit_layer,
                        'room': exit_room,
                        'entrance': exit_entrance
                    }
                })
            # patch all the exits for the dungeon
            for scen_stage, scen_room, scen_index in DUNGEON_EXIT_SCENS[dungeon]:
                self.add_patch_to_stage(scen_stage, {
                    'name': f'Dungeon exit patch - {dungeon} to {entrance}',
                    'type': 'objpatch',
                    'index': scen_index,
                    'room': scen_room,
                    'objtype': 'SCEN',
                    'object': {
                        'name': exit_stage,
                        'layer': exit_layer,
                        'room': exit_room,
                        'entrance': exit_entrance
                    }
                })

            scen_stage, scen_room, scen_index = DUNGEON_FINISH_EXIT_SCEN[dungeon]
            exit_stage, exit_layer, exit_room, exit_entrance = DUNGEON_FINISH_EXITS[entrance]
            self.add_patch_to_stage(scen_stage, {
                'name': f'Dungeon finish exit patch - {dungeon} to {entrance}',
                'type': 'objpatch',
                'index': scen_index,
                'room': scen_room,
                'objtype': 'SCEN',
                'object': {
                    'name': exit_stage,
                    'layer': exit_layer,
                    'room': exit_room,
                    'entrance': exit_entrance,
                    'flag8': 1, # save prompt
                }
            })

            # fix post dungeon storyflags
            inner_stage, inner_room, inner_evnt = POST_DUNGEON_CUTSCENE[dungeon]
            storyflag = POST_DUNGEON_STORYFLAGS[entrance]
            self.add_patch_to_stage(inner_stage, {
                'name': f'Post Dungeon Storyflag fix: {entrance} - {dungeon}',
                'type': 'objpatch',
                'index': inner_evnt,
                'room': inner_room,
                'objtype': 'EVNT',
                'object': {
                    'story_flag1': storyflag
                }
            })

    def shopsanity_patches(self):
        # self.eventpatches['105-Terry'].append({
        #     'name': 'go to Check for Pouch',
        #     'type': 'flowpatch',
        #     'index': 16,
        #     'flow': {
        #         'next': 'Check for Pouch' if self.rando.options['shop-mode'] == 'Vanilla' else 43
        #     }
        # })
        with (Path(__file__).parent / "beedle_texts.yaml").open('r') as f:
            beedle_texts = yaml.safe_load(f)
        # print(beedle_texts)
        for location in BEEDLE_TEXT_PATCHES:
            normal, discounted, normal_price, discount_price = BEEDLE_TEXT_PATCHES[location]
            sold_item = self.rando.logic.done_item_locations[location]
            normal_text =\
                f'That there is a <y<{sold_item}>>.\n' \
                f'I\'m selling it for only <r<{normal_price}>> rupees!\n' \
                f'Want to buy it?\n' \
                f'{BEEDLE_BUY_SWTICH}'
            discount_text =\
                f'That there is a <y<{sold_item}>>.\n' \
                f'Just this once it\'s half off!\n' \
                f'It can be yours for just <r<{discount_price}>> rupees!\n' \
                f'Want to buy it?\n' \
                f'{BEEDLE_BUY_SWTICH}'
            if location in beedle_texts:
                if sold_item in beedle_texts[location]:
                    # item has custom text for Beedle's shop
                    normal_text = f'{beedle_texts[location][sold_item]["normal"]}{BEEDLE_BUY_SWTICH}'
                    discount_text = f'{beedle_texts[location][sold_item]["discount"]}{BEEDLE_BUY_SWTICH}'

            if isinstance(normal, int): # string index is new text
                self.eventpatches['105-Terry'].append({
                    'name': f'{location} Text',
                    'type': 'textpatch',
                    'index': normal,
                    'text': normal_text
                })
            else:
                self.eventpatches['105-Terry'].append({
                    'name': normal,
                    'type': 'textadd',
                    'text': normal_text
                })
            if isinstance(discounted, int):
                self.eventpatches['105-Terry'].append({
                    'name': f'{location} Discount Text',
                    'type': 'textpatch',
                    'index': discounted,
                    'text': discount_text
                })
            else:
                self.eventpatches['105-Terry'].append({
                    'name': discounted,
                    'type': 'textadd',
                    'text': discount_text
                })

    def do_build_arc_cache(self):
        self.rando.progress_callback('building arc cache...')

        with (RANDO_ROOT_PATH / "extracts.yaml").open() as f:
            extracts = yaml.safe_load(f)
        self.patcher.create_oarc_cache(extracts)

    def add_startitem_patches(self):
        # Add sword story/itemflags if required
        start_sword_count = self.rando.starting_items.count('Progressive Sword')
        for i in range(start_sword_count):
            self.patches['global']['startstoryflags'].append(PROGRESSIVE_SWORD_STORYFLAGS[i])
        if start_sword_count > 0:
            self.patches['global']['startitems'].append(PROGRESSIVE_SWORD_ITEMIDS[start_sword_count-1])

        # if 'Sailcloth' in self.rando.starting_items:
        #     self.patches['global']['startstoryflags'].append(32)
        #     self.patches['global']['startitems'].append(15)

        if 'Progressive Pouch' in self.rando.starting_items:
            self.patches['global']['startstoryflags'].append(30) # storyflag for pouch
            self.patches['global']['startstoryflags'].append(931) # rando storyflag for progressive pouch 1
            self.patches['global']['startitems'].append(112) # itemflag for pouch

        # Add storyflags for tablets
        for item in self.rando.starting_items:
            if item in START_ITEM_STORYFLAGS:
                self.patches['global']['startstoryflags'].append(START_ITEM_STORYFLAGS[item])

    def add_required_dungeon_patches(self):
        # Add required dungeon patches to eventpatches
        DUNGEON_TO_EVENTFILE = {
            'Skyview': '201-ForestD1',
            'Earth Temple': '301-MountainD1',
            'Lanayru Mining Facility': '400-Desert',
            'Ancient Cistern': '202-ForestD2',
            'Sandship': '401-DesertD2',
            'Fire Sanctuary': '304-MountainD2',
        }

        REQUIRED_DUNGEON_STORYFLAGS = [902, 903, 926, 927, 928, 929]

        for i, dungeon in enumerate(self.rando.required_dungeons):
            dungeon_events = self.eventpatches[DUNGEON_TO_EVENTFILE[dungeon]]
            required_dungeon_storyflag_event = next(filter(lambda x: x['name'] == 'rando required dungeon storyflag', dungeon_events))
            required_dungeon_storyflag_event['flow']['param2'] = REQUIRED_DUNGEON_STORYFLAGS[i] # param2 is storyflag of event

        required_dungeon_count = len(self.rando.required_dungeons)
        # set flags for unrequired dungeons beforehand
        for required_dungeon_storyflag in REQUIRED_DUNGEON_STORYFLAGS[required_dungeon_count:]:
            self.patches['global']['startstoryflags'].append(required_dungeon_storyflag)

        # patch required dungeon text in
        if required_dungeon_count == 0:
            required_dungeons_text = 'No Dungeons'
        elif required_dungeon_count == 6:
            required_dungeons_text = 'All Dungeons'
        elif required_dungeon_count < 4:
            required_dungeons_text = 'Required Dungeons:\n'+('\n'.join(self.rando.required_dungeons))
        else:
            required_dungeons_text = 'Required: ' + ', '.join(self.rando.required_dungeons)

            # try to fit the text in as few lines as possible, breaking up at spaces if necessary
            cur_line = ''
            combined = ''

            for part in required_dungeons_text.split(' '):
                if len(cur_line + part) > 27: # limit of one line
                    combined += cur_line + '\n'
                    cur_line = part + ' '
                else:
                    cur_line += part + ' '
            combined += cur_line
            required_dungeons_text = combined.strip()

        self.eventpatches['107-Kanban'].append({
            "name": "Knight Academy Billboard text",
            "type": "textpatch",
            "index": 18,
            "text": required_dungeons_text,
        })

    def add_startstoryflags_to_patches(self):
        # add startflags to eventpatches
        startstoryflags = self.patches['global'].get('startstoryflags',None)
        startsceneflags = self.patches['global'].get('startsceneflags',None)
        startitems = self.patches['global'].get('startitems',None)
        def pop_or_default(lst, default=-1):
            if len(lst) == 0:
                return default
            else:
                return lst.pop(0)
        for cs_stage, cs_room, cs_index in START_CUTSCENES:
            if cs_stage.startswith('F0'):
                # make sure to only set sceneflags on skyloft
                self.add_patch_to_stage(cs_stage, {
                    'name': 'Startflags',
                    'type': 'objpatch',
                    'room': cs_room,
                    'index': cs_index,
                    'objtype': 'EVNT',
                    'object': {
                        'item': pop_or_default(startitems),
                        'story_flag1': pop_or_default(startstoryflags),
                        'story_flag2': pop_or_default(startstoryflags),
                        'sceneflag1': pop_or_default(startsceneflags),
                        'sceneflag2': pop_or_default(startsceneflags),
                    },
                })
            else:
                self.add_patch_to_stage(cs_stage, {
                    'name': 'Startflags',
                    'type': 'objpatch',
                    'room': cs_room,
                    'index': cs_index,
                    'objtype': 'EVNT',
                    'object': {
                        'item': pop_or_default(startitems),
                        'story_flag1': pop_or_default(startstoryflags),
                        'story_flag2': pop_or_default(startstoryflags),
                    },
                })
        # for now, we can only set scene and storyflags here, so make sure all items were handled in the event
        assert len(startitems) == 0, "Not all items were handled in events!"

        while startsceneflags or startstoryflags:
            self.add_patch_to_stage('F001r', {
                'name': 'Startflags',
                'type':'objadd',
                'room': 1, # Link's room
                'layer': 0,
                'objtype': 'STAG',
                'object': {
                    "params1": 0xFFFFFF00 | (pop_or_default(startsceneflags) & 0xFF),
                    "params2": 0xFF5FFFFF,
                    "posx": 761,
                    "posy": -22,
                    "posz": -2260,
                    "sizex": 1000,
                    "sizey": 1000,
                    "sizez": 1000,
                    "anglex": pop_or_default(startstoryflags) & 0xFFFF,
                    "angley": 0,
                    "anglez": 65535,
                    "name": "SwAreaT",
                }
            })

    def add_trial_hint_patches(self):
        def find_event(filename, name):
            return next((patch for patch in self.eventpatches[filename] if patch['name'] == name), None)
        # Trial Hints
        trial_checks = {
            # (getting it text patch, inventory text line)
            'Skyloft Silent Realm - Stone of Trials': ('Full SotH text',659, "The song that leads you to the final trial."),
            'Faron Silent Realm - Water Scale': ("Farore's Courage Text",653, "This song opens the trial located in Faron\nWoods."),
            'Lanayru Silent Realm - Clawshots': ("Nayru's Wisdom Text",654, "This song opens the trial located in\nLanayru Desert."),
            'Eldin Silent Realm - Fireshield Earrings': ("Din's Power Text",655, "This song opens the trial located on\nEldin Volcano."),
        }
        for trial_check_name, (obtain_text_name, inventory_text_idx, inventory_text) in trial_checks.items():
            item = self.rando.logic.done_item_locations[trial_check_name]
            if item in self.rando.logic.all_progress_items:
                useful_text = '\nYou might need what it reveals...'
                # print(f'{item} in {trial_check} is useful')
            else:
                useful_text = '\nIt\'s probably not too important...'
                # print(f'{item} in {trial_check} is not useful')
            find_event('003-ItemGet', obtain_text_name)["text"] += useful_text
            self.eventpatches['003-ItemGet'].append({
                'name': "Harp Text",
                'type': "textpatch",
                'index': inventory_text_idx,
                'text': inventory_text + useful_text
            })

    def handle_oarc_add_remove(self):
        remove_stageoarcs = defaultdict(set)

        for stage, stagepatches in self.patches.items():
            if stage == 'global':
                continue
            for patch in stagepatches:
                if patch['type'] == 'oarcadd':
                    self.stageoarcs[(stage, patch['destlayer'])].add(patch['oarc'])
                elif patch['type'] == 'oarcdelete':
                    remove_stageoarcs[(stage, patch['layer'])].add(patch['oarc'])

        for (stage, layer), oarcs in self.stageoarcs.items():
            self.patcher.add_stage_oarc(stage, layer, oarcs)
        for (stage, layer), oarcs in remove_stageoarcs.items():
            self.patcher.delete_stage_oarc(stage, layer, oarcs)

    def add_rando_hash(self):
        if not '002-System' in self.eventpatches:
            self.eventpatches['002-System'] = []

        self.eventpatches['002-System'].append({
            "name": "Rando hash on file select",
            "type": "textpatch",
            "index": 73,
            "text": self.rando.randomizer_hash,
        })

        self.eventpatches['002-System'].append({
            "name": "Rando hash on new file",
            "type": "textpatch",
            "index": 75,
            "text": self.rando.randomizer_hash,
        })

    def bzs_patch_func(self, bzs, stage, room):
        stagepatches = self.patches.get(stage, [])
        stagepatches = list(filter(self.filter_option_requirement, stagepatches))
        modified = False
        if room == None:
            layer_patches = list(filter(lambda x: x['type']=='layeroverride', stagepatches))
            if len(layer_patches) > 1:
                print(f"ERROR: multiple layer overrides for stage {stage}!")
            elif len(layer_patches) == 1:
                layer_override = [OrderedDict(story_flag=x['story_flag'], night=x['night'], layer=x['layer']) for x in layer_patches[0]['override']]
                bzs['LYSE'] = layer_override
                modified = True
        next_id = highest_objid(bzs) + 1
        for objadd in filter(lambda x: x['type']=='objadd' and x.get('room',None)==room, stagepatches):
            layer = objadd.get('layer', None)
            objtype = objadd['objtype'].ljust(4) # OBJ has an whitespace but thats was too error prone for the yaml, so just pad it here
            obj = objadd['object']
            if objtype in ['SOBS','SOBJ','STAS','STAG','SNDT']:
                new_obj = DEFAULT_SOBJ.copy()
            elif objtype in ['OBJS','OBJ ','DOOR']:
                new_obj = DEFAULT_OBJ.copy()
            elif objtype == 'SCEN':
                new_obj = DEFAULT_SCEN.copy()
            elif objtype == 'AREA':
                new_obj = DEFAULT_AREA.copy()
            else:
                print(f'Error: unknown objtype: {objtype}')
                continue
            if 'index' in obj:
                # check index, just to verify index based lists don't have a mistake in them
                if layer is None:
                    objlist = bzs.get(objtype, [])
                else:
                    objlist = bzs['LAY '][f'l{layer}'].get(objtype, [])
                if len(objlist) != obj['index']:
                    print(f'ERROR: wrong index adding object: {json.dumps(objadd)}')
                    continue
            for key, val in obj.items():
                if key in new_obj:
                    new_obj[key] = val
                else:
                    try_patch_obj(new_obj, key, val)
            if 'id' in new_obj:
                new_obj['id'] = (new_obj['id'] & ~0x3FF) | next_id
                next_id += 1
            if layer is None:
                if not objtype in bzs:
                    bzs[objtype] = []
                objlist = bzs[objtype]
            else:
                if not objtype in bzs['LAY '][f'l{layer}']:
                    bzs['LAY '][f'l{layer}'][objtype] = []
                objlist = bzs['LAY '][f'l{layer}'][objtype]
            # add object name to objn if it's some kind of actor
            if objtype in ['SOBS','SOBJ','STAS','STAG','SNDT','OBJS','OBJ ','DOOR']:
                # TODO: this only works if the layer is set
                if not 'OBJN' in bzs['LAY '][f'l{layer}']:
                    bzs['LAY '][f'l{layer}']['OBJN'] = []
                objn = bzs['LAY '][f'l{layer}']['OBJN']
                if not obj['name'] in objn:
                    objn.append(obj['name'])
            objlist.append(new_obj)
            modified = True
            # print(obj)
        for objpatch in filter(lambda x: x['type']=='objpatch' and x.get('room',None)==room, stagepatches):
            obj = get_entry_from_bzs(bzs, objpatch)
            if not obj is None:
                for key, val in objpatch['object'].items():
                    if key in obj:
                        obj[key] = val
                    else:
                        try_patch_obj(obj, key, val)
                modified = True
                # print(f'modified object from {layer} in room {room} with id {objpatch["id"]:04X}')
                # print(obj)
        for objmove in filter(lambda x: x['type']=='objmove' and x.get('room',None)==room, stagepatches):
            obj = get_entry_from_bzs(bzs, objmove, remove=True)
            destlayer = objmove['destlayer']
            if not obj is None:
                layer = objmove['layer']
                objtype = objmove['objtype'].ljust(4)
                obj['id'] = (obj['id'] & ~0x3FF) | next_id
                next_id += 1
                if not objtype in bzs['LAY '][f'l{destlayer}']:
                    bzs['LAY '][f'l{destlayer}'][objtype] = []
                bzs['LAY '][f'l{destlayer}'][objtype].append(obj)
                objn = bzs['LAY '][f'l{destlayer}']['OBJN']
                if not obj['name'] in objn:
                    objn.append(obj['name'])
                modified = True
                # print(f'moved object from {layer} to {destlayer} in room {room} with id {objmove["id"]:04X}')
                # print(obj)
        for objdelete in filter(lambda x: x['type']=='objdelete' and x.get('room',None)==room, stagepatches):
            obj = get_entry_from_bzs(bzs, objdelete, remove=True)
            if not obj is None:
                modified = True
                # print(f'removed object from {layer} in room {room} with id {objdelete["id"]:04X}')
                # print(obj)
        for command in filter(lambda x: x['type']=='objnadd' and x.get('room',None)==room, stagepatches):
            layer = command.get('layer', None)
            name_to_add = command['objn']
            if layer is None:
                if not 'OBJN' in bzs:
                    bzs['OBJN'] = []
                objlist = bzs['OBJN']
            else:
                if not 'OBJN' in bzs['LAY '][f'l{layer}']:
                    bzs['LAY '][f'l{layer}']['OBJN'] = []
                objlist = bzs['LAY '][f'l{layer}']['OBJN']
            objlist.append(name_to_add)

        # patch randomized items on stages
        for objname, layer, objid, itemid in self.rando_stagepatches.get((stage, room),[]):
            modified = True
            RANDO_PATCH_FUNCS[objname](bzs['LAY '][f'l{layer}'], itemid, objid)

        if modified:
            # print(json.dumps(bzs))
            return bzs
        else:
            return None

    def flow_patch(self, msbf, filename):
        modified = False
        flowpatches = self.eventpatches.get(filename, [])
        flowpatches = list(filter(self.filter_option_requirement, flowpatches))

        # dictionary to map flow labels to ids for new flows
        label_to_index = OrderedDict()
        next_index = len(msbf['FLW3']['flow'])
        # fist, fill in all the flow name to index mappings
        for command in filter(lambda x: x['type'] in ['flowadd', 'switchadd'], flowpatches):
            label_to_index[command['name']] = next_index
            next_index += 1
        for command in filter(lambda x: x['type'] == 'flowpatch', flowpatches):
            flowobj = msbf['FLW3']['flow'][command['index']]
            for key, val in command.get('flow', {}).items():
                # special case: next points to a label
                if key == 'next' and not isinstance(val, int):
                    index = label_to_index.get(val, None)
                    if index is None:
                        print(f'ERROR: label {val} not found in patch: {command["flow"]}')
                        continue
                    val = index
                # special case: text points to a label, textindex is param4
                if key == 'param4' and not isinstance(val, int):
                    index = self.text_labels.get(val, None)
                    if index is None:
                        print(f'ERROR: text label {val} not found in patch: {command["flow"]}')
                        continue
                    val = index
                flowobj[key] = val
            if flowobj['type'] == 'switch':
                # patch cases if given
                cases = command.get('cases', None)
                if cases:
                    assert len(cases) == flowobj['param4'] # param4 is number of cases
                    branch_start = flowobj['param5']
                    for i, case in enumerate(cases):
                        if not isinstance(case, int):
                            case = label_to_index.get(case, None)
                            assert not case is None, f'ERROR: text label {val} not found in patch: {command["flow"]}'
                        msbf['FLW3']['branch_points'][branch_start+i] = case
                        # print(f'set {branch_start+i} to {case}')
                    # print(flowobj)
            # print(f'patched flow {command["index"]}, {filename}')
            modified = True
        for command in filter(lambda x: x['type'] in ['flowadd', 'switchadd'], flowpatches):
            assert len(msbf['FLW3']['flow']) == label_to_index[command['name']], f'index has to be the next value in the flow, expected {len(msbf["FLW3"]["flow"])} got {label_to_index[command["name"]]}'
            flowobj = OrderedDict(
                type='type1',
                subType=-1,
                param1=0,
                param2=0,
                next=-1,
                param3=0,
                param4=0,
                param5=0,
            )
            for key, val in command['flow'].items():
                # special case: next points to a label
                if key == 'next' and not isinstance(val, int):
                    index = label_to_index.get(val, None)
                    if index is None:
                        print(f'ERROR: label {val} not found in new flow: {command["flow"]}')
                        continue
                    val = index
                # special case: text points to a label, textindex is param4
                if key == 'param4' and not isinstance(val, int):
                    index = self.text_labels.get(val, None)
                    if index is None:
                        print(f'ERROR: text label {val} not found in new flow: {command["flow"]}')
                        continue
                    val = index
                flowobj[key] = val
            if command['type'] == 'flowadd':
                msbf['FLW3']['flow'].append(flowobj)
                # print(f'added flow {command["name"]}, {filename}')
            else:
                flowobj['type']='switch'
                cases = command['cases']
                for i, _ in enumerate(cases):
                    value = cases[i]
                    if not isinstance(value, int):
                        index = label_to_index.get(value, None)
                        if index is None:
                            print(f'ERROR: label {value} not found in switch: {command}')
                            continue
                        cases[i] = index
                add_msbf_branch(msbf, flowobj, cases)
                # print(f'added switch {command["name"]}, {filename}')
            modified = True
        for command in filter(lambda x: x['type'] == 'entryadd', flowpatches):
            value = command['entry']['value']
            if not isinstance(value, int):
                index = label_to_index.get(value, None)
                if index is None:
                    print(f'ERROR: label {value} not found in new entry: {command["entry"]}')
                    continue
                value = index
            new_entry = OrderedDict(
                name = command['entry']['name'],
                value = value,
            )
            bucket = entrypoint_hash(command["entry"]["name"], len(msbf['FEN1']))
            msbf['FEN1'][bucket].append(new_entry)
            # print(f'added flow entry {command["entry"]["name"]}, {filename}')
            modified = True
        if filename == '003-ItemGet':
            # make progressive mitts
            make_progressive_item(msbf, 93, [35, 231], [56, 99], [904, 905])
            # make progressive swords
            # TODO trainings and goddess sword both set storyflags on their own, could reuse those
            make_progressive_item(msbf, 136, [77, 608, 75, 78, 74, 73], PROGRESSIVE_SWORD_ITEMIDS, PROGRESSIVE_SWORD_STORYFLAGS)
            # make progressive beetle
            make_progressive_item(msbf, 96, [38, 178], [53, 75], [912, 913])
            # make progressive pouch
            make_progressive_item(msbf, 258, [254, 253], [112, 113], [931, 932])
            # make progressive wallets
            make_progressive_item(msbf, 250, [246, 245, 244, 255], [108, 109, 110, 111], [915, 916, 917, 918])
            modified = True

        # patch randomized items
        for evntline, itemid in self.rando_eventpatches.get(filename, []):
            try:
                # can either be a label or a number
                evntline = int(evntline)
            except ValueError:
                index = label_to_index.get(evntline, None)
                if index is None:
                    print(f'ERROR: label {evntline} not found!')
                    continue
                evntline = index
                # print(f'dynamic label: {evntline}')
            modified = True
            msbf['FLW3']['flow'][evntline]['param2'] = itemid
            msbf['FLW3']['flow'][evntline]['param3'] = 9 # give item command

        if modified:
            return msbf
        else:
            return None

    def text_patch(self, msbt, filename):
        # for bucket, lbl_list in enumerate(msbt['LBL1']):
        #     for lbl in lbl_list:
        #         hash_b = entrypoint_hash(lbl['name'], len(msbt['LBL1']))
        #         print(f'smile: {bucket} {hash_b}')
        assert len(msbt['TXT2']) == len(msbt['ATR1'])
        modified = False
        textpatches = self.eventpatches.get(filename, [])
        textpatches = list(filter(self.filter_option_requirement, textpatches))
        for command in filter(lambda x: x['type'] == 'textpatch', textpatches):
            msbt['TXT2'][command['index']] = process_control_sequences(command['text']).encode('utf-16be')
            # print(f'patched text {command["index"]}, {filename}')
            modified = True
        for command in filter(lambda x: x['type'] == 'textadd', textpatches):
            index = len(msbt['TXT2'])
            self.text_labels[command['name']] = index
            msbt['TXT2'].append(process_control_sequences(command['text']).encode('utf-16be'))
            msbt['ATR1'].append({'unk1':command.get('unk1',1), 'unk2':command.get('unk2',0)})
            # the game doesn't care about the name, but it has to exist and be unique
            # only unique within a file but whatever
            entry_name="%s:%d" % (filename[-3:], index)
            new_entry = OrderedDict(
                name = entry_name,
                value = index,
            )
            bucket = entrypoint_hash(entry_name, len(msbt['LBL1']))
            msbt['LBL1'][bucket].append(new_entry)
            # print(f'added text {index}, {filename}')
            modified = True
        if modified:
            return msbt
        else:
            return None

    def do_dol_patch(self):
        self.rando.progress_callback('patching main.dol...')
        # patch main.dol
        dol_bytes = BytesIO((self.patcher.actual_extract_path / 'DATA' / 'sys' / 'main.dol').read_bytes())
        dol = DOL()
        dol.read(dol_bytes)
        apply_dol_patch(self, dol, self.all_asm_patches['main.dol'])
        dol.save_changes()
        write_bytes_create_dirs(self.patcher.modified_extract_path / 'DATA' / 'sys' / 'main.dol', dol_bytes.getbuffer())

    def do_rel_patch(self):
        self.rando.progress_callback('patching rels...')
        rel_arc = U8File.parse_u8(BytesIO((self.patcher.actual_extract_path / 'DATA' / 'files' / 'rels.arc').read_bytes()))
        rel_modified = False
        for file, codepatches in self.all_asm_patches.items():
            if file == 'main.dol': # main.dol
                continue
            rel_data = BytesIO(rel_arc.get_file_data(f'rels/{file}'))
            if rel_data is None:
                print(f'ERROR: rel {file} not found!')
                continue
            rel = REL()
            rel.read(rel_data)
            apply_rel_patch(self, rel, file, codepatches)
            if file == 'd_a_shop_sampleNP.rel' and self.rando.options['shop-mode'] != 'Vanilla':
                self.do_shoptable_rel_patch(rel)
            rel.save_changes()
            rel_arc.set_file_data(f'rels/{file}', rel_data.getbuffer())
            rel_modified = True
        if rel_modified:
            rel_data = rel_arc.to_buffer()
            write_bytes_create_dirs(self.patcher.modified_extract_path / 'DATA' / 'files' / 'rels.arc', rel_data)

    def do_shoptable_rel_patch(self, rel):
        # shopsanity patches
        # 24, 17, 18 is patched extra wallet chain
        # patches the next value in the shop item chain
        shop_item_next_patches = {
            24: 17,
            17: 18,
        }
        shop_price_patches = {
            17: 100,
            18: 100,
        }
        shop_entrypoint_patches = {
            17: 10539,
            18: 10540,
        }
        shop_present_scale_patches = {
            17: 1.2,
            18: 1.2,
        }
        shop_target_height_patches = {
            17: 100,
            18: 100,
        }
        sold_out_storyflag_patches = {
            24: 937,
            17: 938,
            18: 939,
            25: 940, # bug net
            27: 941, # bug medal
        }
        SHOP_LIST_OFFSET = 0x6D8C
        ENTRY_SIZE = 0x54
        for shopindex, (itemid, arcname, modelname) in self.shoppatches.items():
            data_bytes, shop_list_offset = rel.convert_rel_offset_to_section_data_and_relative_offset(SHOP_LIST_OFFSET)
            current_shop_entry_offset = shop_list_offset + ENTRY_SIZE * shopindex
            present_scale = shop_present_scale_patches.get(shopindex, None)
            if not present_scale is None:
                write_float(data_bytes, current_shop_entry_offset + 0x0, present_scale)
            target_height = shop_target_height_patches.get(shopindex, None)
            if not target_height is None:
                write_float(data_bytes, current_shop_entry_offset + 0x8, target_height)
            # item id
            write_u16(data_bytes, current_shop_entry_offset + 0xC, itemid)

            shop_price = shop_price_patches.get(shopindex, None)
            if not shop_price is None:
                write_u16(data_bytes, current_shop_entry_offset + 0xE, shop_price)

            shop_entrypoint = shop_entrypoint_patches.get(shopindex, None)
            if not shop_entrypoint is None:
                write_u16(data_bytes, current_shop_entry_offset + 0x10, shop_entrypoint)

            shop_item_next = shop_item_next_patches.get(shopindex, None)
            if not shop_item_next is None:
                write_u16(data_bytes, current_shop_entry_offset + 0x12, shop_item_next)

            write_str(data_bytes, current_shop_entry_offset + 0x16, arcname, 30)
            write_str(data_bytes, current_shop_entry_offset + 0x34, modelname, 30)
            sold_out_storyflag = sold_out_storyflag_patches.get(shopindex, None)
            if not sold_out_storyflag is None:
                # normally not part of the struct, but the last 2 bytes of the modelname aren't used, so use them for storyflags
                write_u16(data_bytes, current_shop_entry_offset + 0x52, sold_out_storyflag)

    def do_patch_object_pack(self):
        self.rando.progress_callback('patching ObjectPack...')
        # patch object pack
        objpack_data = nlzss11.decompress((self.patcher.actual_extract_path / 'DATA' / 'files' / 'Object' / 'ObjectPack.arc.LZ').read_bytes())
        object_arc = U8File.parse_u8(BytesIO(objpack_data))
        objpack_modified = False
        for oarc in self.patches['global'].get('objpackoarcadd',[]):
            oarc_data = (self.patcher.oarc_cache_path / f'{oarc}.arc').read_bytes()
            object_arc.add_file_data(f'oarc/{oarc}.arc', oarc_data)
            objpack_modified = True
        # arc replacements
        ARC_REPLACEMENTS_PATH = self.rando.exe_root_path / 'arc-replacements'
        ARC_REPLACEMENTS_PATH.mkdir(exist_ok=True)
        for file in ARC_REPLACEMENTS_PATH.glob('*.arc'):
            arcname = file.parts[-1] # includes the .arc extension
            arcdata = file.read_bytes()
            object_arc.set_file_data(f'oarc/{arcname}', arcdata)
            objpack_modified = True
        if objpack_modified:
            objpack_data = object_arc.to_buffer()
            write_bytes_create_dirs(self.patcher.modified_extract_path / 'DATA' / 'files' / 'Object' / 'ObjectPack.arc.LZ', nlzss11.compress(objpack_data))

    def do_patch_title_screen_logo(self):
        # patch title screen logo
        actual_data = (self.rando.actual_extract_path / 'DATA' / 'files' / 'US' / 'Layout' / 'Title2D.arc').read_bytes()
        actual_arc = U8File.parse_u8(BytesIO(actual_data))
        logodata = (self.rando.rando_root_path / 'assets' / 'logo.tpl').read_bytes()
        actual_arc.set_file_data('timg/tr_wiiKing2Logo_00.tpl', logodata)
        (self.rando.modified_extract_path / 'DATA' / 'files' / 'US' / 'Layout' / 'Title2D.arc').write_bytes(actual_arc.to_buffer())


# R-5.8
# Experimentally evaluate the efficiency of the pop method of Python’s list
# class when using varying indices as a parameter, as we did for insert on
# page 205. Report your results akin to Table 5.5.

import timeit

Ns = []

# creating result lists
k_0 = []
k_equal_n_over_2 = []
k_equal_n = []
running_times = [k_0, k_equal_n_over_2, k_equal_n]

min_N_magnitude = 2
max_N_magnitude = 6

ks = ["0", "n//2", "-1"]

# Verifying runtime
for index, running_time in enumerate(running_times):
    for new_int in range(min_N_magnitude, max_N_magnitude):
        n = 10**new_int
        Ns.append(n)

        j = n//10   # testing the method on n//10 elements

        stmt = """
for i in range({}):
    lst.pop({})""".format(j, ks[index])

        setup = """n = {}
lst = [i for i in range(n)]""".format(n)

        t = timeit.Timer(stmt, setup=setup)

        number = 10  # number of repetition to obtain average result
        total_time = sum(t.repeat(number, 1))
        avg_time = total_time / (number*j)  # divide by number and by j to obtain average time for a single pop()

        running_time.append(avg_time)

print("""\n
Average runtime of using pop() method on n/10 element of lists of length n
Lines test for varying positions for the index k, pop(k) for k = 0, n//2, n
Rows test for varying lengths of the list, going from {} to {} by factors of 10\n""".format(min_N_magnitude, max_N_magnitude))

[print(i) for i in running_times]

print("""\n
It is clear that using pop() at the end of the list is O(1),
but using it at the beginning or in the middle is O(n)""")

Source_Code/Chap_05 - Array-Based Sequences/Reinforcement/R-5.8/R-5.8.py


"""
 * Copyright 2020, Departamento de sistemas y Computación
 * Universidad de Los Andes
 *
 *
 * Desarrolado para el curso ISIS1225 - Estructuras de Datos y Algoritmos
 *
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 """
import csv
import config
from DISClib.ADT import list as lt
from DISClib.ADT import map as mp
from DISClib.DataStructures import mapentry as me
from time import process_time
assert config

"""
En este archivo definimos los TADs que vamos a usar,
es decir contiene los modelos con los datos en memoria

"""

# -----------------------------------------------------
# API del TAD Catalogo de Libros
# -----------------------------------------------------
def newCatalog():
    """ Inicializa el catálogo de libros

    Crea una lista vacia para guardar todos los libros

    Se crean indices (Maps) por los siguientes criterios:
    Autores
    ID libros
    Tags
    Año de publicacion

    Retorna el catalogo inicializado.
    """
    catalog = {'peliculas': None,
               'moviesid': None,
               'production_companies': None,
               'genres': None,
               'director_name': None,
               'ids': None,
               'actores' : None,
               'idsActor' : None,
               'pais': None}

    catalog['movies'] = lt.newList('SINGLE_LINKED', compareMoviesIds)
    catalog['moviesid'] = mp.newMap(360000,
                                   maptype='PROBING',
                                   loadfactor=0.4,
                                   comparefunction=compareMapMoviesIds)
    catalog['production_companies'] = mp.newMap(36000,
                                   maptype='CHAINING',
                                   loadfactor=1.0,
                                   comparefunction=compareCompanyByName)
    catalog['genres'] = mp.newMap(180,
                                   maptype='CHAINING',
                                   loadfactor=1.0,
                                   comparefunction=compareCompanyByName)
    catalog['director_name'] = mp.newMap(300000,
                                   maptype='CHAINING',
                                   loadfactor=1.0,
                                   comparefunction=compareDirectorByName)
    catalog['ids'] = mp.newMap(360000,
                                   maptype='PROBING',
                                   loadfactor=0.4,
                                   comparefunction=compareTagIds)
    catalog['actores'] = mp.newMap(300000,
                                   maptype='CHAINING',
                                   loadfactor=1.0,
                                   comparefunction=compareDirectorByName)
    catalog['idsActor'] = mp.newMap(360000,
                                   maptype='PROBING',
                                   loadfactor=0.4,
                                   comparefunction=compareTagIds)
    catalog['pais'] = mp.newMap(1000,
                                   maptype='PROBING',
                                   loadfactor=0.4,
                                   comparefunction=compareCompanyByName)

    return catalog
#Nuevas cosas para agregar
def newCompany(name):
    """
    Crea una nueva estructura para modelar los libros de un autor
    y su promedio de ratings
    """
    author = {'name': "", "movies": None,  "average_rating": 0}
    author['name'] = name
    author['movies'] = lt.newList('SINGLE_LINKED', compareCompanyByName)
    return author

def newGenre(name):
    author = {'name': "", "movies": None,  "average_count": 0}
    author['name'] = name
    author['movies'] = lt.newList('SINGLE_LINKED', compareCompanyByName)
    return author

def newTagBook(name, id):
    """
    Esta estructura crea una relación entre un tag y los libros que han sido
    marcados con dicho tag.  Se guarga el total de libros y una lista con
    dichos libros.
    """
    tag = {'name': '',
           'tag_id': '',
           'total_movies': 0,
           'movies': None,
           'count': 0.0}
    tag['name'] = name
    tag['tag_id'] = id
    tag['movies'] = lt.newList('ARRAY_LIST', compareCompanyByName)
    return tag

def newIdActor(name1, name2, name3, name4, name5, id):
    """
    Esta estructura crea una relación entre un tag y los libros que han sido
    marcados con dicho tag.  Se guarga el total de libros y una lista con
    dichos libros.
    """
    tag = {'name1': name1,
           'name2': name2,
           'name3': name3,
           'name4': name4,
           'name5': name5,
           'tag_id': ''}
    tag['tag_id'] = id
    return tag

def newActor(name, id):
    """
    Esta estructura crea una relación entre un tag y los libros que han sido
    marcados con dicho tag.  Se guarga el total de libros y una lista con
    dichos libros.
    """
    tag = {'name': name,
           'tag_id': '',
           'total_movies': 0,
           'movies': None,
           'count': 0.0}
    tag['tag_id'] = id
    tag['movies'] = lt.newList('ARRAY_LIST', compareCompanyByName)
    return tag

def newCountry(name):
    author = {'name': "", "movies": None}
    author['name'] = name
    author['movies'] = lt.newList('ARRAY_LIST', compareCompanyByName)
    #{'titulo': title, 'fecha de lanzamiento': date, 'director': director}
    return author

def infotitle(title, date, director):
    info = {'titulo': title, 'fecha de lanzamiento': date, 'director': director}
    return info
#Agregar cosas a los diccionarios
def addTag(catalog, tag):
    """
    Adiciona un tag a la tabla de tags dentro del catalogo
    """
    newtag = newTagBook(tag['director_name'], tag['id'])
    mp.put(catalog['director_name'], tag['director_name'], newtag)
    mp.put(catalog['ids'], tag['id'], newtag)

#Requerimiento 3

def addActor(catalog, tag):
    a1 = tag['actor1_name']
    a2 = tag['actor2_name']
    a3 = tag['actor3_name']
    a4 = tag['actor4_name']
    a5 = tag['actor5_name']
    newtag = newIdActor(a1, a2, a3, a4, a5, tag['id'])
    for i in range(1,6):
        mp.put(catalog['actores'], tag['actor' + str(i) + '_name'], newActor(tag['actor' + str(i) + '_name'], tag['id']))
    mp.put(catalog['idsActor'], tag['id'], newtag)

def addMovieActor(catalog, tag):
    """
    Agrega una relación entre un libro y un tag.
    Para ello se adiciona el libro a la lista de libros
    del tag.
    """
    tagid = tag['id']
    entry = mp.get(catalog['idsActor'], tagid)

    if entry:
        for i in range(1, 6):
            tagbook = mp.get(catalog['actores'], me.getValue(entry)['name'+str(i)])
            tagbook['value']['total_movies'] += 1
            movie = mp.get(catalog['moviesid'], tagid)
            valor = me.getValue(movie)
            if (tagbook['value']['count'] == 0.0):
                tagbook['value']['count'] = float(valor['vote_average'])
            else:
                tagbook['value']['count'] += float(valor['vote_average'])
            if movie:
                lt.addLast(tagbook['value']['movies'], valor['original_title'])

def addBookTag(catalog, tag):
    """
    Agrega una relación entre un libro y un tag.
    Para ello se adiciona el libro a la lista de libros
    del tag.
    """
    tagid = tag['id']
    entry = mp.get(catalog['ids'], tagid)

    if entry:
        tagbook = mp.get(catalog['director_name'], me.getValue(entry)['name'])
        tagbook['value']['total_movies'] += 1
        movie = mp.get(catalog['moviesid'], tagid)
        valor = me.getValue(movie)
        if (tagbook['value']['count'] == 0.0):
            tagbook['value']['count'] = float(valor['vote_average'])
        else:
            tagbook['value']['count'] += float(valor['vote_average'])
        if movie:
            lt.addLast(tagbook['value']['movies'], valor['original_title'])

#agregar las peliculas a la lista
def addMovie(catalog, movie):
    lt.addLast(catalog['movies'], movie)
    mp.put(catalog['moviesid'], movie['id'], movie)

#Agregar las películas a una compañia
def addMovieCompany(catalog, companyname, movie):

    authors = catalog['production_companies']
    existauthor = mp.contains(authors, companyname)
    if existauthor:
        entry = mp.get(authors, companyname)
        author = me.getValue(entry)
    else:
        author = newCompany(companyname)
        mp.put(authors, companyname, author)
    lt.addLast(author['movies'], movie)

    authavg = author['average_rating']
    bookavg = movie['vote_average']
    if (authavg == 0.0):
        author['average_rating'] = float(bookavg)
    else:
        author['average_rating'] += float(bookavg)

def addMovieGenre(catalog, genrename, movie):

    authors = catalog['genres']
    existauthor = mp.contains(authors, genrename)
    if existauthor:
        entry = mp.get(authors, genrename)
        author = me.getValue(entry)
    else:
        author = newGenre(genrename)
        mp.put(authors, genrename, author)
    lt.addLast(author['movies'], movie)

    authavg = author['average_count']
    bookavg = movie['vote_count']
    if (authavg == 0.0):
        author['average_count'] = float(bookavg)
    else:
        author['average_count'] += float(bookavg)

def addMovieCountry(catalog, countryname, movie):

    authors = catalog['pais']
    existauthor = mp.contains(authors, countryname)
    a = mp.get(catalog['ids'], movie['id'])
    dire = me.getValue(a)
    if existauthor:
        entry = mp.get(authors, countryname)
        author = me.getValue(entry)
    else:
        author = newCountry(countryname)
        mp.put(authors, countryname, author)
    info = infotitle(movie['original_title'], movie['release_date'], dire['name'])
    lt.addLast(author['movies'], info)

def getMoviesByCompany(catalog, authorname):

    author = mp.get(catalog['production_companies'], authorname)
    if author:
        return me.getValue(author)
    return None

def getMoviesByGenre(catalog, authorname):

    author = mp.get(catalog['genres'], authorname)
    if author:
        return me.getValue(author)
    return None

def getBooksByTag(catalog, tagname):

    tag = mp.get(catalog['director_name'], tagname)
    if tag:
        return me.getValue(tag)
    return None

def getMovieByActor(catalog, tagname):

    tag = mp.get(catalog['actores'], tagname)
    if tag:
        return me.getValue(tag)
    return None

def getMoviesByCountry(catalog, authorname):

    author = mp.get(catalog['pais'], authorname)
    if author:
        return me.getValue(author)
    return None

#los compare de TADmap
def compareCompanyByName(keyname, company):

    authentry = me.getKey(company)
    if (keyname == authentry):
        return 0
    elif (keyname > authentry):
        return 1
    else:
        return -1

def compareDirectorByName(keyname, director):

    authentry = me.getKey(director)
    if (keyname == authentry):
        return 0
    elif (keyname > authentry):
        return 1
    else:
        return -1

def numeroPeliculas(lista1):
    lst=lt.size(lista1)
    return lst

def primeraPelicula(lista1):
    dato = lt.getElement(lista1, 1)
    peliculaP = dato['original_title']
    return peliculaP

def ultimaPelicula(lista1):
    lst=lt.size(lista1)
    dato = lt.getElement(lista1, lst)
    peliculaU = dato['original_title']
    return peliculaU

def fechaEstrenoP(lista1):
    dato = lt.getElement(lista1, 1)
    fechaP = dato['release_date']
    return fechaP

def fechaEstrenoU(lista1):
    lst=lt.size(lista1)
    dato = lt.getElement(lista1, lst)
    fechaU = dato['release_date']
    return fechaU

def promP(lista1):
    dato = lt.getElement(lista1, 1)
    promP = dato['vote_average']
    return promP

def promU(lista1):
    lst=lt.size(lista1)
    dato = lt.getElement(lista1, lst)
    promU = dato['vote_average']
    return promU

def votP(lista1):
    dato = lt.getElement(lista1, 1)
    votP = dato['vote_count']
    return votP

def votU(lista1):
    lst=lt.size(lista1)
    dato = lt.getElement(lista1, lst)
    votU = dato['vote_count']
    return votU

def idiomaP(lista1):
    dato = lt.getElement(lista1, 1)
    idiomaP = dato['spoken_languages']
    return idiomaP

def idiomaU(lista1):
    lst=lt.size(lista1)
    dato = lt.getElement(lista1, lst)
    idiomaU = dato['spoken_languages']
    return idiomaU

# Funciones para agregar informacion al catalogo

# ==============================
# Funciones de consulta
# ==============================

# ==============================
# Funciones de Comparacion
# ==============================

def compareMoviesIds(id1, id2):

    if (id1 == id2):
        return 0
    elif id1 > id2:
        return 1
    else:
        return -1

def compareTagIds(id, tag):
    tagentry = me.getKey(tag)
    if (int(id) == int(tagentry)):
        return 0
    elif (int(id) > int(tagentry)):
        return 1
    else:
        return 0

def compareMapMoviesIds(id, entry):

    identry = me.getKey(entry)
    if (int(id) == int(identry)):
        return 0
    elif (int(id) > int(identry)):
        return 1
    else:
        return -1

def compareRecordIds (element1, element2):
    if int(element1["id"]) == int(element2["id"]):
        return 0
    elif int(element1["id"]) > int(element2["id"]):
        return 1
    return -1


from rlberry.envs import Acrobot
from rlberry.agents import RSKernelUCBVIAgent
from rlberry.wrappers import RescaleRewardWrapper

env = Acrobot()
# rescake rewards to [0, 1]
env = RescaleRewardWrapper(env, (0.0, 1.0))

agent = RSKernelUCBVIAgent(env, n_episodes=500, gamma=0.99, horizon=300,
                           bonus_scale_factor=0.01,
                           min_dist=0.2, bandwidth=0.05, beta=1.0,
                           kernel_type="gaussian")
agent.fit()

env.enable_rendering()
state = env.reset()

time_before_done = 0
ended = False
for tt in range(4*agent.horizon):
    action = agent.policy(state)
    next_state, reward, done, _ = env.step(action)
    if not done and not ended:
        time_before_done += 1
    if done:
        ended = True
    state = next_state

print("steps to achieve the goal for the first time = ", time_before_done)
env.save_video("kernel_acrobot_gaussian.mp4", framerate=15)


#!/usr/bin/env python
import os
import pathlib

import shodan
from dotenv import load_dotenv

dotenv_path = pathlib.Path(__file__).resolve().parent.parent.parent / ".env"
load_dotenv(dotenv_path)
SHODAN_API_KEY = os.environ.get("SHODAN_API_KEY")

class ShodanSearch:

    def __init__(self):
        # shodan key
        self.shodanKeyString = SHODAN_API_KEY
        self.shodanApi = shodan.Shodan(self.shodanKeyString)

    def shodan_key_info(self):
        print("[*] Shodan API info...")

        try:
            info = self.shodanApi.info()
            for inf in info:
                print("{}: {}".format(inf, info[inf]))
        except Exception as err:
            print("[-] Error: {}".format(err))

    def shodan_host_info(self, ip):
        self.shodan_key_info()

        try:
            print("\n[*] Shodan Service...")

            host = self.shodanApi.host(ip)

            print("IP: {}".format(host.get('ip_str')))
            print("Country: {} : {}".format(host.get('country_name', 'Unknown'), host.get('country_code3')))
            print("City: {}".format(host.get('city', 'Unknown')))
            print("Longitude: {}".format(host.get('longitude')))
            print("Latitude: {}".format(host.get('latitude')))
            print("Organization: {}".format(host.get('org')))
            print("Operating System: {}".format(host.get('os')))
            print("Updated: {}".format(host.get('updated')))

            for item in host['data']:
                print("\nPort: {}".format(item['port']))
                print("===============================")
                if 'isp' in item.keys():
                    print("ISP: {}".format(item['isp']))
                if 'product' in item.keys():
                    print("Product: {}".format(item['product']))
                print("Data: {}".format(item['data']))

                if 'vulns' in item.keys():
                    print("[+] Vulnerability!!!")
                    for cve_info in item['vulns'].items():
                        print("ID: {}".format(cve_info[0]))
                        print("CVSS: {}".format(cve_info[1].get('cvss')))
                        print("{}".format(cve_info[1].get('summary')))

        except shodan.APIError as err:
            print("[-] API ERROR: {}".format(err))

class DBEngine:
    def __init__(self):
        pass

    def get_value_deal_except(self, element, value_name):
        try:
            value = element[value_name]
        except:
            value = ""
        return value

    def shodan_ip_get_services(self, ip):
        shodan_api = shodan.Shodan(SHODAN_API_KEY)
        services = []
        host = shodan_api.host(ip)
        for item in host['data']:
            port = self.get_value_deal_except(item, 'port')
            product = self.get_value_deal_except(item, 'product')
            version = self.get_value_deal_except(item, 'version')
            service = {'ip': ip, 'port': port, 'product': product, 'version': version}
            yield service
        #     services.append(service)
        # return services

    def shodan_service_get_ips(self, service, version=""):
        shodan_api = shodan.Shodan(SHODAN_API_KEY)
        matches = []
        print("[*] shodan search: {} {}".format(service, version))
        results = shodan_api.search(f"{service} {version}")
        for item in results['matches']:
            ip = item['ip_str']
            port = item['port']
            product = service
            version = version
            matche = {'ip': ip, 'port': port, 'product': product, 'version': version}
            matches.append(matche)
        return matches

# test for shodan search DB
if __name__ == "__main__":
    search_engine = DBEngine()
    ip = "89.135.83.205"
    print("[TEST] congratulation,{} have those services:".format(ip))
    services = search_engine.shodan_ip_get_services(ip)
    for service in services:
        print(f"{service['ip']}/{service['port']}/{service['product']}/{service['version']}")

    service = "tomcat"
    version = "7.0"
    print("[TEST] congratulation,those ip have operate {}-{}:".format(service, version))
    ips = search_engine.shodan_service_get_ips(service, version)
    for matche in ips:
        print(f"{matche['ip']}/{matche['port']}/{matche['product']}/{matche['version']}")


import pygame as pg
import numpy as np
import os

class Constants:
    def __init__(self):
        self.size = np.array([600, 600])
        self.screen = pg.display.set_mode(self.size)
        self.title = 'Sutda'

        font = os.getcwd() + '/fonts/unispace/unispace rg.ttf'
        self.font = lambda x: font.replace('rg', x)

        self.BLACK  = np.array((  0,   0,   0))
        self.WHITE  = np.array((255, 255, 255))
        self.GRAY   = lambda x: self.WHITE * x
        self.BROWN  = np.array((201, 107,   0))
        self.RED    = np.array((255,   0,   0))
        self.GREEN  = np.array(( 79, 175,   0))
        self.BLUE   = np.array((  0,   0, 255))
        self.ORANGE = np.array((255, 162,   0))
        self.YELLOW = np.array((255, 255,   0))
        self.PURPLE = np.array((180,   0, 180))

        self.L_RED    = np.array((255, 140, 140))
        self.L_ORANGE = np.array((255, 200, 140))
        self.L_YELLOW = np.array((255, 255, 140))
        self.L_GREEN  = np.array((140, 255, 140))
        self.L_SBLUE  = np.array((140, 255, 255))
        self.L_BLUE   = np.array((140, 140, 255))
        self.L_PURPLE = np.array((200, 140, 255))


from matplotlib import pyplot as plt
from sklearn.neighbors import KNeighborsClassifier as nbh
from sklearn.neighbors import DistanceMetric
from sklearn.model_selection import train_test_split
from sklearn import metrics
import pandas as pd
from sklearn import preprocessing
import numpy as np

def glass():
    df = pd.read_csv('glass.csv', sep=',')
    df = df.drop('Id', 1)
    features = df.drop('Type', 1).values
    targets = df['Type'].values
    le = preprocessing.LabelEncoder()
    features_encoded = [le.fit_transform(sample) for sample in features]
    targets_encoded = le.fit_transform(targets)
    nbhrs = []
    scores = []
    total_nbh = []
    total_scores = []
    neigh = nbh()
    classifier = neigh.fit(features_encoded, targets_encoded)
    print(features_encoded)

    for i in features_encoded:
        for j in range(len(i)):
            temp = i.copy()
            temp[j] = 0

glass()