local ____lualib = require("lualib_bundle")
local Set = ____lualib.Set
local Map = ____lualib.Map
local ____exports = {}
local mergeSerializedArray, mergeSerializedTSTLObject, mergeSerializedTable
local ____constants = require("classes.features.other.saveDataManager.constants")
local SAVE_DATA_MANAGER_DEBUG = ____constants.SAVE_DATA_MANAGER_DEBUG
local ____SerializationBrand = require("enums.private.SerializationBrand")
local SerializationBrand = ____SerializationBrand.SerializationBrand
local ____SerializationType = require("enums.SerializationType")
local SerializationType = ____SerializationType.SerializationType
local ____serialization = require("serialization")
local isSerializationBrand = ____serialization.isSerializationBrand
local ____array = require("functions.array")
local isArray = ____array.isArray
local ____deepCopy = require("functions.deepCopy")
local deepCopy = ____deepCopy.deepCopy
local ____log = require("functions.log")
local log = ____log.log
local ____serialization = require("functions.serialization")
local deserializeIsaacAPIClass = ____serialization.deserializeIsaacAPIClass
local isSerializedIsaacAPIClass = ____serialization.isSerializedIsaacAPIClass
local ____table = require("functions.table")
local clearTable = ____table.clearTable
local iterateTableInOrder = ____table.iterateTableInOrder
local ____tstlClass = require("functions.tstlClass")
local isDefaultMap = ____tstlClass.isDefaultMap
local isTSTLMap = ____tstlClass.isTSTLMap
local isTSTLSet = ____tstlClass.isTSTLSet
local ____types = require("functions.types")
local isTable = ____types.isTable
local ____utils = require("functions.utils")
local getTraversalDescription = ____utils.getTraversalDescription
--- `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 ____exports.merge(self, oldObject, newTable, traversalDescription, classConstructors)
    if classConstructors == nil then
        classConstructors = {}
    end
    if SAVE_DATA_MANAGER_DEBUG then
        log("merge is traversing: " .. traversalDescription)
    end
    if not isTable(nil, oldObject) then
        error("The first argument given to the merge function is not a table.")
    end
    if not isTable(nil, newTable) then
        error("The second argument given to the merge function is not a table.")
    end
    if isArray(nil, oldObject) and isArray(nil, newTable) then
        mergeSerializedArray(
            nil,
            oldObject,
            newTable,
            traversalDescription,
            classConstructors
        )
        return
    end
    if isTSTLMap(nil, oldObject) or isTSTLSet(nil, oldObject) or isDefaultMap(nil, oldObject) then
        mergeSerializedTSTLObject(
            nil,
            oldObject,
            newTable,
            traversalDescription,
            classConstructors
        )
    else
        mergeSerializedTable(
            nil,
            oldObject,
            newTable,
            traversalDescription,
            classConstructors
        )
    end
end
function mergeSerializedArray(self, oldArray, newArray, traversalDescription, classConstructors)
    if SAVE_DATA_MANAGER_DEBUG then
        log("merge encountered an array: " .. traversalDescription)
    end
    clearTable(nil, oldArray)
    iterateTableInOrder(
        nil,
        newArray,
        function(____, key, value)
            local deserializedValue = deepCopy(
                nil,
                value,
                SerializationType.DESERIALIZE,
                traversalDescription,
                classConstructors
            )
            oldArray[key] = deserializedValue
        end,
        SAVE_DATA_MANAGER_DEBUG
    )
end
function mergeSerializedTSTLObject(self, oldObject, newTable, traversalDescription, classConstructors)
    if SAVE_DATA_MANAGER_DEBUG then
        log("merge encountered a TSTL object: " .. traversalDescription)
    end
    oldObject:clear()
    local convertStringKeysToNumbers = newTable[SerializationBrand.OBJECT_WITH_NUMBER_KEYS] ~= nil
    iterateTableInOrder(
        nil,
        newTable,
        function(____, key, value)
            if isSerializationBrand(nil, key) then
                return
            end
            local keyToUse = key
            if convertStringKeysToNumbers then
                local numberKey = tonumber(key)
                if numberKey == nil then
                    return
                end
                keyToUse = numberKey
            end
            if isTSTLMap(nil, oldObject) or isDefaultMap(nil, oldObject) then
                local deserializedValue = deepCopy(
                    nil,
                    value,
                    SerializationType.DESERIALIZE,
                    traversalDescription,
                    classConstructors
                )
                oldObject:set(keyToUse, deserializedValue)
            elseif isTSTLSet(nil, oldObject) then
                oldObject:add(keyToUse)
            end
        end,
        SAVE_DATA_MANAGER_DEBUG
    )
end
function mergeSerializedTable(self, oldTable, newTable, traversalDescription, classConstructors)
    if SAVE_DATA_MANAGER_DEBUG then
        log("merge encountered a Lua table: " .. traversalDescription)
    end
    iterateTableInOrder(
        nil,
        newTable,
        function(____, key, value)
            if SAVE_DATA_MANAGER_DEBUG then
                local valueToPrint = value == "" and "(empty string)" or tostring(value)
                log((("merge is merging: " .. traversalDescription) .. " --> ") .. valueToPrint)
            end
            if isSerializationBrand(nil, key) then
                return
            end
            if isSerializedIsaacAPIClass(nil, value) then
                if SAVE_DATA_MANAGER_DEBUG then
                    log("merge found a serialized Isaac API class.")
                end
                local deserializedObject = deserializeIsaacAPIClass(nil, value)
                oldTable[key] = deserializedObject
                return
            end
            if isTable(nil, value) then
                local oldValue = oldTable[key]
                if not isTable(nil, oldValue) then
                    oldValue = {}
                    oldTable[key] = oldValue
                end
                traversalDescription = getTraversalDescription(nil, key, traversalDescription)
                ____exports.merge(
                    nil,
                    oldValue,
                    value,
                    traversalDescription,
                    classConstructors
                )
            else
                oldTable[key] = value
            end
        end,
        SAVE_DATA_MANAGER_DEBUG
    )
end
return ____exports
