"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModUpgraded = void 0;
const isaac_typescript_definitions_1 = require("isaac-typescript-definitions");
const callbacks_1 = require("../callbacks");
const decorators_1 = require("../decorators");
const ISCFeature_1 = require("../enums/ISCFeature");
const ModCallbackCustom_1 = require("../enums/ModCallbackCustom");
const features_1 = require("../features");
const debugFunctions_1 = require("../functions/debugFunctions");
const enums_1 = require("../functions/enums");
const log_1 = require("../functions/log");
const tstlClass_1 = require("../functions/tstlClass");
const types_1 = require("../functions/types");
const utils_1 = require("../functions/utils");
/**
 * `isaacscript-common` has many custom callbacks that you can use in your mods. Instead of
 * hijacking the vanilla `Mod` object, we provide a `ModUpgraded` object for you to use, which
 * extends the base class and adds a new method of `AddCallbackCustom`.
 *
 * To upgrade your mod, use the `upgradeMod` helper function.
 *
 * By specifying one or more optional features when upgrading your mod, you will get a version of
 * `ModUpgraded` that has extra methods corresponding to the features that were specified. (This
 * corresponds to the internal-type `ModUpgradedWithFeatures` type, which extends `ModUpgraded`.)
 */
class ModUpgraded {
    // -----------------
    // Vanilla variables
    // -----------------
    Name;
    // ----------------
    // Custom variables
    // ----------------
    /** We store a copy of the original mod object so that we can re-implement its functions. */
    mod;
    debug;
    timeThreshold;
    callbacks;
    features;
    // -----------
    // Constructor
    // -----------
    constructor(mod, debug, timeThreshold) {
        this.Name = mod.Name;
        this.mod = mod;
        this.debug = debug;
        this.timeThreshold = timeThreshold;
        this.callbacks = (0, callbacks_1.getCallbacks)();
        this.features = (0, features_1.getFeatures)(this, this.callbacks);
    }
    // ---------------
    // Vanilla methods
    // ---------------
    AddCallback(modCallback, ...args) {
        this.AddPriorityCallback(modCallback, isaac_typescript_definitions_1.CallbackPriority.DEFAULT, ...args);
    }
    AddPriorityCallback(modCallback, priority, ...args) {
        if (this.debug) {
            const callback = args[0];
            const optionalArg = args[1];
            const parentFunctionDescription = (0, log_1.getParentFunctionDescription)();
            const customCallback = type(modCallback) === "string";
            const callbackName = customCallback
                ? `${modCallback} (custom callback)`
                : `ModCallback.${isaac_typescript_definitions_1.ModCallback[modCallback]}`;
            const signature = parentFunctionDescription === undefined
                ? callbackName
                : `${parentFunctionDescription} - ${callbackName}`;
            /**
             * We don't use the "log" helper function here since it will always show the same "unknown"
             * prefix.
             */
            const callbackWithLogger = (
            // @ts-expect-error The compiler is not smart enough to know that the callback args should
            // match the callback.
            ...callbackArgs) => {
                const startTime = (0, debugFunctions_1.getTime)();
                Isaac.DebugString(`${signature} - START`);
                // @ts-expect-error The compiler is not smart enough to know that the callback args should
                // match the callback.
                // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, n/no-callback-literal
                const returnValue = callback(...callbackArgs);
                const elapsedTime = (0, debugFunctions_1.getElapsedTimeSince)(startTime);
                if (this.timeThreshold === undefined ||
                    this.timeThreshold <= elapsedTime) {
                    Isaac.DebugString(`${signature} - END - time: ${elapsedTime}`);
                }
                else {
                    Isaac.DebugString(`${signature} - END`);
                }
                // eslint-disable-next-line @typescript-eslint/no-unsafe-return
                return returnValue;
            };
            const newArgs = [callbackWithLogger, optionalArg];
            // @ts-expect-error The compiler is not smart enough to know that the callback args should
            // match the callback.
            this.mod.AddPriorityCallback(modCallback, priority, ...newArgs);
        }
        else {
            this.mod.AddPriorityCallback(modCallback, priority, ...args);
        }
    }
    HasData() {
        return this.mod.HasData();
    }
    LoadData() {
        return this.mod.LoadData();
    }
    RemoveCallback(modCallback, callback) {
        this.mod.RemoveCallback(modCallback, callback);
    }
    RemoveData() {
        this.mod.RemoveData();
    }
    SaveData(data) {
        this.mod.SaveData(data);
    }
    // ---------------------
    // Custom public methods
    // ---------------------
    /**
     * Registers a function to be executed when an in-game event happens.
     *
     * This method is specifically for events that are provided by the IsaacScript standard library.
     * For example, the `ModCallbackCustom.POST_BOMB_EXPLODE` event corresponds to when a bomb
     * explodes.
     */
    AddCallbackCustom(modCallbackCustom, ...args) {
        this.AddPriorityCallbackCustom(modCallbackCustom, isaac_typescript_definitions_1.CallbackPriority.DEFAULT, ...args);
    }
    /**
     * The same as the `ModUpgraded.AddCallbackCustom` method, but allows setting a custom priority.
     * By default, callbacks are added with a priority of 0, so this allows you to add early or late
     * callbacks as necessary. See the `CallbackPriority` enum.
     */
    AddPriorityCallbackCustom(modCallbackCustom, priority, ...args) {
        const callbackClass = this.callbacks[modCallbackCustom];
        // @ts-expect-error The compiler is not smart enough to figure out that the parameters match.
        // eslint-disable-next-line isaacscript/require-variadic-function-argument
        callbackClass.addSubscriber(priority, ...args);
        this.initFeature(callbackClass);
    }
    /**
     * Unregisters a function that was previously registered with the `AddCallbackCustom` method.
     *
     * This method is specifically for events that are provided by the IsaacScript standard library.
     * For example, the `ModCallbackCustom.POST_BOMB_EXPLODE` event corresponds to when a bomb
     * explodes.
     *
     * This method does not care about the tertiary argument. In other words, regardless of the
     * conditions of how you registered the callback, it will be removed.
     */
    RemoveCallbackCustom(modCallbackCustom, callback) {
        const callbackClass = this.callbacks[modCallbackCustom];
        // @ts-expect-error The compiler is not smart enough to figure out that the parameters match.
        callbackClass.removeSubscriber(callback);
        this.uninitFeature(callbackClass);
    }
    /**
     * Logs every custom callback or extra feature that is currently enabled. Useful for debugging or
     * profiling.
     */
    logUsedFeatures() {
        // Custom callbacks
        for (const [modCallbackCustomString, callbackClass] of Object.entries(this.callbacks)) {
            if (callbackClass.numConsumers === 0) {
                continue;
            }
            const modCallbackCustom = (0, types_1.parseIntSafe)(modCallbackCustomString);
            (0, utils_1.assertDefined)(modCallbackCustom, `Failed to convert the string "${modCallbackCustomString}" representing a "ModCallbackCustom" value to a number.`);
            if (!(0, enums_1.isEnumValue)(modCallbackCustom, ModCallbackCustom_1.ModCallbackCustom)) {
                error(`Failed to convert the number ${modCallbackCustom} to a "ModCallbackCustom" value.`);
            }
            (0, log_1.log)(`- ModCallbackCustom.${ModCallbackCustom_1.ModCallbackCustom[modCallbackCustom]} (${modCallbackCustom})`);
        }
        // Extra features
        for (const [iscFeatureString, featureClass] of Object.entries(this.features)) {
            if (featureClass.numConsumers === 0) {
                continue;
            }
            const iscFeature = (0, types_1.parseIntSafe)(iscFeatureString);
            (0, utils_1.assertDefined)(iscFeature, `Failed to convert the string "${iscFeatureString}" representing a "ISCFeature" value to a number.`);
            if (!(0, enums_1.isEnumValue)(iscFeature, ISCFeature_1.ISCFeature)) {
                error(`Failed to convert the number ${iscFeature} to a "ISCFeature" value.`);
            }
            (0, log_1.log)(`- ISCFeature.${ISCFeature_1.ISCFeature[iscFeature]} (${iscFeature})`);
        }
    }
    // ----------------------
    // Custom private methods
    // ----------------------
    /**
     * This is used to initialize both custom callbacks and "extra features".
     *
     * This mirrors the `uninitFeature` method.
     */
    initFeature(feature) {
        feature.numConsumers++;
        if (feature.initialized) {
            return;
        }
        feature.initialized = true;
        if (feature.v !== undefined) {
            if (feature.featuresUsed === undefined) {
                feature.featuresUsed = [];
            }
            if (!feature.featuresUsed.includes(ISCFeature_1.ISCFeature.SAVE_DATA_MANAGER)) {
                feature.featuresUsed.unshift(ISCFeature_1.ISCFeature.SAVE_DATA_MANAGER);
            }
        }
        if (feature.featuresUsed !== undefined) {
            for (const featureUsed of feature.featuresUsed) {
                const featureClass = this.features[featureUsed];
                this.initFeature(featureClass);
            }
        }
        if (feature.callbacksUsed !== undefined) {
            for (const callbackTuple of feature.callbacksUsed) {
                const [modCallback, callbackFunc, optionalArgs] = callbackTuple;
                // TypeScript is not smart enough to know that the arguments match the function.
                this.AddPriorityCallback(modCallback, isaac_typescript_definitions_1.CallbackPriority.IMPORTANT, callbackFunc, ...(optionalArgs ?? []));
            }
        }
        if (feature.customCallbacksUsed !== undefined) {
            for (const callbackTuple of feature.customCallbacksUsed) {
                const [modCallback, callbackFunc, optionalArgs] = callbackTuple;
                // TypeScript is not smart enough to know that the arguments match the function.
                this.AddPriorityCallbackCustom(modCallback, isaac_typescript_definitions_1.CallbackPriority.IMPORTANT, callbackFunc, ...(optionalArgs ?? []));
            }
        }
        if (feature.v !== undefined) {
            const className = (0, tstlClass_1.getTSTLClassName)(feature);
            (0, utils_1.assertDefined)(className, "Failed to get the name of a feature.");
            const saveDataManagerClass = this.features[ISCFeature_1.ISCFeature.SAVE_DATA_MANAGER];
            saveDataManagerClass.saveDataManager(className, feature.v, feature.vConditionalFunc);
        }
    }
    /**
     * This is used to uninitialize both custom callbacks and "extra features".
     *
     * This mirrors the `initFeature` method.
     */
    uninitFeature(feature) {
        if (feature.numConsumers <= 0) {
            const className = (0, tstlClass_1.getTSTLClassName)(feature) ?? "unknown";
            error(`Failed to uninit feature "${className}" since it has ${feature.numConsumers} consumers, which should never happen.`);
        }
        if (!feature.initialized) {
            const className = (0, tstlClass_1.getTSTLClassName)(feature) ?? "unknown";
            error(`Failed to uninit feature "${className}" since it was not initialized, which should never happen.`);
        }
        feature.numConsumers--;
        if (feature.numConsumers > 0) {
            return;
        }
        feature.initialized = false;
        if (feature.featuresUsed !== undefined) {
            for (const featureUsed of feature.featuresUsed) {
                const featureClass = this.features[featureUsed];
                this.uninitFeature(featureClass);
            }
        }
        if (feature.callbacksUsed !== undefined) {
            for (const callbackTuple of feature.callbacksUsed) {
                const [modCallback, callbackFunc] = callbackTuple;
                this.RemoveCallback(modCallback, callbackFunc);
            }
        }
        if (feature.customCallbacksUsed !== undefined) {
            for (const callbackTuple of feature.customCallbacksUsed) {
                const [modCallback, callbackFunc] = callbackTuple;
                this.RemoveCallbackCustom(modCallback, callbackFunc);
            }
        }
        if (feature.v !== undefined) {
            const className = (0, tstlClass_1.getTSTLClassName)(feature);
            (0, utils_1.assertDefined)(className, "Failed to get the name of a feature.");
            const saveDataManagerClass = this.features[ISCFeature_1.ISCFeature.SAVE_DATA_MANAGER];
            saveDataManagerClass.saveDataManagerRemove(className);
        }
    }
    /**
     * Returns the names of the exported class methods from the features that were added. This is
     * called from the "upgradeMod" function, but we want to mark it as private so that end-users
     * don't have access to it.
     */
    initOptionalFeature(feature) {
        const featureClass = this.features[feature];
        this.initFeature(featureClass);
        return getExportedMethodsFromFeature(featureClass);
    }
}
exports.ModUpgraded = ModUpgraded;
/**
 * In this context, "exported" methods are methods that are annotated with the "@Exported"
 * decorator, which signify that the method should be attached to the `ModUpgraded` class.
 *
 * Exported methods are stored in an internal static array on the class that is created by the
 * decorator.
 */
function getExportedMethodsFromFeature(featureClass) {
    const constructor = (0, tstlClass_1.getTSTLClassConstructor)(featureClass);
    const exportedMethodNames = constructor[decorators_1.EXPORTED_METHOD_NAMES_KEY];
    if (exportedMethodNames === undefined) {
        return [];
    }
    return exportedMethodNames.map((name) => {
        const featureClassRecord = featureClass;
        // We cannot split out the method to a separate variable or else the "self" parameter will not
        // be properly passed to the method.
        if (featureClassRecord[name] === undefined) {
            error(`Failed to find a decorated exported method: ${name}`);
        }
        // In order for "this" to work properly in the method, we have to wrap the method invocation in
        // an arrow function.
        const wrappedMethod = (...args) => 
        // We use a non-null assertion since we have already validated that the function exists. (See
        // the above comment.)
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        featureClassRecord[name](...args);
        return [name, wrappedMethod];
    });
}
