"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.deepCopy = void 0;
const DefaultMap_1 = require("../classes/DefaultMap");
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 isaacAPIClass_1 = require("./isaacAPIClass");
const log_1 = require("./log");
const serialization_2 = require("./serialization");
const sort_1 = require("./sort");
const tstlClass_1 = require("./tstlClass");
const types_1 = require("./types");
const utils_1 = require("./utils");
function deepCopy(value, serializationType = SerializationType_1.SerializationType.NONE, traversalDescription = "", classConstructors = new LuaMap(), insideMap = false) {
    if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
        let logString = `deepCopy is operating on: ${traversalDescription}`;
        if (serializationType === SerializationType_1.SerializationType.SERIALIZE) {
            logString += " (serializing)";
        }
        else if (serializationType === SerializationType_1.SerializationType.DESERIALIZE) {
            logString += " (deserializing)";
        }
        logString += `: ${value}`;
        (0, log_1.log)(logString);
    }
    const valueType = type(value);
    switch (valueType) {
        // First, handling the trivial case of primitives.
        case "nil":
        case "boolean":
        case "number":
        case "string": {
            return value;
        }
        // Second, handle values that cannot be serialized.
        case "function":
        case "thread": {
            if (serializationType === SerializationType_1.SerializationType.SERIALIZE) {
                error(`The deep copy function does not support serialization of "${traversalDescription}", since it is type: ${valueType}`);
            }
            if (serializationType === SerializationType_1.SerializationType.DESERIALIZE) {
                error(`The deep copy function does not support deserialization of "${traversalDescription}", since it is type: ${valueType}`);
            }
            // We cannot copy this, so simply return the reference.
            return value;
        }
        case "table": {
            const luaMap = value;
            return deepCopyTable(luaMap, serializationType, traversalDescription, classConstructors, insideMap);
        }
        case "userdata": {
            return deepCopyUserdata(value, serializationType, traversalDescription);
        }
    }
}
exports.deepCopy = deepCopy;
function deepCopyTable(luaMap, serializationType, traversalDescription, classConstructors, insideMap) {
    // First, handle the cases of TSTL classes or serialized TSTL classes.
    if ((0, tstlClass_1.isDefaultMap)(luaMap) || luaMap.has(SerializationBrand_1.SerializationBrand.DEFAULT_MAP)) {
        return deepCopyDefaultMap(luaMap, serializationType, traversalDescription, classConstructors, insideMap);
    }
    if ((0, tstlClass_1.isTSTLMap)(luaMap) || luaMap.has(SerializationBrand_1.SerializationBrand.MAP)) {
        return deepCopyMap(luaMap, serializationType, traversalDescription, classConstructors, insideMap);
    }
    if ((0, tstlClass_1.isTSTLSet)(luaMap) || luaMap.has(SerializationBrand_1.SerializationBrand.SET)) {
        return deepCopySet(luaMap, serializationType, traversalDescription, classConstructors, insideMap);
    }
    const className = (0, tstlClass_1.getTSTLClassName)(luaMap);
    if (className === "WeakMap") {
        error(`The deep copy function does not support copying the "WeakMap" class for: ${traversalDescription}`);
    }
    if (className === "WeakSet") {
        error(`The deep copy function does not support copying the "WeakSet" class for: ${traversalDescription}`);
    }
    if (className !== undefined || luaMap.has(SerializationBrand_1.SerializationBrand.TSTL_CLASS)) {
        return deepCopyTSTLClass(luaMap, serializationType, traversalDescription, classConstructors, insideMap);
    }
    // This is not a TSTL Map/Set/class. If it has a metatable, abort.
    checkMetatable(luaMap, traversalDescription);
    // Handle the special case of serialized Isaac API classes.
    if ((0, serialization_2.isSerializedIsaacAPIClass)(luaMap) &&
        serializationType === SerializationType_1.SerializationType.DESERIALIZE) {
        return (0, serialization_2.deserializeIsaacAPIClass)(luaMap);
    }
    // Handle the special case of an array.
    if ((0, array_1.isArray)(luaMap)) {
        return deepCopyArray(luaMap, serializationType, traversalDescription, classConstructors, insideMap);
    }
    // Base case: copy a normal Lua table
    return deepCopyNormalLuaTable(luaMap, serializationType, traversalDescription, classConstructors, insideMap);
}
function deepCopyDefaultMap(defaultMap, serializationType, traversalDescription, classConstructors, insideMap) {
    if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
        (0, log_1.log)("deepCopy is copying a DefaultMap.");
    }
    const constructorArg = (0, tstlClass_1.isDefaultMap)(defaultMap)
        ? defaultMap.getConstructorArg()
        : undefined; // The undefined case is handled explicitly in the "getNewDefaultMap" function.
    // First, handle the special case of serializing a DefaultMap instantiated with a factory
    // function. If this is the case, then we cannot serialize it (because there is no way to
    // serialize a function).
    if (serializationType === SerializationType_1.SerializationType.SERIALIZE &&
        !(0, types_1.isPrimitive)(constructorArg)) {
        if (insideMap) {
            // The case of a DefaultMap within another map is complicated. Unlike a DefaultMap attached to
            // a "normal" object, the `merge` function will have no reference to the factory function that
            // was used to instantiate it. Thus, there is no way to copy this object. In this case, we
            // throw a run-time error to immediately alert the end-user that their data structure is
            // invalid.
            error("Failed to deep copy a DefaultMap because it was instantiated with a factory function and was also inside of an array, map, or set. For more information, see: https://isaacscript.github.io/main/gotchas#failed-to-deep-copy-a-defaultmap");
        }
        else {
            // In most cases, the DefaultMap will be attached to a normal table element. In this case, if
            // we serialize it as a normal `Map`, then everything will work out fine, because the `merge`
            // function only needs to copy the values (and not instantiate the object itself).
            return deepCopyMap(defaultMap, serializationType, traversalDescription, classConstructors, insideMap);
        }
    }
    const newDefaultMap = getNewDefaultMap(defaultMap, serializationType, traversalDescription, constructorArg);
    insideMap = true;
    const { entries, convertedNumberKeysToStrings } = getCopiedEntries(defaultMap, serializationType, traversalDescription, classConstructors, insideMap);
    if (convertedNumberKeysToStrings) {
        // Differentiating between the two types looks superfluous but is necessary for TSTL to produce
        // the proper set method call.
        if ((0, tstlClass_1.isDefaultMap)(newDefaultMap)) {
            newDefaultMap.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, "");
        }
        else {
            newDefaultMap.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, "");
        }
    }
    for (const [key, value] of entries) {
        // Differentiating between the two types looks superfluous but is necessary for TSTL to produce
        // the proper set method call.
        if ((0, tstlClass_1.isDefaultMap)(newDefaultMap)) {
            newDefaultMap.set(key, value);
        }
        else {
            newDefaultMap.set(key, value);
        }
    }
    insideMap = false;
    return newDefaultMap;
}
/**
 * The new copied default map with either be a TSTL `DefaultMap` class or a Lua table, depending on
 * whether we are serializing.
 */
function getNewDefaultMap(defaultMap, serializationType, traversalDescription, constructorArg) {
    switch (serializationType) {
        case SerializationType_1.SerializationType.NONE: {
            // eslint-disable-next-line isaacscript/no-invalid-default-map
            return new DefaultMap_1.DefaultMap(constructorArg);
        }
        case SerializationType_1.SerializationType.SERIALIZE: {
            // Since we are serializing, the new object will be a Lua table. (At this point, we already
            // handled the special case of a DefaultMap instantiated with a factory function.)
            const newDefaultMap = new LuaMap();
            newDefaultMap.set(SerializationBrand_1.SerializationBrand.DEFAULT_MAP, "");
            newDefaultMap.set(SerializationBrand_1.SerializationBrand.DEFAULT_MAP_VALUE, constructorArg);
            return newDefaultMap;
        }
        case SerializationType_1.SerializationType.DESERIALIZE: {
            if ((0, tstlClass_1.isDefaultMap)(defaultMap)) {
                error(`Failed to deserialize a default map of "${traversalDescription}", since it was not a Lua table.`);
            }
            const defaultMapValue = defaultMap.get(SerializationBrand_1.SerializationBrand.DEFAULT_MAP_VALUE);
            (0, utils_1.assertDefined)(defaultMapValue, `Failed to deserialize a default map of "${traversalDescription}", since there was no serialization brand of: ${SerializationBrand_1.SerializationBrand.DEFAULT_MAP_VALUE}`);
            // eslint-disable-next-line isaacscript/no-invalid-default-map
            return new DefaultMap_1.DefaultMap(defaultMapValue);
        }
    }
}
function deepCopyMap(map, serializationType, traversalDescription, classConstructors, insideMap) {
    if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
        (0, log_1.log)("deepCopy is copying a Map.");
    }
    let newMap;
    if (serializationType === SerializationType_1.SerializationType.SERIALIZE) {
        // Since we are serializing, the new object will be a Lua table.
        newMap = new LuaMap();
        newMap.set(SerializationBrand_1.SerializationBrand.MAP, "");
    }
    else {
        newMap = new Map();
    }
    insideMap = true;
    const { entries, convertedNumberKeysToStrings } = getCopiedEntries(map, serializationType, traversalDescription, classConstructors, insideMap);
    if (convertedNumberKeysToStrings) {
        // Differentiating between the two types looks superfluous but is necessary for TSTL to produce
        // the proper set method call.
        if ((0, tstlClass_1.isTSTLMap)(newMap)) {
            newMap.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, "");
        }
        else {
            newMap.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, "");
        }
    }
    for (const [key, value] of entries) {
        // Differentiating between the two types looks superfluous but is necessary for TSTL to produce
        // the proper set method call.
        if ((0, tstlClass_1.isTSTLMap)(newMap)) {
            newMap.set(key, value);
        }
        else {
            newMap.set(key, value);
        }
    }
    insideMap = false;
    return newMap;
}
function deepCopySet(set, serializationType, traversalDescription, classConstructors, insideMap) {
    if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
        (0, log_1.log)("deepCopy is copying a Set.");
    }
    let newSet;
    if (serializationType === SerializationType_1.SerializationType.SERIALIZE) {
        // For serialization purposes, we represent a `Set` as a table with keys that match the
        // keys/values in the Set and values of an empty string.
        newSet = new LuaMap();
        newSet.set(SerializationBrand_1.SerializationBrand.SET, "");
    }
    else {
        newSet = new Set();
    }
    const { entries, convertedNumberKeysToStrings } = getCopiedEntries(set, serializationType, traversalDescription, classConstructors, insideMap);
    if (convertedNumberKeysToStrings) {
        // Differentiating between the two types looks superfluous but is necessary for TSTL to produce
        // the proper set method call.
        if ((0, tstlClass_1.isTSTLSet)(newSet)) {
            // We should never be serializing an object of type `Set`.
            error("The deep copy function cannot convert number keys to strings for a Set.");
        }
        else {
            newSet.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, "");
        }
    }
    for (const [key] of entries) {
        // Differentiating between the two types looks superfluous but is necessary for TSTL to produce
        // the proper set method call.
        if ((0, tstlClass_1.isTSTLSet)(newSet)) {
            newSet.add(key);
        }
        else {
            newSet.set(key, "");
        }
    }
    return newSet;
}
function deepCopyTSTLClass(tstlClass, serializationType, traversalDescription, classConstructors, insideMap) {
    if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
        (0, log_1.log)("deepCopy is copying a TSTL class.");
    }
    let newClass;
    switch (serializationType) {
        case SerializationType_1.SerializationType.NONE: {
            // We can use the class constructor from the old class.
            newClass = (0, tstlClass_1.newTSTLClass)(tstlClass);
            break;
        }
        case SerializationType_1.SerializationType.SERIALIZE: {
            newClass = new LuaMap();
            // We brand it with the name of the class so that we can run the corresponding constructor
            // during deserialization.
            const tstlClassName = (0, tstlClass_1.getTSTLClassName)(tstlClass);
            if (tstlClassName !== undefined) {
                newClass.set(SerializationBrand_1.SerializationBrand.TSTL_CLASS, tstlClassName);
            }
            break;
        }
        case SerializationType_1.SerializationType.DESERIALIZE: {
            const tstlClassName = tstlClass.get(SerializationBrand_1.SerializationBrand.TSTL_CLASS);
            (0, utils_1.assertDefined)(tstlClassName, "Failed to deserialize a TSTL class since the brand did not contain the class name.");
            const classConstructor = classConstructors.get(tstlClassName);
            (0, utils_1.assertDefined)(classConstructor, `Failed to deserialize a TSTL class since there was no constructor registered for a class name of "${tstlClassName}". If this mod is using the save data manager, it must register the class constructor with the "saveDataManagerRegisterClass" method.`);
            // eslint-disable-next-line new-cap
            newClass = new classConstructor();
            break;
        }
    }
    const { entries, convertedNumberKeysToStrings } = getCopiedEntries(tstlClass, serializationType, traversalDescription, classConstructors, insideMap);
    if (convertedNumberKeysToStrings) {
        newClass.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, "");
    }
    for (const [key, value] of entries) {
        newClass.set(key, value);
    }
    return newClass;
}
function deepCopyArray(array, serializationType, traversalDescription, classConstructors, insideMap) {
    if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
        (0, log_1.log)("deepCopy is copying an array.");
    }
    const newArray = [];
    for (const value of array) {
        const newValue = deepCopy(value, serializationType, traversalDescription, classConstructors, insideMap);
        newArray.push(newValue);
    }
    return newArray;
}
function deepCopyNormalLuaTable(luaMap, serializationType, traversalDescription, classConstructors, insideMap) {
    if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
        (0, log_1.log)("deepCopy is copying a normal Lua table.");
    }
    const newTable = new LuaMap();
    const { entries, convertedNumberKeysToStrings } = getCopiedEntries(luaMap, serializationType, traversalDescription, classConstructors, insideMap);
    if (convertedNumberKeysToStrings) {
        newTable.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, "");
    }
    for (const [key, value] of entries) {
        newTable.set(key, value);
    }
    return newTable;
}
/**
 * Recursively clones the object's entries, automatically converting number keys to strings, if
 * necessary.
 *
 * This should work on objects/tables, maps, sets, default maps, and classes.
 */
function getCopiedEntries(object, serializationType, traversalDescription, classConstructors, insideMap) {
    // First, shallow copy the entries. We cannot use "pairs" to iterate over a `Map` or `Set`. We
    // cannot use "[...pairs(object)]", as it results in a run-time error.
    const entries = [];
    if ((0, tstlClass_1.isTSTLMap)(object) || (0, tstlClass_1.isTSTLSet)(object) || (0, tstlClass_1.isDefaultMap)(object)) {
        for (const [key, value] of object.entries()) {
            entries.push([key, value]);
        }
    }
    else {
        for (const [key, value] of pairs(object)) {
            entries.push([key, value]);
        }
    }
    if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
        entries.sort(sort_1.sortTwoDimensionalArray);
    }
    // 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 = serializationType === SerializationType_1.SerializationType.DESERIALIZE &&
        entries.some(([key]) => key === (0, types_1.asString)(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS));
    const hasNumberKeys = entries.some(([key]) => (0, types_1.isNumber)(key));
    const convertNumberKeysToStrings = serializationType === SerializationType_1.SerializationType.SERIALIZE && hasNumberKeys;
    // Second, deep copy the entries.
    const copiedEntries = [];
    for (const [key, value] of entries) {
        // When deserializing, we do not need to copy the serialization brands that are used to denote
        // the object type.
        if ((0, serialization_1.isSerializationBrand)(key)) {
            continue;
        }
        traversalDescription = (0, utils_1.getTraversalDescription)(key, traversalDescription);
        const newValue = deepCopy(value, serializationType, traversalDescription, classConstructors, insideMap);
        let keyToUse = key;
        if (convertStringKeysToNumbers) {
            const numberKey = tonumber(key);
            if (numberKey !== undefined) {
                keyToUse = numberKey;
            }
        }
        if (convertNumberKeysToStrings) {
            keyToUse = tostring(key);
        }
        copiedEntries.push([keyToUse, newValue]);
    }
    return {
        entries: copiedEntries,
        convertedNumberKeysToStrings: convertNumberKeysToStrings,
    };
}
/**
 * Lua tables can have metatables, which make writing a generic deep cloner impossible. The deep
 * copy function will refuse to copy a table type that has a metatable, outside of specifically
 * supported TSTL objects.
 */
function checkMetatable(luaMap, traversalDescription) {
    const metatable = getmetatable(luaMap);
    if (metatable === undefined) {
        return;
    }
    const tableDescription = traversalDescription === ""
        ? "the table to copy"
        : `"${traversalDescription}"`;
    error(`The deepCopy function detected that ${tableDescription} has a metatable. Copying tables with metatables is not supported, unless they are explicitly handled by the save data manager. (e.g. TypeScriptToLua Maps, TypeScriptToLua Sets, etc.)`);
}
/** Isaac API classes are of type "userdata". End-user code cannot create userdata. */
function deepCopyUserdata(value, serializationType, traversalDescription) {
    if (!(0, serialization_2.isCopyableIsaacAPIClass)(value)) {
        const className = (0, isaacAPIClass_1.getIsaacAPIClassName)(value) ?? "Unknown";
        error(`The deep copy function does not support serializing "${traversalDescription}", since it is an Isaac API class of type: ${className}`);
    }
    switch (serializationType) {
        case SerializationType_1.SerializationType.NONE: {
            return (0, serialization_2.copyIsaacAPIClass)(value);
        }
        case SerializationType_1.SerializationType.SERIALIZE: {
            return (0, serialization_2.serializeIsaacAPIClass)(value);
        }
        case SerializationType_1.SerializationType.DESERIALIZE: {
            return error(`The deep copy function can not deserialize "${traversalDescription}", since it is userdata.`);
        }
    }
}
