import * as mc from "@minecraft/server";
import { waitForLoopEvent, waitForTicks } from "./events";
import { getScoreAsBool } from "./scores";
import { runCommand } from "./commands";
import { mcerror, mclog } from "./log";
import { getRandomInteger } from "./math";
import { GlobalNamespace } from "./namespace";
import { addTag, removeTag } from "./entity";
/**
 * A popup message, to be used with the {@link PopupSystem}
 */
export class PopupMessage {
    /**
     * Creates a new popup message
     * @param messages The message data for this popup
     */
    constructor(messages, random = false) {
        this.aborted = false;
        this.messageList = messages;
        this.random = random;
    }
    /**
     * Displays the message to a player. Should only be called by the {@link PopupSystem}
     * @param player The player who should see the message
     */
    async displayMessage(player) {
        this.aborted = false;
        if (!this.random) {
            await this.displaySequenceMessage(player);
        }
        else {
            await this.displayRandomMessage(player);
        }
    }
    async displaySequenceMessage(player) {
        for (const message of this.messageList) {
            if (this.aborted) {
                return;
            }
            if (message.sound)
                player.playSound(message.sound);
            player.onScreenDisplay.setActionBar(`{dialogue:${message.message},dialogue_stay:${Math.ceil(message.duration)}}`);
            await waitForTicks(message.duration * 20 - 10);
            if (this.aborted) {
                player.onScreenDisplay.setActionBar(``);
                return;
            }
            player.onScreenDisplay.setActionBar(`{dialogue_stop:${message.message}}`);
            await waitForTicks(10);
        }
    }
    async displayRandomMessage(player) {
        const message = this.messageList[getRandomInteger(0, this.messageList.length - 1)];
        if (this.aborted) {
            return;
        }
        if (message.sound)
            player.playSound(message.sound);
        player.onScreenDisplay.setActionBar(`{dialogue:${message.message},dialogue_stay:${Math.ceil(message.duration)}}`);
        await waitForTicks(message.duration * 20 - 10);
        if (this.aborted) {
            player.onScreenDisplay.setActionBar(``);
            return;
        }
        player.onScreenDisplay.setActionBar(`{dialogue_stop:${message.message}}`);
        await waitForTicks(10);
    }
    /**
     * Abort this popup message
     */
    abort() {
        this.aborted = true;
    }
}
/**
 * A popup system for spawning dialogue objects with complex timing and synchronized audio
 */
export class PopupSystem {
    /**
     * Creates a new popup system
     * @param name The name of the popup group
     * @param messages The list of popups in this group
     */
    constructor(name, messages) {
        this.dialogueTagPrefix = 'Dialogue';
        this.players = [];
        this.dialogueSuspended = false;
        this.playing = false;
        this.popupMessages = messages;
        this.name = name;
        this.dialoguePendingObjective = this.name + 'DialoguePending';
        if (this.getDialoguePending()) {
            this.startPopup();
        }
    }
    /**
     * Starts a popup if any are pending
     */
    async startPopup() {
        this.playing = true;
        await waitForLoopEvent(20, () => !getScoreAsBool('YouTube', '.var'));
        for (const popupMessage of this.popupMessages) {
            this.players = mc.world.getPlayers({ tags: [`${GlobalNamespace.Namespace}${this.dialogueTagPrefix}${popupMessage.tag}`] });
            for (const player of this.players) {
                this.stopsounds();
                this.currentMessage = popupMessage;
                await popupMessage.message.displayMessage(player);
                if (this.dialogueSuspended) {
                    popupMessage.message.abort();
                    this.playing = false;
                    return;
                }
                this.removeDialogueTag(player, popupMessage.tag);
            }
        }
        this.setDialoguePending(false);
        this.playing = false;
    }
    /**
     * Stops all sounds associated with the popup system
     */
    stopsounds() {
        this.players.forEach(player => {
            for (const popupMessage of this.popupMessages) {
                popupMessage.message.messageList.forEach(message => player.runCommand(`stopsound @s ${message.sound}`));
            }
        });
    }
    /**
     * Adds a dialogue tag to the player, this queue the popup
     * @param player The player who should view the dialogue
     * @param tag The tag id of the popup message
     */
    addDialogueTag(player, tag) {
        addTag(player, this.dialogueTagPrefix + tag);
        this.setDialoguePending(true);
    }
    /**
     * Removes a dialogeu tag from the player
     * @param player The player whos dialogue should be cleared
     * @param tag The tag id of the message to remove
     */
    removeDialogueTag(player, tag) {
        removeTag(player, this.dialogueTagPrefix + tag);
    }
    /**
     * Overwites the status of pending, starts the popup if needed
     * @param state Whether pending should be true or false
     */
    setDialoguePending(state) {
        runCommand(`scoreboard objectives add ${this.dialoguePendingObjective} dummy`);
        runCommand(`scoreboard players set .var ${this.dialoguePendingObjective} ${state ? 1 : 0}`);
        if (state && !this.dialogueSuspended && !this.playing) {
            this.startPopup();
        }
    }
    /**
     * Gets the status of pending dialogue
     * @returns True if there is pending dialogue, false otherwise
     */
    getDialoguePending() {
        return getScoreAsBool(this.dialoguePendingObjective, '.var');
    }
    /**
     * Suspends the dialogue system without aborting it. This should be called whenever the dialogue needs to be paused for a higher priority system
     */
    suspend() {
        var _a;
        this.dialogueSuspended = this.getDialoguePending();
        this.stopsounds();
        (_a = this.currentMessage) === null || _a === void 0 ? void 0 : _a.message.abort();
    }
    /**
     * Resumes the dialogue system, restarting any suspened message and continuing down the queue
     */
    resume() {
        this.dialogueSuspended = false;
        if (this.getDialoguePending() && !this.playing) {
            this.startPopup();
        }
    }
}
/**
 * A behavior that an NPC can run
 */
export class Behavior {
    /**
     * Creates a new behavior
     * @param data The behavior's data
     */
    constructor(data) {
        var _a;
        this.startTime = 0;
        this.finished = false;
        this.localRandom = 0;
        this.aborted = false;
        this.ID = data.ID;
        this.onEntry = data.onEntry;
        this.running = data.running;
        this.onExit = data.onExit;
        this.transitions = (_a = data.transitions) !== null && _a !== void 0 ? _a : [];
    }
    /**
     * Handles the onEntry function
     * @param entity The entity who is the NPC
     */
    async entry(entity) {
        mclog(`${entity.nameTag} (${entity.typeId}) Starting Behavior: ${this.ID}`, "behaviors");
        this.startTime = mc.system.currentTick;
        this.finished = false;
        this.localRandom = Math.random();
        this.aborted = false;
        if (this.onEntry) {
            await this.onEntry(entity);
        }
    }
    /**
     * Handles the running function
     * @param entity The entity who is the NPC
     */
    async run(entity) {
        mclog(`${entity.nameTag} (${entity.typeId}) Running Behavior: ${this.ID}`, "behaviors");
        if (this.running) {
            await this.running(entity, this.aborted);
        }
        this.finished = true;
    }
    /**
     * Evaluates whether a transition out of this state should occur
     * @param entity The entity who is the NPC
     */
    async evaluateTransitions(entity) {
        for (const transition of this.transitions) {
            if (transition.condition({
                entity,
                age: mc.system.currentTick - this.startTime,
                finished: this.finished,
                random: this.getLocalRandom(transition.randomMin, transition.randomMax),
            })) {
                return transition.name;
            }
        }
        await waitForTicks(0);
        return await this.evaluateTransitions(entity);
    }
    /**
     * Handles the exit function
     * @param entity The entity who is the NPC
     */
    async exit(entity) {
        mclog(`${entity.nameTag} (${entity.typeId}) Ending Behavior: ${this.ID}`, "behaviors");
        this.aborted = true;
        if (this.onExit) {
            await this.onExit(entity);
        }
    }
    /**
     * A random number to be used within a transition (this is needed so new random numbers aren't generated every tick the transtion is evaluated)
     * @param min The smallest number this can be
     * @param max The largetst number this can be
     * @returns A value betwen min and max (inclusive)
     */
    getLocalRandom(min, max) {
        min = min !== null && min !== void 0 ? min : 0;
        max = max !== null && max !== void 0 ? max : 1;
        return Math.floor(this.localRandom * (max - min + 1)) + min;
    }
}
/**
 * @remarks Creates an NPC object using state controller similar to those in animation controllers.
 * This class has numerous issues and should be rebuilt.
 * See https://github.com/Dotzip-Developments/craftees-custom-hearts-dx/blob/main/scripts/craftee_dialogue.ts for an example of how this class is used.
 */
export class NPC {
    /**
     * Creates a new NPC
     * @param data The NPC's data
     */
    constructor(data) {
        this.behaviorList = data.behaviorList;
        this.entity = data.npcEntity;
        this.startBehavior(data.defaultBehavior);
    }
    /**
     * Handles swapping behaviors
     * @param newBehavior The behavior to start
     */
    async startBehavior(newBehavior) {
        if (this.currentBehavior) {
            await this.currentBehavior.exit(this.entity);
        }
        this.currentBehavior = newBehavior;
        await this.currentBehavior.entry(this.entity);
        this.currentBehavior.run(this.entity);
        const transitionName = await this.currentBehavior.evaluateTransitions(this.entity);
        const transitionBehavior = this.behaviorList.find((behavior) => behavior.ID === transitionName);
        if (transitionBehavior) {
            this.startBehavior(transitionBehavior);
        }
        else {
            mcerror(`${this.entity.nameTag} (${this.entity.typeId}) Does Not Have Behavior: ${transitionName}\nRestarting Behavior: ${this.currentBehavior.ID}`);
            this.startBehavior(this.currentBehavior);
        }
    }
}
