import { BlockVolume, EnchantmentType, GameMode, ItemStack, system, world } from "@minecraft/server";
import { bottomCenter, createVector, sendDebugMessage, vectorAdd, vectorSubtract } from "./JMmcUtils";
import { ActionFormData } from "@minecraft/server-ui";

// for reference, the pricing data used is located here: https://docs.google.com/spreadsheets/d/17DSVQYIzSVwmd4TwBKI8mJCaMfwDCrQXPlCMAdoQPvE/edit?gid=0#gid=0
// the basic formula is 
// {weight / total weight of enchant category (armors, swords, fishing poles, etc.)} * level cost multiplier = price
// this would be very easy to implement as a dynamic solution rather than the implemented lookup method,
// and I would have preferred to do that as it is more futureproofed;
// but somebody wanted to use a spreadsheet instead of letting javascript do math 🤷‍♂️

var MinecraftEnchantmentTypes = [
    {id:"minecraft:aqua_affinity",key:"waterWorker",pricing:[103,171,240,308]},
    {id:"minecraft:bane_of_arthropods",key:"damage.arthropods",pricing:[168,279,391,502,614]},
    // {id:"minecraft:binding",key:"curse.binding"pricing:[a gazillion]},
    {id:"minecraft:blast_protection",key:"protect.explosion",pricing:[513,855,1197,1539]},
    {id:"minecraft:breach",key:"heavy_weapon.breach",pricing:[149,248,347,446]},
    {id:"minecraft:channeling",key:"tridentChanneling",pricing:[270]},
    {id:"minecraft:density",key:"heavy_weapon.density",pricing:[119,198,277,356,436]},
    {id:"minecraft:depth_strider",key:"waterWalker",pricing:[513,855,1197]},
    {id:"minecraft:efficiency",key:"digging",pricing:[35,59,82,105,129]},
    {id:"minecraft:feather_falling",key:"protect.fall",pricing:[205,342,479,616]},
    {id:"minecraft:fire_aspect",key:"fire",pricing:[419,698]},
    {id:"minecraft:fire_protection",key:"protect.fire",pricing:[205,342,479,616]},
    {id:"minecraft:flame",key:"arrowFire",pricing:[203]},
    {id:"minecraft:fortune",key:"lootBonusDigger",pricing:[176,293,410]},
    {id:"minecraft:frost_walker",key:"frostwalker",pricing:[513,855]},
    {id:"minecraft:impaling",key:"tridentImpaling",pricing:[135,255,315,405,495]},
    {id:"minecraft:infinity",key:"arrowInfinite",pricing:[405]},
    {id:"minecraft:knockback",key:"knockback",pricing:[167,279,361,502,614]},
    {id:"minecraft:looting",key:"lootBonus",pricing:[419,698,977]},
    {id:"minecraft:loyalty",key:"tridentLoyalty",pricing:[54,90,126]},
    {id:"minecraft:luck_of_the_sea",key:"lootBonusFishing",pricing:[54,90,126]},
    {id:"minecraft:lure",key:"fishingSpeed",pricing:[54,90,126]},
    {id:"minecraft:mending",key:"mending",pricing:[1823]},
    {id:"minecraft:multishot",key:"crossbowMultishot",pricing:[230]},
    {id:"minecraft:piercing",key:"crossbowPiercing",pricing:[92,153,214]},
    {id:"minecraft:power",key:"arrowDamage",pricing:[41,68,95,122,149]},
    {id:"minecraft:projectile_protection",key:"protect.projectile",pricing:[205,342,479,616]},
    {id:"minecraft:protection",key:"protect.all",pricing:[103,171,239,309]},
    {id:"minecraft:punch",key:"arrowKnockback",pricing:[203,338]},
    {id:"minecraft:quick_charge",key:"crossbowQuickCharge",pricing:[92,153,214]},
    {id:"minecraft:respiration",key:"oxygen",pricing:[513,855,1197]},
    {id:"minecraft:riptide",key:"tridentRiptide",pricing:[135,225,315]},
    {id:"minecraft:sharpness",key:"damage.all",pricing:[84,140,195,251,307]},
    {id:"minecraft:silk_touch",key:"untouching",pricing:[351]},
    {id:"minecraft:smite",key:"damage.undead",pricing:[167,279,391,502,614]},
    {id:"minecraft:soul_speed",key:"soul_speed",pricing:[1026,1710,2394]},
    {id:"minecraft:swift_sneak",key:"swift_sneak",pricing:[1026,1710,2394]},
    {id:"minecraft:thorns",key:"thorns",pricing:[1026,1710,2394]},
    {id:"minecraft:unbreaking",key:"durability",pricing:[729,1215,1701]},
    // {id:"minecraft:vanishing",key:"curse.vanishing",pricing:[a billion]},
    {id:"minecraft:wind_burst",key:"heavy_weapon.windburst",pricing:[594,990]}
]
var MinecraftDimensionTypes = ((MinecraftDimensionTypes2) => {
    MinecraftDimensionTypes2["Nether"] = "minecraft:nether";
    MinecraftDimensionTypes2["Overworld"] = "minecraft:overworld";
    MinecraftDimensionTypes2["TheEnd"] = "minecraft:the_end";
    return MinecraftDimensionTypes2;
  })(MinecraftDimensionTypes || {})

world.afterEvents.entitySpawn.subscribe(event => {
    const entity = event.entity
    if (entity.typeId !== "lb_ga:bookshelf_golem") return
    sendDebugMessage(`${entity.typeId} spawned, applying properties...`)
    BS_seed(entity)
    BS_stock(entity)
})

function BS_foundTable(entity) {
    const foundProperty = "lb_ga:bookshelf_has_found_table"
    if (!entity.getDynamicPropertyIds().includes(foundProperty)) entity.setDynamicProperty(foundProperty,JSON.stringify(false))

    function getState() {
        return JSON.parse(entity.getDynamicProperty(foundProperty))
    }

    function find() {
        entity.setDynamicProperty(foundProperty,JSON.stringify(true))
    }

    return {
        hasFound: function() {
            return getState()
        },
        find: function() {
            return find()
        }
    }
}

function BS_stock(entity) {
    const stockProperty = "lb_ga:bookshelf_stock_length"
    if (!entity.getDynamicPropertyIds().includes(stockProperty)) setStock(4)
    
    function getStock() {
        return JSON.parse(entity.getDynamicProperty(stockProperty))
    }

    function upgrade() {
        const curStockAmount = getStock()
        if (curStockAmount < MinecraftEnchantmentTypes.length) {
            setStock(curStockAmount + 1)
            entity.dimension.playSound("random.levelup",entity.location)
        }
    }

    function setStock(newStock) {
        sendDebugMessage(`setting ${entity.typeId}'s stock count to ${JSON.stringify(newStock)}`)
        entity.setDynamicProperty(stockProperty,JSON.stringify(newStock))
    }

    function stockArray(length) {
        let seed = BS_seed(entity).getSeed()
        function seededRandom() {
            const x = Math.sin(seed++) * 10000
            return x - Math.floor(x)
        }

        // shuffle
        const shuffledArray = [...MinecraftEnchantmentTypes]
        for (let i = shuffledArray.length - 1; i > 0; i--) {
            // curiously, this line MUST have a semicolon at the end, otherwise it gets misinterpreted
            // and it tries to run both of these lines as one
            const j = Math.floor(seededRandom(seed + i) * (i + 1));
            [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]]
        }
    
        return shuffledArray.slice(0, Math.floor(length + 4));
    }
    return {
        level: function() {
            return getStock()
        },
        get: function(length) {
            return stockArray(length)
        },
        upgrade: function() {
            return upgrade()
        }
    }
}

function BS_seed(entity) {
    const seedProperty = "lb_ga:bookshelf_entity_seed"
    if (!entity.getDynamicPropertyIds().includes(seedProperty)) setSeed()
    
    function getSeed() {
        return JSON.parse(entity.getDynamicProperty(seedProperty))
    }

    function setSeed(randomInt = Math.floor(Math.random() * 1000000000)) {
        sendDebugMessage(`setting ${entity.typeId}'s seed to ${randomInt}`)
        entity.setDynamicProperty(seedProperty,JSON.stringify(randomInt))
    }

    return {
        getSeed: function() {
            return getSeed()
        }
    }
}

export function BS_books(entity) {
    const bookProperty = "lb_ga:book_count"

    function getData() {
        sendDebugMessage(`got ${entity.getProperty(bookProperty)} books`)
        return entity.getProperty(bookProperty)
    }

    function setData(newData) {
        if (newData < 0) throw new Error(`cannot set book value negative! ${newData}`)
        if (newData > 24) throw new Error(`cannot set book value higher than 24! ${newData}`)
        sendDebugMessage(`setting book quantity to ${newData}`)
        entity.setProperty(bookProperty,newData)
    }

    return {
        query: function() {
            return getData()
        },
        decrement: function() {
            return setData(getData() - 1)
        },
        initialize: function() {
            return setData(12)
        }
    }
}

function findEnchantTable(location, dimension, maxDistance = 7) {

    const blockArea = new BlockVolume(
        createVector(location.x - maxDistance, Math.min(location.y - maxDistance, dimension.heightRange.max), location.z - maxDistance),
        createVector(location.x + maxDistance, Math.max(location.y + maxDistance, dimension.heightRange.min), location.z + maxDistance)
    )

    const enchantTables = dimension.getBlocks(
        blockArea,
        {includeTypes: ["minecraft:enchanting_table"]}
    )

    for (const location of enchantTables.getBlockLocationIterator()) {
        return location
    }
    return null
}

function countBookshelves(location, dimension,  maxDistance = 10) {
    location = bottomCenter(location)
    let bookshelfCount = 0
    const bookshelvesNeeded = MinecraftEnchantmentTypes.length - 4

    const blockArea = new BlockVolume(
        createVector(location.x - maxDistance, Math.min(location.y - maxDistance, dimension.heightRange.max), location.z - maxDistance),
        createVector(location.x + maxDistance, Math.max(location.y + maxDistance, dimension.heightRange.min), location.z + maxDistance)
    )

    const bookshelves = dimension.getBlocks(
        blockArea,
        {includeTypes: ["minecraft:bookshelf"]}
    )

    for (const location of bookshelves.getBlockLocationIterator()) {
        bookshelfCount++
        if (bookshelfCount >= bookshelvesNeeded) return true
    }
    return false
}

function chooseEnchantForm(golem) {
    try {
        const isNearTable = findEnchantTable(golem.location,golem.dimension)
        const bookshelfCount = countBookshelves(golem.location,golem.dimension)
        sendDebugMessage(`found ${bookshelfCount} bookshelves, needed: ${MinecraftEnchantmentTypes.length - 4}`)
        
        const form = new ActionFormData()
        const buttons = BS_stock(golem).get(bookshelfCount)
        let handwrittenBook = -1,
        bookCount = BS_books(golem).query()
        if (!isNearTable) {
            form.title({translate:"enchant.noTable.title"})
            let message = {
                rawtext:[
                    {
                        translate:`enchant.noTable.${BS_foundTable(golem).hasFound() ? "lost" : "neverHad"}.body_${Math.floor(Math.random() * 6)}`
                    },
                    {text:"\n\n"},
                    {
                        translate:`enchant.noTable.availableEnchants`,
                        with: {
                            rawtext: [
                                {
                                    text:JSON.stringify(bookshelfCount)
                                },
                                {
                                    translate:`enchant.noTable.bookshelf.${bookshelfCount === 1 ? "singular" : "plural"}`
                                }
                            ]
                        }
                    },
                    {text:"\n"},
                ]
            }
            for (const button of buttons) {
                message.rawtext.push({text:"\n     "})
                message.rawtext.push({translate:"enchantment." + button.key})
            }
            message.rawtext.push({text:"\n\n"})
            form.button({translate:"text.button.ok"})
            form.body(message)
        } else {
            // mark golem as having found a table, for future dialog purposes
            BS_foundTable(golem).find()
            if (bookCount > 0) {
                form.title({translate:"enchant.type"})
                if (bookshelfCount < MinecraftEnchantmentTypes.length - 4) form.body({translate:`enchant.moreBookshelves_${Math.floor(Math.random() * 4)}`})
                for (const button of buttons) {
                    form.button({translate:"enchantment." + button.key},"textures/items/book_enchanted")
                }
            } else {
                form.title({translate:"enchant.noBooks.title"})
                form.body({translate:"enchant.noBooks.body",with:["\n"]})
                form.button({translate:"text.button.ok"})
            }
        }
        return {
            continueForms: isNearTable && bookCount > 0 ? true: false,
            form: form,
            // buttons data is needed for deciphering which button was pressed!
            buttons: buttons,
            handwritten: handwrittenBook
        }
    } catch (error) {
        sendDebugMessage(error)
    }
}

function enchantLevelForm(enchantTypeString) {
    const form = new ActionFormData()
    form.title({translate:"enchant.chooseLevel"})
    // form.body(`Current XP level: ${player.level}`) // number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
    const enchant = new EnchantmentType(enchantTypeString)
    for (let level = 1; level <= enchant.maxLevel; level++) {
        form.button({translate:`enchant.level`,with:[JSON.stringify(level)]})
    }
    return form
}

function getTotalXpAtLevel(level) {
    if (level === undefined || level < 0) throw new Error(`invalid level: ${level}`)
    let result
    if (level < 17) {
        result = (level * level) + (6 * level)
    } else if (level < 32) {
        result = Math.floor((2.5 * level * level) - (40.5 * level) + 360)
    } else {
        result = Math.floor((4.5 * level * level) - (162.5 * level) + 2220)
    }
    return result
}

function getXpForLevel(level) {
    if (level === 0) {
        return 0
    } else if (level === 1) {
        return getTotalXpAtLevel(1)
    } else if (level > 1) {
        return getTotalXpAtLevel(level) - getTotalXpAtLevel(level-1)
    } else throw new Error (`Invalid level input! ${level}`)
}

function purchaseForm(currentXP,enchantData,player) {
    const form = new ActionFormData()
    const price = enchantData.enchant.pricing[enchantData.level - 1]
    let buttons = 1, levelCount = 0, curLevel = player.level, countingXp = price
    while (countingXp > 0 && curLevel > 1) {
        curLevel--
        const thisLevelXp = getXpForLevel(curLevel)
        // if the remaining cost is higher than this level's total capacity
        if (countingXp > thisLevelXp) {
            countingXp -= thisLevelXp
            levelCount++
        } else {
            levelCount+= countingXp/thisLevelXp
            countingXp-= countingXp
        }
    }
    // if we counted all the way down to level 0, but we still haven't hit cost, go back to the player's current level and count upwards until we hit cost
    if (countingXp > 0) {
        curLevel = player.level - 1
        while (countingXp > 0) {
            curLevel++
            const thisLevelXp = getXpForLevel(curLevel)
            if (countingXp > thisLevelXp) {
                countingXp -= thisLevelXp
                levelCount++
            } else {
                levelCount+= countingXp/thisLevelXp
                countingXp-= countingXp
            }
        }
    }
    levelCount = Math.round(levelCount * 100)/100
    const rawText = [
        {text:"\n"},
        {
            translate:"enchant.purchase.body",with:{
                rawtext:[
                    {
                        translate:"enchantment." + enchantData.enchant.key
                    },
                    {
                        text:enchantData.level
                    },
                    {
                        text:"§g" + JSON.stringify(price) + "§r"
                    },
                    {
                        text:"" + JSON.stringify(levelCount) + ""}]
            }
        },
        {text:"\n\n"},
        {
            translate:"enchant.purchase.currentXP",with:
            [
                `§${currentXP < price ? "4" : "a"}${currentXP}`,
                // player level + partial filled xp bar, rounded to 2 decimals
                `${player.level + (Math.round((player.xpEarnedAtCurrentLevel / player.totalXpNeededForNextLevel) * 100) / 100)}`]
        },
    ]
    if (currentXP < price) {
        rawText.push({text:"\n§4"})
        rawText.push({translate:"enchant.purchase.insufficient"})
    } else {
        form.button({translate:"enchant.purchase.price",with:[JSON.stringify(price)]})
        buttons++
    }
    rawText.push({text:"\n\n"})

    form.title({translate:"enchant.purchase.title"})
    form.body({rawtext:rawText})
    form.button({translate:"enchant.purchase.cancel"})
    sendDebugMessage(`buttonLength: ${buttons}`)
    return {
        form: form,
        buttonLength: buttons,
        price: price
    }
}

function removeXP(player,amount) {
    if (amount < 1) throw new Error(`cannot remove ${amount} experience from ${player.nameTag}`)
    if (player.getTotalXp() < amount) sendDebugMessage(`§4Warning!§r removing ${amount} xp from ${player.nameTag}, but they only have ${player.getTotalXp()} to begin with!`)

    const originalXp = player.getTotalXp()
    player.resetLevel()
    player.addExperience(Math.max(originalXp - amount,0))
}

async function bookshelfGUI(player, golem) {
    try {
        const baseForm = chooseEnchantForm(golem)
        const enchantedBook = new ItemStack("enchanted_book")
        const enchantComponent = enchantedBook.getComponent("enchantable")
        // if golem is ready, player will pick enchant type here
        let firstResponse = await baseForm.form.show(player)
        if (firstResponse.canceled || !baseForm.continueForms) return
        const chosenEffect = baseForm.buttons[firstResponse.selection].id

        // player chooses from available levels for the selected enchant
        const levelResponse = await enchantLevelForm(chosenEffect).show(player)
        if (levelResponse.canceled) return
        if (player.getGameMode() !== GameMode.creative) {
            const currentXP = player.getTotalXp()
            const finalForm = purchaseForm(currentXP,{
                enchant: baseForm.buttons[firstResponse.selection],
                level: JSON.stringify(levelResponse.selection + 1)
            },player)
            // calculate price and present player with buy button (if eligible)
            const purchaseResponse = await finalForm.form.show(player)
            // button length of 2 means there was both a buy (button 0) AND cancel (button 1)
            if (purchaseResponse.canceled || finalForm.buttonLength === 1 || (finalForm.buttonLength === 2 && purchaseResponse.selection === 1)) return
            removeXP(player,finalForm.price)
        }
        enchantComponent.addEnchantment({level:levelResponse.selection + 1,type:new EnchantmentType(chosenEffect)})
        const book = golem.dimension.spawnItem(enchantedBook,golem.location)
        golem.dimension.playSound("block.enchanting_table.use",golem.location)
        // remove book from golem property
        BS_books(golem).decrement()
        const impulse = {
            x:(player.location.x - golem.location.x)/10,
            y:((player.location.y - golem.location.y) + 3)/10,
            z:(player.location.z - golem.location.z)/10
        }
        book.applyImpulse(impulse)
    } catch (error) {
        sendDebugMessage(error)
    }
}

world.beforeEvents.playerInteractWithEntity.subscribe(event => {
    if (event.target.typeId !== "lb_ga:bookshelf_golem") return
    if (event.player.getComponent("equippable")?.getEquipment("Mainhand")?.typeId === "minecraft:lead") return
    system.run(() => {
        if (event.itemStack?.typeId !== "minecraft:book") bookshelfGUI(event.player,event.target)
    })
})