import * as mc from "@minecraft/server";
import * as utils from "../utils/index";
import { DeathEffects, DropTiming, MOB_DATA, MOB_EQUIPMENT, COLOR_MAP, Settings } from "../shared/index";
import { DeathTally } from "./death_tally";
import { HORSE_TYPES, HorseActor } from "./horse_actor";
import { EquipmentActor } from "./equipment_actor";
import { ItemDrops } from "./item_drops";
mc.world.afterEvents.entityDie.subscribe(event => {
    handleDeath(event.deadEntity, event.damageSource.damagingEntity);
});
mc.world.afterEvents.entityHealthChanged.subscribe(async (event) => {
    var _a, _b;
    if ((event.entity.typeId === "minecraft:wither" || event.entity.typeId === "minecraft:ender_dragon") && event.newValue <= 0) {
        let { dimension, location } = event.entity;
        await utils.waitForLoopEvent(1, () => {
            if (event.entity.isValid())
                ({ dimension, location } = event.entity);
            return !event.entity.isValid();
        });
        const { x, z } = location;
        const y = utils.clamp(location.y, (_b = (_a = dimension.getTopmostBlock(location, location.y)) === null || _a === void 0 ? void 0 : _a.location.y) !== null && _b !== void 0 ? _b : 0, dimension.heightRange.max);
        attemptGraveSpawn(event.entity.typeId, dimension, new utils.Vector3({ x, y, z }));
    }
});
mc.world.afterEvents.playerBreakBlock.subscribe(({ player }) => {
    const event = ({ entity, eventId }) => {
        if (eventId.includes("start_twitching")) {
            handleDeath(entity, player);
        }
    };
    mc.world.afterEvents.dataDrivenEntityTrigger.subscribe(event, { entityTypes: ["minecraft:creaking"] });
    utils.waitForEvent(1, () => mc.world.afterEvents.dataDrivenEntityTrigger.unsubscribe(event));
}, { blockTypes: ["minecraft:creaking_heart"] });
function handleDeath(deadEntity, damagingEntity) {
    const data = MOB_DATA[deadEntity.typeId];
    utils.mclog(`Entity died: ${deadEntity.typeId}`, "death_handler");
    if (!data)
        return;
    const victim = new utils.SafetyCheckedEntity(deadEntity);
    if (victim.typeId === "minecraft:player") {
        DeathTally.addDeath(victim.id);
    }
    if ((damagingEntity === null || damagingEntity === void 0 ? void 0 : damagingEntity.typeId) === "minecraft:player") {
        DeathTally.addKill(damagingEntity.id, victim.typeId);
    }
    // Check if the entity has a condition and if it fails, return
    if (data.condition && !data.condition(victim)) {
        utils.mclog(`Failed condition for ${victim.typeId}`, "death_handler");
        return;
    }
    try {
        const deadMobId = victim.typeId.replace("minecraft:", "ldz_250121:dead_");
        const deadMob = new DeadMob(utils.spawnEntitySafe(victim.dimension, deadMobId, victim.location, { rotation: victim.getRotation() }));
        deadMob.transferVictim(victim, damagingEntity === null || damagingEntity === void 0 ? void 0 : damagingEntity.id);
        victim.remove();
    }
    catch (error) {
        utils.mcerror(error);
        return;
    }
}
function getFloor(victim) {
    var _a;
    if (victim.isInWater)
        return victim.location;
    const block = (_a = victim.dimension.getBlockFromRay(victim.location, utils.Vector3.DOWN, { includeLiquidBlocks: true })) === null || _a === void 0 ? void 0 : _a.block;
    if (!block)
        return victim.location;
    const { x, z } = victim.location;
    let y = block.location.y;
    if (block.isLiquid)
        y -= 0.5;
    return new utils.Vector3({ x, y, z }).up();
}
mc.world.afterEvents.entitySpawn.subscribe(event => {
    var _a;
    const animation = (_a = MOB_DATA[event.entity.typeId]) === null || _a === void 0 ? void 0 : _a.inverseAnimation;
    if (animation) {
        utils.waitForEvent(2, () => {
            if (event.entity.isValid()) {
                event.entity.playAnimation(animation, { controller: "ctrl.custom_death" });
            }
        });
    }
});
function attemptGraveSpawn(typeId, dimension, location, killerId) {
    if (!mc.world.gameRules.doMobLoot) {
        utils.mclog(`doMobLoot is disabled, skipping grave spawn for ${typeId}`, "death_handler");
        return;
    }
    const data = MOB_DATA[typeId];
    // Clamp Bonus from mob kills to 6, which adds with the standard baseline for a max of 7%
    const bonus = Math.min(data.graveSpawnBonus * (killerId ? DeathTally.getEntityKills(killerId, typeId) : 0), 6);
    const chance = data.graveSpawnBase + bonus;
    const random = utils.getRandomInteger(0, 100);
    if (random <= chance || Settings.MobGravestoneGuaranteed) {
        if (!Settings.MobGravestone) {
            utils.mclog(`Grave spawn is disabled, skipping grave spawn for ${typeId}`, "death_handler");
        }
        else {
            dimension.spawnItem(new mc.ItemStack(data.graveTypeId, 1), location);
            utils.mclog(`${typeId} rolled ${random} <= ${chance} and spawned ${data.graveTypeId}`, "death_handler");
        }
    }
    else {
        utils.mclog(`${typeId} rolled ${random} > ${chance} and failed to spawn ${data.graveTypeId}`, "death_handler");
    }
}
export class DeadMob extends utils.Actor {
    constructor() {
        super(...arguments);
        this.data = MOB_DATA[this.typeId.replace("ldz_250121:dead_", "minecraft:")];
        this.itemCache = [];
    }
    get AnimFinishedTime() {
        var _a;
        return ((_a = this.data.duration) !== null && _a !== void 0 ? _a : 1.5) * 20;
    }
    async transferVictim(victim, killerId) {
        this.killerId = killerId;
        this.victimId = victim.typeId;
        this.cacheItemDrops();
        this.triggerDamageFlash();
        this.transferComponentData(victim);
        await this.fall();
        utils.waitForEvent(this.AnimFinishedTime, () => {
            this.dropCachedItems();
            this.playDeathEffect();
        }, this.typeId + "_death");
    }
    transferComponentData(victim) {
        const color = victim.getComponent("minecraft:color");
        const scale = victim.getComponent("minecraft:scale");
        const baby = victim.getComponent("minecraft:is_baby");
        const color2 = victim.getComponent("minecraft:color2");
        const tamed = victim.getComponent("minecraft:is_tamed");
        const skin_id = victim.getComponent("minecraft:skin_id");
        const variant = victim.getComponent("minecraft:variant");
        const shaking = victim.getComponent("minecraft:is_shaking");
        const sheared = victim.getComponent("minecraft:is_sheared");
        const saddled = victim.getComponent("minecraft:is_saddled");
        const mark_variant = victim.getComponent("minecraft:mark_variant");
        const climate_variant = victim.getProperty("minecraft:climate_variant");
        if (baby)
            this.triggerEvent(`add_is_baby`);
        if (tamed)
            this.triggerEvent(`add_is_tamed`);
        if (shaking)
            this.triggerEvent(`add_is_shaking`);
        if (sheared)
            this.triggerEvent(`add_is_sheared`);
        if (saddled)
            this.triggerEvent(`add_is_saddled`);
        if (color)
            this.triggerEvent(`add_color_${color.value}`);
        if (skin_id)
            this.triggerEvent(`add_skin_id_${skin_id.value}`);
        if (variant)
            this.triggerEvent(`add_variant_${variant.value}`);
        if (scale)
            this.triggerEvent(`add_scale_${scale.value.toFixed(1)}`);
        if (mark_variant)
            this.triggerEvent(`add_mark_variant_${mark_variant.value}`);
        if (climate_variant)
            this.setProperty("minecraft:climate_variant", climate_variant);
        if (color2)
            this.transferTropicalFishData(color2);
        this.transferHorseData(victim);
        this.transferEquipment(victim);
    }
    transferTropicalFishData(color2) {
        const rgb = COLOR_MAP[color2.value];
        if (rgb) {
            this.setProperty("r", rgb.r);
            this.setProperty("g", rgb.g);
            this.setProperty("b", rgb.b);
        }
    }
    transferHorseData(victim) {
        if (HORSE_TYPES.includes(victim.typeId)) {
            const ageable = victim.getComponent("minecraft:ageable");
            const actor = HorseActor.getActor(victim.id, HorseActor);
            if (ageable && actor) {
                let age = actor.Age;
                utils.mclog(`Age: ${age}`, this.ActorId);
                age = utils.clamp((age / (ageable.duration * 2)) + 0.5, 0.5, 1);
                utils.mclog(`Age: ${age}`, this.ActorId);
                this.triggerEvent(`add_scale_${age.toFixed(1)}`);
            }
        }
    }
    transferEquipment(victim) {
        const actor = utils.Actor.getActor(victim.id, EquipmentActor);
        if (actor) {
            for (const [slot, item] of Object.entries(actor.EquipmentMap)) {
                this.runCommand(`replaceitem entity @s ${slot} 0 ${item}`);
            }
        }
        else if (victim instanceof DeadMob) {
            for (const [slot, items] of Object.entries(MOB_EQUIPMENT)) {
                for (const item of items) {
                    if (victim.runCommand(`testfor @s[hasitem={item=${item},location=${slot}}]`).successCount) {
                        this.runCommand(`replaceitem entity @s ${slot} 0 ${item}`);
                        break;
                    }
                }
            }
        }
    }
    clearEquipment() {
        this.itemCache.forEach(item => {
            for (const [slot, items] of Object.entries(MOB_EQUIPMENT)) {
                if (items.includes(item.typeId)) {
                    utils.mclog(`Removing ${item} from ${slot}`);
                    this.runCommand(`replaceitem entity @s ${slot} 0 air`);
                }
            }
        });
    }
    triggerDamageFlash() {
        utils.waitForEvent(1, () => this.applyDamage(0.1, { cause: mc.EntityDamageCause.selfDestruct }));
    }
    async fall() {
        this.ground = getFloor(this);
        const offset = Math.max(this.location.y - this.ground.y, 0.1);
        try {
            this.setProperty("y_offset", offset);
            await utils.waitForTicks(offset + this.AnimFinishedTime);
            const mob = new DeadMob(utils.spawnEntitySafe(this.dimension, this.typeId, this.ground, { rotation: this.getRotation() }));
            mob.triggerEvent("add_physics");
            mob.setProperty("y_offset", 0);
            mob.transferEquipment(this);
            mob.playDeathEffect();
            this.dropCachedItems();
            this.remove();
        }
        catch (error) { }
    }
    async cacheItemDrops() {
        let drops = ItemDrops.getDropsAtLocation(mc.system.currentTick, this.location);
        if (!drops.length) {
            await utils.waitForEvent(2, () => {
                drops = ItemDrops.getDropsAtLocation(mc.system.currentTick - 1, this.location);
            });
        }
        this.itemCache = drops.map(drop => drop.item);
        if (Settings.DropTiming === DropTiming.after) {
            drops.forEach(({ entity }) => entity.remove());
        }
        else {
            this.dropXpOrbs();
            utils.waitForEvent(1, this.clearEquipment.bind(this));
            attemptGraveSpawn(this.victimId, this.dimension, this.location, this.killerId);
        }
    }
    dropCachedItems() {
        if (!this.isValid())
            return;
        if (Settings.DropTiming === DropTiming.after) {
            this.dropXpOrbs();
            this.clearEquipment();
            this.itemCache.forEach(item => this.dimension.spawnItem(item, this.location));
            attemptGraveSpawn(this.victimId, this.dimension, this.location, this.killerId);
        }
    }
    dropXpOrbs() {
        if (!this.isValid() || !mc.world.gameRules.doMobLoot)
            return;
        const xp = typeof this.data.xpDrop === "function" ? this.data.xpDrop(this, this.killerId) : this.data.xpDrop;
        for (let index = 0; index < (xp !== null && xp !== void 0 ? xp : 0); index++) {
            this.dimension.spawnEntity("minecraft:xp_orb", this.location);
        }
    }
    playDeathEffect() {
        if (!this.isValid())
            return;
        utils.abortEvents(this.ActorId + "_death");
        this.addTag("finished_anim");
        if (this.data.ignoreEffects) {
            this.remove();
            return;
        }
        switch (Settings.DeathEffects) {
            case DeathEffects.particles: {
                const map = new mc.MolangVariableMap();
                map.setVector3("variable.aabb", new utils.Vector3(this.getHeadLocation().distance(this.location) / 2));
                this.dimension.spawnParticle("minecraft:death_explosion_emitter", this.location, map);
            }
            case DeathEffects.disappear: {
                this.remove();
                break;
            }
            default:
                break;
        }
    }
}
