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

const debug = new Debugger('mythicus_tw:mcevent');
debug.enabled = false;
debug.priority = 1;

/**
 * Access nested keys in an object.
 * 
 * @param {Object} obj - Object to access.
 * @param {String[]} keys - Array of keys in nested structure.
 * @returns {any} - Value at the specified nested keys if existent, else undefined.
 */
function accessNestedKeys(obj, keys) {
    if (typeof keys === 'string') {
        keys = keys.split('.');
    }

    let result = obj;
    for (const key of keys) {
        if (result && typeof result === 'object' && key in result) {
            result = result[key];
        } else {
            return undefined;
        }
    }

    return result;
}

function getKeyByValue(obj, value) {
    for (const key in obj) {
        if (obj.hasOwnProperty(key) && obj[key] === value) {
            return key;
        }
    }

    return undefined;
}

function getAnyKeyByValue(obj, value) {
    for (const key in obj) {
        if (obj[key] === value) {
            return key;
        }
    }

    return undefined;
}



/**
 * @typedef {Object} MinecraftEventOptions
 * @property {Null|String|(data: any) => Boolean} [condition=null] - Condition for this listener or function to trigger.
 */

/**
 * @typedef {Object} MinecraftEventManagerSubscription
 * @property {String} name - Name of this subscription.
 * @property {(data: any) => void} callback - Callback for this subscription.
 * @property {MinecraftEventOptions} options - Options for this subscription.
 */





export class MinecraftEvent {

    /**
     * Registers a new Minecraft event with the given event id for listeners.
     *
     * @param {String} id - Identifier of event to parse.
    */
    static registerForListener(id) {
        this.subscribe(id, (data) => {
            const listeners = MinecraftEventListener.parseEvent(id);
            if (listeners === undefined) return;

            for (const listener of listeners) {
                listener.conditionalTrigger(data);
            }
        });
    }
    /**
     * Registers a new Minecraft event with the given event id for the manager.
     *
     * @param {String} id - Identifier of event to parse.
    */
    static registerForManager(id) {
        this.subscribe(id, (data) => {
            const subscriptions = MinecraftEventManager.parseEvent(id);
            if (subscriptions === undefined) return;

            for (const subscription of subscriptions) {
                MinecraftEventManager.trigger(subscription, data);
            }
        });
    }
    /**
     * Subscribes to an event.
     *
     * @param {String} id - Identifier of event to parse.
     * @param {(data: any) => void} callback - Callback function.
    */
    static subscribe(id, callback) {
        const event = this.parse(id);

        if (event === undefined || !('subscribe' in event) || typeof event.subscribe !== 'function') return;

        event.subscribe(callback);
    }

    /**
     * Parses associated event.
     *
     * @param {String} id - Identifier of event to parse.
     * @returns {any|undefined} Returns event if exists, undefined otherwise.
    */
    static parse(id) {
        const event = accessNestedKeys(world, id);

        if (event === undefined || !('subscribe' in event) || typeof event.subscribe !== 'function') return undefined;

        return event;
    }
    /**
     * Existence of event.
     *
     * @param {String} id - Identifier of event to parse.
     * @returns {Boolean} Returns true if event exists, false otherwise.
    */
    static exists(id) {
        return this.parse(id) !== undefined;
    }
    /**
     * Returns string representation of event signal.
     *
     * @param {any} id - Identifier of event to parse.
     * @returns {String} Returns string reprentation of event, empty string if undefined.
    */
    static toString(id) {
        for (const category of ['afterEvents', 'beforeEvents']) {
            const name = getAnyKeyByValue(world[category], id);

            if (name !== undefined) return category + '.' + name;
        }

        return '';
    }

}

/**
 * Object storing all active MinecraftEventListener instances.
 *
 * @type {Object.<string, Object.<string, MinecraftEventListener>>}
*/
const minecraftEventListeners = {};

export class MinecraftEventListener {

    /**
     * Creates an instance of MinecraftEventListener.
     *
     * @param {String|any} event - Event name (e.g. 'afterEvents.itemUse') or signal (e.g world.afterEvents.itemUse).
     * @param {String} id - Identifier of listener.
     * @param {MinecraftEventOptions} [options={ condition: null }] - Optional parameters for event.
    */
    constructor(event, id, options = { options: null }) {
        this.event = typeof event === 'string' ? event : MinecraftEvent.toString(event);
        this.id = id;
        this.options = options;
        this.callbacks = [];

        this.#setup(event, id);
    }
    /**
     * Additional setup code for the listener called at construction.
     *
     * @param {String} event - Event name.
     * @param {String} id - Identifier of listener.
    */
    #setup(event, id) {
        if (!MinecraftEvent.exists(event)) {
            debug.error(`MinecraftEventListener ${id} failed to register due to invalid event [${event}].`);
            return;
        };

        if (minecraftEventListeners[event] === undefined) {
            MinecraftEvent.registerForListener(event);
            minecraftEventListeners[event] = {};
        }

        debug.log(`MinecraftEventListener registered: [${event}, ${id}]`);
        minecraftEventListeners[event][id] = this;
    }


    /**
     * Subscribes to an event callback function.
     *
     * @param {(data: any) => void} callback - Callback function.
    */
    subscribe(callback) {
        this.callbacks.push(callback);
    }
    /**
     * Unsubscribes from an event callback function.
     *
     * @param {(event: any) => void} callback - Callback function.
    */
    unsubscribe(callback) {
        if (this.callbacks.includes(callback)) {
            this.callbacks.splice(this.callbacks.indexOf(callback));
        }
    }
    /**
     * Triggers all subscribed callbacks.
     *
     * @param {any} data - Event data.
    */
    trigger(data) {
        for (let callback of this.callbacks) {
            callback(data);
        }
    }
    /**
     * Conditional trigger for subscribed callbacks.
     *
     * @param {any} data - Event data.
    */
    conditionalTrigger(data) {
        const condition = this.options.condition;
        if (condition === null) {
            this.trigger(data);
        } else if (typeof condition === 'string' && this.id === accessNestedKeys(data, condition)) {
            this.trigger(data);
        } else if (typeof condition === 'function' && condition(data) === true) {
            this.trigger(data);
        }
    }

    /**
     * Parse all listeners associated with the event.
     *
     * @param {String} event - Event name.
     * @returns {MinecraftEventListener[]|undefined} Returns listeners if existent, undefined otherwise.
    */
    static parseEvent(event) {
        if (typeof minecraftEventListeners[event] !== 'object') return undefined;

        return Object.values(minecraftEventListeners[event]) ?? undefined;
    }
    /**
     * Parse associated listeners.
     *
     * @param {String} event - Event name.
     * @param {String} id - Identifier of listener to parse.
     * @returns {MinecraftEventListener|undefined} Returns listener if existent, undefined otherwise.
    */
    static parse(event, id) {
        if (typeof minecraftEventListeners[event] !== 'object') return undefined;

        return minecraftEventListeners[event][id] ?? undefined;
    }
    /**
     * Subscribes to a new callback for associated listener. Listener is created if no existing instance found.
     *
     * @param {String|any} event - Event name or signal.
     * @param {String} id - Identifier.
     * @param {(data: any) => void} callback - Callback function.
     * @param {MinecraftEventOptions} [options={ condition: null }] - Optional parameters for event.
     * @returns {MinecraftEventListener} Returns listener associated with event, id.
    */
    static subscribe(event, id, callback, options = { condition: null }) {
        if (typeof event !== 'string') event = MinecraftEvent.toString(event);

        let listener = MinecraftEventListener.parse(event, id);

        if (!listener) {
            return new MinecraftEventListener(event, id, options).subscribe(callback);
        } else {
            if (listener.options.condition !== options.condition) {
                debug.warn(`MinecraftEventListener [${event}, ${id}] tried to subscribe to a new callback with different conditions.`);
            }
            listener.subscribe(callback);
            return listener;
        }
    }

}

export class MinecraftEventManager {

    /**
     * Object storing all Manager subscriptions.
     *
     * @type {Object.<string, Object.<string, MinecraftEventManagerSubscription>>}
    */
    static #subscriptions = {};



    /**
     * Subscribes to a new callback for a Minecraft event.
     *
     * @param {String|any} event - Minecraft event name or signal.
     * @param {String} id - Identifier of this subscription.
     * @param {(data: any) => void} callback - Callback function.
     * @param {MinecraftEventOptions} [options={ condition: null }] - Optional parameters for event.
    */
    static subscribe(event, id, callback, options = { condition: null }) {
        if (typeof event !== 'string') event = MinecraftEvent.toString(event);

        if (!MinecraftEvent.exists(event)) {
            debug.error(`MinecraftEventManager subscription failed due to invalid event [${event}, ${id}].`);
            return;
        };

        if (MinecraftEventManager.parse(event, id) !== undefined) {
            debug.error(`MinecraftEventManager cannot have duplicate subscriptions [${event}, ${id}].`);
            return;
        }

        if (this.#subscriptions[event] === undefined) {
            MinecraftEvent.registerForManager(event);
            this.#subscriptions[event] = {};
        }

        debug.log(`MinecraftEventManager subscribed to [${event}, ${id}].`);
        this.#subscriptions[event][id] = { name: this.getNameForId(id), callback: callback, options: options };
    }
    /**
     * Unsubscribes from the specified event id.
     *
     * @param {String|any} event - Minecraft event name or signal.
     * @param {String} id - Identifier of subscription.
    */
    static unsubscribe(event, id) {
        if (typeof event !== 'string') event = MinecraftEvent.toString(event);

        if (!MinecraftEvent.exists(event)) {
            debug.error(`MinecraftEventManager unsubscription failed due to invalid event [${event}, ${id}].`);
            return;
        };

        if (MinecraftEventManager.parse(event, id) === undefined) {
            debug.warn(`MinecraftEventManager tried to unsubscribe from a non-existent event [${event}, ${id}].`);
            return;
        }

        debug.log(`MinecraftEventManager unsubscribed from [${event}, ${id}].`);
        delete this.#subscriptions[event][id];
    }
    /**
     * Triggers callback for subscription.
     * @param {MinecraftEventManagerSubscription} subscription - Event manager subscription data.
     * @param {any} data - Event data.
    */
    static trigger(subscription, data) {
        const { name, callback, options: { condition } } = subscription;

        if (condition === null) {
            callback(data);
        } else if (typeof condition === 'string' && name === accessNestedKeys(data, condition)) {
            callback(data);
        } else if (typeof condition === 'function' && condition(data) === true) {
            callback(data);
        }
    }

    /**
     * Parse all subscriptions associated with the event.
     *
     * @param {String} event - Event name.
     * @returns {MinecraftEventManagerSubscription[]|undefined} Returns subscriptions if existent, undefined otherwise.
    */
    static parseEvent(event) {
        if (typeof this.#subscriptions[event] !== 'object') return undefined;

        return Object.values(this.#subscriptions[event]) ?? undefined;
    }
    /**
     * Parse subscription for given event id.
     *
     * @param {String} event - Event name.
     * @param {String} id - Identifier of subscription to parse.
     * @returns {MinecraftEventManagerSubscription|undefined} Returns subscription if existent, undefined otherwise.
    */
    static parse(event, id) {
        if (typeof this.#subscriptions[event] !== 'object') return undefined;

        return this.#subscriptions[event][id] ?? undefined;
    }

    /**
     * Returns formatted name for given id.
     *
     * @param {String} id - Identifier of subscription to parse.
     * @returns {String} Formatted name.
    */
    static getNameForId(id) {
        if (!id.includes(':') || id.startsWith('minecraft:')) return id;

        return id.slice(id.indexOf(':') + 1);
    }

}