"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SaveDataManager = void 0;
const isaac_typescript_definitions_1 = require("isaac-typescript-definitions");
const decorators_1 = require("../../../decorators");
const ModCallbackCustom_1 = require("../../../enums/ModCallbackCustom");
const SaveDataKey_1 = require("../../../enums/SaveDataKey");
const SerializationType_1 = require("../../../enums/SerializationType");
const deepCopy_1 = require("../../../functions/deepCopy");
const frames_1 = require("../../../functions/frames");
const log_1 = require("../../../functions/log");
const stage_1 = require("../../../functions/stage");
const tstlClass_1 = require("../../../functions/tstlClass");
const types_1 = require("../../../functions/types");
const utils_1 = require("../../../functions/utils");
const ReadonlySet_1 = require("../../../types/ReadonlySet");
const Feature_1 = require("../../private/Feature");
const glowingHourGlass_1 = require("./saveDataManager/glowingHourGlass");
const loadFromDisk_1 = require("./saveDataManager/loadFromDisk");
const restoreDefaults_1 = require("./saveDataManager/restoreDefaults");
const saveToDisk_1 = require("./saveDataManager/saveToDisk");
const NON_USER_DEFINED_CLASS_NAMES = new ReadonlySet_1.ReadonlySet([
    "Map",
    "Set",
    "DefaultMap",
]);
class SaveDataManager extends Feature_1.Feature {
    /**
     * We store a local reference to the mod object so that we can access the corresponding methods
     * that read and write to the "save#.dat" file.
     */
    mod;
    /**
     * The save data map is indexed by subscriber name. We use Lua tables instead of TypeScriptToLua
     * Maps for the master map so that we can access the variables via the in-game console when
     * debugging. (TSTL Maps don't expose the map keys as normal keys.)
     */
    saveDataMap = new LuaMap();
    /**
     * When mod feature data is initialized, we copy the initial values into a separate map so that we
     * can restore them later on.
     */
    saveDataDefaultsMap = new LuaMap();
    /**
     * Each mod feature can optionally provide a function that can control whether the save data is
     * written to disk.
     */
    saveDataConditionalFuncMap = new LuaMap();
    /**
     * We backup some save data keys on every new room for the purposes of restoring it when Glowing
     * Hour Glass is used.
     *
     * Note that the save data is backed up in serialized form so that we can use the `merge` function
     * to restore it.
     */
    saveDataGlowingHourGlassMap = new LuaMap();
    /**
     * End-users can register their classes with the save data manager for proper serialization when
     * contained in nested maps, sets, and arrays.
     */
    classConstructors = new LuaMap();
    // Other variables
    inARun = false;
    restoreGlowingHourGlassDataOnNextRoom = false;
    /** @internal */
    constructor(mod) {
        super();
        this.callbacksUsed = [
            // 3
            [
                isaac_typescript_definitions_1.ModCallback.POST_USE_ITEM,
                this.postUseItemGlowingHourGlass,
                [isaac_typescript_definitions_1.CollectibleType.GLOWING_HOUR_GLASS],
            ],
            // 9
            [isaac_typescript_definitions_1.ModCallback.POST_PLAYER_INIT, this.postPlayerInit],
            // 17
            [isaac_typescript_definitions_1.ModCallback.PRE_GAME_EXIT, this.preGameExit],
            // 18
            // We want to avoid a needless dependency on the `GameReorderedCallbacks` feature.
            // eslint-disable-next-line deprecation/deprecation
            [isaac_typescript_definitions_1.ModCallback.POST_NEW_LEVEL, this.postNewLevel],
        ];
        this.customCallbacksUsed = [
            [ModCallbackCustom_1.ModCallbackCustom.POST_NEW_ROOM_EARLY, this.postNewRoomEarly],
        ];
        this.mod = mod;
    }
    // ModCallback.POST_USE_ITEM (3)
    // CollectibleType.GLOWING_HOUR_GLASS (422)
    postUseItemGlowingHourGlass = (_collectibleType, _rng, _player, _useFlags, _activeSlot, _customVarData) => {
        this.restoreGlowingHourGlassDataOnNextRoom = true;
        return undefined;
    };
    // ModCallback.POST_PLAYER_INIT (9)
    postPlayerInit = (_player) => {
        // We want to only load data once per run to handle the case of a player using Genesis, a second
        // player joining the run, and so on.
        if (this.inARun) {
            return;
        }
        this.inARun = true;
        // Handle the race-condition of using the Glowing Hourglass and then resetting the run.
        this.restoreGlowingHourGlassDataOnNextRoom = false;
        // We want to unconditionally load save data on every new run since there might be persistent
        // data that is not tied to an individual run.
        (0, loadFromDisk_1.loadFromDisk)(this.mod, this.saveDataMap, this.classConstructors);
        const isContinued = (0, frames_1.isAfterGameFrame)(0);
        if (!isContinued) {
            (0, restoreDefaults_1.restoreDefaultsForAllFeaturesAndKeys)(this.saveDataMap, this.saveDataDefaultsMap);
        }
        // On continued runs, the `POST_NEW_LEVEL` callback will not fire, so we do not have to worry
        // about saved data based on level getting overwritten.
    };
    // ModCallback.PRE_GAME_EXIT (17)
    preGameExit = () => {
        // We unconditionally save variables to disk (because regardless of a save & quit or a death,
        // persistent variables should be recorded).
        (0, saveToDisk_1.saveToDisk)(this.mod, this.saveDataMap, this.saveDataConditionalFuncMap);
        // Mark that we are going to the menu. (Technically, the `POST_ENTITY_REMOVE` callback may fire
        // before actually going to the menu, but that must be explicitly handled.)
        this.inARun = false;
        // At this point, we could blow away the existing save data or restore defaults, but it is not
        // necessary since we will have to do it again in the `POST_PLAYER_INIT` callback. Furthermore,
        // the `POST_ENTITY_REMOVE` callback may fire after the `PRE_GAME_EXIT` callback, so wiping data
        // now could result in bugs for features that depend on that (e.g. `PickupIndexCreation`).
    };
    // ModCallback.POST_NEW_LEVEL (18)
    postNewLevel = () => {
        (0, restoreDefaults_1.restoreDefaultsForAllFeaturesKey)(this.saveDataMap, this.saveDataDefaultsMap, SaveDataKey_1.SaveDataKey.LEVEL);
        // We save data to disk at the beginning of every floor (for the 2nd floor and beyond) to
        // emulate what the game does internally. (This mitigates data loss in the event of a crash).
        if (!(0, stage_1.onFirstFloor)()) {
            (0, saveToDisk_1.saveToDisk)(this.mod, this.saveDataMap, this.saveDataConditionalFuncMap);
        }
    };
    // ModCallbackCustom.POST_NEW_ROOM_EARLY
    postNewRoomEarly = () => {
        (0, restoreDefaults_1.restoreDefaultsForAllFeaturesKey)(this.saveDataMap, this.saveDataDefaultsMap, SaveDataKey_1.SaveDataKey.ROOM);
        // Handle the Glowing Hourglass.
        if (this.restoreGlowingHourGlassDataOnNextRoom) {
            this.restoreGlowingHourGlassDataOnNextRoom = false;
            (0, glowingHourGlass_1.restoreGlowingHourGlassBackup)(this.saveDataMap, this.saveDataConditionalFuncMap, this.saveDataGlowingHourGlassMap, this.classConstructors);
        }
        else {
            (0, glowingHourGlass_1.makeGlowingHourGlassBackup)(this.saveDataMap, this.saveDataConditionalFuncMap, this.saveDataGlowingHourGlassMap);
        }
    };
    saveDataManager(key, v, conditionalFunc) {
        if ((0, tstlClass_1.isTSTLClass)(key)) {
            const className = (0, tstlClass_1.getTSTLClassName)(key);
            (0, utils_1.assertDefined)(className, 'Failed to get the class name for the submitted class (as part of the "key" parameter) when registering new data with the save data manager.');
            key = className;
        }
        if (!(0, types_1.isString)(key)) {
            error(`The save data manager requires that keys are strings or TSTL classes. You tried to use a key of type: ${typeof key}`);
        }
        if (this.saveDataMap.has(key)) {
            error(`The save data manager is already managing save data for a key of: ${key}`);
        }
        // First, recursively look through the new save data for any classes, so we can register them
        // with the save data manager.
        this.storeClassConstructorsFromObject(v);
        // Add the new save data to the map.
        this.saveDataMap.set(key, v);
        // Convert the boolean to a function, if necessary. (Having the argument be a boolean is
        // necessary in order for the overloads to work properly.)
        if (conditionalFunc === false) {
            conditionalFunc = () => false;
        }
        // If the only key in the save data is "room", then we don't have to worry about saving this
        // data to disk (because the room would be reloaded upon resuming a continued run).
        const saveDataKeys = Object.keys(v);
        if (saveDataKeys.length === 1 && saveDataKeys[0] === "room") {
            conditionalFunc = () => false;
        }
        // Make a copy of the initial save data so that we can use it to restore the default values
        // later on.
        const saveDataCopy = (0, deepCopy_1.deepCopy)(v, SerializationType_1.SerializationType.NONE, key);
        this.saveDataDefaultsMap.set(key, saveDataCopy);
        // Store the conditional function for later, if present.
        if (conditionalFunc !== undefined) {
            this.saveDataConditionalFuncMap.set(key, conditionalFunc);
        }
    }
    /**
     * Recursively traverses an object, collecting all of the class constructors that it encounters.
     */
    storeClassConstructorsFromObject(luaMap) {
        const tstlClassName = (0, tstlClass_1.getTSTLClassName)(luaMap);
        if (tstlClassName !== undefined &&
            !NON_USER_DEFINED_CLASS_NAMES.has(tstlClassName)) {
            this.classConstructors.set(tstlClassName, luaMap);
        }
        for (const [_key, value] of luaMap) {
            if ((0, types_1.isTable)(value)) {
                this.storeClassConstructorsFromObject(value);
            }
        }
    }
    /**
     * The save data manager will automatically load variables from disk at the appropriate times
     * (i.e. when a new run is started). Use this function to explicitly force the save data manager
     * to load all of its variables from disk immediately.
     *
     * Obviously, doing this will overwrite the current data, so using this function can potentially
     * result in lost state.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`.
     *
     * @public
     */
    saveDataManagerLoad() {
        (0, loadFromDisk_1.loadFromDisk)(this.mod, this.saveDataMap, this.classConstructors);
    }
    /**
     * The save data manager will automatically save variables to disk at the appropriate times (i.e.
     * when the run is exited). Use this function to explicitly force the save data manager to write
     * all of its variables to disk immediately.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`.
     *
     * @public
     */
    saveDataManagerSave() {
        (0, saveToDisk_1.saveToDisk)(this.mod, this.saveDataMap, this.saveDataConditionalFuncMap);
    }
    /**
     * Sets the global variable of "g" equal to all of the save data variables for this mod.
     *
     * This can make debugging easier, as you can access the variables from the game's debug console.
     * e.g. `l print(g.feature1.run.foo)`
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`.
     *
     * @public
     */
    saveDataManagerSetGlobal() {
        g = this.saveDataMap;
    }
    /**
     * By default, the save data manager will not be able to serialize/deserialize classes that are
     * nested inside of maps, sets, and arrays, because it does not have access to the corresponding
     * class constructor. If you want to use nested classes in this way, then use this function to
     * register the class constructor with the save data manager. Once registered, the save data
     * manager will automatically run the constructor when deserializing (in addition to copying over
     * the data fields).
     *
     * This function is variadic, which means you can pass as many classes as you want to register.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`.
     *
     * @public
     */
    saveDataManagerRegisterClass(...tstlClasses) {
        for (const tstlClass of tstlClasses) {
            const { name } = tstlClass;
            (0, utils_1.assertDefined)(
            // Since we are accepting untrusted user input, this might not be a real TSTL class.
            name, "Failed to register a class with the save data manager due to not being able to derive the name of the class.");
            this.classConstructors.set(name, tstlClass);
        }
    }
    /**
     * Removes a previously registered key from the save data manager. This is the opposite of the
     * "saveDataManager" method.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`.
     *
     * @public
     */
    saveDataManagerRemove(key) {
        if (!(0, types_1.isString)(key)) {
            error(`The save data manager requires that keys are strings. You tried to use a key of type: ${typeof key}`);
        }
        if (!this.saveDataMap.has(key)) {
            error(`The save data manager is not managing save data for a key of: ${key}`);
        }
        // Delete the save data from the map.
        this.saveDataMap.delete(key);
        this.saveDataDefaultsMap.delete(key);
        this.saveDataConditionalFuncMap.delete(key);
        this.saveDataGlowingHourGlassMap.delete(key);
    }
    /**
     * The save data manager will automatically reset variables at the appropriate times, like when a
     * player enters a new room. Use this function to explicitly force the save data manager to reset
     * a specific variable group.
     *
     * For example:
     *
     * ```ts
     * const v = {
     *   room: {
     *     foo: 123,
     *   },
     * };
     *
     * mod.saveDataManager("file1", v);
     *
     * // Then, later on, to explicit reset all of the "room" variables:
     * mod.saveDataManagerReset("file1", "room");
     * ```
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`.
     *
     * @public
     */
    saveDataManagerReset(key, childObjectKey) {
        if (!(0, types_1.isString)(key)) {
            error(`The save data manager requires that keys are strings. You tried to use a key of type: ${typeof key}`);
        }
        const saveData = this.saveDataMap.get(key);
        (0, utils_1.assertDefined)(saveData, `The save data manager is not managing save data for a key of: ${key}`);
        (0, restoreDefaults_1.restoreDefaultForFeatureKey)(this.saveDataDefaultsMap, key, saveData, childObjectKey);
    }
    /**
     * Helper function to check to see if the game is in the menu, as far as the save data manager is
     * concerned. This function will return true when the game is first opened until the
     * `POST_PLAYER_INIT` callback fires. It will also return true in between the `PRE_GAME_EXIT`
     * callback firing and the `POST_PLAYER_INIT` callback firing.
     *
     * This function is useful because the `POST_ENTITY_REMOVE` callback fires after the
     * `PRE_GAME_EXIT` callback. Thus, if save data needs to be updated from the `POST_ENTITY_REMOVE`
     * callback and the player is in the process of saving and quitting, the feature will have to
     * explicitly call the `saveDataManagerSave` function.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`.
     *
     * @public
     */
    saveDataManagerInMenu() {
        return !this.inARun;
    }
    /**
     * Helper function to see all of the mod features that are using the save data manager. Useful for
     * debugging if a certain mod feature is not getting its data saved correctly.
     *
     * @public
     */
    saveDataManagerLogSubscribers() {
        (0, log_1.log)("List of save data manager subscribers:");
        const keys = Object.keys(this.saveDataMap);
        keys.sort();
        for (const key of keys) {
            (0, log_1.log)(`- ${key}`);
        }
    }
}
exports.SaveDataManager = SaveDataManager;
__decorate([
    decorators_1.Exported
], SaveDataManager.prototype, "saveDataManager", null);
__decorate([
    decorators_1.Exported
], SaveDataManager.prototype, "saveDataManagerLoad", null);
__decorate([
    decorators_1.Exported
], SaveDataManager.prototype, "saveDataManagerSave", null);
__decorate([
    decorators_1.Exported
], SaveDataManager.prototype, "saveDataManagerSetGlobal", null);
__decorate([
    decorators_1.Exported
], SaveDataManager.prototype, "saveDataManagerRegisterClass", null);
__decorate([
    decorators_1.Exported
], SaveDataManager.prototype, "saveDataManagerRemove", null);
__decorate([
    decorators_1.Exported
], SaveDataManager.prototype, "saveDataManagerReset", null);
__decorate([
    decorators_1.Exported
], SaveDataManager.prototype, "saveDataManagerInMenu", null);
__decorate([
    decorators_1.Exported
], SaveDataManager.prototype, "saveDataManagerLogSubscribers", null);
