import * as mc from "@minecraft/server";
import * as log from './log';
import { overworld } from "./dimension";
import { getRandomInteger } from "./math";
import { SafetyCheckedEntity, SafetyCheckedPlayer } from "./entity";
import { addName, GlobalNamespace } from "./namespace";
const systemEventIDs = new Map();
/**
 * @remarks Resolves a promise after the scheduled ticks.
 * @param ticks The number of ticks to wait before running the resolving.
 * @param cancelCategory Optional tag that allows this promise to be aborted.
 * @returns A Promise that resolves after the specified number of ticks.
 * @example
 * Basic example
 * ```typescript
 * await waitForTicks(20); // Causes the execution to be paused here for 20 ticks.
 * mclog('Finished Waiting');
 * ```
 * @example
 * Abort example
 * ```typescript
 * async function waitThenMessage(player: SafetyCheckedPlayer) {
 * 	await waitForTicks(20, `${player.id}.message`);
 * 	mclog('Finished Waiting or Aborted'); // This message will print after 20 ticks OR immediately if the event is aborted
 * }
 *
 * getAllPlayersSafe().forEach(safePlayer => {
 * 	waitThenMessage(safePlayer);
 * 	if (safePlayer.hasTag('cancel')) {
 * 		abortEvents(`${safePlayer.id}.message`);
 * 		safePlayer.removeTag('cancel');
 * 	}
 * });
 * ```
 */
export async function waitForTicks(ticks, cancelCategory) {
    if (cancelCategory && !systemEventIDs.has(cancelCategory)) {
        systemEventIDs.set(cancelCategory, new Map());
    }
    return new Promise(resolve => {
        var _a;
        let id = mc.system.runTimeout(() => {
            var _a;
            resolve();
            if (cancelCategory)
                (_a = systemEventIDs.get(cancelCategory)) === null || _a === void 0 ? void 0 : _a.delete(id);
        }, ticks);
        if (cancelCategory)
            (_a = systemEventIDs.get(cancelCategory)) === null || _a === void 0 ? void 0 : _a.set(id, resolve);
    });
}
/**
 * @remarks Resolves a promise after the scheduled event.
 * @param ticks The number of ticks to wait before running the event.
 * @param callback The event, this can return a generic value. Note the callback will always return undefined if the event is aborted.
 * @param cancelCategory Optional tag that allows this promise to be aborted.
 * @returns A Promise that resolves to a value or undefined when the event finishes.
 * @example
 * Basic example
 * ```typescript
 * waitForEvent(20, () => mclog('Finished Waiting'));
 * ```
 * @example
 * Await example
 * ```typescript
 * await waitForEvent(20, () => mclog('Finished Waiting'));
 * mclog('Continuing Function'); // This won't run until the 20 ticks finish.
 * ```
 * @example
 * Abort example
 * ```typescript
 * async function waitThenMessage(player: SafetyCheckedPlayer) {
 * 	await waitForEvent(20, () => mclog('Finished Waiting'), `${player.id}.message`); // The mclog will only run if the event is not aborted.
 * 	mclog('Continuing'); // This message will print after 20 ticks OR immediately if the event is aborted
 * }
 *
 * getAllPlayersSafe().forEach(safePlayer => {
 * 	waitThenMessage(safePlayer);
 * 	if (safePlayer.hasTag('cancel')) {
 * 		abortEvents(`${safePlayer.id}.message`);
 * 		safePlayer.removeTag('cancel');
 * 	}
 * });
 * ```
 */
export async function waitForEvent(ticks, callback, cancelCategory) {
    if (cancelCategory && !systemEventIDs.has(cancelCategory)) {
        systemEventIDs.set(cancelCategory, new Map());
    }
    return new Promise(resolve => {
        var _a;
        let id = 0;
        const event = async function () {
            var _a;
            const val = await callback();
            resolve(val);
            if (cancelCategory)
                (_a = systemEventIDs.get(cancelCategory)) === null || _a === void 0 ? void 0 : _a.delete(id);
        };
        id = mc.system.runTimeout(event, ticks);
        if (cancelCategory)
            (_a = systemEventIDs.get(cancelCategory)) === null || _a === void 0 ? void 0 : _a.set(id, resolve);
    });
}
/**
 * @remarks Runs a command on a loop, terminating itself when callback returns true.
 * @param ticks How often (in ticks) the callback should be run.
 * @param callback A callback that ends the loop if it returns true.
 * @param cancelCategory Optional tag that allows this promise to be aborted.
 * @param runImmediately If true, the callback will be run on this tick, then on every loop. Note that the first run cannot be aborted. Defaults to true.
 * @returns A Promise that resolves when the callback returns true.
 * @example
 * Infinite Clock
 * ```typescript
 * waitForLoopEvent(20, () => {
 *     mclog(`Current Time: ${mc.system.currentTick}`);
 * });
 * ```
 * @example
 * Wait For Condition
 * ```typescript
 * const player = getAllPlayersSafe()[0];
 * await waitForLoopEvent(0, () => !player.isSneaking); // Will wait here until the player is not sneaking.
 * mclog(`${player.name} Stopped Sneaking`);
 * ```
 */
export async function waitForLoopEvent(ticks, callback, cancelCategory, runImmediately = true) {
    if (cancelCategory && !systemEventIDs.has(cancelCategory)) {
        systemEventIDs.set(cancelCategory, new Map());
    }
    return new Promise(resolve => {
        var _a;
        let id = 0;
        const event = async function () {
            if (await callback()) {
                resolve();
                mc.system.clearRun(id);
            }
        };
        if (runImmediately && ticks > 0)
            event();
        id = mc.system.runInterval(event, ticks);
        if (cancelCategory)
            (_a = systemEventIDs.get(cancelCategory)) === null || _a === void 0 ? void 0 : _a.set(id, resolve);
    });
}
/**
 * @remarks Resolves when the world has loaded enough to target players. This should run before any code that uses getAllPlayers.
 * @example
 * ```typescript
 * async function main() {
 * 	await utils.waitForWorldLoad();
 *
 * 	utils.mclog("Hello World!");
 * 	// Initialize other systems
 * }
 * ```
 */
export async function waitForWorldLoad() {
    await waitForLoopEvent(5, async () => {
        const result = await overworld.runCommandAsync('testfor @a');
        return result.successCount > 0;
    });
}
/**
 * @remarks Resolves when the namespace has been set. This can be used to ensure the namespace is set before running any code that uses it.
 * @example
 * ```typescript
 * async function foo() {
 * 	await utils.waitForNamespace();
 * 	utils.mclog(`Namespace: ${utils.GlobalNamespace.Namespace}`);
 * }
 * ```
 */
export async function waitForNamespace() {
    if (GlobalNamespace.Namespace !== '' || GlobalNamespace.Ignore)
        return;
    return new Promise(resolve => {
        const changed = () => {
            resolve();
            afterEvents.onNamespaceChanged.unsubscribe(changed);
        };
        afterEvents.onNamespaceChanged.subscribe(changed);
    });
}
/**
 * @remarks Cancels events created by waitForTicks, waitForEvent, & waitForLoopEvent that were marked with the given category.
 * @param cancelCategory The category to be cancelled.
 * @example
 * ```typescript
 * async function waitThenMessage(player: SafetyCheckedPlayer) {
 * 	await waitForTicks(20, `${player.id}.message`);
 * 	mclog('Finished Waiting or Aborted');
 * }
 *
 * getAllPlayersSafe().forEach(safePlayer => {
 * 	waitThenMessage(safePlayer);
 * 	if (safePlayer.hasTag('cancel')) {
 * 		abortEvents(`${safePlayer.id}.message`); // This will cancel any currently running events with the same tag.
 * 		safePlayer.removeTag('cancel');
 * 	}
 * });
 * ```
 */
export function abortEvents(cancelCategory) {
    var _a;
    log.mclog(`Aborting ${cancelCategory} Events`, 'verbose');
    if (systemEventIDs.has(cancelCategory)) {
        (_a = systemEventIDs.get(cancelCategory)) === null || _a === void 0 ? void 0 : _a.forEach((resolve, id) => {
            mc.system.clearRun(id);
            resolve(undefined);
        });
        systemEventIDs.delete(cancelCategory);
    }
}
/**
 * @remarks Calls an event based on random chance.
 * @param chance The chance that the callback will be called. 1 is gauranteed, while 100 means a 1/100 chance.
 * @param callback The callback to be executed if the random chance is true.
 * @example
 * ```typescript
 * // There's a 1/7 chance of running the callback.
 * randomChanceEvent(7, () => {
 * 	mclog('Winner!');
 * });
 * ```
 */
export function randomChanceEvent(chance, callback) {
    if (getRandomInteger(1, chance) === 1) {
        callback();
    }
}
export class PrivateAfterEvent {
    constructor() {
        this.callbacks = new Set();
    }
    /**
     * @remarks Subscribes a callback to be executed by the event.
     * @param callback The callback to be subscribed.
     */
    subscribe(callback) {
        this.callbacks.add(callback);
    }
    /**
     * @remarks Unsubscribes a callback so it will no longer be executed. Note this must be the exact same callback object, not an identical callback.
     * @param callback The callback to be unsubscribed.
     */
    unsubscribe(callback) {
        this.callbacks.delete(callback);
    }
    /**
     * @remarks This method should not be called except by {@link UtilsAfterEvents}.
     * @param event The event to raise.
     */
    raise(event) {
        this.callbacks.forEach(callback => callback(event));
    }
}
export class PrivateScriptEvent {
    constructor() {
        this.register = new Map();
        this.worldLoaded = false;
    }
    /**
     * @remarks Subscribes a callback to a specific scriptevent id. Note that this takes a generic which can be used to specify the data type provided by the scriptevent
     * message. Messages can be any serializable data type.
     * @param id The id of the scriptevent. Note that if no namespace is provided it will automatically add the global namespace. **Only one callback can be registered per id**.
     * @param callback The callback to be executed when the event is raised.
     */
    async subscribe(id, callback) {
        if (!this.worldLoaded) {
            await waitForWorldLoad();
            this.worldLoaded = true;
        }
        if (!id.includes(":"))
            id = addName(id);
        this.register.set(id, callback);
    }
    /**
     * @remarks Unsubscribes a callback from a specific scriptevent id.
     * @param id The id of the scriptevent to unsubscribe from.
     */
    unsubscribe(id) {
        if (!id.includes(":"))
            id = addName(id);
        this.register.delete(id);
    }
    raise(id, event) {
        var _a;
        if (!id.includes(":"))
            id = addName(id);
        (_a = this.register.get(id)) === null || _a === void 0 ? void 0 : _a(event);
    }
}
class UtilsAfterEvents {
    /**
     * @remarks The event handler for player's interacting with entities.
     * @example
     * Script
     * ```typescript
     * utils.afterEvents.onInteractWithEntity.subscribe(event => {
     *     utils.mclog(`${event.player.name} Interacted With ${event.victim.typeId}`);
     * })
     * ```
     * Entity
     * ```json
     * {
     *     "components": {
     *         "minecraft:interact": {
     *             "interactions": [
     *                 {
     *                     "on_interact": {
     *                         "target": "self",
     *                         "event": "interact"
     *                     }
     *                 }
     *             ]
     *         }
     *     },
     *     "events": {
     *         "interact": {
     *             "sequence": [
     *                 {
     *                     "queue_command": {
     *                         "command": "scriptevent ldz_240723:interact victim",
     *                         "target": "self"
     *                     }
     *                 },
     *                 {
     *                     "queue_command": {
     *                         "command": "scriptevent ldz_240723:interact player",
     *                         "target": "other"
     *                     }
     *                 }
     *             ]
     *         }
     *     }
     * }
     * ```
     */
    get onInteractWithEntity() {
        return this.onInteractWithEntityLocal;
    }
    /**
     * @remarks The event handler for custom script events.
     * @example
     * Script
     * ```typescript
     * function initializeExplosionHandler() {
     *     afterEvents.scriptEventRecieve.subscribe<{radius: number, options?: mc.ExplosionOptions}>("explosion", event => {
     *         if (event.sourceEntity) {
     *             event.sourceEntity.dimension.createExplosion(event.sourceEntity.location, event.data.radius, event.data.options);
     *         }
     *     });
     * }
     * ```
     * Command
     * ```mcfunction
     * /scriptevent <namespace>:explosion {"radius":3,"options":{"breaksBlocks":false}}
     * ```
     */
    get scriptEventRecieve() {
        return this.onScriptEventRecieveLocal;
    }
    /**
     * @remarks The event handler for when the namespace changes.
     * @example
     * ```typescript
     * afterEvents.onNamespaceChanged.subscribe(namespace => {
     *     mclog(`Namespace Changed to ${namespace}`);
     * });
     * ```
     */
    get onNamespaceChanged() {
        return this.onNamespaceChangedLocal;
    }
    constructor() {
        this.onInteractWithEntityLocal = new PrivateAfterEvent();
        this.onScriptEventRecieveLocal = new PrivateScriptEvent();
        this.onNamespaceChangedLocal = new PrivateAfterEvent();
        this.interactWithEntitySetup();
        this.scriptEventRecieveSetup();
    }
    interactWithEntitySetup() {
        mc.world.afterEvents.playerInteractWithEntity.subscribe(event => {
            this.onInteractWithEntityLocal.raise({ player: new SafetyCheckedPlayer(event.player), victim: new SafetyCheckedEntity(event.target) });
        });
    }
    scriptEventRecieveSetup() {
        mc.system.afterEvents.scriptEventReceive.subscribe(event => {
            let data = event.message === "" ? undefined : event.message;
            if (data) {
                try {
                    data = JSON.parse(data);
                }
                catch (e) { }
            }
            const customEvent = {
                sourceEntity: event.sourceEntity ? new SafetyCheckedEntity(event.sourceEntity) : undefined,
                initiator: event.initiator ? new SafetyCheckedEntity(event.initiator) : undefined,
                sourceBlock: event.sourceBlock,
                sourceType: event.sourceType,
                data,
            };
            this.onScriptEventRecieveLocal.raise(event.id, customEvent);
        });
    }
}
export const afterEvents = new UtilsAfterEvents();
