"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModFeature = exports.MOD_FEATURE_CUSTOM_CALLBACKS_KEY = exports.MOD_FEATURE_CALLBACKS_KEY = void 0;
const array_1 = require("../functions/array");
const tstlClass_1 = require("../functions/tstlClass");
const types_1 = require("../functions/types");
const utils_1 = require("../functions/utils");
exports.MOD_FEATURE_CALLBACKS_KEY = "__callbacks";
exports.MOD_FEATURE_CUSTOM_CALLBACKS_KEY = "__customCallbacks";
const WRAPPED_CALLBACK_METHODS_KEY = "__wrappedCallbackMethods";
const WRAPPED_CUSTOM_CALLBACK_METHODS_KEY = "__wrappedCustomCallbacksMethods";
/**
 * Helper class for mods that want to represent their individual features as classes. Extend your
 * mod features from this class in order to enable the `@Callback` and `@CustomCallback` decorators
 * that automatically subscribe to callbacks.
 *
 * It is recommended that you use the `initModFeatures` helper function to instantiate all of your
 * mod classes (instead of instantiating them yourself). This is so that any attached `v` objects
 * are properly registered with the save data manager; see below.
 *
 * If you are manually instantiating a mod feature yourself, then:
 *
 * - You must pass your upgraded mod as the first argument to the constructor.
 * - In almost all cases, you will want the callback functions to be immediately subscribed after
 *   instantiating the class. However, if this is not the case, you can pass `false` as the optional
 *   second argument to the constructor.
 *
 * If your mod feature has a property called `v`, it will be assumed that these are variables that
 * should be managed by the save data manager. Unfortunately, due to technical limitations with
 * classes, this registration will only occur if you initialize the class with the `initModFeatures`
 * helper function. (This is because the parent class does not have access to the child's properties
 * upon first construction.)
 */
class ModFeature {
    mod;
    /**
     * An optional method that allows for conditional callback execution. If specified, any class
     * method that is annotated with a `@Callback` or `@CallbackCustom` decorator will only be fired
     * if the executed conditional function returns true.
     *
     * This property is used to easily turn entire mod features on and off (rather than repeating
     * conditional logic and early returning at the beginning of every callback function).
     *
     * Since the specific information for the firing callback is passed as arguments into the
     * conditional method, you can also write logic that would only apply to a specific type of
     * callback.
     *
     * By default, this is set to null, which means that all callback methods will fire
     * unconditionally. Override this property in your class if you need to use it.
     *
     * The function has the following signature:
     *
     * ```ts
     * <T extends boolean>(
     *   vanilla: T, // Whether this is a vanilla or custom callback.
     *   modCallback: T extends true ? ModCallback : ModCallbackCustom,
     *   ...callbackArgs: unknown[] // This would be e.g. `pickup: EntityPickup` for the `POST_PICKUP_INIT` callback.
     * ) => boolean;
     * ```
     */
    shouldCallbackMethodsFire = null;
    /**
     * Whether the feature has registered its callbacks yet.
     *
     * This will almost always be equal to true unless you explicitly passed `false` to the second
     * argument of the constructor.
     */
    initialized = false;
    constructor(mod, init = true) {
        this.mod = mod;
        if (init) {
            this.init();
        }
    }
    /**
     * Runs the `Mod.AddCallback` and `ModUpgraded.AddCallbackCustom` methods for all of the decorated
     * callbacks. Also registers/unregisters the "v" variable on the save data manager.
     *
     * @param init Optional. Whether to initialize or uninitialize. Default is true.
     */
    init(init = true) {
        if (this.initialized === init) {
            return;
        }
        this.initialized = init;
        const constructor = (0, tstlClass_1.getTSTLClassConstructor)(this);
        (0, utils_1.assertDefined)(constructor, "Failed to get the TSTL class constructor for a mod feature.");
        const tstlClassName = (0, tstlClass_1.getTSTLClassName)(this);
        (0, utils_1.assertDefined)(tstlClassName, "Failed to get the TSTL class name for a mod feature.");
        initDecoratedCallbacks(this, constructor, tstlClassName, true, init);
        initDecoratedCallbacks(this, constructor, tstlClassName, false, init);
        initSaveDataManager(this, tstlClassName, init);
    }
    /**
     * Runs the `Mod.RemoveCallback` and `ModUpgraded.RemoveCallbackCustom` methods for all of the
     * decorated callbacks.
     *
     * This is just an alias for `ModFeature.init(false)`.
     */
    uninit() {
        this.init(false);
    }
}
exports.ModFeature = ModFeature;
function initDecoratedCallbacks(modFeature, constructor, tstlClassName, vanilla, init) {
    const modFeatureConstructor = constructor;
    const callbackTuplesKey = vanilla
        ? exports.MOD_FEATURE_CALLBACKS_KEY
        : exports.MOD_FEATURE_CUSTOM_CALLBACKS_KEY;
    const callbackTuples = modFeatureConstructor[callbackTuplesKey];
    if (callbackTuples === undefined) {
        return;
    }
    if (!(0, array_1.isArray)(callbackTuples)) {
        error(`Failed to initialize/uninitialize the decorated callbacks on a mod feature since the callback arguments on the key of "${callbackTuplesKey}" was not an array and was instead of type: ${type(callbackTuples)}`);
    }
    for (const callbackTuple of callbackTuples) {
        if (!(0, array_1.isArray)(callbackTuple)) {
            error(`Failed to initialize/uninitialize the decorated callbacks on a mod feature since one of the callback arguments on the key of "${callbackTuplesKey}" was not an array and was instead of type: ${type(callbackTuple)}`);
        }
        const modCallback = callbackTuple[0];
        if (!(0, types_1.isInteger)(modCallback)) {
            error(`Failed to get the callback number from the callback tuple for class: ${tstlClassName}`);
        }
        const priority = callbackTuple[1];
        if (!(0, types_1.isInteger)(priority)) {
            error(`Failed to get the callback priority from the callback tuple for class: ${tstlClassName}`);
        }
        const callback = callbackTuple[2];
        if (!(0, types_1.isFunction)(callback)) {
            error(`Failed to get the callback function from the callback tuple for class: ${tstlClassName}`);
        }
        const parameters = callbackTuple[3];
        // We must pass false as the second argument to `isArray` since the callback parameters may not
        // necessarily be contiguous. (They might be separated by `undefined` values.)
        if (!(0, array_1.isArray)(parameters, false)) {
            error(`Failed to get the callback parameters from the callback tuple for class: ${tstlClassName}`);
        }
        // eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/prefer-destructuring
        const mod = modFeature["mod"];
        if (init) {
            addCallback(modFeature, modFeatureConstructor, mod, modCallback, // eslint-disable-line isaacscript/strict-enums
            priority, callback, parameters, vanilla);
        }
        else {
            removeCallback(modFeatureConstructor, mod, modCallback, // eslint-disable-line isaacscript/strict-enums
            vanilla);
        }
    }
}
function addCallback(modFeature, modFeatureConstructor, mod, modCallback, priority, callback, // eslint-disable-line @typescript-eslint/ban-types
parameters, vanilla) {
    // We need to wrap the callback in a new function so that we can explicitly pass the class as the
    // first argument. (Otherwise, the method will not be able to properly access `this`.
    const wrappedCallback = (...callbackArgs) => {
        // eslint-disable-next-line @typescript-eslint/dot-notation
        const conditionalFunc = modFeature["shouldCallbackMethodsFire"];
        if (conditionalFunc !== null) {
            const shouldRun = conditionalFunc(vanilla, modCallback, ...callbackArgs);
            if (!shouldRun) {
                return undefined;
            }
        }
        const castedCallback = callback;
        return castedCallback(modFeature, ...callbackArgs);
    };
    // We need to save the wrapped function for later (so we can unregister them).
    if (vanilla) {
        const modCallbackVanilla = modCallback;
        let wrappedMethodsMap = modFeatureConstructor[WRAPPED_CALLBACK_METHODS_KEY];
        if (wrappedMethodsMap === undefined) {
            wrappedMethodsMap = new Map();
            modFeatureConstructor[WRAPPED_CALLBACK_METHODS_KEY] = wrappedMethodsMap;
        }
        wrappedMethodsMap.set(modCallbackVanilla, wrappedCallback);
    }
    else {
        const modCallbackCustom = modCallback;
        let wrappedMethodsMap = modFeatureConstructor[WRAPPED_CUSTOM_CALLBACK_METHODS_KEY];
        if (wrappedMethodsMap === undefined) {
            wrappedMethodsMap = new Map();
            modFeatureConstructor[WRAPPED_CUSTOM_CALLBACK_METHODS_KEY] =
                wrappedMethodsMap;
        }
        wrappedMethodsMap.set(modCallbackCustom, wrappedCallback);
    }
    if (vanilla) {
        mod.AddPriorityCallback(modCallback, priority, wrappedCallback, ...parameters);
    }
    else {
        mod.AddPriorityCallbackCustom(modCallback, priority, wrappedCallback, ...parameters);
    }
}
function removeCallback(modFeatureConstructor, mod, modCallback, vanilla) {
    if (vanilla) {
        const modCallbackVanilla = modCallback;
        const wrappedMethodsMap = modFeatureConstructor[WRAPPED_CALLBACK_METHODS_KEY];
        if (wrappedMethodsMap === undefined) {
            return;
        }
        const wrappedCallback = wrappedMethodsMap.get(modCallbackVanilla);
        mod.RemoveCallback(modCallback, wrappedCallback);
    }
    else {
        const modCallbackCustom = modCallback;
        const wrappedMethodsMap = modFeatureConstructor[WRAPPED_CUSTOM_CALLBACK_METHODS_KEY];
        if (wrappedMethodsMap === undefined) {
            return;
        }
        const wrappedCallback = wrappedMethodsMap.get(modCallbackCustom);
        mod.RemoveCallbackCustom(modCallback, wrappedCallback);
    }
}
/**
 * This will only work for end-users who are calling the `ModFeature.init` method explicitly. (See
 * the discussion in the `ModFeature` comment.)
 */
function initSaveDataManager(modFeature, tstlClassName, init) {
    // Do nothing if this class does not have any variables.
    const { v } = modFeature;
    if (v === undefined) {
        return;
    }
    if (!(0, types_1.isTable)(v)) {
        error('Failed to initialize a mod feature class due to having a "v" property that is not an object. (The "v" property is supposed to be an object that holds the variables for the class, managed by the save data manager.)');
    }
    // Do nothing if we have not enabled the save data manager.
    // eslint-disable-next-line @typescript-eslint/dot-notation
    const mod = modFeature["mod"];
    const saveDataManagerMethodName = init
        ? "saveDataManager"
        : "saveDataManagerRemove";
    const saveDataManagerMethod = mod[saveDataManagerMethodName];
    (0, utils_1.assertDefined)(saveDataManagerMethod, 'Failed to initialize a mod feature class due to having a "v" object and not having the save data manager initialized. You must pass "ISCFeature.SAVE_DATA_MANAGER" to the "upgradeMod" function.');
    if (typeof saveDataManagerMethod !== "function") {
        error(`The "${saveDataManagerMethodName}" property of the "ModUpgraded" object was not a function.`);
    }
    if (init) {
        saveDataManagerMethod(tstlClassName, v);
    }
    else {
        saveDataManagerMethod(tstlClassName);
    }
}
