import { BlockPermutation, BlockVolume, EntityComponentTypes, EntityDamageCause, EquipmentSlot, GameMode, ItemComponentTypes, ItemLockMode, ItemStack, MolangVariableMap, system, world } from "@minecraft/server"
import { blockFaceToDirection, distance, sendDebugMessage, vectorAdd, vectorMagnitude, vectorRoundDown, vectorSubtract } from "./JM_Utils"
import { getEnchants } from "./enchantToEffect"
import "./blocks"
import "./miniboss"
import "./scriptevent"
import "./book"

import "./armors/armors"
import './events/itemUse'

import "./floating_heart"
import { copperExists } from "./blocks"
import "./heads"

const tags = {
    thorns: "lb_as:thorns",
    frostwalk: "lb_as:frost_walker"
}

world.beforeEvents.entityRemove.subscribe(removeEvent => {
    const entity = removeEvent.removedEntity
    if (entity.typeId === "minecraft:armor_stand" && entity.hasTag("lb_as:convert_flag")) {
        const location = entity.location
        const rotation = entity.getRotation()
        location.y++ // the items drop from the center of the entity for some reason, so we must raise the search area to avoid false positives
        const dimension = entity.dimension
        const items = dimension.getEntities({ type: "minecraft:item", location: location, maxDistance: 2, closest: 5 }) // find item entities
        const itemStacks = items.map(item => item.getComponent(EntityComponentTypes.Item).itemStack) // create array of itemStacks now, after this tick they don't exist anymore
        const id = entity.id // because next tick this entity is removed, its ID will be inaccessible
        system.run(() => {
            const newEntity = world.getEntity(newIds[id])
            delete newIds[id]
            newEntity.teleport(newEntity.location, { rotation: rotation })

            const inventory = newEntity.getComponent(EntityComponentTypes.Inventory) // the living stand's inventory
            const container = inventory.container
            const string = id.toString() // this is how we'll recall the correct structure when un-aliving
            newEntity.setDynamicProperty(`lb_as:structId`, string)

            for (let i = 0; i < items.length; i++) {
                const itemStack = itemStacks[i]
                if (itemStack.typeId === "minecraft:armor_stand") continue // obviously don't care about the vanilla armor stand item entity
                equipEntity(newEntity, itemStack, container)
                items[i].remove()
            }
        })
    }
})

const skulls = [ "skeleton_skull", "wither_skeleton_skull", "creeper_head", "dragon_head", "piglin_head", "zombie_head"]

function equipEntity(entity, itemStack, container) {
    const [namespace, shortName] = itemStack.typeId.split(":")
    const slot = getAppropriateSlot(itemStack)
    const enchants = getEnchants(itemStack)
    // if enchanted and not in the mainhand slot, this is where it would spawn in our secret glinted version
    const durabilityComponent = itemStack.getComponent(ItemComponentTypes.Durability)
    const damage = durabilityComponent ? durabilityComponent.damage : 0
    entity.runCommand(`replaceitem entity @s ${slot} 0 ${namespace === "minecraft" ? "" : `${namespace}:`}${shortName} 1 ${damage} {"keep_on_death":{}, "item_lock":{"mode":"lock_in_slot"}, "minecraft:can_place_on":{"blocks":["minecraft:end_portal"]}}`)
    container.addItem(itemStack)
    if (enchants) {
        if (slot === "slot.weapon.mainhand") {
            for (const enchant of enchants) {
                sendDebugMessage(`applying enchant ${enchant.type.id} ${enchant.level} to ${itemStack.typeId}`)
                entity.runCommand(`enchant @s ${enchant.type.id} ${enchant.level}`)
            }
        } else {
            // const data = enchantData(entity).get()
            for (const enchant of enchants) {
                switch (enchant.type.id) {
                    case "thorns":
                        entity.addTag(tags.thorns)
                        break
                    case "frost_walker":
                        entity.addTag(tags.frostwalk)
                        break
                    case "protection":
                    case "fire_protection":
                    case "blast_protection":
                    case "projectile_protection":
                        entity.triggerEvent(`lb_as:prot${enchant.level}`)
                        break
                    case "depth_strider":
                        entity.triggerEvent("lb_as:depth_strider")
                        break
                    default:
                        sendDebugMessage(`ignoring enchant ${enchant.type.id} ${enchant.level}`)
                }

                // const thisEnch = data[enchant.type.id] ? data[enchant.type.id] : {levels: []}
                // thisEnch.levels.push(enchant.level)
                // data[enchant.type.id] = thisEnch

                // enchantData(entity).set(data)
            }
        }
    }
    if (slot === "slot.armor.head" && skulls.includes(shortName)) entity.addTag(`lb_as:${shortName}`)
    // world.sendMessage(`final: ${JSON.stringify(enchantData(entity).get())}`)
}

// effect-able enchants:
// protections

function stringIncludesArrayItem(string, array) {
    for (const element of array) {
        if (string.includes(element)) return true
    }
    return false
}

function getAppropriateSlot(itemStack) {
    const slots = {
        head: ["_helmet", "_head", "_skull", "carved_pumpkin"],
        chest: ["_chestplate", "elytra"],
        legs: ["_leggings"],
        feet: ["_boots"]
    }
    let slot = "slot.weapon.mainhand"
    for (const slotType in slots) {
        if (stringIncludesArrayItem(itemStack.typeId, slots[slotType])) {
            slot = `slot.armor.${slotType}`
            break
        }
    }
    return slot
}

world.beforeEvents.playerInteractWithEntity.subscribe(interactEvent => {
    if (interactEvent.target.typeId === "minecraft:armor_stand") {
        if (toLiving(interactEvent)) interactEvent.cancel = true
    } else if (interactEvent.target.typeId === "lb_as:armor") {
        toInanimate(interactEvent)
    }
})

// toLiving() stores the new entity's id here so it can be retrieved before the old entity is removed from the world
let newIds = {}

function toLiving(event) {
    const item = event.itemStack
    if (!item || (item.typeId !== "lb_as:blackstone_heart" && item.typeId !== "lb_as:wooden_heart")) return false
    const entity = event.target
    const structOrigin = vectorRoundDown(entity.location)
    if (structOrigin.y >= entity.dimension.heightRange.max || structOrigin.y < entity.dimension.heightRange.min) return false
    const slot = event.player.selectedSlotIndex
    system.run(() => {
        entity.addTag("lb_as:convert_flag") // entity removal listener looks for this tag to verify removal
        world.structureManager.createFromWorld(`lb_as:${entity.id}`, entity.dimension, structOrigin, structOrigin, { includeBlocks: false })
        const newEntity = entity.dimension.spawnEntity("lb_as:armor", entity.location)
        newIds[entity.id] = newEntity.id // save start location to be read (and deleted) later when spawning new entity
        newEntity.teleport(newEntity.location, { rotation: entity.getRotation() })

        const teleportLocation = entity.location
        teleportLocation.y = entity.dimension.heightRange.max - 2
        entity.teleport(teleportLocation) // must move the entity so that when it dies, its items are not instantly picked up by nearby players before the script can find and remove them
        entity.kill() // forces entity to drop items given by the player that we've put in its inventory

        if (item.typeId == "lb_as:blackstone_heart") newEntity.triggerEvent("lb_as:evil_armor") //Make the armor stand evil if a blackstone heart was used
        if (item.typeId == "lb_as:wooden_heart") newEntity.triggerEvent("lb_as:good_armor") //Make the armor stand good if a wooden heart was used

        if (event.player.getGameMode() !== GameMode.Creative) {
            let newItem
            if (item.amount > 1) {
                item.amount--
                newItem = item
            }
            event.player.getComponent(EntityComponentTypes.Inventory).container.setItem(slot, newItem)
        }
    })
    return true
}

function toInanimate(event) {
    const entity = event.target
    const dimension = entity.dimension
    const structId = entity.getDynamicProperty("lb_as:structId")
    const location = entity.location
    // hopefully this way the player will never see this happen
    location.y = location.y < 200 ? dimension.heightRange.max - 1 : dimension.heightRange.min // who decided the max value is actually out of bounds!?!?!?!?! 😡

    system.run(() => {
        let stand
        try {
            world.structureManager.place(`lb_as:${structId}`, dimension, location) // load struct
            world.structureManager.delete(structId) // delete from memory
            stand = dimension.getEntities({ type: "minecraft:armor_stand", location: location, maxDistance: 1 })[0] // get the entity from the loaded struct
        } catch (error) {
            // no structure found; retrieve equipment from inventory and equip new entity
            stand = dimension.spawnEntity("armor_stand", entity.location)
            const inventory = entity.getComponent(EntityComponentTypes.Inventory).container
            for (let index = 0; index < inventory.size; index++) {
                const item = inventory.getItem(index)
                if (!item) continue
                // vanilla items are not allwed to use the minecraft namespace in the replaceitem command,
                // however, non-vanilla items require namespacing
                const [namespace, shortName] = item.typeId.split(":")
                if (shortName.endsWith("helmet")) stand.runCommand(`replaceitem entity @s slot.armor.head 0 ${namespace === "minecraft" ? "" : `${namespace}:`}${shortName}`)
                else if (shortName.endsWith("chestplate")) stand.runCommand(`replaceitem entity @s slot.armor.chest 0 ${namespace === "minecraft" ? "" : `${namespace}:`}${shortName}`)
                else if (shortName.endsWith("leggings")) stand.runCommand(`replaceitem entity @s slot.armor.legs 0 ${namespace === "minecraft" ? "" : `${namespace}:`}${shortName}`)
                else if (shortName.endsWith("boots")) stand.runCommand(`replaceitem entity @s slot.armor.feet 0 ${namespace === "minecraft" ? "" : `${namespace}:`}${shortName}`)
                else stand.runCommand(`replaceitem entity @s slot.weapon.mainhand 0 ${namespace === "minecraft" ? "" : `${namespace}:`}${shortName}`)
            }
        }
        if (!stand) {
            sendDebugMessage(`§4unable to locate stand from structure ${structId}`)
            return
        }
        stand.teleport(entity.location, { facingLocation: event.player.location }) // our living stand entity has serious issues with rotations.  Facing the player works as an alternative, and makes decent diagetic sense

        stand.removeTag("lb_as:convert_flag") // left over from when saved in the struct in the first place
        stand.dimension.spawnItem(new ItemStack(`lb_as:${entity.getProperty("lb_as:is_good") ? "wooden" : "blackstone"}_heart`, 1), stand.location)
        entity.remove()
    })
}

// this event catches living stands that die and removes their equipped drops, so only their inventory items remain
world.afterEvents.entityDie.subscribe(deathEvent => {
    const deadEntity = deathEvent.deadEntity
    if (deadEntity.typeId !== "lb_as:armor") return
    deadEntity.dimension.getEntities({
        type: "minecraft:item",
        location: deadEntity.location,
        maxDistance: 2
    }).forEach(itemEntity => {
        const itemStack = itemEntity.getComponent(EntityComponentTypes.Item).itemStack
        if (
            (
                deadEntity.hasTag("lb_as:noDrops")
            ) ||
            (
                itemStack.keepOnDeath &&
                itemStack.lockMode === ItemLockMode.slot &&
                itemStack.getCanPlaceOn().includes("end_portal")
            )
        ) itemEntity.remove()
    })
})

// loops through all items stored in inventory and checks to see if entity is wearing an item with the same typeId in the appropriate slot.
// if not, it removes the item from the inventory (we're assuming the equipped item has been broken)
function checkInvAgainstEquip(entity) {
    const inventory = entity.getComponent(EntityComponentTypes.Inventory).container
    for (let index = 0; index < inventory.size; index++) {
        const item = inventory.getItem(index)
        if (!item) continue
        const slot = getAppropriateSlot(item)
        if (entity.runCommand(`testfor @s[hasitem={item=${item.typeId},location=${slot},slot=0}]`).successCount === 0) {
            inventory.setItem(index)
            const dimension = entity.dimension
            const structId = entity.getDynamicProperty("lb_as:structId")
            const location = entity.location
            // hopefully this way the player will never see this happen
            location.y = location.y < 200 ? dimension.heightRange.max - 1 : dimension.heightRange.min // who decided the max value is actually out of bounds!?!?!?!?! 😡

            // updated saved struct entity
            world.structureManager.place(`lb_as:${structId}`, dimension, location) // load struct
            world.structureManager.delete(`lb_as:${structId}`) // delete from memory
            const stand = dimension.getEntities({ type: "minecraft:armor_stand", location: location, maxDistance: 1 })[0] // get the entity from the loaded struct
            if (!stand) {
                sendDebugMessage(`§4unable to locate stand from structure ${structId}`)
                return
            }

            stand.runCommand(`replaceitem entity @s ${slot} 0 air`)
            world.structureManager.createFromWorld(`lb_as:${structId}`, dimension, location, location, { includeBlocks: false })
            stand.removeTag("lb_as:convert_flag")
            stand.remove()
        }
    }
}

// check inventory against equipped items when hurt.  Note that when an entity takes lethal damage, this doesn't do anything-
// the damage event seems to not fire until after the entity has already dropped its inventory.
// I decided this is fine - the player will have never seen the entity without the armor, so they have no expectation
// that the armor has broken (technically they could hear the break sound, but meh)
world.afterEvents.entityHurt.subscribe(hurtEvent => {
    const entity = hurtEvent.hurtEntity
    if (entity.typeId !== "lb_as:armor") return
    checkInvAgainstEquip(entity)

    // const enchants = enchantData(entity).get()
    // if (enchants.thorns) {
    //     const data = enchants.thorns

    // }

    if (entity.hasTag(tags.thorns) && Math.random() <= 0.5) {
        const attacker = hurtEvent.damageSource.damagingEntity
        const projectile = hurtEvent.damageSource.damagingProjectile
        if (!attacker && !projectile) return
        // const cause = hurtEvent.damageSource.cause
        if (attacker) attacker.applyDamage(Math.ceil(Math.random() * 5), { cause: EntityDamageCause.thorns, damagingEntity: entity })
        else throw new Error(`no attacker!`)
    }
})

const guidebookTag = "lb_as:guidebook"

system.runInterval(() => {
    const dimensions = []
    const permutation = BlockPermutation.resolve("minecraft:frosted_ice")
    world.getAllPlayers().forEach(player => {
        if (!dimensions.includes(player.dimension.id)) dimensions.push(player.dimension.id)
		// check for guidebook
		if (!player.hasTag(guidebookTag) && system.currentTick % 20 === 0) {
			const inventory = player.getComponent(EntityComponentTypes.Inventory).container
            try {
                inventory.addItem(new ItemStack("lb_as:book",1))
            } catch (error) {
                sendDebugMessage(error)
            }
			for (let slotIndex = 0; slotIndex < inventory.size; slotIndex++) {
				let item = inventory.getItem(slotIndex)
				if (!item) continue
                if (item.typeId === "lb_as:book") player.addTag(guidebookTag)
			}
		}
    })
    dimensions.forEach(id => {
        const dimension = world.getDimension(id)
        // horse jump animation:
        dimension.getEntities({ type: "lb_as:armor_horse" }).forEach(horse => {
            const rideComponent = horse.getComponent(EntityComponentTypes.Rideable)
            const riders = rideComponent?.getRiders()
            horse.setProperty("lb_as:horse_jump", !horse.isFalling && !horse.isOnGround && riders?.length > 0)
            if (rideComponent && riders[0]?.typeId === "lb_as:armor") {
                if (!horse.getEffect("speed")) horse.addEffect("speed", 20000000, {amplifier: 10, showParticles: false})
            } else {
                if (horse.getEffect("speed")) horse.removeEffect("speed")
            }
        })
        // not horse jump animation:
        dimension.getEntities({ type: "lb_as:armor", tags: [tags.frostwalk] }).forEach(entity => {
            if (vectorMagnitude(entity.getVelocity()) < 0.05) return
            const blockVolume = new BlockVolume(
                vectorSubtract(entity.location, { x: 2, y: 1, z: 2 }),
                vectorAdd(entity.location, { x: 2, y: -1, z: 2 })
            )
            const blocks = dimension.getBlocks(blockVolume, {
                includeTypes: ["minecraft:water", "minecraft:frosted_ice"]
            },
                true)
            for (const location of blocks.getBlockLocationIterator()) {
                const block = dimension.getBlock(location)
                if (!block) continue
                if (block.above().isAir) block.setPermutation(permutation)
            }
        })
        // horse protection effect based on armor
        dimension.getEntities({type:"lb_As:armor_horse"}).forEach(entity => {
            const inv = entity.getComponent('minecraft:inventory')?.container.getItem(0)
            if (inv !== undefined) {
                if (inv.typeId === 'minecraft:leather_horse_armor') {
                    entity.addEffect('resistance', 1, {amplifier:1,showParticles:false})
                }
                if (inv.typeId === 'minecraft:copper_horse_armor') {
                    entity.addEffect('resistance', 1, {amplifier:1.1,showParticles:false})
                }
                if (inv.typeId === 'minecraft:iron_horse_armor') {
                    entity.addEffect('resistance', 1, {amplifier:1.3,showParticles:false})
                }
                if (inv.typeId === 'minecraft:gold_horse_armor') {
                    entity.addEffect('resistance', 1, {amplifier:1.6,showParticles:false})
                }
                if (inv.typeId === 'minecraft:diamond_horse_armor') {
                    entity.addEffect('resistance', 1, {amplifier:2,showParticles:false})
                }
            }
        })
    })
})

function getSwordTypeFromArmor(armorType) {
    let weaponType
    switch(armorType) {
        case "leather":
            weaponType = "wooden"
            break
        case "chainmail":
            weaponType = "stone"
            break
        default:
            weaponType = armorType
            break
    }
    return "minecraft:" + weaponType + "_sword"
}

system.beforeEvents.startup.subscribe(({ itemComponentRegistry }) => {
    itemComponentRegistry.registerCustomComponent("lb_as:stand_placer", {
        onUseOn: (useEvent, { params }) => {
            const block = useEvent.block[blockFaceToDirection(useEvent.blockFace)]()
            if (!block) return
            const newEntity = block.dimension.spawnEntity("lb_as:armor", block.bottomCenter())
            newEntity.triggerEvent(`lb_as:${params.evil ? "evil" : "good"}_armor`)
            const inventory = newEntity.getComponent(EntityComponentTypes.Inventory).container
            if (params.weapon) equipEntity(newEntity, new ItemStack(params.weapon, 1), inventory)
            if (!params.horse && params.armor) {
                if (!params.weapon) equipEntity(newEntity, new ItemStack(getSwordTypeFromArmor(params.armor), 1), inventory)

                for (const armor of ["helmet", "chestplate", "leggings", "boots"]) {                
                    equipEntity(newEntity, new ItemStack(`${(armor === "helmet" && params.weapon === "trident") ? "turtle_helmet" : `${params.armor}_${armor}`}`, 1), inventory)
                }
            }
            if (params.horse) {
                const armorArray = ["leather", "iron", "golden", "diamond"]
                if (copperExists) armorArray.push("copper")
                const armorType = armorArray[Math.floor(Math.random() * armorArray.length)]
                equipEntity(newEntity, new ItemStack(getSwordTypeFromArmor(armorType), 1), inventory)
                for (const armor of ["helmet", "chestplate", "leggings", "boots"]) { // equip stand
                    equipEntity(newEntity, new ItemStack(`${armorType}_${armor}`, 1), inventory)
                }
                const horse = block.dimension.spawnEntity("lb_as:armor_horse", block.bottomCenter())
                horse.triggerEvent(`lb_as:equip_${armorType}_armor`)
                horse.triggerEvent(`lb_as:awaken_${params.evil ? "evil" : "good"}`)
            }
            // decrement
            if (useEvent.source.getGameMode() !== GameMode.Creative) {
                let item = useEvent.itemStack
                if (item.amount > 1) item.amount--
                else item = undefined
                useEvent.source.getComponent(EntityComponentTypes.Equippable).setEquipment(EquipmentSlot.Mainhand, item)
            } else newEntity.addTag("lb_as:noDrops") // disables drops when this entity dies
        }
    })
})

// Event which tests if the player is in creative mode so the horse can be destroyed in one hit 
// or <deathHits> hits within <deathHitInterval> ticks in survival
const deathHits = 2
const deathHitInterval = 5
const dataToArmor = ["leather", "copper", "iron", "golden", "diamond"]
let tickHorseObj = {} //{{id: hits, lastHitTick}, {}, ...}
class HorseHitTick {
    constructor() {
        this.hits = 1
        this.lastHitTick = system.currentTick
    }

    addHit() {
        if (system.currentTick - this.lastHitTick > deathHitInterval) this.hits = 1
        else this.hits++
        this.lastHitTick = system.currentTick
        return this.hits
    }
}

world.afterEvents.entityHitEntity.subscribe(hitEvent => {
    const entity = hitEvent.hitEntity
    const player = hitEvent.damagingEntity
    if (entity.typeId !== 'lb_as:armor_horse' || player.typeId !== 'minecraft:player') return
    if (!entity.getProperty('lb_as:is_alive')) {
        if (player.getGameMode() === GameMode.Creative) {
            entity.remove()
        } else {
            entity.playAnimation("animation.lb_as_armor_horse.shake")
            const data = tickHorseObj[entity.id]
            if (!data) {
                tickHorseObj[entity.id] = new HorseHitTick()
            } else {
                const hits = data.addHit()
                if (hits >= deathHits) {
                    const markVariant = entity.getComponent(EntityComponentTypes.MarkVariant).value // stores armor type
                    if (markVariant > 0) entity.dimension.spawnItem(new ItemStack(`${dataToArmor[markVariant - 1]}_horse_armor`), entity.location)
                    entity.dimension.spawnItem(new ItemStack('lb_as:horse_armor_stand', 1), entity.location)
                    entity.remove()
                    delete tickHorseObj[entity.id]
                }
            }
        }
    }
})

// periodically cleanup old hit event records
system.runInterval(() => {
    for (const id in tickHorseObj) {
        if (system.currentTick - tickHorseObj[id].lastHitTick > 10) {
            delete tickHorseObj[id]
        }
    }
},100)