import * as mc from "@minecraft/server";
import * as events from './events';
import { Vector3 } from "./vector3";
import { SafetyCheckedEntity, SafetyCheckedPlayer } from "./entity";
import { lerp, normalize } from "./math";
/**
 * @remarks Entities that should never be included in raycasts.
 */
export const IGNORED_ENTITIES = ["minecraft:painting", "minecraft:item", "minecraft:xp_orb", "minecraft:arrow", "minecraft:ender_pearl", "minecraft:snowball", "minecraft:egg", "minecraft:fireworks_rocket", "minecraft:leash_knot", "minecraft:lightning_bolt"];
/**
 * @remarks A class used to handle melee attacks.
 */
export class MeleeAttack {
    /**
     * @remarks Creates a new MeleeAttack from the interface options.
     * @param data The {@link IMeleeAttackOptions} to be used by this attack.
     * @example
     * ```typescript
     * new utils.MeleeAttack({ attackID: 'ldz:weapon', hitCallbacks: [onWeaponHit], duration: 0.1, minAngle: -30, maxAngle: 30, range: 4 });
     * ```
     */
    constructor(data) {
        var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
        this.itemBinding = this.attackItemEvent.bind(this);
        this.itemOnBinding = this.attackItemOnEvent.bind(this);
        this.hitEntityBinding = this.onHitEntityEvent.bind(this);
        this.hitBlockBinding = this.onHitBlockEvent.bind(this);
        this.scriptBinding = this.attackScriptEvent.bind(this);
        this.attackID = data.attackID;
        this.damage = (_a = data.damage) !== null && _a !== void 0 ? _a : 5;
        this.delay = (_b = data.delay) !== null && _b !== void 0 ? _b : 0;
        this.duration = (_c = data.duration) !== null && _c !== void 0 ? _c : 1;
        this.minAngle = (_d = data.minAngle) !== null && _d !== void 0 ? _d : 0;
        this.maxAngle = (_e = data.maxAngle) !== null && _e !== void 0 ? _e : 0;
        this.minAngleVertical = (_f = data.minAngleVertical) !== null && _f !== void 0 ? _f : 0;
        this.maxAngleVertical = (_g = data.maxAngleVertical) !== null && _g !== void 0 ? _g : 0;
        this.degreeCheck = (_h = data.degreeCheck) !== null && _h !== void 0 ? _h : 5;
        this.range = (_j = data.range) !== null && _j !== void 0 ? _j : 5;
        this.piercing = (_k = data.piercing) !== null && _k !== void 0 ? _k : false;
        this.disableDebug = (_l = data.disableDebug) !== null && _l !== void 0 ? _l : false;
        this.validTargets = (_m = data.validTargets) !== null && _m !== void 0 ? _m : {};
        this.validTargets.excludeTypes = (_o = this.validTargets.excludeTypes) !== null && _o !== void 0 ? _o : [];
        (_p = this.validTargets.excludeTypes) === null || _p === void 0 ? void 0 : _p.push(...IGNORED_ENTITIES);
        this.hitCallbacks = (_q = data.hitCallbacks) !== null && _q !== void 0 ? _q : [];
        this.swingCallbacks = (_r = data.swingCallbacks) !== null && _r !== void 0 ? _r : [];
        if (data.subscribeToItemEvent)
            mc.world.afterEvents.itemStartUse.subscribe(this.itemBinding);
        if (data.subscribeToItemEvent)
            mc.world.afterEvents.itemStartUseOn.subscribe(this.itemOnBinding);
        if (data.subscribeToHitEvent)
            mc.world.afterEvents.entityHitEntity.subscribe(this.hitEntityBinding);
        if (data.subscribeToHitEvent)
            mc.world.afterEvents.entityHitBlock.subscribe(this.hitBlockBinding);
        if (data.subscribeToScriptEvent)
            mc.system.afterEvents.scriptEventReceive.subscribe(this.scriptBinding);
    }
    /**
     * @remarks Performs this attack from the player.
     * @param player The player who performs the attack.
     */
    attack(player) {
        this.performAttack(player);
    }
    /**
     * @remarks Adds a callback to be executed whenever an attack is performed.
     * @param callback The callback to be added. Takes in a {@link SafetyCheckedPlayer} as a parameter.
     */
    addSwingCallback(callback) {
        this.swingCallbacks.push(callback);
    }
    /**
     * @remarks Adds a callback to be executed whenever an attack succeeds.
     * @param callback The callback to be added. Takes in a {@link SafetyCheckedPlayer} and a {@link SafetyCheckedEntity} as a parameter.
     */
    addHitCallback(callback) {
        this.hitCallbacks.push(callback);
    }
    attackItemEvent(event) {
        if (event.itemStack.typeId === this.attackID) {
            this.performAttack(new SafetyCheckedPlayer(event.source));
        }
    }
    attackItemOnEvent(event) {
        var _a;
        if (((_a = event.itemStack) === null || _a === void 0 ? void 0 : _a.typeId) === this.attackID) {
            this.performAttack(new SafetyCheckedPlayer(event.source));
        }
    }
    attackScriptEvent(event) {
        if (event.id === this.attackID && event.sourceEntity && event.sourceEntity instanceof mc.Player) {
            this.performAttack(new SafetyCheckedPlayer(event.sourceEntity));
        }
    }
    onHitEntityEvent(event) {
        var _a, _b, _c, _d;
        const attacker = new SafetyCheckedEntity(event.damagingEntity);
        if (attacker.typeId === "minecraft:player" && ((_b = (_a = attacker.Equipment) === null || _a === void 0 ? void 0 : _a.getEquipment(mc.EquipmentSlot.Mainhand)) === null || _b === void 0 ? void 0 : _b.typeId) === this.attackID) {
            const cooldown = (_d = (_c = attacker.Equipment) === null || _c === void 0 ? void 0 : _c.getEquipment(mc.EquipmentSlot.Mainhand)) === null || _d === void 0 ? void 0 : _d.getComponent(mc.ItemCooldownComponent.componentId);
            if (cooldown && cooldown.getCooldownTicksRemaining(attacker.getPlayer().UnsafeEntity) > 0)
                return;
            attacker.nameTag = "§a§t§t§a§c§k§e§r";
            events.waitForEvent(1, () => attacker.nameTag = attacker.getPlayer().name);
            this.performAttack(attacker.getPlayer());
            if (cooldown) {
                cooldown.startCooldown(attacker.getPlayer().UnsafeEntity);
            }
        }
    }
    onHitBlockEvent(event) {
        var _a, _b, _c, _d;
        const attacker = new SafetyCheckedEntity(event.damagingEntity);
        if (attacker.typeId === "minecraft:player" && ((_b = (_a = attacker.Equipment) === null || _a === void 0 ? void 0 : _a.getEquipment(mc.EquipmentSlot.Mainhand)) === null || _b === void 0 ? void 0 : _b.typeId) === this.attackID) {
            const cooldown = (_d = (_c = attacker.Equipment) === null || _c === void 0 ? void 0 : _c.getEquipment(mc.EquipmentSlot.Mainhand)) === null || _d === void 0 ? void 0 : _d.getComponent(mc.ItemCooldownComponent.componentId);
            if (cooldown && cooldown.getCooldownTicksRemaining(attacker.getPlayer().UnsafeEntity) > 0)
                return;
            attacker.nameTag = "§a§t§t§a§c§k§e§r";
            events.waitForEvent(1, () => attacker.nameTag = attacker.getPlayer().name);
            this.performAttack(attacker.getPlayer());
            if (cooldown) {
                cooldown.startCooldown(attacker.getPlayer().UnsafeEntity);
            }
        }
    }
    async performAttack(player) {
        // Swing callbacks occur immediately, regardless of the attack success
        this.swingCallbacks.forEach(callback => callback(player));
        // Wait for the delay, when used with an animation this should be point in the animation when damage should occur
        if (this.delay)
            await events.waitForTicks(this.delay * 20);
        // Get angle increment per frame
        const hAngleIncrementAmount = Math.round((Math.abs(this.minAngle) + Math.abs(this.maxAngle)) / this.duration);
        const vAngleIncrementAmount = Math.round((Math.abs(this.minAngleVertical) + Math.abs(this.maxAngleVertical)) / this.duration);
        const hitsPerFrame = Math.max(hAngleIncrementAmount, vAngleIncrementAmount);
        // Set current angle to the start point, this will update at the end of each frame
        let hCurrentAngle = this.maxAngle;
        let vCurrentAngle = this.maxAngleVertical;
        // Our victim ids are cached to prevent hitting the same entity with multiple rays of the same attack
        // The player's id is added initially to prevent them from being hit
        const victimIds = [player.id];
        // For each frame of the attack
        for (let tick = 0; tick < this.duration; tick++) {
            // Initialize the attack
            let hitEntities = [];
            const origin = new Vector3(player.getHeadLocation());
            // For each hit on this frame
            for (let frameTime = 0; frameTime <= hitsPerFrame; frameTime++) {
                // Calculate the angle values based on frame progress
                const hAngle = -lerp(hCurrentAngle, hCurrentAngle - hAngleIncrementAmount, normalize(0, hitsPerFrame, frameTime));
                const vAngle = -lerp(vCurrentAngle, vCurrentAngle - vAngleIncrementAmount, normalize(0, hitsPerFrame, frameTime));
                // Determine the raycast direction
                const direction = player.getViewDirection().rotatePitch(vAngle).rotateYaw(hAngle, player.RelativeUp).normalize();
                // Use particles to show raycast to devs
                this.debugRaycasts(player, origin, direction);
                // Get and filter victims of raycasts
                let rayEntities = player.dimension.getEntitiesFromRay(origin, direction, { maxDistance: this.range }).map(hit => new SafetyCheckedEntity(hit.entity));
                rayEntities = rayEntities.filter(entity => !victimIds.includes(entity.id) && entity.matches(this.validTargets));
                rayEntities.forEach(entity => victimIds.push(entity.id));
                // Clamp value to first hit if not piercing
                if (!this.piercing && rayEntities.length > 1)
                    rayEntities.length = 1;
                // Add filtered victims to entities to be hit this frame
                hitEntities.push(...rayEntities);
            }
            // Perform the hit for the entities found this frame
            this.onHit(hitEntities, player);
            // Update our angles
            hCurrentAngle -= hAngleIncrementAmount;
            vCurrentAngle -= vAngleIncrementAmount;
            // Wait one frame
            await events.waitForTicks(0);
        }
    }
    debugRaycasts(player, origin, direction) {
        if (!this.disableDebug && player.hasTag("dev")) {
            for (let index = 0; index < this.range; index++) {
                player.dimension.spawnParticle("minecraft:endrod", origin.add(direction.multiply(index)));
            }
        }
    }
    onHit(entities, player) {
        entities.forEach(entity => {
            if (entity.isValid()) {
                entity.applyDamage(this.damage, { cause: mc.EntityDamageCause.entityAttack, damagingEntity: player.UnsafeEntity });
                this.hitCallbacks.forEach(callback => callback(player, entity));
            }
        });
    }
}
