var _a;
import * as mc from "@minecraft/server";
import * as utils from "../utils/index";
/**
 * @remarks The base NPC class. This or one of its children should be extended from whenever a new NPC is created.
 */
export class NPCBase {
    /**
     * @remarks The {@link utils.SafetyCheckedEntity} associated with this NPC.
     */
    get Entity() {
        return this.entity;
    }
    /**
     * @remarks The id of the entity associated with this NPC.
     */
    get ID() {
        return this.Entity.id;
    }
    /**
     * @remarks The {@link mc.EntityHealthComponent} of the entity associated with this NPC if valid.
     */
    get Health() {
        return this.Entity.getComponent(mc.EntityHealthComponent.componentId);
    }
    /**
     * @remarks The current {@link Task} this entity is performing.
     */
    get CurrentTask() {
        return this.currentTask;
    }
    /**
     * @remarks Creates a new instance of the NPC class associated with an entity.
     * @param entity The entity to associate with this NPC instance.
     * @param defaultTask An optional default task for this NPC, any task assigned to default should be one with no task options.
     * @throws Will throw an error if an instance of the NPC class has already been created using that entity, each entity can only be associated with one NPC instance.
     * @example
     * ```typescript
     * mc.world.afterEvents.entitySpawn.subscribe(event => {
     *     if (event.entity.typeId === "ldz_crcomp:craftee") {
     *         try {
     *             new PlayerCompanion(event.entity);
     *         } catch (error) {
     *              utils.mcerror(error);
     *         }
     *     }
     * });
     * ```
     */
    constructor(entity, defaultTask) {
        this.components = {};
        this.broadcastEvents = {};
        this.TypeId = this.constructor.name;
        if (NPCBase.npcList[entity.id] !== undefined) {
            utils.mcerror(`${entity.id} Is Already An NPC`, "npc");
            throw `${entity.id} Is Already An NPC`;
        }
        utils.mclog(`Instatiated ${this.TypeId} For Entity ${entity.id}`, "npc");
        this.entity = new utils.SafetyCheckedEntity(entity);
        this.defaultTask = defaultTask;
        if (this.defaultTask) {
            this.startTask(new this.defaultTask(this), false);
        }
        this.onConstructed();
        NPCBase.onInstantiate(this);
    }
    /**
     * @remarks Called at the end of the constructor, generally you can override this method to add components and perform other child-specific setup.
     */
    onConstructed() { }
    /**
     * @remarks Destructs an NPC class, aborting its tasks, removing it's components and freeing it for garbage collection.
     * The {@link DestructOnRemoveEventComponent} will call this automatically whenever the associated entity is despawned if the component has been added.
     * @param reason An optional reason why the entity was destroyed, will be printed to the dev chat.
     */
    async destructor(reason) {
        var _b;
        utils.mclog(`Destructing ${this.TypeId} (${this.ID}) Because Of ${reason}`, "npc");
        for (const component of Object.keys(this.components)) {
            this.removeComponent(component);
        }
        (_b = this.currentTask) === null || _b === void 0 ? void 0 : _b.abort();
        NPCBase.onDestruct(this);
        if (this.Entity.isValid()) {
            this.Entity.remove();
        }
    }
    /**
     * @remarks Adds a component to this entity. When adding a component, a new instance should be added; **do not** share instances of a component class between multiple NPCs.
     * @param component The component to add to this NPC.
     * @returns The component that was added, or the existing component if an instance of that component already existed on the NPC.
     * @remarks
     * ```typescript
     * npc.addComponent(new DestructOnRemoveEventComponent());
     * ```
     */
    addComponent(component) {
        if (this.components[component.id]) {
            utils.mcwarn(`${this.ID} Already Has Component With ID ${component.id}`, "npc");
            return this.components[component.id];
        }
        utils.mclog(`${this.ID} Added Component ${component.id}`, "npc");
        this.components[component.id] = component;
        component.onComponentAdded(this);
        return component;
    }
    /**
     * @remarks Removes a component from an NPC.
     * @param componentId The type id of the component that should be removed.
     * @returns True if the component existed and was removed, false otherwise.
     * @example
     * ```typescript
     * npc.removeComponent(DestructOnRemoveEventComponent.ComponentId)
     * ```
     */
    removeComponent(component) {
        const name = typeof component === "string" ? component : component.name;
        const componentFound = this.components[name];
        if (componentFound) {
            utils.mclog(`${this.ID} Removed Component ${componentFound.id}`, "npc");
            componentFound.onComponentRemoved(this);
            delete this.components[name];
            return true;
        }
        return false;
    }
    /**
     * @remarks Gets the component ids of this NPC.
     * @returns An array of the ids of all the components attached to this NPC.
     */
    getComponentIds() {
        return Object.keys(this.components);
    }
    /**
     * @remarks Gets a given component on an NPC.
     * @param componentId The type id of the component to get.
     * @returns The component instance, or undefined if the component doesn't exist on this NPC.
     * @example
     * ```
     * npc.getComponent(DestructOnRemoveEventComponent.ComponentId) as DestructOnRemoveEventComponent|undefined;
     * ```
     */
    getComponent(component) {
        return this.components[component.name];
    }
    /**
     *
     * @param task The {@link Task} this NPC should start.
     * @param abortCurrent Aborts the current task in favor of the new task if true.
     * @example
     * ```typescript
     * companion.startTask(new GoDiggingTask(companion), true); // From Craftee Companion scripts/playerCompanion/interactMenu.ts
     * ```
     */
    async startTask(task, abortCurrent) {
        var _b, _c;
        if (!this.Entity.isValid()) {
            (_b = this.currentTask) === null || _b === void 0 ? void 0 : _b.abort();
            return;
        }
        if (abortCurrent) {
            await ((_c = this.currentTask) === null || _c === void 0 ? void 0 : _c.abort());
        }
        this.currentTask = task;
        await this.currentTask.start();
        if (this.currentTask === task && this.defaultTask) {
            this.startTask(new this.defaultTask(this), false);
        }
    }
    /**
     * @remarks Subscribes a callback to an event on this NPC.
     * @param event The event name to subscribe to.
     * @param callback The callback to execute when the event is raised.
     */
    subscribeToBroadcast(event, callback) {
        if (!this.broadcastEvents[event])
            this.broadcastEvents[event] = [];
        this.broadcastEvents[event].push(callback);
    }
    /**
     * @remarks Unsubscribes a callback from an event on this NPC.
     * @param event The event name to unsubscribe from.
     * @param callback The callback to be remove from the subscribed list.
     */
    unsubscribeToBroadcast(event, callback) {
        if (!this.broadcastEvents[event])
            return;
        utils.removeFromArray(this.broadcastEvents[event], callback);
    }
    /**
     * @remarks Executes all of the callbacks subscribed to the event.
     * @param event The event to raise.
     */
    broadcastEvent(event) {
        if (!this.broadcastEvents[event])
            return;
        this.broadcastEvents[event].forEach(callback => callback(this));
    }
    /**
     * @remarks Finds all NPCs matching a query.
     * @param dimension The dimension in which to find NPCs.
     * @param query The entity query to filter for.
     * @returns An array of {@link NPCBase} instances for entities matching the conditions.
     * @example
     * ```typescript
     * const crafteeList = getNPCS(utils.overworld, {type: "ldz_crcomp:craftee"});
     * ```
     */
    static getNPCS(dimension, query) {
        return dimension.getEntities(query).filter(entity => this.npcList[entity.id] !== undefined).map(entity => this.npcList[entity.id]);
    }
    /**
     * @remarks Gets an individual NPC using an entity id.
     * @param id The entity id belonging to the npc we should get.
     * @returns The {@link NPCBase} class instance of the entity with the id, or undefined if the entity doesn't exist or is not an NPC.
     * @example
     * ```typescript
     * const npc = getNPC(entity.id);
     * ```
     */
    static getNPC(id) {
        return this.npcList[id];
    }
    /**
     * @remarks Automatically instantiates any entities matching a certain query as an NPC type.
     * @param query The {@link mc.EntityQueryOptions} an entity should match to be instantiated.
     * @param npcType The NPC type the entity should be instantiated as.
     */
    static async instantiateNPC(query, npcType) {
        await utils.waitForWorldLoad();
        mc.world.afterEvents.entitySpawn.subscribe(event => {
            if (event.entity.isValid() && event.entity.matches(query)) {
                try {
                    new npcType(event.entity);
                }
                catch (error) { }
            }
        });
        mc.world.afterEvents.entityLoad.subscribe(event => {
            if (event.entity.isValid() && event.entity.matches(query)) {
                try {
                    new npcType(event.entity);
                }
                catch (error) { }
            }
        });
        try {
            utils.overworld.getEntities(query).forEach(entity => new npcType(entity));
            utils.nether.getEntities(query).forEach(entity => new npcType(entity));
            utils.end.getEntities(query).forEach(entity => new npcType(entity));
        }
        catch (error) { }
    }
    static onInstantiate(npc) {
        this.npcList[npc.Entity.id] = npc;
        this.spawnCallbacks.forEach(callback => callback(npc));
        Object.keys(this.broadcastEvents).forEach(event => this.broadcastEvents[event].forEach(callback => npc.subscribeToBroadcast(event, callback)));
    }
    static onDestruct(npc) {
        delete this.npcList[npc.Entity.id];
        this.despawnCallbacks.forEach(callback => callback(npc));
    }
}
_a = NPCBase;
// #region Static NPC
NPCBase.npcList = {};
NPCBase.spawnCallbacks = [];
NPCBase.despawnCallbacks = [];
NPCBase.broadcastEvents = {};
/**
 * @remarks Contains a set of events related to npcs.
 */
NPCBase.npcEvents = {
    /**
     * @remarks This event fires when an NPC is instantiated.
     */
    spawn: {
        /**
         * @remarks Subscribes to the event.
         */
        subscribe: (callback) => {
            utils.addUniqueToArray(_a.spawnCallbacks, callback);
        },
        /**
         * @remarks Unsubscribes from the event.
         */
        unsubscribe: (callback) => {
            utils.removeFromArray(_a.spawnCallbacks, callback);
        },
    },
    /**
     * @remarks This event fires when an NPC is destructed. Note that the entity may be invalid depending on the destruction reason.
     */
    despawn: {
        /**
         * @remarks Subscribes to the event.
         */
        subscribe: (callback) => {
            utils.addUniqueToArray(_a.despawnCallbacks, callback);
        },
        /**
         * @remarks Unsubscribes from the event.
         */
        unsubscribe: (callback) => {
            utils.removeFromArray(_a.despawnCallbacks, callback);
        },
    },
    /**
     * @remarks This event fires whenever an NPC broadcasts an event matching the event type. Note that this applies to all NPCs,
     * so if two different NPC types have an event called `onHurt` you will need to filter for the NPC type you want.
     */
    broadcast: {
        /**
         * @remarks Subscribes to the event.
         */
        subscribe: (event, callback) => {
            if (!_a.broadcastEvents[event])
                _a.broadcastEvents[event] = [];
            _a.broadcastEvents[event].push(callback);
            Object.values(_a.npcList).forEach(npc => npc.subscribeToBroadcast(event, callback));
        },
        /**
         * @remarks Unsubscribes from the event.
         */
        unsubscribe: (event, callback) => {
            if (!_a.broadcastEvents[event])
                return;
            utils.removeFromArray(_a.broadcastEvents[event], callback);
            Object.values(_a.npcList).forEach(npc => npc.unsubscribeToBroadcast(event, callback));
        },
    },
};
