import * as mc from "@minecraft/server";
import * as events from './events';
import { Vector3 } from "./vector3";
import { SafetyCheckedEntity, SafetyCheckedPlayer } from "./entity";
import { GlobalNamespace } from "./namespace";
/**
 * @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;
        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.degreeCheck = (_f = data.degreeCheck) !== null && _f !== void 0 ? _f : 5;
        this.range = (_g = data.range) !== null && _g !== void 0 ? _g : 5;
        this.piercing = (_h = data.piercing) !== null && _h !== void 0 ? _h : false;
        this.disableDebug = (_j = data.disableDebug) !== null && _j !== void 0 ? _j : false;
        this.validTargets = (_k = data.validTargets) !== null && _k !== void 0 ? _k : {};
        this.validTargets.excludeTypes = (_l = this.validTargets.excludeTypes) !== null && _l !== void 0 ? _l : [];
        (_m = this.validTargets.excludeTypes) === null || _m === void 0 ? void 0 : _m.push(...IGNORED_ENTITIES);
        this.hitCallbacks = (_o = data.hitCallbacks) !== null && _o !== void 0 ? _o : [];
        this.swingCallbacks = (_p = data.swingCallbacks) !== null && _p !== void 0 ? _p : [];
        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;
        const attacker = new SafetyCheckedEntity(event.damagingEntity);
        if (((_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 && attacker.typeId === "minecraft:player") {
            const name = attacker.nameTag;
            attacker.nameTag = GlobalNamespace.Namespace + "attack";
            events.waitForEvent(1, () => attacker.nameTag = name);
            this.performAttack(attacker.getPlayer());
        }
    }
    onHitBlockEvent(event) {
        var _a, _b;
        const attacker = new SafetyCheckedEntity(event.damagingEntity);
        if (((_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 && attacker.typeId === "minecraft:player") {
            const name = attacker.nameTag;
            attacker.nameTag = GlobalNamespace.Namespace + "attack";
            events.waitForEvent(1, () => attacker.nameTag = name);
            this.performAttack(attacker.getPlayer());
        }
    }
    async performAttack(player) {
        this.swingCallbacks.forEach(callback => callback(player));
        if (this.delay)
            await events.waitForTicks(this.delay * 20);
        const angleIncrementAmount = Math.round((Math.abs(this.minAngle) + Math.abs(this.maxAngle)) / this.duration);
        let currentAngle = this.maxAngle;
        const victimIds = [player.id];
        for (let tick = 0; tick < this.duration; tick++) {
            let hitEntities = [];
            for (let angle = currentAngle; angle >= currentAngle - angleIncrementAmount; angle -= this.degreeCheck) {
                const origin = new Vector3(player.getHeadLocation());
                const direction = new Vector3(player.getViewDirection()).rotateByAngle(angle).normalize();
                this.debugRaycasts(player, origin, direction);
                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));
                if (!this.piercing && rayEntities.length > 1)
                    rayEntities.length = 1;
                hitEntities.push(...rayEntities);
            }
            this.onHit(hitEntities, player);
            currentAngle -= angleIncrementAmount;
            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));
            }
        });
    }
}
