"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.merge = void 0;
const constants_1 = require("../classes/features/other/saveDataManager/constants");
const SerializationBrand_1 = require("../enums/private/SerializationBrand");
const SerializationType_1 = require("../enums/SerializationType");
const serialization_1 = require("../serialization");
const array_1 = require("./array");
const deepCopy_1 = require("./deepCopy");
const log_1 = require("./log");
const serialization_2 = require("./serialization");
const table_1 = require("./table");
const tstlClass_1 = require("./tstlClass");
const types_1 = require("./types");
const utils_1 = require("./utils");
/**
 * `merge` takes the values from a new table and recursively merges them into an old object (while
 * performing appropriate deserialization).
 *
 * This function is used to merge incoming data from the "save#.dat" file into a mod's variables.
 * Merging is useful instead of blowing away a table entirely because mod code often relies on the
 * local table/object references.
 *
 * This function always assumes that the new table is serialized data and will attempt to perform
 * deserialization on the objects within. In other words, unlike the `deepCopy` function, the
 * `merge` function will always operates in the mode of `SerializationType.DESERIALIZE`. For the
 * types of objects that will be deserialized, see the documentation for the `deepCopy` function.
 *
 * This function does not iterate over the old object, like you would naively expect. This is
 * because it is common for a variable to have a type of `something | null`. If this is the case,
 * the key would not appear when iterating over the old object (because a value of null transpiles
 * to nil, which means the table key does not exist). Thus, we must instead iterate over the new
 * object and copy the values backwards. The consequence of this is that `merge` can copy over old
 * variables that are no longer used in the code, or copy over old variables of a different type,
 * which can cause run-time errors. In such cases, users will have to manually delete their save
 * data.
 *
 * @param oldObject The old object to merge the values into. This can be either a Lua table, a TSTL
 *                  map, or a TSTL set.
 * @param newTable The new table to merge the values from. This must be a Lua table that represents
 *                 serialized data. In other words, it should be created with the `deepCopy`
 *                 function using `SerializationType.SERIALIZE`.
 * @param traversalDescription Used to track the current key that we are operating on for debugging
 *                             purposes. Use a name that corresponds to the name of the merging
 *                             table.
 * @param classConstructors Optional. A Lua table that maps the name of a user-defined TSTL class to
 *                          its corresponding constructor. If the `deepCopy` function finds any
 *                          user-defined TSTL classes when recursively iterating through the given
 *                          object, it will use this map to instantiate a new class. Default is an
 *                          empty Lua table.
 */
function merge(oldObject, newTable, traversalDescription, classConstructors = new LuaMap()) {
    if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
        (0, log_1.log)(`merge is traversing: ${traversalDescription}`);
    }
    if (!(0, types_1.isTable)(oldObject)) {
        error("The first argument given to the merge function is not a table.");
    }
    if (!(0, types_1.isTable)(newTable)) {
        error("The second argument given to the merge function is not a table.");
    }
    // First, handle the special case of an array with a shallow copy.
    if ((0, array_1.isArray)(oldObject) && (0, array_1.isArray)(newTable)) {
        mergeSerializedArray(oldObject, newTable, traversalDescription, classConstructors);
        return;
    }
    // Depending on whether we are working on a Lua table or a TypeScriptToLua object, we need to
    // iterate in a specific way.
    if ((0, tstlClass_1.isTSTLMap)(oldObject) || (0, tstlClass_1.isTSTLSet)(oldObject) || (0, tstlClass_1.isDefaultMap)(oldObject)) {
        mergeSerializedTSTLObject(oldObject, newTable, traversalDescription, classConstructors);
    }
    else {
        mergeSerializedTable(oldObject, newTable, traversalDescription, classConstructors);
    }
}
exports.merge = merge;
function mergeSerializedArray(oldArray, newArray, traversalDescription, classConstructors) {
    if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
        (0, log_1.log)(`merge encountered an array: ${traversalDescription}`);
    }
    // Assume that we should blow away all array values with whatever is present in the incoming
    // array.
    (0, table_1.clearTable)(oldArray);
    (0, table_1.iterateTableInOrder)(newArray, (key, value) => {
        const deserializedValue = (0, deepCopy_1.deepCopy)(value, SerializationType_1.SerializationType.DESERIALIZE, traversalDescription, classConstructors);
        oldArray.set(key, deserializedValue);
    }, constants_1.SAVE_DATA_MANAGER_DEBUG);
}
function mergeSerializedTSTLObject(
// eslint-disable-next-line isaacscript/prefer-readonly-parameter-types
oldObject, newTable, traversalDescription, classConstructors) {
    if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
        (0, log_1.log)(`merge encountered a TSTL object: ${traversalDescription}`);
    }
    // We blow away the old object and recursively copy over all of the incoming values.
    oldObject.clear();
    // During serialization, we brand some Lua tables with a special identifier to signify that it has
    // keys that should be deserialized to numbers.
    const convertStringKeysToNumbers = newTable.has(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS);
    (0, table_1.iterateTableInOrder)(newTable, (key, value) => {
        if ((0, serialization_1.isSerializationBrand)(key)) {
            return;
        }
        let keyToUse = key;
        if (convertStringKeysToNumbers) {
            const numberKey = tonumber(key);
            if (numberKey === undefined) {
                return;
            }
            keyToUse = numberKey;
        }
        if ((0, tstlClass_1.isTSTLMap)(oldObject) || (0, tstlClass_1.isDefaultMap)(oldObject)) {
            const deserializedValue = (0, deepCopy_1.deepCopy)(value, SerializationType_1.SerializationType.DESERIALIZE, traversalDescription, classConstructors);
            oldObject.set(keyToUse, deserializedValue);
        }
        else if ((0, tstlClass_1.isTSTLSet)(oldObject)) {
            oldObject.add(keyToUse);
        }
    }, constants_1.SAVE_DATA_MANAGER_DEBUG);
}
function mergeSerializedTable(oldTable, newTable, traversalDescription, classConstructors) {
    if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
        (0, log_1.log)(`merge encountered a Lua table: ${traversalDescription}`);
    }
    (0, table_1.iterateTableInOrder)(newTable, (key, value) => {
        if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
            const valueToPrint = value === "" ? "(empty string)" : `${value}`;
            (0, log_1.log)(`merge is merging: ${traversalDescription} --> ${valueToPrint}`);
        }
        if ((0, serialization_1.isSerializationBrand)(key)) {
            return;
        }
        // Handle the special case of serialized Isaac API classes.
        if ((0, serialization_2.isSerializedIsaacAPIClass)(value)) {
            if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
                (0, log_1.log)("merge found a serialized Isaac API class.");
            }
            const deserializedObject = (0, serialization_2.deserializeIsaacAPIClass)(value);
            oldTable.set(key, deserializedObject);
            return;
        }
        if ((0, types_1.isTable)(value)) {
            let oldValue = oldTable.get(key);
            if (!(0, types_1.isTable)(oldValue)) {
                // The child table does not exist on the old table. However, we still need to copy over
                // the new table, because we need to handle data types like `Foo | null`. Thus, set up a
                // blank sub-table on the old table, and continue to recursively merge.
                oldValue = new LuaMap();
                oldTable.set(key, oldValue);
            }
            traversalDescription = (0, utils_1.getTraversalDescription)(key, traversalDescription);
            merge(oldValue, value, traversalDescription, classConstructors);
        }
        else {
            // Base case: copy the value
            oldTable.set(key, value);
        }
    }, constants_1.SAVE_DATA_MANAGER_DEBUG);
}
