import { EntityDamageCause, EntityEquippableComponent, EntityInventoryComponent, EntityTypeFamilyComponent, EquipmentSlot, GameMode, ItemStack, Player, system, world } from "@minecraft/server";
import { Vector3 } from "./vector/vector3";
import { namespace } from "./namespace";
import * as pickaxes from "./items/pickaxes/index"
import * as axes from "./items/axes/index"
import * as hoes from "./items/hoes/index"
import * as util from "./util/index"
import * as misc from "./items/misc/index"
import * as bows from "./items/bows/index"
import * as chestplates from "./items/armour/chest/index"
import * as shovels from "./items/shovels/index"
import * as swords from "./items/swords/index"
import { getBlockFace, getBlocksToBreak } from "./items/get5x5Blocks";
import { hackerTableForm, mainForm } from "./forms";
import { setupTutorial, teleportToCurrentRoom, tutorialRunning } from "./tutorial";

// TODO: remove before packaging
// world.afterEvents.worldInitialize.subscribe(() => {
//     console.error("World initialized")
//     setupTutorial()
//     world.setDynamicProperty(`${namespace}:tutorial_done`, false)
//     world.setDefaultSpawnLocation(new Vector3(-445.5, 20, -276.5))

//     world.getAllPlayers().forEach((player) => {
//         player.setDynamicProperty(`${namespace}:book_given`, undefined)
//         player.setDynamicProperty(`${namespace}:in_tutorial`, undefined)
//     })
// })

world.afterEvents.playerSpawn.subscribe((event) => {
    const player = event.player

    // give book if tutorial is done
    if (world.getDynamicProperty(`${namespace}:tutorial_done`) && !tutorialRunning && event.initialSpawn) {
        /**
         * @type {EntityInventoryComponent}
         */
        const inv = player.getComponent('inventory')
        // check if player already has the book
        for (let i = 0; i < inv.container.size; i++) {
            if (inv.container.getItem(i)?.typeId == `${namespace}:guide_book`) {
                // return if player already has the book
                return
            }
        }

        // give player book since they don't have it
        player.getComponent('inventory').container.addItem(new ItemStack(`${namespace}:guide_book`))
    }
    // console.error(!world.getDynamicProperty(`${namespace}:tutorial_done`), !tutorialRunning, player.getDynamicProperty(`${namespace}:in_tutorial`))
    // player joined while tutorial not running, set up tutorial
    if (!world.getDynamicProperty(`${namespace}:tutorial_done`) && !tutorialRunning) {
        // console.error("setup tutorial")
        world.setDynamicProperty(`${namespace}:tutorial_room_number`, 0)
        setupTutorial()
    }
    // player joined while tutorial is running
    else if (!world.getDynamicProperty(`${namespace}:tutorial_done`)) {
        // teleport new player to current tutorial location
        teleportToCurrentRoom(player)
        if (player.getGameMode() != GameMode.survival) player.setGameMode(GameMode.survival)
        player.runCommand(`xp -999999L @s`)
        player.setDynamicProperty(`${namespace}:in_tutorial`, true)
    }
})

world.afterEvents.entityDie.subscribe((event) => {
    const source = event.damageSource
    const player = source.damagingEntity
    const deadEnt = event.deadEntity
    if (player?.typeId !== "minecraft:player" || deadEnt?.typeId == "minecraft:player") return //validate before obtaining other values from them
    const dim = player.dimension
    try {
        const deathEntLoc = deadEnt.location
        const spawnLoc = new Vector3(deathEntLoc.x, deathEntLoc.y + util.random.randomBetween(1, 2), deathEntLoc.z) //ensure it spawns in the player's view

        dim.spawnParticle(`${namespace}:glitch`, spawnLoc)
        dim.runCommand(`playsound ${namespace}:death_static @a ${spawnLoc.x} ${spawnLoc.y} ${spawnLoc.z}`)

        const glitchShard = new ItemStack(`${namespace}:glitch_shard`)

        /**
         * @type {EntityTypeFamilyComponent}
         */
        const family = deadEnt.getComponent("type_family")
        family.hasTypeFamily(`hacker_boss`) ? glitchShard.amount = 15 : glitchShard.amount = 1

        glitchShard.setLore([`${util.colours.reset}Used to increase odds of getting good items`])

        system.runTimeout(() => {
            dim.spawnItem(glitchShard, spawnLoc)
        }, 2 * 20)
    }
    catch (e) { }
})

world.afterEvents.entityHitEntity.subscribe((event) => {
    const source = event.damagingEntity
    const target = event.hitEntity

    const srcEquippable = source.getComponent("equippable")
    const srcMainhand = util.getEquipmentSlot(srcEquippable, EquipmentSlot.Mainhand)

    if (srcMainhand != undefined) {
        switch (srcMainhand.typeId) {
            case `${namespace}:frog_sword`:
                swords.frogSword(target)
                break
            case `${namespace}:knockback_sword`:
                swords.knockbackSword(source, target)
                break
            case `${namespace}:hacker_sword`:
                //use applyDamage instead of kill for EntityApplyDamageOptions
                try {
                    if (target?.typeId != `${namespace}:hacker_table` && target?.typeId != `${namespace}:item_duper`) {
                        target.applyDamage(100000, { cause: EntityDamageCause.entityAttack, damagingEntity: source })
                        target.dimension.playSound(`${namespace}:hacker_sword`, target.location)
                    }
                }
                catch (e) { }

        }
    }

    switch (target.typeId) {
        case `${namespace}:hacker_table`:
            source.runCommand(`fill ${target.location.x} ${target.location.y} ${target.location.z} ${target.location.x} ${target.location.y} ${target.location.z} air destroy`)
            // target.remove()
            break
        case `${namespace}:item_duper`:
            if (source.isSneaking && source.getDynamicProperty(`${namespace}:in_tutorial`) == false) {
                source.runCommand(`fill ${target.location.x} ${target.location.y} ${target.location.z} ${target.location.x} ${target.location.y} ${target.location.z} air`)
                const item = new ItemStack(`${namespace}:item_duper`)
                item.setLore([`${util.colours.reset}${util.colours.dark_gray}Duplicates the item that is used on it`])
    
                const heldItemId = target.getDynamicProperty(`${namespace}:dupe_item`)
                if (heldItemId != undefined) {
                    const heldItem = new ItemStack(target.getDynamicProperty(`${namespace}:dupe_item`), 1)
                    const lore = target.getDynamicProperty(`${namespace}:dupe_item_lore`).split("1288")
                    if (lore[0].length != 0) {
                        heldItem.setLore(lore)
                    }
                    target.dimension.spawnItem(heldItem, target.location)
                }
    
                target.dimension.spawnItem(item, target.location)
                target.remove()
            }
            else {
                misc.itemDuper.itemDuperInit(source, target.dimension.getBlock(target.location))
            }
            break
    }
})

world.afterEvents.playerPlaceBlock.subscribe((event) => {
    const player = event.player
    const dim = player.dimension
    const block = event.block

    if (block.permutation.matches(`${namespace}:super_dry_sponge`)) {
        const loc = block.location
        const radius = 15 //15 is max
        dim.runCommand(`fill ${loc.x - radius} ${loc.y - radius} ${loc.z - radius} ${loc.x + radius} ${loc.y + radius} ${loc.z + radius} air replace seagrass`)
        dim.runCommand(`fill ${loc.x - radius} ${loc.y - radius} ${loc.z - radius} ${loc.x + radius} ${loc.y + radius} ${loc.z + radius} air replace kelp`)
        dim.runCommand(`fill ${loc.x - radius} ${loc.y - radius} ${loc.z - radius} ${loc.x + radius} ${loc.y + radius} ${loc.z + radius} air replace water`)
        dim.runCommand(`fill ${loc.x - radius} ${loc.y - radius} ${loc.z - radius} ${loc.x + radius} ${loc.y + radius} ${loc.z + radius} air replace flowing_water`)
        dim.runCommand(`fill ${loc.x - radius} ${loc.y - radius} ${loc.z - radius} ${loc.x + radius} ${loc.y + radius} ${loc.z + radius} air replace lava`)
        dim.runCommand(`fill ${loc.x - radius} ${loc.y - radius} ${loc.z - radius} ${loc.x + radius} ${loc.y + radius} ${loc.z + radius} air replace flowing_lava`)
    }
})

world.beforeEvents.playerBreakBlock.subscribe((event) => {
    const player = event.player
    const item = event.itemStack
    const block = event.block
    const dim = event.dimension
    const perm = block.permutation
    const blockLoc = block.location;
    const face = getBlockFace(player)
    const type = block.typeId

    switch (item?.typeId) {
        case `${namespace}:mega_pickaxe`:
            if (type.includes("stone") || type.includes("deepslate") || type.includes("ore") || type == "ancient_debris" || type.includes("quartz") ||
                type.includes("terracotta") || type.includes("blackstone") || type.includes("sandstone") || type.includes("prismarine") || (type.includes("concrete") && !type.endsWith("powder")) ||
                type == "mud_brick" || type.includes("nether_brick") || block.hasTag("stone") || type.includes("obsidian") || type.includes("tuff") || type.includes("ice") || type.includes("netherrack")) {
                const pickaxeBlocks = getBlocksToBreak(blockLoc, face, dim)
                pickaxes.megaPickaxe.sortBlocksAndBreak(pickaxeBlocks, dim, player)
            }
            break
        case `${namespace}:vein_pickaxe`:
            if (!util.tagIds.veinPickaxeTags.some(tag => block.hasTag(tag)) && !util.blockIds.veinPickaxeBlocks.some(veinBlock => perm.matches(veinBlock)) && !type.includes("obsidian")) {
                if (player.getDynamicProperty(`${namespace}:in_tutorial`) == true) {
                    event.cancel = true
                    return
                }

                return
            }
            pickaxes.veinMine(block, player)
            break

        case `${namespace}:super_fortune_pickaxe`:
            pickaxes.superFortunePickaxe(block, dim)
            break

        case `${namespace}:mega_shovel`:
            if (!block.hasTag("dirt") && !block.hasTag("sand") && !block.hasTag("gravel") && !block.hasTag("grass") && !block.hasTag("snow") && !perm.matches("clay") && !perm.matches("concretePowder") && !perm.matches("mycelium") && !perm.matches("soul_sand") && !perm.matches("soul_soil") && !block.typeId == "minecraft:snow") return
            const shovelBlocks = getBlocksToBreak(blockLoc, face, dim)
            shovels.sortBlocksAndBreak(shovelBlocks, dim)
            break

        case `${namespace}:treecapitator`:
            if (!block.hasTag("log") && !perm.matches("crimson_stem") && !perm.matches("warped_stem") && !block.typeId.includes("wood") && !block.typeId.includes("cherry_log")) return
            axes.treecapitator.getLogsAndBreak(block)
            break

        case `${namespace}:replenish_hoe`:
            if (block.hasTag("minecraft:crop") || perm.matches("nether_wart") || perm.matches("melon_stem") || perm.matches("pumpkin_stem")) {
                const crop = hoes.replenish.getCropType(block)
                if (!hoes.replenish.hasSeeds(player, crop[1])) return
                system.run(() => {
                    dim.runCommand(`fill ${block.location.x} ${block.location.y} ${block.location.z} ${block.location.x} ${block.location.y} ${block.location.z} air destroy`)
                    dim.runCommand(`setblock ${block.location.x} ${block.location.y} ${block.location.z} ${crop[0]}`)
                    player.runCommand(`clear @s ${crop[1]} 0 1`)
                })
            }
            break
    }

    if (player.getDynamicProperty(`${namespace}:in_tutorial`) == true) {
        if (player.getGameMode() == GameMode.creative) return
        event.cancel = true
    }

    if (type == `${namespace}:item_duper`) {
        if (player.getDynamicProperty(`${namespace}:in_tutorial`) == true) return
        system.run(() => {
            block.dimension.getEntitiesAtBlockLocation(block.location).filter(ent => ent.typeId == `${namespace}:item_duper`).forEach((duper) => duper.remove())
        })
    }
})

// world.afterEvents.entityHitBlock.subscribe((event) => {
//     const player = event.damagingEntity
//     const block = event.hitBlock

//     if (block.permutation.matches(`${namespace}:item_duper`)) {
//         misc.itemDuper.itemDuperInit(player, block)
//     }
// })

world.afterEvents.itemUseOn.subscribe((event) => {
    const player = event.source
    const item = event.itemStack
    const block = event.block
    const face = event.blockFace
    const loc = util.faceOffset(face, block.location)

    switch (item.typeId) {
        case `${namespace}:replenish_hoe`:
            player.dimension.runCommand(`playsound use.gravel @a ${block.location.x} ${block.location.y} ${block.location.z}`)
            break
    }
})


world.afterEvents.playerPlaceBlock.subscribe((event) => {
    const player = event.player
    const block = event.block
    const dim = event.dimension

    if (block.typeId == `${namespace}:hacker_table`) {
        const loc = block.location
        const e = dim.spawnEntity(`${namespace}:hacker_table`, new Vector3(loc.x + 0.5, loc.y, loc.z + 0.5))
        dim.playSound(`${namespace}:hacker_table_place`, loc)
    }
    else if (block.typeId == `${namespace}:item_duper`) {
        const loc = block.location
        const e = dim.spawnEntity(`${namespace}:item_duper`, new Vector3(loc.x + 0.5, loc.y, loc.z + 0.5))
        dim.playSound(`${namespace}:duplicator_place`, loc)
    }
})

world.beforeEvents.itemUseOn.subscribe((event) => {
    const item = event.itemStack
    const block = event.block
    const face = event.blockFace
    const loc = util.faceOffset(face, block.location)
    const dim = event.block.dimension

    switch (item.typeId) {
        case `${namespace}:infinite_lava_bucket`:
            system.run(() => {
                dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} flowing_lava`)
                dim.playSound(`${namespace}:lava_bucket`, loc)

            })
            event.cancel = true
            break
        case `${namespace}:infinite_water_bucket`:
            system.run(() => {
                if (event.source.getDynamicProperty(`${namespace}:in_tutorial`) == true) {
                    // valid locations in tutorial
                    let validLocations = [
                        new Vector3(-329, 19, -280),
                        new Vector3(-321, 19, -280),
                        new Vector3(-313, 19, -280),
                        new Vector3(-329, 19, -274),
                        new Vector3(-321, 19, -274),
                        new Vector3(-313, 19, -274)
                    ]

                    if (!validLocations.some(validLoc => validLoc.equals(loc))) {
                        event.cancel = true
                        return
                    }
                }
                dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} flowing_water`)
                dim.playSound(`${namespace}:water_bucket`, loc)
            })
            event.cancel = true
            break
    }

    switch (block.typeId) {
        case `${namespace}:hacker_table`:
            event.cancel = true
            break
        case `${namespace}:item_duper`:
            event.cancel = true
            break
    }
})

/**
 * @type {Player[]}
 */
let isUsingMachineBow = []
world.afterEvents.itemStartUse.subscribe((event) => {
    const player = event.source
    const item = event.itemStack

    switch (item.typeId) {
        case `${namespace}:machine_bow`:
            isUsingMachineBow.push(player.id)
            break
    }
})

world.afterEvents.itemStopUse.subscribe((event) => {
    const player = event.source
    const item = event.itemStack

    switch (item?.typeId) {
        case `${namespace}:machine_bow`:
            isUsingMachineBow.splice(isUsingMachineBow.indexOf(player.id), 1)
            break
    }
})

world.afterEvents.itemUse.subscribe((event) => {
    const player = event.source
    const item = event.itemStack
    const dim = player.dimension

    switch (item.typeId) {
        case `${namespace}:teleport_compass`:
            misc.teleportCompass(player)
            break
        case `${namespace}:hacker_clock`:
            misc.hackerClock(player)
            break
        case `${namespace}:teleporter`:
            misc.teleporter(player)
            break
        case `${namespace}:random_ore_pickaxe`:
            pickaxes.randomOrePickaxe(player)
            break
        case `${namespace}:hacker_command_block`:
            misc.hackerCommandBlock(player)
            break
        case `${namespace}:guide_book`:
            mainForm(player)
            break
    }
})

world.afterEvents.itemCompleteUse.subscribe((event) => {
    const player = event.source
    const item = event.itemStack

    switch (item.typeId) {
        case `${namespace}:hacker_apple`:
            misc.hackerApple(player)
            break
    }
})

world.beforeEvents.playerLeave.subscribe((event) => {
    const players = world.getAllPlayers()
    if (players.length == 1) {
        system.run(() => {
            players[0].dimension.getEntities({ type: `${namespace}:dummy` }).forEach((dummy) => {
                dummy.remove()
            })
        })
    }
})

world.afterEvents.projectileHitBlock.subscribe((event) => {
    const projectile = event.projectile

    switch (projectile.typeId) {
        case `minecraft:arrow`:
            try {
                if (projectile.getDynamicProperty("block_ha_despawn") == true) {
                    projectile.remove()
                }
            }
            catch (e) { }
            break
    }
})

let canJump = true
let tick = 0
let canHitTimer = 0
/**
 * @type {Map<string, string[]>}
 */
let lastTickArmour = new Map()
system.runInterval(() => {
    const players = world.getAllPlayers()
    tick = (tick + 1) % (20 * 60)

    if (tick % 20 == 0) {
        misc.itemDuper.dupeItems()
    }

    players.forEach(player => {
        /**
         * @type {EntityEquippableComponent}
         */
        const equippable = player.getComponent("equippable")
        let mainHand = util.getEquipmentSlot(equippable, EquipmentSlot.Mainhand)
        let bootsSlot = util.getEquipmentSlot(equippable, EquipmentSlot.Feet)
        let leggingsSlot = util.getEquipmentSlot(equippable, EquipmentSlot.Legs)
        let chestplateSlot = util.getEquipmentSlot(equippable, EquipmentSlot.Chest)
        let helmetSlot = util.getEquipmentSlot(equippable, EquipmentSlot.Head)

        // Initialize player's armor tracking if not exists
        if (!lastTickArmour.has(player.id)) {
            lastTickArmour.set(player.id, [])
        }

        const playerLastArmor = lastTickArmour.get(player.id)

        //check if new armour has been equipped since the last tick
        if (!playerLastArmor.includes(bootsSlot?.typeId) && bootsSlot?.typeId.includes(`${namespace}`)) player.dimension.playSound(`${namespace}:armour_equip`, player.location)
        if (!playerLastArmor.includes(leggingsSlot?.typeId) && leggingsSlot?.typeId.includes(`${namespace}`) && leggingsSlot != undefined) player.dimension.playSound(`${namespace}:armour_equip`, player.location)
        if (!playerLastArmor.includes(chestplateSlot?.typeId) && chestplateSlot?.typeId.includes(`${namespace}`) && chestplateSlot != undefined) player.dimension.playSound(`${namespace}:armour_equip`, player.location)
        if (!playerLastArmor.includes(helmetSlot?.typeId) && helmetSlot?.typeId.includes(`${namespace}`) && helmetSlot != undefined) player.dimension.playSound(`${namespace}:armour_equip`, player.location)

        if (isUsingMachineBow.includes(player.id)) {
            bows.machineBow(player)
        }

        //hacker set bonus
        if (bootsSlot != undefined && leggingsSlot != undefined && chestplateSlot != undefined && helmetSlot != undefined) {
            if (bootsSlot.typeId == `${namespace}:hacker_boots` && leggingsSlot.typeId == `${namespace}:hacker_leggings` && chestplateSlot.typeId == `${namespace}:hacker_chestplate` && helmetSlot.typeId == `${namespace}:hacker_helmet` && tick % (20 * 0.25) == 0) {
                const eTime = 20 * 11
                player.addEffect("resistance", eTime, { amplifier: 3, showParticles: false })
                player.addEffect("fire_resistance", eTime, { amplifier: 0, showParticles: false })
                player.addEffect("night_vision", eTime, { amplifier: 0, showParticles: false })
                player.addEffect("regeneration", eTime, { amplifier: 1, showParticles: false })
                player.addEffect("water_breathing", eTime, { amplifier: 0, showParticles: false })
                player.addEffect("saturation", eTime, { amplifier: 0, showParticles: false })
                player.addEffect("strength", eTime, { amplifier: 1, showParticles: false })
            }
        }

        if (bootsSlot != undefined) {
            //fly boots
            if (bootsSlot.typeId == `${namespace}:fly_boots` && !player.isOnGround && player.isJumping && canJump && !player.isSwimming) {
                const direction = player.getVelocity()
                player.applyKnockback(direction.x, direction.z, 0.4, 0.5)
                player.dimension.playSound(`${namespace}:double_jump`, player.location)
                canJump = false

                system.runTimeout(() => {
                    canJump = true
                }, 0.2 * 20)
            }

            //speed boots
            if (tick % (20 * 0.25) == 0 && bootsSlot.typeId == `${namespace}:speed_boots`) {
                player.addEffect("speed", 20 * 11, { amplifier: 2, showParticles: false })
            }
        }
        if (helmetSlot != undefined) {

        }
        if (chestplateSlot != undefined) {
            if (chestplateSlot.typeId == `${namespace}:infinity_chestplate` && canHitTimer <= 0) {
                chestplates.infinityChestplate(player)
                canHitTimer = 10
            }
            if (canHitTimer > 0) {
                canHitTimer--
            }
        }
        if (mainHand != undefined) {
        }

        // Clear and update the player's armor tracking for next tick
        playerLastArmor.length = 0

        if (bootsSlot != undefined) playerLastArmor.push(bootsSlot.typeId)
        if (leggingsSlot != undefined) playerLastArmor.push(leggingsSlot.typeId)
        if (chestplateSlot != undefined) playerLastArmor.push(chestplateSlot.typeId)
        if (helmetSlot != undefined) playerLastArmor.push(helmetSlot.typeId)
    })
})

world.afterEvents.playerInteractWithEntity.subscribe(event => {
    const player = event.player
    const target = event.target

    switch (target.typeId) {
        case `${namespace}:hacker_table`:
            hackerTableForm(player)
            break
        case `${namespace}:item_duper`:
            misc.itemDuper.itemDuperInit(player, target.dimension.getBlock(target.location))
            break
    }
})

world.afterEvents.dataDrivenEntityTrigger.subscribe((event) => {
    const entity = event.entity
    const id = event.eventId

    switch (id) {
        // case `${namespace}:hacker_table`:
        //     hackerTableForm(player)
        //     break
        // case `${namespace}:item_duper`:
        //     misc.itemDuper.itemDuperInit(player, entity.dimension.getBlock(entity.location))
        //     break
        case `${namespace}:hacker_table_despawn`:
            entity.runCommand(`fill ${entity.location.x} ${entity.location.y} ${entity.location.z} ${entity.location.x} ${entity.location.y} ${entity.location.z} air destroy`)
            break
    }
})