import { BlockPermutation, BlockVolume, EntityComponentTypes, EquipmentSlot, GameMode, ItemStack, MolangVariableMap, StructureAnimationMode, system, world } from "@minecraft/server";
import { bottomCenter, center, createVector, sendDebugMessage, vectorAbove, vectorAdd, vectorH_Angle, vectorSubtract, vectorToString } from "./JM_Utils";

const directions = { //reversed
    north: 0,
    east: 90,
    south: 180,
    west: 270
}

const slots = {
    head: "slot.armor.head",
    chest: "slot.armor.chest",
    legs: "slot.armor.legs",
    feet: "slot.armor.feet",
    mainhand: "slot.weapon.mainhand"
}

const armorToSlotObj = {
    helmet: slots.head,
    chestplate: slots.chest,
    leggings: slots.legs,
    boots: slots.feet
}

function toSlot(typeId) {
    for (const key in armorToSlotObj) {
        if (typeId.includes(key)) {
            return armorToSlotObj[key]
        }
    }
    return slots.mainhand
}

// slot name and the chance of it being selected for an armor piece
const armorSlots = [
    // ["slot.weapon.mainhand", 0.075, "sword",],
    [slots.head, 0.3, "helmet"],
    [slots.chest, 0.1, "chestplate"],
    [slots.legs, 0.2, "leggings"],
    [slots.feet, 0.4, "boots"]
]

// when equipping armor, these determine which tier will be used (leather is default)
const armorTierChoices = {
    yesCopper: {
        golden: 0.1,
        copper: 0.2,
        chainmail: 0.35
    },
    noCopper: {
        golden: 0.15,
        chainmail: 0.4
    }
}

export let copperExists

// chance a sword will be equipped
const weaponChance = 0.25

// when equipping a sword, these determine which tier will be used (wood is default)
const weaponTierChoices = {
    yesCopper: {
        golden: 0.1,
        copper: 0.2,
        stone: 0.35
    },
    noCopper: {
        golden: 0.5,
        stone: 0.4
    }
}

system.run(() => {
    copperExists = new ItemStack("copper_sword", 1).typeId.length > 0
})

export const startBlockLocationProperty = "lb_as:heartBlockLocation"

// structures do something strange and cause ticking blocks to tick twice in the same tick, which causes too many entities to spawn.
// this is used to limit the entities to one spawn per block
const spawned = new Set()

system.beforeEvents.startup.subscribe(startEvent => {
    startEvent.blockComponentRegistry.registerCustomComponent("lb_as:stand_block", {
        onTick: (tickEvent, params) => {    
            const { params: {summon, evil} } = params
            const block = tickEvent.block

            const locationString = vectorToString(block.location)
            if (spawned.has(locationString)) return // limit spawns to one per block

            const orientation = block.permutation.getState("minecraft:cardinal_direction")
            switch (summon) {
                case "armor_stand": 
                    const armorTiers = copperExists ? armorTierChoices.yesCopper : armorTierChoices.noCopper
                    const weaponTiers = copperExists ? weaponTierChoices.yesCopper : weaponTierChoices.noCopper
                    if (!orientation) return // block isn't fully loaded yet, wait for next tick
                    world.structureManager.place(`lb_as:stands/${Math.floor(Math.random() * 13)}`, block.dimension, block.location, {})
                    const stand = block.dimension.getEntities({type: "minecraft:armor_stand", location: block.bottomCenter(), maxDistance: 0.1})[0]
                    try {
                        stand.teleport(stand.location, {rotation:{x:0, y:directions[orientation]}})
                    } catch (error) {
                        sendDebugMessage(error)
                        sendDebugMessage(`error was encountered trying to apply ${orientation} ${directions[orientation]} at ${JSON.stringify(stand.location)}`)
                    }
                    spawned.add(locationString)

                    for (const [slotName, chance, slotType] of armorSlots) {
                        if (Math.random() > chance) continue;

                        const roll = Math.random();
                        let tier = "leather";

                        if (roll <= armorTiers.chainmail) {
                            if (copperExists && roll <= armorTiers.copper) {
                                tier = roll <= armorTiers.golden ? "golden" : "copper";
                            } else {
                                tier = roll <= armorTiers.golden ? "golden" : "chainmail";
                            }
                        }

                        stand.runCommand(`replaceitem entity @s ${slotName} 0 ${tier}_${slotType}`);
                    }

                    if (Math.random() <= weaponChance) {
                        const roll = Math.random();
                        let tier = "wooden";

                        if (roll <= weaponTiers.stone) {
                            if (copperExists && roll <= weaponTiers.copper) {
                                tier = roll <= weaponTiers.golden ? "golden" : "copper";
                            } else {
                                tier = roll <= weaponTiers.golden ? "golden" : "stone";
                            }
                        }

                        stand.runCommand(`replaceitem entity @s slot.weapon.mainhand 0 ${tier}_sword`);
                    }

                    break
                default:
                    block.setType("air")
                    const newEntity = block.dimension.spawnEntity(summon, block.bottomCenter())
                    if (summon === "lb_as:armor_horse") {
                        newEntity.setProperty("lb_as:pose", Math.floor(Math.random() * 10))
                        if (evil) newEntity.triggerEvent("lb_as:awaken_evil")
                        if (!orientation) return
                        newEntity.teleport(newEntity.location, {rotation:{x:0, y:directions[orientation]}})
                    }
                    spawned.add(locationString)
            }
            system.runTimeout(() => spawned.delete(locationString), 20)
        }
    })
    startEvent.blockComponentRegistry.registerCustomComponent("lb_as:summon_stand", {
        onTick: (tickEvent) => {
            const block = tickEvent.block
            if (block.permutation.getState("lb_as:complete")) return // ignore completed
            let stand
            const hasSpawned = block.permutation.getState("lb_as:spawned")
            if (hasSpawned === undefined) return
            const blockType = block.typeId.split(":")[1].split("_")[0]
            if (hasSpawned === false) { // block has not spawned starting armor stand yet
                stand = block.dimension.spawnEntity("armor_stand", block.above().bottomCenter())
                equipStand(stand, blockType)
                block.setPermutation(block.permutation.withState("lb_as:spawned", true))
                const heartCenter = findHeartBlock(block)
                if (heartCenter) {
                    // center was found; calculate facing direction
                    const startCenter = block.center()
                    const rotationVect = {
                        x: startCenter.x - heartCenter.x,
                        z: startCenter.z - heartCenter.z
                    }
                    const angle = vectorH_Angle(rotationVect)
                    stand.teleport(stand.location, {rotation: {x:0, y:angle}})
                }
            }

            // monitor armor stand status
            if (!stand) {
                stand = block.dimension.getEntities({type:"armor_stand", location: block.above().bottomCenter(), maxDistance:0.1, closest: 1})[0]
                if (!stand) {
                    // no stand found, set empty if not completed
                    if (!block.permutation.getState("lb_as:complete")) block.setPermutation(BlockPermutation.resolve(block.typeId).withState("lb_as:spawned", true))
                    return
                }
            }

            let permutation = armorBlockPermutation(block, stand) // returns correct permutation if different than current permutation
            if (permutation) {
                if (isComplete(permutation)) {
                    permutation = permutation.withState("lb_as:complete", true)
                    const boss = block.dimension.spawnEntity(`lb_as:armor_${blockType}`, stand.location)
                    boss.teleport(boss.location, {rotation: stand.getRotation()})
                    const heartLocation = findHeartBlock(block)
                    if (heartLocation) boss.setDynamicProperty(startBlockLocationProperty, heartLocation)
                    stand.remove()
                }
                block.setPermutation(permutation)
            }   // else undefined, no change has happened; do nothing
        }
    })
    startEvent.blockComponentRegistry.registerCustomComponent("lb_as:completed_heart", {
        onTick: (tickEvent) => {
            tickEvent.block.setType("air")
            triggerPyramidBuild(tickEvent.block.location, tickEvent.block.dimension)
        }
    })
    startEvent.blockComponentRegistry.registerCustomComponent("lb_as:heart_cheat", {
        onPlayerInteract: (interactEvent) => {
            const player = interactEvent.player
            const equippable = player.getComponent(EntityComponentTypes.Equippable)
            let item = equippable.getEquipment(EquipmentSlot.Mainhand)
            if (item?.typeId === "lb_as:gilded_blackstone_heart" && player.getGameMode() === GameMode.Creative) {
                interactEvent.block.setPermutation(interactEvent.block.permutation.withState("lb_as:heart_filled", 15))
                if (item.amount > 1) item.amount--
                else item = undefined
                equippable.setEquipment(EquipmentSlot.Mainhand, item)
            }
        }
    })
    startEvent.blockComponentRegistry.registerCustomComponent("lb_as:crying", {
        onTick: (tickEvent, params) => {
            if (tickEvent.dimension.getPlayers({location: tickEvent.block.location, maxDistance: 16}).length === 0) return
            const particleLocation = tearLocation(tickEvent.block)
            if (!particleLocation) return
            const { params: { red, green, blue }} = params
            const color = new MolangVariableMap()
            color.setColorRGB("variable.tear_color", {
                red: red,
                green: green,
                blue: blue
            })
            tickEvent.dimension.spawnParticle("lb_as:tear", particleLocation, color)
        }
    })
})

function tearLocation(block) {
    const location = block.location
    const offset = 0.08
    const faces = ['north', 'south', 'east', 'west', 'below'];
    for (let index = faces.length - 1; index >= 0; index--) {
        if (!block[faces[index]]().isAir) faces.splice(index, 1)
    }
    const face = faces[Math.floor(Math.random() * faces.length)];

    const output = createVector(location.x + Math.random(), location.y + Math.random(), location.z + Math.random())

    switch (face) {
        case 'north':
            output.z = location.z - offset;
            break;
        case 'south':
            output.z = location.z + 1 + offset;
            break;
        case 'west':
            output.x = location.x - offset;
            break;
        case 'east':
            output.x = location.x + 1 + offset;
            break;
        case 'below':
            output.y = location.y - offset;
            break;
        default:
            return
    }

    return output;
}

function findHeartBlock(startBlock) {
    const searchSize = 5
    const searchVect = createVector(searchSize, searchSize, searchSize)
    const volume = new BlockVolume(vectorSubtract(startBlock.location, searchVect), vectorAdd(startBlock.location, searchVect))
    const hearts = startBlock.dimension.getBlocks(volume, {
            includeTypes: ["lb_as:heart_block"]
        },
        true)
    let heartCenter
    for (const location of hearts.getBlockLocationIterator()) {
        heartCenter = center(location)
        break
    }
    return heartCenter
}

const ladderAnimSeconds = 6
const pyramidAnimSeconds = 10

function triggerPyramidBuild(location, dimension) {
    const heartEntity = dimension.spawnEntity("lb_as:floating_heart", bottomCenter(location))
    heartEntity.triggerEvent("prevent_pickup")
    let ticks = (ladderAnimSeconds + 4) * 20
    function flyUp() {
        heartEntity.applyKnockback({x:0, z:0}, 0.3)
        ticks--
        if (ticks > 0) system.run(flyUp)
        else heartEntity.remove()
    }
    system.runTimeout(() => {
        dimension.spawnEntity("lightning_bolt", bottomCenter(location))
        dimension.playSound("pyramid_appears", location)
        location.y-= 3
        flyUp()
    },20)
    system.runTimeout(() => {
        world.structureManager.place("lb_as:column_3", dimension, location, {animationMode: StructureAnimationMode.Layers, animationSeconds: ladderAnimSeconds})
        system.runTimeout(() => {
            // ladder
            world.structureManager.place("lb_as:ladder", dimension, vectorAdd(location, createVector(0, 0, 1)), {animationMode: StructureAnimationMode.Layers, animationSeconds: ladderAnimSeconds})
            // blocks
            world.structureManager.place("lb_as:column_4", dimension, vectorAdd(location, createVector(1, 0, 0)), {animationMode: StructureAnimationMode.Layers, animationSeconds: ladderAnimSeconds})
            world.structureManager.place("lb_as:column_2", dimension, vectorSubtract(location, createVector(1, 0, 0)), {animationMode: StructureAnimationMode.Layers, animationSeconds: ladderAnimSeconds})
            system.runTimeout(() => {
                world.structureManager.place("lb_as:column_5", dimension, vectorAdd(location, createVector(2, 0, 0)), {animationMode: StructureAnimationMode.Layers, animationSeconds: ladderAnimSeconds})
                world.structureManager.place("lb_as:column_1", dimension, vectorSubtract(location, createVector(2, 0, 0)), {animationMode: StructureAnimationMode.Layers, animationSeconds: ladderAnimSeconds})
                system.runTimeout(() => {
                    world.structureManager.place("lb_as:pyramid", dimension, vectorAdd(location, createVector(-16, 32, -16)), {animationMode: StructureAnimationMode.Layers, animationSeconds: pyramidAnimSeconds})
                }, ladderAnimSeconds * 20)
            },1)
        },1)
    },30)
}

const equips = {
    leather: {
        wooden_sword: 1,
        leather_helmet: 0.3,
        leather_chestplate: 0.15,
        leather_leggings: 0.25,
        leather_boots: 0.5
    },
    iron: {
        iron_sword: 0.1,
        iron_helmet: 0.25,
        iron_chestplate: 0.1,
        iron_leggings: 0.15,
        iron_boots: 0.35
    },
    gold: {
        golden_sword: 0.1,
        golden_helmet: 0.1,
        golden_chestplate: 0.01,
        golden_leggings: 0.08,
        golden_boots: 0.15
    },
    diamond: {}
}

function equipStand(entity, blockType) {
    const chances = equips[blockType]
    if (!chances) throw new Error(`invalid: ${blockType}`)
    const equip = []
    for (const itemType in chances) {
        if (Math.random() < chances[itemType]) {
            equip.push(itemType)
        }
    }
    // if all were chosen, remove one
    if (equip.length === 0 && (blockType !== "diamond" && blockType !== "gold")) {
        equip.push(Object.keys(chances)[Math.floor(Math.random() * Object.keys(chances).length)])
    } else if (equip.length === Object.keys(chances).length && equip.length > 0) {
        sendDebugMessage(`too long! before: ${equip.length}`)
        equip.splice(Math.floor(Math.random() * equip.length), 1)
        sendDebugMessage(`after: ${equip.length}`)
    }
    for (const item of equip) {
        entity.runCommand(`replaceitem entity @s ${toSlot(item)} 0 ${item}`)
    }
}

function isComplete(permutation) {
    return permutation.getState("lb_as:helmet") &&
    permutation.getState("lb_as:chestplate") &&
    permutation.getState("lb_as:leggings") &&
    permutation.getState("lb_as:boots") &&
    permutation.getState("lb_as:sword")
}

function armorBlockPermutation(block, stand) {
    let tier = block.typeId.split(":")[1].split("_")[0]
    if (tier === "gold") tier += "en"
    let permutation = block.permutation
    const hasHelmet = stand.runCommand(`testfor @s[hasitem={item=${tier}_helmet,location=${slots.head},slot=0}]`).successCount > 0
    const hasChestplate = stand.runCommand(`testfor @s[hasitem={item=${tier}_chestplate,location=${slots.chest},slot=0}]`).successCount > 0
    const hasLeggings = stand.runCommand(`testfor @s[hasitem={item=${tier}_leggings,location=${slots.legs},slot=0}]`).successCount > 0
    const hasBoots = stand.runCommand(`testfor @s[hasitem={item=${tier}_boots,location=${slots.feet},slot=0}]`).successCount > 0
    const hasSword = stand.runCommand(`testfor @s[hasitem={item=${tier === "leather" ? "wooden" : tier}_sword,location=${slots.mainhand},slot=0}]`).successCount > 0

    let flag = false

    if (permutation.getState("lb_as:helmet") !== hasHelmet) {
        flag = true
        permutation = permutation.withState("lb_as:helmet", hasHelmet)
    }
    if (permutation.getState("lb_as:chestplate") !== hasChestplate) {
        flag = true
        permutation = permutation.withState("lb_as:chestplate", hasChestplate)
    }
    if (permutation.getState("lb_as:leggings") !== hasLeggings) {
        flag = true
        permutation = permutation.withState("lb_as:leggings", hasLeggings)
    }
    if (permutation.getState("lb_as:boots") !== hasBoots) {
        flag = true
        permutation = permutation.withState("lb_as:boots", hasBoots)
    }
    if (permutation.getState("lb_as:sword") !== hasSword) {
        flag = true
        permutation = permutation.withState("lb_as:sword", hasSword)
    }

    return flag ? permutation : undefined
}