const {Config} = require("./Config");
const Log = require("./Log");
const {checkType, createHiddenProp} = require("./Utility");
const {EventBase, EventBusBase} = require("./EventBase");

let tickCount;
let isSync;
let initialized = false;
let hadTick;
let timerHandle;

class SynchronizeEvent extends EventBase {
    /**
     * Creates a new event to be sent over the significance bus
     *
     * @param {string} sourceName - The name of the source of the event.
     * @param {string} sourceType - The type of the source.
     */
    constructor(sourceName, sourceType) {
        super();

        checkType("PerceptionEvent.constructor", "sourceName", sourceName, "string");
        checkType("PerceptionEvent.constructor", "sourceType", sourceType, "string");
        createHiddenProp(this, "_sourceName", sourceName, true);
        createHiddenProp(this, "_sourceType", sourceType, true);
    }

    // eslint-disable-next-line jsdoc/require-jsdoc
    get sourceName() {
        return this._sourceName || "initializing";
    }

    // eslint-disable-next-line jsdoc/require-jsdoc
    get sourceType() {
        return this._sourceType || "initializing";
    }

    // eslint-disable-next-line jsdoc/require-jsdoc
    get allowedEventTypes() {
        return new Set(["tick"]);
    }

    // eslint-disable-next-line jsdoc/require-jsdoc
    get eventBus() {
        return synchronizeEventBus;
    }
}

const synchronizeEventBus = new EventBusBase(SynchronizeEvent);

/**
 * A singleton class used to synchronize intrinsics, significance, and perceptions.
 * There isn't a biological or cognitive analog, rather this is a crutch to overcome
 * how digital systems interface with their environments.
 */
class Synchronize {
    /**
     * Initialize the Synchronize system
     */
    static init() {
        tickCount = 0;
        isSync = Config.get("environment-synchronous");
        initialized = true;
        hadTick = false;

        if (isSync) {
            Synchronize.startWatchdog();
        } else {
            Log.warn("Synchronize detected asynchronous environment: this mode is untested");
            timerHandle = setInterval(_asyncTick, Config.get("environment-async-time"));
        }
    }

    /**
     * Used by synchronus environments to indicate that a synchronous step has been taken.
     *
     * @throws Error if used with an asynchronous environment (Config "environment-synchronous" === `false`)
     */
    static async nextTick() {
        if (!initialized) {
            throw new Error("Please call Synchronize.init() before Synchronize.nextTick()");
        }

        if (!isSync) {
            throw new Error("Synchronize.nextTick should only be called in a synchronous environment (see: Config('environment-synchronous'))");
        }

        hadTick = true;

        return _nextTick();
    }

    /**
     * Register a callback that will be triggered by `nextTick`.
     * Internally this calls `addListener` on an `EventEmitter`.
     *
     * @param   {Promise<Function>} cb The callback to be called by `nextTick`
     */
    static async register(cb) {
        if (!initialized) {
            throw new Error("Please call Synchronize.init() before Synchronize.register()");
        }

        return synchronizeEventBus.addListener("tick", cb);
    }

    /**
     * Removes a Synchronize listener
     *
     * @param  {Function} cb The callback function that was passed to `register`
     * @returns {Promise}      A Promise that resolves when the listener has been removed.
     */
    static async unregister(cb) {
        return synchronizeEventBus.removeListener("tick", cb);
    }

    /**
     * Number of ticks that have occurred
     */
    static get tickCount() {
        if (!initialized) {
            throw new Error("Please call Synchronize.init() before getting Synchronize.tickCount");
        }

        return tickCount;
    }

    /**
     * Terminate the Synchronization sub-system. Mostly used for testing.
     */
    static async shutdown() {
        initialized = false;
        tickCount = undefined;
        clearInterval(timerHandle);
        return synchronizeEventBus.removeAllListeners();
    }

    /**
     * The default synchronus watchdog. Mostly used internally and exposed for testing.
     */
    static syncWatchdog() {
        if (!hadTick) {
            throw new Error(`Synchronize synchronous watchdog timed out after ${Config.get("environment-sync-watchdog-timeout")}ms without Synchronize.nextTick() being called`);
        }

        hadTick = false;
    }

    /**
     * Pauses the watchdog. Primarily used in Breakpoint.
     */
    static pauseWatchdog() {
        if (!initialized) {
            return;
        }

        clearInterval(timerHandle);
    }

    /**
     * Restarts the watchdog. Primarily used in Breakpoint.
     */
    static startWatchdog() {
        if (!initialized) {
            return;
        }

        timerHandle = setInterval(Synchronize.syncWatchdog, Config.get("environment-sync-watchdog-timeout"));
    }
}

async function _nextTick() {
    tickCount++;
    let e = new SynchronizeEvent("synchronize", "synchronize");
    return e.emit("tick", tickCount);
}

async function _asyncTick() {
    return _nextTick()
        .catch((err) => {
            throw err;
        });
}

module.exports = {
    Synchronize,
};