--[[
    UNREAL ENGINE BACK END - MOST USERS SHOULD NOT EDIT THIS FILE

    This is the only module that changes Dragonwilds loot tables. It matches a
    configured bag ID to loaded BP_Consumables_ItemEmitter objects, keeps only
    drops whose item references resolved safely, and rebuilds each matching
    Unreal TArray from a correctly typed original entry.
]]

local Logger = require("logger")

local LootBuilder = {}


local function mark(checkpoint, message)
    if type(checkpoint) == "function" then
        checkpoint("Builder: " .. message)
    end
end


local function sortedBagIds(config)
    local bagIds = {}

    if type(config) ~= "table" then
        return bagIds
    end

    for bagId in pairs(config) do
        bagIds[#bagIds + 1] = bagId
    end

    table.sort(bagIds)
    return bagIds
end


local function resolvedLoot(bagId, configuredLoot, itemReferences)
    local ready = {}
    local skipped = 0

    for _, loot in ipairs(configuredLoot) do
        local reference = itemReferences[loot.Item]

        if reference then
            ready[#ready + 1] = {
                Config = loot,
                Reference = reference,
            }
        else
            skipped = skipped + 1
            Logger.Warning("BUILDER", bagId .. " ignored unresolved drop "
                .. tostring(loot.Item))
        end
    end

    return ready, skipped
end


local function collectEmitters(checkpoint)
    if type(FindAllOf) ~= "function" then
        Logger.Error("BUILDER", "UE4SS FindAllOf() is unavailable")
        return nil
    end

    mark(checkpoint, "BEFORE FindAllOf(BP_Consumables_ItemEmitter_C)")
    local found, sourcesOrError = pcall(
        FindAllOf,
        "BP_Consumables_ItemEmitter_C"
    )
    mark(checkpoint, "AFTER FindAllOf(BP_Consumables_ItemEmitter_C)")

    if not found then
        Logger.Error("BUILDER", "Could not enumerate loot emitters: "
            .. tostring(sourcesOrError))
        return nil
    end

    if type(sourcesOrError) ~= "table" then
        Logger.Warning("BUILDER", "No loot emitters found; configuration not applied")
        return {}
    end

    local emitters = {}
    for _, source in pairs(sourcesOrError) do
        local inspected, valid, fullName = pcall(function()
            if source == nil or not source:IsValid() then
                return false
            end

            return true, tostring(source:GetFullName())
        end)

        if not inspected then
            Logger.Warning("BUILDER", "Unreadable loot emitter ignored: "
                .. tostring(valid))
        elseif not valid then
            Logger.Warning("BUILDER", "Invalid loot emitter ignored")
        elseif not fullName or fullName == "" then
            Logger.Warning("BUILDER", "Unnamed loot emitter ignored")
        else
            emitters[#emitters + 1] = {
                Object = source,
                FullName = fullName,
            }
        end
    end

    return emitters
end


local function readLootArray(bagId, emitter)
    local read, itemsOrError = pcall(function()
        return emitter.Object["Items to Drop"]
    end)

    if not read then
        Logger.Warning("BUILDER", "Could not read Items to Drop for " .. bagId
            .. "; emitter ignored (" .. tostring(itemsOrError) .. ")")
        return nil
    end

    if itemsOrError == nil then
        Logger.Warning("BUILDER", bagId
            .. " emitter has no Items to Drop array; emitter ignored")
        return nil
    end

    return itemsOrError
end


local function readArrayCount(bagId, items)
    local read, countOrError = pcall(function()
        return items:GetArrayNum()
    end)

    if not read then
        Logger.Warning("BUILDER", "Could not read loot-array size for " .. bagId
            .. ": " .. tostring(countOrError))
        return nil
    end

    if type(countOrError) ~= "number"
        or countOrError < 0
        or countOrError ~= math.floor(countOrError) then
        Logger.Warning("BUILDER", "Invalid loot-array size for " .. bagId
            .. ": " .. tostring(countOrError))
        return nil
    end

    return countOrError
end


local function readTemplate(bagId, items)
    local read, templateOrError = pcall(function()
        return items[1]
    end)

    if not read or templateOrError == nil then
        Logger.Warning("BUILDER", "Cannot rebuild " .. bagId
            .. ": original loot table has no usable template entry")
        return nil
    end

    -- Read the exact four fields that this mod writes before clearing the
    -- array. If this is not the expected Dragonwilds struct, leave it alone.
    local checked, checkError = pcall(function()
        local _ = templateOrError.ItemDataClass
        _ = templateOrError.MinToDrop
        _ = templateOrError.MaxToDrop
        _ = templateOrError.ProbabilityOfDrop
    end)

    if not checked then
        Logger.Warning("BUILDER", "Unexpected loot-entry type for " .. bagId
            .. "; emitter ignored (" .. tostring(checkError) .. ")")
        return nil
    end

    return templateOrError
end


local function emptyLootArray(bagId, items, checkpoint)
    mark(checkpoint, bagId .. " BEFORE Empty()")
    local emptied, emptyError = pcall(function()
        items:Empty()
    end)

    if not emptied then
        Logger.Error("BUILDER", "Could not clear loot array for " .. bagId
            .. ": " .. tostring(emptyError))
        return false
    end

    mark(checkpoint, bagId .. " AFTER Empty()")
    return true
end


local function appendLootEntry(bagId, position, items, template, prepared, checkpoint)
    local itemId = prepared.Config.Item
    mark(checkpoint, bagId .. " ENTRY " .. tostring(position)
        .. " BEFORE template copy: " .. itemId)

    local added, addError = pcall(function()
        local newIndex = items:GetArrayNum() + 1

        -- Copying the original entry first gives Unreal a correctly typed
        -- struct. Only the four supported fields are then replaced.
        items[newIndex] = template
        local entry = items[newIndex]

        if entry == nil then
            error("template copy returned no new entry")
        end

        entry.ItemDataClass = prepared.Reference
        entry.MinToDrop = prepared.Config.Min
        entry.MaxToDrop = prepared.Config.Max
        entry.ProbabilityOfDrop = prepared.Config.DropRate
    end)

    if not added then
        Logger.Error("BUILDER", "Could not add " .. itemId .. " to " .. bagId
            .. ": " .. tostring(addError))
        return false
    end

    mark(checkpoint, bagId .. " ENTRY " .. tostring(position)
        .. " COMPLETE: " .. itemId)
    return true
end


local function rebuildLootArray(bagId, items, ready, checkpoint)
    local originalCount = readArrayCount(bagId, items)

    if originalCount == nil then
        return false, 0
    end

    -- An explicitly empty Loot table intentionally clears the bag and needs no
    -- template. A non-empty rebuild must begin with the game's typed struct.
    if #ready == 0 then
        if originalCount == 0 then
            return true, 0
        end

        if emptyLootArray(bagId, items, checkpoint) then
            return true, 0
        end

        return false, 0
    end

    if originalCount == 0 then
        Logger.Warning("BUILDER", "Cannot rebuild " .. bagId
            .. ": original loot table is empty")
        return false, 0
    end

    mark(checkpoint, bagId .. " BEFORE reading template")
    local template = readTemplate(bagId, items)

    if not template then
        mark(checkpoint, bagId .. " FAILED reading template")
        return false, 0
    end

    if not emptyLootArray(bagId, items, checkpoint) then
        return false, 0
    end

    local addedCount = 0
    for position, prepared in ipairs(ready) do
        if appendLootEntry(
            bagId,
            position,
            items,
            template,
            prepared,
            checkpoint
        ) then
            addedCount = addedCount + 1
        else
            Logger.Error("BUILDER", bagId .. " stopped after "
                .. tostring(addedCount) .. " of " .. tostring(#ready)
                .. " drop(s); inspect the error above")
            return false, addedCount
        end
    end

    return true, addedCount
end


function LootBuilder.ApplyConfig(config, itemReferences, checkpoint)
    local summary = {
        ConfiguredBags = 0,
        ChangedBags = 0,
        MatchedEmitters = 0,
        ChangedEmitters = 0,
        SkippedDrops = 0,
    }

    local bagIds = sortedBagIds(config)
    summary.ConfiguredBags = #bagIds

    if #bagIds == 0 then
        Logger.Info("No valid bag configurations to apply")
        return summary
    end

    local emitters = collectEmitters(checkpoint)
    if not emitters then
        return summary
    end

    for _, bagId in ipairs(bagIds) do
        local configuredLoot = config[bagId].Loot
        local ready, skipped = resolvedLoot(
            bagId,
            configuredLoot,
            itemReferences
        )
        summary.SkippedDrops = summary.SkippedDrops + skipped

        -- A deliberately empty list clears a bag. A non-empty list whose every
        -- item failed resolution leaves the original bag untouched.
        if #configuredLoot > 0 and #ready == 0 then
            Logger.Warning("BUILDER", bagId
                .. " has no usable resolved drops; original loot preserved")
        else
            local matchCount = 0
            local changedCount = 0

            for _, emitter in ipairs(emitters) do
                if emitter.FullName:find(bagId, 1, true) then
                    matchCount = matchCount + 1
                    local items = readLootArray(bagId, emitter)

                    if items then
                        local changed = rebuildLootArray(
                            bagId,
                            items,
                            ready,
                            checkpoint
                        )

                        if changed then
                            changedCount = changedCount + 1
                        end
                    end
                end
            end

            summary.MatchedEmitters = summary.MatchedEmitters + matchCount
            summary.ChangedEmitters = summary.ChangedEmitters + changedCount

            if matchCount == 0 then
                Logger.Warning("BUILDER", "No loaded loot emitter matched " .. bagId)
            else
                if changedCount > 0 then
                    summary.ChangedBags = summary.ChangedBags + 1
                end

                Logger.Info(bagId .. ": applied " .. tostring(#ready)
                    .. " drop(s) to " .. tostring(changedCount) .. " of "
                    .. tostring(matchCount) .. " matching emitter(s); skipped "
                    .. tostring(skipped) .. " unresolved drop(s)")
            end
        end
    end

    return summary
end


return LootBuilder
