var _a;
import * as mc from "@minecraft/server";
import { afterEvents, waitForEvent, waitForNamespace } from "./events";
import { mclog } from "./log";
import { SafetyCheckedEntity, SafetyCheckedPlayer } from "./entity";
import { removeFromArray } from "./array";
import { overworld, nether, end } from "./dimension";
import { addName } from "./namespace";
export class Actor extends SafetyCheckedEntity {
    /**
     * @remarks Creates a new actor associated with an entity.
     * @param entity The entity associated with this actor.
     * @throws Throws an error if the entity has already been assigned as an actor.
     */
    constructor(entity) {
        if (Actor.actorRegister.has(entity.id)) {
            throw `${entity.id} is already an Actor`;
        }
        super(entity);
        /**
         * @remarks The class name of this actor used internally.
         */
        this.ActorId = this.constructor.name;
        this.components = {};
        this.events = {};
        Actor.onConstruct(this);
        waitForEvent(0, () => this.onConstructed());
        mclog(`Instantiated ${entity.id} as ${this.ActorId}`, "actor");
    }
    /**
     * @remarks Destroys an actor, this will be called automatically if the entity is removed, or will remove the entity if it still exists.
     */
    destructor() {
        if (this.isValid())
            return;
        Object.keys(this.components).forEach(comp => this.removeActorComponent(comp));
        Actor.onDestruct(this);
        mclog(`Destroyed ${this.ActorId} instance ${this.id}`, "actor");
    }
    /**
     * @remarks Called during the constructor. This should be used rather than overriding the constructor for inheritited classes.
     */
    onConstructed() { }
    /**
     * @remarks Adds an {@link ActorComponent}.
     * @param component The component to be added.
     * @returns True if the component was added, false otherwise (if the component is already added).
     */
    addActorComponent(comp, ...args) {
        if (this.components[comp.name]) {
            return false;
        }
        const component = new comp(this, args[0]);
        mclog(`${this.ActorId} ${this.id} added component ${component.id}`, "actor");
        this.components[component.id] = component;
        component.onComponentAdded();
        return true;
    }
    /**
     * @remarks Removes an {@link ActorComponent}.
     * @param component The component to remove, by name or type.
     * @returns True if the component was removed, false otherwise (if the component doesn't exist).
     */
    removeActorComponent(component) {
        const name = typeof component === "string" ? component : component.name;
        const foundComponent = this.components[name];
        if (foundComponent) {
            mclog(`${this.ActorId} ${this.id} removed component ${name}`, "actor");
            foundComponent.onComponentRemoved();
            delete this.components[name];
            return true;
        }
        return false;
    }
    /**
     * @remarks Gets the ids of all of the attached {@link ActorComponent}s.
     * @returns A string array of all the component ids.
     */
    getActorComponentIds() {
        return Object.keys(this.components);
    }
    /**
     * @remarks Gets an attached component by type.
     * @param component The component type.
     * @returns The attached component.
     */
    getActorComponent(component) {
        return this.components[component.name];
    }
    /**
     * @remarks Gets all of the attached {@link ActorComponent}s.
     * @returns All of the attached {@link ActorComponent}s.
     */
    getActorComponents() {
        return Object.values(this.components);
    }
    /**
     * @remarks Subscribes a callback to the event category on this actor. Can be used to send messages between components without direct references.
     * @param event The event name.
     * @param callback The callback to be executed.
     */
    subscribe(event, callback) {
        if (!this.events[event])
            this.events[event] = [];
        this.events[event].push(callback);
    }
    /**
     * @remarks Unsubscribes a callback from a category.
     * @param event The event name.
     * @param callback The callback to be removed/
     */
    unsubscribe(event, callback) {
        if (!this.events[event])
            return;
        removeFromArray(this.events[event], callback);
    }
    /**
     * @remarks Calls all of the subscribed callbacks of the event type.
     * @param event The event to raise.
     */
    raiseEvent(event) {
        if (!this.events[event])
            return;
        this.events[event].forEach(callback => callback(this));
    }
    static onConstruct(actor) {
        this.actorRegister.set(actor.id, actor);
    }
    static onDestruct(actor) {
        this.actorRegister.delete(actor.id);
    }
    /**
     * @remarks Gets all actors of the specified type. Note that this includes actors in unloaded chunks, but not actors who have never been loaded this session.
     * @param type The type of {@link Actor} to get.
     * @returns An array of actors matching the type.
     */
    static getActors(type) {
        const arr = [];
        for (const actor of this.actorRegister.values()) {
            if (actor.ActorId === type.name) {
                arr.push(actor);
            }
        }
        return arr;
    }
    /**
     * @remarks Gets an actor by id and type.
     * @param id The id of the {@link Actor}/{@link mc.Entity}.
     * @param type The type of {@link Actor} expected.
     * @returns The {@link Actor} instance of the specified type, or undefined if the Actor/Entity doesn't exists or the Actor with that id is of the wrong type.
     */
    static getActor(id, type) {
        const actor = this.actorRegister.get(id);
        if (actor instanceof type) {
            return actor;
        }
    }
    /**
     * @remarks Makes any {@link mc.Entity} who matches the query into an {@link Actor} of the specified type.
     * @param query The {@link mc.EntityQueryOptions} an entity must match to be made into an {@link Actor}.
     * @param type The type of {@link Actor} the entity should be made into.
     */
    static async makeActors(query, type) {
        await waitForNamespace();
        this.actorSpawnTable.set(query, type);
        try {
            overworld.getEntities(query).forEach(entity => new type(entity));
            nether.getEntities(query).forEach(entity => new type(entity));
            end.getEntities(query).forEach(entity => new type(entity));
        }
        catch (error) { }
    }
}
_a = Actor;
//#region Static Actor
Actor.actorRegister = new Map();
Actor.actorSpawnTable = new Map();
(() => {
    mc.world.afterEvents.entityRemove.subscribe(event => {
        const actor = _a.actorRegister.get(event.removedEntityId);
        actor === null || actor === void 0 ? void 0 : actor.destructor();
    });
    mc.world.afterEvents.entitySpawn.subscribe(event => {
        for (const [query, actor] of _a.actorSpawnTable) {
            if (event.entity.isValid() && event.entity.matches(query)) {
                try {
                    new actor(event.entity);
                }
                catch (error) { }
            }
        }
    });
    mc.world.afterEvents.entityLoad.subscribe(event => {
        for (const [query, actor] of _a.actorSpawnTable) {
            if (event.entity.isValid() && event.entity.matches(query)) {
                try {
                    new actor(event.entity);
                }
                catch (error) { }
            }
        }
    });
})();
/**
 * @remarks A component associated with {@link Actor}s.
 */
export class ActorComponent {
    /**
     * @remarks Creates a new component with options.
     * @param options The options associated with this component.
     */
    constructor(actor, options) {
        this.actor = actor;
        this.options = options;
        this.id = this.constructor.name;
    }
    /**
     * @remarks Called automatically when an {@link Actor} adds this component. Should be extended by the child instance.
     * @param actor The {@link Actor} this component is attached to.
     */
    onComponentAdded() {
    }
    /**
     * @remarks Called automatically when an {@link Actor} removes this component. Note that **this will be called when the actor is removed** and should
     * perfom cleanup if the actor is invalid.
     * @param actor The {@link Actor} this component is being removed from.
     */
    onComponentRemoved() { }
}
/**
 * @remarks An interact component that calls the `onInteract` method on an actor who has this component.
 */
export class InteractComponent extends ActorComponent {
    onComponentAdded() {
        InteractComponent.registry.set(this.actor.id, this);
    }
    onComponentRemoved() {
        InteractComponent.registry.delete(this.actor.id);
    }
}
InteractComponent.registry = new Map();
(() => {
    mc.world.afterEvents.playerInteractWithEntity.subscribe(event => {
        const component = InteractComponent.registry.get(event.target.id);
        component === null || component === void 0 ? void 0 : component.actor.onInteract(new SafetyCheckedPlayer(event.player));
    });
})();
export class NPCDialogueComponent extends ActorComponent {
    constructor() {
        super(...arguments);
        this.playerData = new Map();
    }
    onComponentAdded() {
        NPCDialogueComponent.registry.set(this.actor.id, this);
        this.actor.triggerEvent(this.options.componentEvent);
        this.actor.removeEffect("slowness");
    }
    onComponentRemoved() {
        NPCDialogueComponent.registry.delete(this.actor.id);
    }
    get InDialogue() {
        for (const data of this.playerData) {
            if (data[1].inDialogue)
                return true;
        }
        return false;
    }
    showDialogue(player, sceneTag) {
        var _b, _c;
        const bookmark = (_c = (_b = this.playerData.get(player.id)) === null || _b === void 0 ? void 0 : _b.bookmark) !== null && _c !== void 0 ? _c : this.options.defaultDialogueTag;
        sceneTag = sceneTag !== null && sceneTag !== void 0 ? sceneTag : bookmark;
        if (!sceneTag.includes(":"))
            sceneTag = addName(sceneTag);
        if (player.getGameMode() === mc.GameMode.creative)
            player.setGameMode(mc.GameMode.spectator);
        this.actor.runCommand(`dialogue open @s "${player.name}" ${sceneTag}`);
    }
    onDialogueOpened(player, options) {
        var _b, _c;
        const data = { bookmark: options.sceneTag, inDialogue: true };
        this.playerData.set(player.id, data);
        const sound = (_b = options.sound) !== null && _b !== void 0 ? _b : this.options.defaultSound;
        this.actor.playAnimation((_c = options.animation) !== null && _c !== void 0 ? _c : this.options.dialogueStartAnimation, { controller: "controller.dialogue", players: [player.nameTag] });
        if (sound)
            this.actor.playSoundAt(sound, { players: [player] });
        this.actor.addEffect("slowness", 999999, { amplifier: 255, showParticles: false });
    }
    onDialogueClosed(player, options) {
        var _b;
        const data = (_b = this.playerData.get(player.id)) !== null && _b !== void 0 ? _b : { bookmark: this.options.defaultDialogueTag, inDialogue: false };
        data.inDialogue = false;
        this.playerData.set(player.id, data);
        if (options.animation) {
            this.actor.playAnimation(options.animation, { controller: "controller.dialogue", players: [player.nameTag] });
        }
        else {
            this.actor.playAnimation(this.options.dialogueStopAnimation, { controller: "controller.dialogue", players: [player.nameTag] });
        }
        this.actor.stopSound({ players: [player] });
        if (options.sound)
            this.actor.playSoundAt(options.sound, { players: [player] });
        waitForEvent(1, () => {
            var _b;
            if (!((_b = this.playerData.get(player.id)) === null || _b === void 0 ? void 0 : _b.inDialogue) && player.isValid() && player.getGameMode() === mc.GameMode.spectator) {
                player.setGameMode(mc.GameMode.creative);
            }
            if (!this.InDialogue) {
                this.actor.removeEffect("slowness");
                mclog(`${this.actor.ActorId} is no longer in dialogue`, this.id);
            }
        });
    }
}
NPCDialogueComponent.registry = new Map();
(() => {
    afterEvents.scriptEventRecieve.subscribe("dialogue_open", event => {
        var _b, _c, _d;
        const component = NPCDialogueComponent.registry.get((_c = (_b = event.sourceEntity) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : "");
        const player = (_d = event.initiator) === null || _d === void 0 ? void 0 : _d.getPlayer();
        if (player)
            component === null || component === void 0 ? void 0 : component.onDialogueOpened(player, event.data);
    });
    afterEvents.scriptEventRecieve.subscribe("dialogue_close", event => {
        var _b, _c, _d;
        const component = NPCDialogueComponent.registry.get((_c = (_b = event.sourceEntity) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : "");
        const player = (_d = event.initiator) === null || _d === void 0 ? void 0 : _d.getPlayer();
        if (player)
            component === null || component === void 0 ? void 0 : component.onDialogueClosed(player, event.data);
    });
})();
// #endregion
