import { world, system, ScriptEventCommandMessageAfterEvent } from "@minecraft/server";
import { Debugger } from "./debug.js";

const debug = new Debugger('mythicus:scriptevent');
debug.enabled = false;
debug.priority = 1;

/**
 * Object storing all active scriptEventListeners.
 *
 * @type {Object.<String, ScriptEventListener>}
 */
const scriptEventListeners = {};

/**
 * Array storing all active scriptEventListeners prefixes.
 *
 * @type {String[]}
 */
const scriptEventPrefixes = [];


/**
 * @typedef {Object} ScriptEventListenerOptions
 * @property {Boolean} [isPrefix=false] If true, listens to all undefined ids with this prefix.
 * @property {String[]|undefined} [entityTypeIds=undefined] If defined, requires source entity type from this list.
 * @property {Boolean} [isNumericalMsg=undefined] If true, requires message of the event to be numerical.
 */

/**
 * A listener class for Minecraft's script events.
 *
 * @class
 */
export class ScriptEventListener {

    /**
     * Creates a new instance of ScriptEventListener.
     *
     * @param {String} id - Identifier of listener.
     * @param {ScriptEventListenerOptions|undefined} options - Optional conditions for listener.
    */
    constructor(id, options = undefined) {
        if (id in scriptEventListeners) {
            debug.error(`An instance with this id [${id}] already exists.`);
            throw new Error(`An instance with this id [${id}] already exists.`);
        }

        this.id = id;
        this.options = options;
        this.callbacks = [];
        
        this.#setup();
    }
    /**
     * Additional setup code for the listener called at construction.
     *
     * @param {String} id - Identifier.
    */
    #setup() {
        if (this.options?.isPrefix === true) scriptEventPrefixes.push(this.id);
        scriptEventListeners[this.id] = this;
    }


    /**
     * Subscribes to an event callback function.
     *
     * @param {(event: ScriptEventCommandMessageAfterEvent) => void} callback - Callback function.
    */
    subscribe(callback) {
        this.callbacks.push(callback);
    }
    /**
     * Unsubscribes from an event callback function.
     *
     * @param {(event: ScriptEventCommandMessageAfterEvent) => void} callback - Callback function.
    */
    unsubscribe(callback) {
        if (this.callbacks.includes(callback)) {
            this.callbacks.splice(this.callbacks.indexOf(callback));
        }
    }
    /**
     * Triggers all subscribed callbacks.
     *
     * @param {ScriptEventCommandMessageAfterEvent} event - Event data.
    */
    trigger(event) {
        if (this.options?.entityTypeIds?.includes(event.sourceEntity?.typeId) === false) return;
        if (this.options?.isNumericalMsg === true && isNaN(event.message)) return;

        for (let callback of this.callbacks) {
            callback(event);
        }
    }



    /**
     * Gets the listener associated with the given id.
     *
     * @param {String} id - Identifier of listener to parse.
     * @returns {ScriptEventListener|undefined} Listener or undefined if non-existent.
    */
    static parse(id) {
        const listener = this.parseExact(id);
        if (listener) return listener;

        for (const prefix of scriptEventPrefixes.sort((a, b) => b.length - a.length)) {
            if (id.startsWith(prefix)) return this.parseExact(prefix); 
        }

        return undefined;
    }
    /**
     * Parses the exact listener associated with the given id.
     *
     * @param {String} id - Identifier of listener to parse.
     * @returns {ScriptEventListener|undefined} Listener or undefined if non-existent.
    */
    static parseExact(id) {
        return scriptEventListeners[id] ?? undefined;
    }
    /**
     * Returns the validity of this listener id.
     *
     * @param {String} id - Identifier of listener to check.
     * @returns {bool} True if listener exists, false otherwise.
    */
    static isValid(id) {
        return (id in scriptEventListeners);
    }
    /**
     * Used to subscribe to a new callback. Creates listener if not previously existent.
     *
     * @param {String} id - Identifier of listener.
     * @param {(event: ScriptEventCommandMessageAfterEvent) => void} callback - Callback function.
     * @param {ScriptEventListenerOptions|undefined} options - Optional conditions for listener.
     * @returns {ScriptEventListener} New instance of ScriptEventListener or the one associated with given id if already existent.
    */
    static subscribe(id, callback, options = undefined) {
        let listener = ScriptEventListener.parseExact(id);

        if (!listener) {
            return new ScriptEventListener(id, options).subscribe(callback);
        } else {
            listener.subscribe(callback);
            return listener;
        }
    }

}




system.afterEvents.scriptEventReceive.subscribe((event) => {

    const listener = ScriptEventListener.parse(event.id);
    if (listener) listener.trigger(event);

}, { namespaces: ["mythicus"] });



ScriptEventListener.subscribe("mythicus:", (event) => {
    debug.warn(`Invalid scriptevent [§c${event.id}§r].`);
    debug.log("§g/scriptevent mythicus:list");
}, { isPrefix: true })

ScriptEventListener.subscribe("mythicus:list", (event) => {
    const listeners = Object.keys(scriptEventListeners).filter(x => !scriptEventPrefixes.includes(x));
    world.sendMessage("\n§gList of scriptevent ids:");
    world.sendMessage(listeners.sort((a, b) => b.length - a.length).map(prefix => prefix + '\n'));

    world.sendMessage("\n§gList of scriptevent prefixes:");
    world.sendMessage(scriptEventPrefixes.sort((a, b) => b.length - a.length).map(prefix => prefix + '\n'));
});