import { system, world, MinecraftItemTypes, ItemStack, Items, MinecraftEnchantmentTypes, Enchantment, BlockLocation, Location, MinecraftDimensionTypes } from '@minecraft/server'

system.events.beforeWatchdogTerminate.subscribe(e => {
    e.cancel = true
    console.warn(e.terminateReason)
})

import { ActionFormData, ModalFormData } from '@minecraft/server-ui'
import * as fl from './fluffylib.js'

const overworld = world.getDimension(MinecraftDimensionTypes.overworld)

function getTerminalData(terminal) {
    for (const tag of terminal.getTags()) {
        if (tag.startsWith('storagesystem')) {
            return tag.slice(13)
        }
    }
    return false
}

function setTerminalData(terminal, newData, oldTag) {
    if (oldTag) {
        terminal.removeTag('storagesystem' + oldTag)
    }
    terminal.addTag('storagesystem' + newData)
}

function terminalNeedsUpdate(terminal) {
    return JSON.parse(getTerminalData(terminal)).version != 2
}

function updateTerminalJson(terminal) {
    const tag = getTerminalData(terminal)
    const json = JSON.parse(tag)

    // version check
    if (!json.version) {
        const newJson = {
            version: 1,
            items: {}
        }

        for (const [key_, value] of Object.entries(json)) {
            let key = JSON.parse(key_)

            if (key[5]) {
                key.pop()
            }

            value.name = getItemName(terminal, key[0], key[1])
            value.texture = getItemTexture(terminal, key[0], key[1])

            newJson.items[JSON.stringify(key)] = value
        }

        setTerminalData(terminal, JSON.stringify(newJson), tag)
    }
    else if (json.version === 1) {
        json.version = 2

        for (const [_, item] of Object.entries(json)) {
            delete item.texture
            delete item.name
        }

        setTerminalData(terminal, JSON.stringify(json), tag)
    }

    if (terminalNeedsUpdate(terminal)) updateTerminalJson(terminal)
}

function abbreviateNumber(value) {
    let newValue = value;
    if (value >= 1000) {
        let suffixes = ["", "k", "M", "b", "t", "q", "Q", "s", "S", "o"];
        let suffixNum = Math.floor(("" + value).length / 3);
        let shortValue = '';
        for (let precision = 2; precision >= 1; precision--) {
            shortValue = parseFloat((suffixNum != 0 ? (value / Math.pow(1000, suffixNum)) : value).toPrecision(precision));
            let dotLessShortValue = (shortValue + '').replace(/[^a-zA-Z 0-9]+/g, '');
            if (dotLessShortValue.length <= 2) { break; }
        }
        if (shortValue % 1 != 0) shortValue = shortValue.toFixed(1);
        newValue = shortValue + suffixes[suffixNum];
    }
    return newValue;
}

function getItemName(terminal, itemId, data) {
    return `${itemId.split(/:(.*)/s)[1].replaceAll('_', ' ')} (${data})`
}

function getItemTexture(terminal, itemId, data) {
    if (itemId.startsWith('minecraft:')) {
        if (itemId.endsWith('candle')) return `textures/blocks/candles/${itemId.split(/:(.*)/s)[1]}`
        else if (itemId.endsWith('glazed_terracotta')) return `textures/blocks/glazed_terracotta_${itemId.split('_glazed_terracotta')[0]}`
        else if (itemId.includes('deepslate')) return `textures/blocks/deepslate/${itemId.split(/:(.*)/s)[1]}`
        else if (itemId.includes('_door') && !['minecraft:wooden_door', 'minecraft:mangrove_door', 'minecraft:warped_door', 'minecraft:crimson_door'].includes(itemId))
            return `textures/blocks/door_${itemId.split(/:(.*)/s)[1].split('_door')[0]}_upper`
    }

    switch (itemId) {
        case 'minecraft:planks': return 'textures/blocks/planks_oak'
        case 'minecraft:ancient_debris': return 'textures/blocks/ancient_debris_side'
        case 'minecraft:anvil': return 'textures/blocks/anvil_top_damaged_0'
        case 'minecraft:azalea': return 'textures/blocks/azalea_plant'
        case 'minecraft:bamboo': return 'textures/items/bamboo'
        case 'minecraft:barrel': return 'textures/blocks/barrel_side'
        case 'minecraft:basalt': return 'textures/blocks/basalt_side'
        case 'minecraft:bed': return 'textures/blocks/bed_head_side'
        case 'minecraft:bee_nest': return 'textures/blocks/bee_nest_front'
        case 'minecraft:beehive': return 'textures/blocks/beehive_front'
        case 'minecraft:beetroot': return 'textures/items/beetroot'
        case 'minecraft:bell': return 'textures/blocks/bell_side'
        case 'minecraft:big_dripleaf': return 'textures/blocks/big_dripleaf_top'
        case 'minecraft:blast_furnace': return 'textures/blocks/blast_furnace_front_off'
        case 'minecraft:bone_block': return 'textures/blocks/bone_block_side'
        case 'minecraft:brick_block': return 'textures/blocks/brick'
        case 'minecraft:cactus': return 'textures/blocks/cactus_side'
        case 'minecraft:cake': return 'textures/items/cake'
        case 'minecraft:campfire': return 'textures/items/campfire'
        case 'minecraft:soul_campfire': return 'textures/items/soul_campfire'
        case 'minecraft:cartography_table': return 'textures/blocks/cartography_table_top'
        case 'minecraft:cauldron': return 'textures/items/cauldron'
        case 'minecraft:chain': return 'textures/items/chain'
        case 'minecraft:chest': return 'textures/blocks/chest_front'
        case 'minecraft:mossy_cobblestone': return 'textures/blocks/cobblestone_mossy'
        case 'minecraft:cobblestone_wall': return 'textures/blocks/cobblestone'
        case 'minecraft:composter': return 'textures/blocks/composter_side'
        case 'minecraft:concrete': return 'textures/blocks/concrete_white'
        case 'minecraft:concrete_powder': return 'textures/blocks/concrete_powder_white'
        case 'minecraft:conduit': return 'textures/blocks/conduit_open'
        case 'minecraft:coral': return 'textures/blocks/coral_plant_blue'
        case 'minecraft:coral_block': return 'textures/blocks/coral_blue'
        case 'minecraft:coral_fan': return 'textures/blocks/coral_fan_blue'
        case 'minecraft:coral_fan_dead': return 'textures/blocks/coral_fan_blue_dead'
        case 'minecraft:crafting_table': return 'textures/blocks/crafting_table_side'
        case 'minecraft:crimson_nylium': return 'textures/blocks/crimson_nylium_side'
        case 'minecraft:crimson_planks': return 'textures/blocks/huge_fungus/crimson_planks'
        case 'minecraft:crimson_trapdoor': return 'textures/blocks/huge_fungus/crimson_trapdoor'
        case 'minecraft:crimson_sign': return 'textures/items/sign_crimson'
        case 'minecraft:dark_prismarine': return 'textures/blocks/prismarine_dark'
        case 'minecraft:dark_oak_sign': return 'textures/blocks/planks_big_oak'
        case 'minecraft:daylight_detector': return 'textures/blocks/daylight_detector_top'
        case 'minecraft:podzol': return 'textures/blocks/dirt_podzol_top'
        case 'minecraft:dispenser': return 'textures/blocks/dispenser_front_horizontal'
        case 'minecraft:wooden_door': return 'textures/blocks/door_wood_upper'
        case 'minecraft:warped_door': return 'textures/blocks/huge_fungus/warped_door_top'
        case 'minecraft:mangrove_door': return 'textures/blocks/mangrove_door_top'
        case 'minecraft:crimson_door': return 'textures/blocks/huge_fungus/crimson_door_top'
        case 'minecraft:dried_kelp_block': return 'textures/blocks/dried_kelp_side_a'
        case 'minecraft:dropper': return 'textures/blocks/dropper_front_horizontal'
        case 'minecraft:enchanting_table': return 'textures/blocks/enchanting_table_side'
        case 'minecraft:ender_chest': return 'textures/blocks/ender_chest_front'
        case 'minecraft:fletching_table': return 'textures/blocks/fletching_table_side1'
        case 'minecraft:flowering_azalea': return 'textures/blocks/flowering_azalea_side'
        case 'minecraft:furnace': return 'textures/blocks/furnace_front_off'
        case 'minecraft:glass_pane': return 'textures/blocks/glass'
        case 'minecraft:glow_frame': return 'textures/items/glow_item_frame'
        case 'minecraft:grass': return 'textures/blocks/grass_side_carried'
        case 'minecraft:grindstone': return 'textures/blocks/grindstone_round'
        case 'minecraft:hay_block': return 'textures/blocks/hay_block_side'
        case 'minecraft:honey_block': return 'textures/blocks/honey_side'
        case 'minecraft:honeycomb_block': return 'textures/blocks/honeycomb'
        case 'minecraft:hopper': return 'textures/items/hopper'
        case 'minecraft:packed_ice': return 'textures/blocks/ice_packed'
        case 'minecraft:jukebox': return 'textures/blocks/jukebox_side'
        case 'minecraft:kelp': return 'textures/items/kelp'
        case 'minecraft:leaves2':
        case 'minecraft:leaves': return 'textures/blocks/leaves_oak_carried'
        case 'minecraft:lectern': return 'textures/blocks/lectern_front'
        case 'minecraft:lodestone': return 'textures/blocks/lodestone_side'
        case 'minecraft:log':
        case 'minecraft:log2': return 'textures/blocks/log_oak'
        case 'minecraft:loom': return 'textures/blocks/loom_front'
        case 'minecraft:mangrove_log': return 'textures/blocks/mangrove_log_side'
        case 'minecraft:mangrove_roots': return 'textures/blocks/mangrove_roots_side'
        case 'minecraft:melon_block': return 'textures/blocks/melon_side'
        case 'minecraft:muddy_mangrove_roots': return 'textures/blocks/muddy_mangrove_roots_side'
    }

    try {
        terminal.runCommandAsync(`fill ~~~ ~~~ ${itemId} ${data} replace bedrock`)
    }
    catch (e) {
        if (JSON.parse(e).blockName) {
            return `textures/blocks/${itemId.split(/:(.*)/s)[1]}`
        }

        return `textures/items/${itemId.split(/:(.*)/s)[1]}`
    }
}

function enchantListToArray(enchantList) {
    let arr = []

    for (const enchant in MinecraftEnchantmentTypes) {
        let level = enchantList.hasEnchantment(MinecraftEnchantmentTypes[enchant])
        if (level > 0) {
            arr.push({
                type: enchant,
                level: level
            })
        }
    }

    return arr
}

function enchantArrayToString(enchantArr) { // enchantArr being one generated by enchantListToArray
    let strArr = []

    for (const enchant of enchantArr) {
        strArr.push(`${enchant.type} ${enchant.level}`)
    }

    return strArr.join(', ')
}

function addItemToStorage(storageEntity, item) {
    let tag = getTerminalData(storageEntity)
    let json = JSON.parse(tag)

    // use these constants to tell the uniqueness of items
    let itemKey = JSON.stringify([
        item.typeId, // id
        item.data, // data
        item.nameTag, // nametag
        item.getLore(), // lore
        enchantListToArray(item.getComponent('enchantments').enchantments) // enchants
    ])

    if (json.items[itemKey]) {
        json.items[itemKey].amount += item.amount
    }
    else {
        json.items[itemKey] = {
            amount: item.amount
        }
    }

    setTerminalData(storageEntity, JSON.stringify(json), tag)
}

function setStorageItemAmount(storageEntity, itemKey, newAmount) {
    let tag = getTerminalData(storageEntity)
    let json = JSON.parse(tag)

    if (newAmount > 0) {
        json.items[itemKey].amount = newAmount
    }
    else {
        delete json.items[itemKey]
    }

    setTerminalData(storageEntity, JSON.stringify(json), tag)
}

function getItemStackFromKey(parsedKey) {
    let item = new ItemStack(Items.get(parsedKey[0]), 1, parsedKey[1])
    item.nameTag = parsedKey[2]
    item.setLore(parsedKey[3])

    let enchants = item.getComponent('enchantments')
    let enchantments = enchants.enchantments
    for (const enchant of parsedKey[4]) {
        enchantments.addEnchantment(new Enchantment(MinecraftEnchantmentTypes[enchant.type], enchant.level))
    }
    enchants.enchantments = enchantments // no clue why this works

    return item
}

function giveItemStack(player, parsedKey, amount) {
    for (let i = 0; i < amount; i++) {
        let item = getItemStackFromKey(parsedKey)

        let inventory = player.getComponent('inventory').container
        if (inventory.emptySlotsCount > 0) {
            inventory.addItem(item)
        }
        else {
            player.dimension.spawnItem(item, player.location)
        }
    }
}

function queryStorageItems(search, player, storageEntity, page = 0) {
    let searchItemName = ''
    const namespaces = []

    for (const s of search.toLowerCase().split(' ')) {
        if (s.startsWith('@')) {
            namespaces.push(s.slice(1))
            continue
        }
        else if (s.startsWith('\\@')) {
            searchItemName += ' @'
            continue
        }

        searchItemName += ` ${s}`
    }

    searchItemName = searchItemName.trim()

    const items = []

    for (const entry of Object.entries(JSON.parse(getTerminalData(storageEntity)).items)) {
        const key = JSON.parse(entry[0])
        if ((!searchItemName || getItemName(storageEntity, key[0], key[1]).toLowerCase().includes(searchItemName)) && (namespaces.length === 0 || namespaces.includes(key[0].split(':')[0])))
            items.push(entry)
    }

    const messageArr = []
    if (searchItemName) messageArr.push(`%\storageSystem.ui.search.resultsPage.label2 ${searchItemName}`)
    if (namespaces.length > 0) messageArr.push(`%\storageSystem.ui.search.resultsPage.label3 ${namespaces.join(', ')}`)

    showStorage(player, storageEntity, page, items, `%\storageSystem.ui.search.resultsPage.label1 ${messageArr.join(' ')}:`, () => {
        queryStorageItems(searchQuery, player, storageEntity, page)
    })
}

function showSearch(player, storageEntity, page) {
    new ModalFormData()
        .title('%\storageSystem.ui.title')
        .textField('%\storageSystem.ui.search.label', '%\storageSystem.ui.search.placeholder')
        .show(player).then(response => {
            if (!response.isCanceled) {
                queryStorageItems(response.formValues[0], player, storageEntity)
                return
            }

            showStorage(player, storageEntity, page)
        })
}

function showSecurityOptions(player, terminal, page) {
    const menu = new ActionFormData()
        .title('%\storageSystem.ui.title')
        .body('%\storageSystem.ui.whitelist.title')
        .button('%\storageSystem.ui.whitelist.addPlayer', 'textures/ui/plus')

    const tag = getTerminalData(terminal)
    const data = JSON.parse(tag)

    for (const player of data.security.whitelist) menu.button(player, Math.floor(Math.random() * 2) === 0 ? 'textures/ui/icon_steve' : 'textures/ui/icon_alex')

    menu.show(player).then(response => {
        if (response.isCanceled) {
            showOptions(player, terminal, page)
            return
        }

        if (response.selection === 0) {

            new ModalFormData()
                .title('%\storageSystem.ui.title')
                .textField('%\storageSystem.ui.whitelist.addPlayerPage.textField.label', '%\storageSystem.ui.whitelist.addPlayerPage.textField.placeholder')
                .show(player).then(response2 => {
                    if (response2.isCanceled) {
                        showSecurityOptions(player, terminal, page)
                        return
                    }

                    const input = response2.formValues[0]

                    if (!input) {
                        player.runCommandAsync('tellraw @s { "rawtext": [ { "translate": "storageSystem.security.whitelist.invalidString" } ] }')
                        return
                    }

                    if (data.security.owner === input || data.security.whitelist.includes(input)) {
                        player.runCommandAsync(`tellraw @s { "rawtext": [ {"translate": "storageSystem.security.whitelist.alreadyWhitelisted", "with": { "rawtext": [ {"text": "${input}"} ] } } ] }`)
                        return
                    }

                    data.security.whitelist.push(response2.formValues[0])
                    setTerminalData(terminal, JSON.stringify(data), tag)
                    showSecurityOptions(player, terminal, page)
                })

            return
        }

        data.security.whitelist.splice(response.selection - 1, 1)
        setTerminalData(terminal, JSON.stringify(data), tag)
        showSecurityOptions(player, terminal, page)
    })
}

function showOptions(player, terminal, page) {
    new ActionFormData()
        .title('%\storageSystem.ui.title')
        .body('%\storageSystem.ui.options')
        .button('%\storageSystem.ui.pickUp', 'textures/ui/realms_red_x')
        .button('%\storageSystem.ui.security', 'textures/items/security_upgrade')
        .show(player).then(response => {
            if (response.isCanceled) {
                showStorage(player, terminal, page)
                return
            }

            const tag = getTerminalData(terminal)
            const data = JSON.parse(tag)

            switch (response.selection) {
                case 0:
                    if (Object.keys(data.items).length > 0) {
                        const item = new ItemStack(Items.get('astoragenet:used_storage_drive'))
                        item.setLore([JSON.stringify(data.items)])
                        player.dimension.spawnItem(item, player.location)
                    }

                    if (data.security) player.dimension.spawnItem(new ItemStack(Items.get('astoragenet:security_upgrade')), player.location)

                    player.dimension.spawnItem(new ItemStack(Items.get('astoragenet:storage_terminal')), player.location)
                    terminal.kill()
                    break

                case 1:
                    if (!data.security) {
                        player.runCommandAsync('tellraw @s { "rawtext": [ { "translate": "storageSystem.action.requiresSecurityUpgrade" } ] }')
                        break
                    }

                    showSecurityOptions(player, terminal, page)
                    break
            }
        })
}

function showStorage(player, storageEntity, page = 0, items = null, menuBody = '', requestCallback = null) { // requestCallback being whats called after requesting an item
    if (!items) {
        items = Object.entries(JSON.parse(getTerminalData(storageEntity)).items)
    }

    const itemsInPage = items.slice(page * 10, (page + 1) * 10)

    let menu = new ActionFormData()
        .title('%\storageSystem.ui.title')
        .body(menuBody)

    if (itemsInPage.length > 0) {
        menu.button('%\storageSystem.ui.options', 'textures/ui/settings_glyph_color_2x')
        menu.button('%\storageSystem.ui.search.label', 'textures/ui/magnifyingGlass')

        let itemNames = []

        for (const [key, item] of itemsInPage) {
            let keyArr = JSON.parse(key)

            let name = `${getItemName(storageEntity, keyArr[0], keyArr[1])}${(keyArr[2]) ? ` "${keyArr[2]}"` : ''}${(keyArr[4].length > 0) ? `\n§8${enchantArrayToString(keyArr[4])}` : ''}${(keyArr[3].length > 0) ? `\n§5${keyArr[3].join(', ')}` : ''}`
            itemNames.push(name)

            menu.button(`${abbreviateNumber(item.amount)} ${name}`, getItemTexture(storageEntity, keyArr[0], keyArr[1]))
        }

        menu.button('%\storageSystem.ui.prevPage', 'textures/ui/arrowLeft')
        menu.button('%\storageSystem.ui.nextPage', 'textures/ui/arrowRight')

        menu.show(player).then(response => {
            if (!response.isCanceled) {

                if (response.selection === 0) {
                    showOptions(player, storageEntity, page)
                    return
                }
                else if (response.selection === 1) {
                    showSearch(player, storageEntity, page)
                    return
                }
                else if (response.selection === itemNames.length + 2) {
                    showStorage(player, storageEntity, page - 1, items, menuBody, requestCallback)
                    return
                }
                else if (response.selection === itemNames.length + 3) {
                    showStorage(player, storageEntity, page + 1, items, menuBody, requestCallback)
                    return
                }

                let selection = response.selection - 2

                let item = itemsInPage[selection][1]
                let key = itemsInPage[selection][0]
                let parsedKey = JSON.parse(key)

                new ModalFormData()
                    .title('%\storageSystem.ui.title')
                    .slider(`%XstorageSystem.ui.requestItemPage.slider.label1 \n${itemNames[selection]}\n§r--\n%XstorageSystem.ui.requestItemPage.slider.label2`, 1, item.amount, 1, Math.min(Math.round(item.amount / 2), 64))
                    .textField('%\storageSystem.ui.requestItemPage.textField', '')
                    .show(player).then(modalResponse => {
                        if (!modalResponse.isCanceled) {
                            let parsedTextField = parseInt(modalResponse.formValues[1])
                            let amountRequested = (parsedTextField && parsedTextField <= item.amount && parsedTextField > 0) ? parsedTextField : modalResponse.formValues[0]
                            giveItemStack(player, parsedKey, amountRequested)
                            setStorageItemAmount(storageEntity, key, item.amount - amountRequested)
                        }

                        if (requestCallback) {
                            requestCallback()
                            return
                        }

                        showStorage(player, storageEntity, page)
                    })
            }
        })
    }
    else {
        menu.body('%\storageSystem.ui.empty')
        menu.button('%\storageSystem.ui.options', 'textures/ui/settings_glyph_color_2x')
        menu.show(player).then(response => {
            if (!response.isCanceled) {
                showOptions(player, storageEntity, 0)
            }
        })
    }
}

function showWirelessInterfaceUI(player, terminal) {
    new ActionFormData()
        .title('%wirelessInterface.ui.title')
        .button('%wirelessInterface.ui.inputItem')
        .button('%wirelessInterface.ui.requestItem')
        .show(player).then(response => {
            if (!response.isCanceled) {
                const inventory = player.getComponent('inventory').container

                if (response.selection === 0) {
                    const menu = new ActionFormData()
                        .title('%wirelessInterface.ui.title')
                        .body('%wirelessInterface.ui.inventory.label')

                    const items = []

                    for (let i = 0; i < inventory.size; i++) {
                        const item = inventory.getItem(i)

                        if (item) {
                            items.push(i)
                            menu.button(`${item.amount} ${getItemName(terminal, item.typeId, item.data)}`, getItemTexture(terminal, item.typeId, item.data))
                        }
                    }

                    menu.show(player).then(response2 => {
                        if (response2.isCanceled) {
                            showWirelessInterfaceUI(player, terminal)
                            return
                        }

                        const itemIndex = items[response2.selection]
                        addItemToStorage(terminal, inventory.getItem(itemIndex))
                        inventory.setItem(itemIndex, new ItemStack(MinecraftItemTypes.acaciaBoat, 0))
                        showWirelessInterfaceUI(player, terminal)
                    })
                }
                else {
                    showStorage(player, terminal)
                }
            }
        })
}

world.events.entityHit.subscribe(e => {
    if (e.entity.typeId === 'minecraft:player' && e.hitEntity) {

        // interacting with terminal
        if (e.hitEntity.typeId === 'astoragenet:storage_terminal') {
            const tag = getTerminalData(e.hitEntity)
            const data = JSON.parse(tag)

            if (data.security && data.security.owner != e.entity.name && !data.security.whitelist.includes(e.entity.name)) {
                e.entity.runCommandAsync('tellraw @s { "rawtext": [ { "translate": "storageSystem.security.locked" } ] }')
                return
            }

            if (e.entity.isSneaking) { // if sneaking put held item into storage
                let inventory = e.entity.getComponent('inventory').container
                let heldItem = inventory.getItem(e.entity.selectedSlot)
                if (heldItem) {
                    if (heldItem.typeId === 'astoragenet:used_storage_drive') {
                        if (heldItem.getLore().length === 1) {
                            Object.assign(data.items, JSON.parse(heldItem.getLore()[0]))
                            setTerminalData(e.hitEntity, JSON.stringify(data), tag)

                            inventory.setItem(e.entity.selectedSlot, new ItemStack(MinecraftItemTypes.acaciaBoat, 0))
                        }
                        else {
                            e.entity.runCommandAsync('tellraw @s { "rawtext": [ { "translate": "storageSystem.action.unableToReadDriveData" } ] }')
                        }
                    }
                    else if (heldItem.typeId === 'astoragenet:wireless_interface') {
                        heldItem.setLore([`[${e.hitEntity.location.x}, ${e.hitEntity.location.y}, ${e.hitEntity.location.z}]`])
                        inventory.setItem(e.entity.selectedSlot, heldItem)
                        e.entity.runCommandAsync('tellraw @s {"rawtext": [{"translate": "wirelessInterface.connected"}]}')
                    }
                    else if (heldItem.typeId === 'astoragenet:security_upgrade') {
                        if (data.security) {
                            e.entity.runCommandAsync('tellraw @s {"rawtext": [{"translate": "storageSystem.action.alreadyHasUpgrade"}]}')
                            return
                        }

                        data.security = {
                            owner: e.entity.name,
                            whitelist: []
                        }
                        setTerminalData(e.hitEntity, JSON.stringify(data), tag)

                        inventory.setItem(e.entity.selectedSlot, new ItemStack(MinecraftItemTypes.acaciaBoat, 0))
                    }
                    else {
                        inventory.setItem(e.entity.selectedSlot, new ItemStack(MinecraftItemTypes.acaciaBoat, 0))
                        addItemToStorage(e.hitEntity, heldItem)
                    }
                }

                return
            }

            showStorage(e.entity, e.hitEntity)
        }

        // setting export bus item
        else if (e.hitEntity.typeId === 'astoragenet:terminal_export_bus') {
            let inventory = e.entity.getComponent('inventory').container
            let heldItem = inventory.getItem(e.entity.selectedSlot)
            if (heldItem) {
                let tag = getTerminalData(e.hitEntity)
                let json = JSON.parse(tag)

                json.item = {
                    id: heldItem.typeId,
                    data: heldItem.data
                }

                setTerminalData(e.hitEntity, JSON.stringify(json), tag)

                e.entity.runCommandAsync(`tellraw @s { "rawtext": [ { "translate": "exportBus.itemSet", "with": {"rawtext": [{"text": "${getItemName(e.hitEntity, heldItem.typeId, heldItem.data)}"}]} } ] }`)
            }
        }

        else if (e.hitEntity.typeId === 'astoragenet:level_emitter') {
            const tag = getTerminalData(e.hitEntity)
            const data = JSON.parse(tag)

            if (e.entity.isSneaking) {
                const inventory = e.entity.getComponent('inventory').container
                const heldItem = inventory.getItem(e.entity.selectedSlot)
                if (heldItem) {
                    data.item = {
                        id: heldItem.typeId,
                        data: heldItem.data
                    }

                    setTerminalData(e.hitEntity, JSON.stringify(data), tag)

                    e.entity.runCommandAsync(`tellraw @s { "rawtext": [ { "translate": "levelEmitter.itemSet", "with": {"rawtext": [{"text": "${getItemName(e.hitEntity, heldItem.typeId, heldItem.data)}"}]} } ] }`)
                }

                return
            }

            const levelEmitter = e.hitEntity
            const player = e.entity

            new ModalFormData()
                .dropdown(`%levelEmitter.ui.body${(data.item) ? `\n\n%levelEmitter.ui.currentItem.label ${getItemName(levelEmitter, data.item.typeId, data.item.data)}` : ''}\n\n%levelEmitter.ui.operator.label`, ['<', '>', '==', '!='], data.operator)
                .textField('%levelEmitter.ui.amount.label', '%levelEmitter.ui.amount.placeholder', data.amount.toString())
                .show(player).then(response => {
                    data.operator = response.formValues[0]
                    data.amount = parseInt(response.formValues[1])

                    if (isNaN(data.amount)) {
                        player.runCommandAsync(`tellraw @s { "rawtext": [ { "translate": "levelEmitter.invalidAmount" } ] }`)
                        return
                    }

                    setTerminalData(levelEmitter, JSON.stringify(data), tag)
                })
        }
    }
})

world.events.dataDrivenEntityTriggerEvent.subscribe(e => {
    if (e.entity.typeId === 'astoragenet:storage_terminal') setTerminalData(e.entity, '{"version":2,"items":{}}', null)

    else if (e.typeId === 'astoragenet:tick') {
        if (e.entity.typeId === 'astoragenet:level_emitter') {
            const data = JSON.parse(getTerminalData(e.entity))

            if (data.item) {
                const query = { location: e.entity.location, closest: 1, maxDistance: 30, type: 'astoragenet:storage_terminal' }
                const terminal = [...overworld.getEntities(query)][0]

                if (terminal) {
                    let amount = 0
                    for (const [key_, item] of Object.entries(JSON.parse(getTerminalData(terminal)).items)) {
                        const key = JSON.parse(key_)

                        if (key[0] === data.item.typeId && key[1] === data.item.data) {
                            amount += item.amount
                        }
                    }

                    if (data.operator === 0 && amount < data.amount) {
                        e.entity.triggerEvent('astoragenet:turn_on')
                        return
                    }
                    else if (data.operator === 1 && amount > data.amount) {
                        e.entity.triggerEvent('astoragenet:turn_on')
                        return
                    }
                    else if (data.operator === 2 && amount === data.amount) {
                        e.entity.triggerEvent('astoragenet:turn_on')
                        return
                    }
                    else if (data.operator === 3 && amount != data.amount) {
                        e.entity.triggerEvent('astoragenet:turn_on')
                        return
                    }

                    e.entity.triggerEvent('astoragenet:turn_off')
                }
            }
        }
        else if (e.entity.typeId === 'astoragenet:terminal_import_bus' && !fl.isRedstonePowered(fl.locationToBlockLocation(e.entity.location), e.entity.dimension)) {
            let chest = e.entity.dimension.getBlock(new BlockLocation(Math.floor(e.entity.location.x), e.entity.location.y + 1, Math.floor(e.entity.location.z)))
            if (chest.getComponent('inventory')) { // check if block has an inventory (is a chest-like object)
                let inventory = chest.getComponent('inventory').container

                let query = { location: e.entity.location, closest: 1, maxDistance: 30, type: 'astoragenet:storage_terminal' }
                let terminal = [...e.entity.dimension.getEntities(query)][0]

                if (terminal) {
                    for (let i = 0; i < inventory.size; i++) {
                        let item = inventory.getItem(i)
                        if (item) {
                            inventory.setItem(i, new ItemStack(MinecraftItemTypes.acaciaBoat, 0))
                            addItemToStorage(terminal, item)
                        }
                    }
                }
            }
        }
        else if (e.entity.typeId === 'astoragenet:terminal_export_bus' && !fl.isRedstonePowered(fl.locationToBlockLocation(e.entity.location), e.entity.dimension)) {
            let json = JSON.parse(getTerminalData(e.entity))

            if (json.item) {
                let query = { location: e.entity.location, closest: 1, maxDistance: 30, type: 'astoragenet:storage_terminal' }
                let terminal = [...e.entity.dimension.getEntities(query)][0]
                if (terminal) {
                    for (const [key_, item] of Object.entries(JSON.parse(getTerminalData(terminal)).items)) {
                        let key = JSON.parse(key_)

                        if (key[0] === json.item.typeId && key[1] === json.item.data) {
                            const chest = e.entity.dimension.getBlock(new BlockLocation(Math.floor(e.entity.location.x), e.entity.location.y + 1, Math.floor(e.entity.location.z)))
                            const chestInv = chest.getComponent('inventory')

                            if (chestInv && chestInv.container.emptySlotsCount > 0) chestInv.container.addItem(getItemStackFromKey(key))
                            else e.entity.dimension.spawnItem(getItemStackFromKey(key), e.entity.location)

                            setStorageItemAmount(terminal, key_, item.amount - 1)
                        }
                    }
                }
            }
        }
    }
    else if (e.typeId === 'minecraft:entity_spawned') {
        if (e.entity.typeId === 'astoragenet:terminal_export_bus') setTerminalData(e.entity, '{"version":1,"item":null}', null)
        else if (e.entity.typeId === 'astoragenet:level_emitter') setTerminalData(e.entity, '{"version":1,"item":null,"operator":0,"amount":0}', null)
    }
})

world.events.tick.subscribe(() => {
    const query = { families: ['level_emitter_on'] }

    for (const levelEmitter of overworld.getEntities(query)) fl.emitRedstoneSignal(fl.locationToBlockLocation(levelEmitter.location), levelEmitter.dimension)
})

world.events.entityCreate.subscribe(e => {
    if (e.entity.typeId === 'astoragenet:storage_terminal' && terminalNeedsUpdate(e.entity)) updateTerminalJson(e.entity)
})

world.events.itemUse.subscribe(e => {
    if (e.item.typeId === 'astoragenet:wireless_interface') {
        if (e.item.getLore().length != 1) {
            e.source.runCommandAsync('tellraw @s {"rawtext": [{"translate": "wirelessInterface.notConnected"}]}')
            return
        }

        const locationArr = JSON.parse(e.item.getLore()[0])

        const query = { maxDistance: 1, closest: 1, type: 'astoragenet:storage_terminal' }
        query.location = new Location(locationArr[0], locationArr[1], locationArr[2])

        const terminal = [...e.source.dimension.getEntities(query)][0]

        if (!terminal) {
            e.source.runCommandAsync('tellraw @s {"rawtext": [{"translate": "wirelessInterface.unableToFindConnectedSystem"}]}')
            return
        }

        const data = JSON.parse(getTerminalData(terminal))
        if (data.security && data.security.owner != e.source.name && !data.security.whitelist.includes(e.source.name)) {
            e.source.runCommandAsync('tellraw @s { "rawtext": [ { "translate": "storageSystem.security.locked" } ] }')
            return
        }

        showWirelessInterfaceUI(e.source, terminal)
    }
})