import { world, MinecraftItemTypes, ItemStack, Items, MinecraftEnchantmentTypes, Enchantment, BlockLocation, EntityQueryOptions, Location, MinecraftDimensionTypes } from 'mojang-minecraft'
import { ActionFormData, ModalFormData } from 'mojang-minecraft-ui'
import * as fl from './fluffylib.js'

// added storage system security upgrade
// added options menu to storage system
// added storage system level emitter
// replaced interact to pick up with pick up button in storage system options
// import and export buses now stop when receiving a redstone signal
// export buses now show real item name instead of item id when setting the export item
// chat messages are now translated to all languages
// made various small changes and optimizations

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) {
    const data = JSON.parse(getTerminalData(terminal))
    return data.version != 1
}

function updateTerminalJson(terminal) {
    let newJson

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

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

        for (const [key_, value] of Object.entries(oldJson)) {
            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)
    }

    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) {
    try {
        return terminal.runCommand(`replaceitem entity @s slot.inventory 0 ${itemId} 1 ${data}`).itemName
    }
    catch {
        return `${itemId.split(/:(.*)/s)[1].replaceAll('_', ' ')} (${data})`
    }
}

function getItemTexture(terminal, itemId, data) {
    try {
        terminal.runCommand(`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({
                id: enchant,
                level: level
            })
        }
    }

    return arr
}

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

    for (const enchant of enchantArr) {
        strArr.push(`${enchant.id} ${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.id, // 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,
            name: getItemName(storageEntity, item.id, item.data),
            texture: getItemTexture(storageEntity, item.id, item.data)
        }
    }

    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.id], 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 searchQuery = search.toLowerCase()
    let items = []

    for (const entry of Object.entries(JSON.parse(getTerminalData(storageEntity)).items)) {
        if (entry[1].name.toLowerCase().includes(searchQuery)) {
            items.push(entry)
        }
    }

    showStorage(player, storageEntity, page, items, `Items related to ${searchQuery}:`, () => {
        queryStorageItems(searchQuery, player, storageEntity, page)
    })
}

function showSearch(player, storageEntity, page) {
    new ModalFormData()
        .title('Storage System')
        .textField('Search', 'Search query')
        .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('Storage System')
        .body('Whitelisted Players:')
        .button('Whitelist Player', 'textures/ui/plus')

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

    for (const player of data.security.whitelist) menu.button(player)

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

        if (response.selection == 0) {

            new ModalFormData()
                .title('Storage System')
                .textField('Whitelist player:', 'Player name (case-sensitive)')
                .show(player).then(response2 => {
                    if (response2.isCanceled) {
                        showSecurityOptions(player, terminal, page)
                        return
                    }

                    const input = response2.formValues[0]

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

                    if (data.security.owner == input || data.security.whitelist.includes(input)) {
                        player.runCommand(`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('Storage System')
        .body('Options')
        .button('Pick Up', 'textures/ui/realms_red_x')
        .button('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.runCommand('tellraw @s { "rawtext": [ { "translate": "storageSystem.action.requiresSecurityUpgrade" } ] }')
                        return
                    }

                    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('Storage System')
        .body(menuBody)

    if (itemsInPage.length > 0) {
        menu.button('Options', 'textures/ui/settings_glyph_color_2x')
        menu.button('Search', 'textures/ui/magnifyingGlass')

        let itemNames = []

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

            let name = `${item.name}${(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}`, item.texture)
        }

        menu.button('Previous Page', 'textures/ui/arrowLeft')
        menu.button('Next Page', '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('Storage System')
                    .slider(`Requesting:\n${itemNames[selection]}\n§r--\nHow many of this item to request?`, 1, item.amount, 1, Math.min(Math.round(item.amount / 2), 64))
                    .textField('Or type a number here (if this field is invalid, the slider will be used)', '')
                    .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('No items to display. Sneak and punch while holding an item to add it')
        menu.button('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('Storage System Wireless Interface')
        .button('Input Item')
        .button('Request Item')
        .show(player).then(response => {
            if (!response.isCanceled) {
                const inventory = player.getComponent('inventory').container

                if (response.selection == 0) {
                    const menu = new ActionFormData()
                        .title('Storage System Wireless Interface')
                        .body('Items in your inventory')

                    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.id, item.data)}`, getItemTexture(terminal, item.id, 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.air, 0))
                        showWirelessInterfaceUI(player, terminal)
                    })
                }
                else {
                    showStorage(player, terminal)
                }
            }
        })
}

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

        // interacting with terminal
        if (e.hitEntity.id == '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.runCommand('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.id == '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.air, 0))
                        }
                        else {
                            e.entity.runCommand('tellraw @s { "rawtext": [ { "translate": "storageSystem.action.unableToReadDriveData" } ] }')
                        }
                    }
                    else if (heldItem.id == '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.runCommand('tellraw @s {"rawtext": [{"translate": "wirelessInterface.connected"}]}')
                    }
                    else if (heldItem.id == 'astoragenet:security_upgrade') {
                        if (data.security) {
                            e.entity.runCommand('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.air, 0))
                    }
                    else {
                        inventory.setItem(e.entity.selectedSlot, new ItemStack(MinecraftItemTypes.air, 0))
                        addItemToStorage(e.hitEntity, heldItem)
                    }
                }

                return
            }

            showStorage(e.entity, e.hitEntity)
        }

        // setting export bus item
        else if (e.hitEntity.id == '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.id,
                    data: heldItem.data
                }

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

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

        else if (e.hitEntity.id == '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.id,
                        data: heldItem.data
                    }

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

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

                return
            }

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

            new ModalFormData()
                .dropdown(`Sneak and punch while holding an item to set the item.${(data.item) ? `\n\nCurrent Item: ${getItemName(levelEmitter, data.item.id, data.item.data)}` : ''}\n\nOperator:`, ['<', '>', '==', '!='], data.operator)
                .textField('Amount:', 'Integer amount', data.amount.toString())
                .show(player).then(response => {
                    data.operator = response.formValues[0]
                    data.amount = parseInt(response.formValues[1])

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

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

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

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

            if (data.item) {
                const query = new EntityQueryOptions()
                query.location = e.entity.location
                query.closest = 1
                query.maxDistance = 30
                query.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.id && 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.id == '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 = new EntityQueryOptions()
                query.location = e.entity.location
                query.closest = 1
                query.maxDistance = 30
                query.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.air, 0))
                            addItemToStorage(terminal, item)
                        }
                    }
                }
            }
        }
        else if (e.entity.id == '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 = new EntityQueryOptions()
                query.location = e.entity.location
                query.closest = 1
                query.maxDistance = 30
                query.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.id && key[1] == json.item.data) {
                            e.entity.dimension.spawnItem(getItemStackFromKey(key), e.entity.location)
                            setStorageItemAmount(terminal, key_, item.amount - 1)
                        }
                    }
                }
            }
        }
    }
    else if (e.id == 'minecraft:entity_spawned') {
        if (e.entity.id == 'astoragenet:terminal_export_bus') setTerminalData(e.entity, '{"version":1,"item":null}', null)
        else if (e.entity.id == 'astoragenet:level_emitter') setTerminalData(e.entity, '{"version":1,"item":null,"operator":0,"amount":0}', null)
    }
})

world.events.tick.subscribe(() => {
    const query = new EntityQueryOptions()
    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.id == 'astoragenet:storage_terminal' && terminalNeedsUpdate(e.entity)) updateTerminalJson(e.entity)
})

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

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

        const query = new EntityQueryOptions()
        query.maxDistance = 1
        query.closest = 1
        query.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.runCommand('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.runCommand('tellraw @s { "rawtext": [ { "translate": "storageSystem.security.locked" } ] }')
            return
        }

        showWirelessInterfaceUI(e.source, terminal)
    }
})