Module:IngredientsHierarchy

From Against the Storm Official Wiki
Revision as of 15:15, 11 November 2024 by Aeredor (talk | contribs) (Testing)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Documentation for this module may be created at Module:IngredientsHierarchy/doc

---@module IngredientsHierarchy
local IngredientsHierarchy = {}



--region Dependencies

local GoodsData = require("Module:GoodsData")
local ServicesData = require("Module:ServicesData")

local DataModelsWithRecipes = {
    require("Module:CampsData"),
    require("Module:FarmsData"),
    require("Module:FishingData"),
    require("Module:GatheringData"),
    require("Module:InstitutionsData"),
    require("Module:RainCollectorsData"),
    require("Module:WorkshopsData"),
}

local TEMPLATE_RESOURCE_LINK = "Resource_link"
local TEMPLATE_SERVICE_LINK = "Service_link"

--endregion



--region Private constants

local NODE_ICON_SIZE = "medium"
local LEAF_ICON_SIZE = "small"

local INDEX_OPTION_ID = "name" -- this is for backwards compatibility, it's actually an ID

--endregion



--region Private methods

---@private
---Generic table utility to merge two arrays into the first, ensuring uniqueness.
---
---@param mainArray table first array
---@param arrayToAdd table array to add to the first
local function mergeArrays(mainArray, arrayToAdd)

    --Quickly return from edge cases
    if not arrayToAdd or #arrayToAdd < 1 then
        return mainArray
    end
    if not mainArray or #mainArray < 1 then
        return arrayToAdd
    end

    --Create a set for fast lookup of items already in mainArray
    local seen = {}
    for _, item in ipairs(mainArray) do
        seen[item] = true
    end

    --Append only unique items from arrayToAdd
    for _, item in ipairs(arrayToAdd) do
        if not seen[item] then
            table.insert(mainArray, item)
            seen[item] = true  -- Mark as seen so we don't add it again
        end
    end

    return mainArray
end



---@private
---Flattens the provided recipe data into just its ingredient IDs.
---
---@param DataModel BaseDataModel a data model that implements the recipe query interface
---@param recipeData table pair of recipe data retrieved from a data model
---@return table array of ingredient IDs
local function getIngredientsList(DataModel, recipeData)

    if not recipeData then
        return {}
    end

    local ingredientsList = {}
    for i = 1, DataModel:getRecipeNumIngredientSlots(recipeData) do
        for j = 1, DataModel:getRecipeIngredientNumOptions(recipeData, i) do
            local ingredientID = DataModel:getRecipeIngredientOptionIDAt(recipeData, i, j)
            --Thankfully, ingredients are always goods, never services.
            table.insert(ingredientsList, ingredientID)
        end
    end

    return ingredientsList
end



function IngredientsHierarchy.makeHierarchy(rootID)

    --First go through all the data models and extract an array of ingredients, with no duplicates.
    local ingredientsList = {}
    for _, DataModel in ipairs(DataModelsWithRecipes) do

        local recipeList = DataModel:getIDsAndRecipesWhereProductID(rootID)

        --There will always be at least one recipe, but we don't know whether any have ingredients yet.
        if #recipeList > 0 then
            --Go through all of those recipes, still for just this data model, and pull out a flat list of ingredients.
            for _, recipe in ipairs(recipeList) do

                local newList = getIngredientsList(DataModel, recipe)

                --Check if it's a leaf node, if so, add to the running ingredients list
                if #newList > 0 then
                    ingredientsList = mergeArrays(ingredientsList, newList)
                end
            end
        end
    end

    --Recurse only if it's not a leaf node, but we have to wait to do this until we get through all the data models.
    if #ingredientsList > 0 then
        --We need to remove any ingredients that turn into tables, but keep them in order and in an array with no gaps. To do this, we'll create a new list to return and re-add subtables and string alike, in order.
        local returnList = {}
        for _, newRoot in ipairs(ingredientsList) do
            local newNode = IngredientsHierarchy.makeHierarchy(newRoot)
            if type(newNode) == "table" then
                returnList[newRoot] = newNode
            else
                --When not a table, need to add the ingredient to the list we'll return
                table.insert(returnList, newRoot)
            end
        end
        return returnList
    else
        --No new ingredients, so just return the string itself
        return rootID
    end
end



---@private
---Runs through the provided table, counting how many times each entry appears in the hierarchy. The second parameter is cumulative, while descending the tree.
---
---@param hierarchyTable table hierarchy of products and ingredients
---@param countingTable table (not needed on first call), the running tally
function IngredientsHierarchy.countInstances(hierarchyTable, countingTable)

    --Only require the second parameter on recursion.
    if not countingTable then
        countingTable = {}
    end

    for key, item in pairs(hierarchyTable) do

        local id = ""
        --We recurse for tables, but we still need to add their items to the total
        if type(item) == "table" then
            IngredientsHierarchy.countInstances(item, countingTable)
            id = key
        else
            id = item
        end

        if not countingTable[id] then
            countingTable[id] = 1
        else
            countingTable[id] = countingTable[id] + 1
        end
    end

    return countingTable
end



---@private
---Counts all the unique items in the provided table and sorts the counts. The result is a sorted array of pairs, where pair.id and pair.count store the ID of the good or service.
---
---@param hierarchyTable table hierarchy of products and ingredients
---@return table sorted array of pairs of IDs and counts
local function countAndSort(hierarchyTable)

    local countingTable = countInstances(hierarchyTable)

    local sortedCountingTable = {}
    for ingredient, totalCount in pairs(countingTable) do
        table.insert(sortedCountingTable, { id = ingredient, count = totalCount })
    end

    table.sort(sortedCountingTable, function(a, b)
        return a.count > b.count
    end)

    return sortedCountingTable
end



function IngredientsHierarchy.reduceBelowDepth(hierarchyTable, depth)

    local reducedTable = {}
    if depth == 0 then
        --This, conveniently, flattens the table, we just have to extract the names.
        countingTable = IngredientsHierarchy.countInstances(hierarchyTable)
        for id, _ in pairs(countingTable) do
            table.insert(reducedTable, id)
        end
    else
        --Recurse down each subtable
        for _, item in pairs(hierarchyTable) do
            if type(item) == "table" then
                table.insert(reducedTable, IngredientsHierarchy.reduceBelowDepth(item, depth - 1))
            else
                table.insert(reducedTable, item)
            end
        end
    end

    return reducedTable
end

--endregion



--region Public methods


--function renderLink(displayName, level)
--
--    local viewParameters = {
--        ["name"] = displayName,
--        ["size"] = level > 1 and LEAF_ICON_SIZE or NODE_ICON_SIZE,
--        ["display"] = "notext",
--    }
--
--    local id = GoodsData.getGoodID(displayName)
--    if id then
--        --If the ID exists, then we use the resource link template.
--        return "| " .. level .. " " .. mw.getCurrentFrame():expandTemplate{
--            title = TEMPLATE_RESOURCE_LINK,
--            args = viewParameters,
--        }
--    else
--        --If it's not a good, then use the service link template.
--        return "| " .. level .. " " .. mw.getCurrentFrame():expandTemplate{
--            title = TEMPLATE_SERVICE_LINK,
--            args = viewParameters,
--        }
--    end
--end
--
--
--
--function printHierarchy(hierarchy, level)
--
--    for key, value in pairs(hierarchy) do
--        if type(value) == "table" then
--            mw.log(renderLink(key, level))
--            printHierarchy(value, level + 1)
--        else
--            --Lead nodes
--            mw.log(renderLink(value, level))
--        end
--    end
--end



function IngredientsHierarchy.main(frame)

    local productIDList = {}
    --Convert the parameters to IDs instead of names.
    for _, productName in ipairs(frame.args) do

        local productID = GoodsData.getGoodID(productName)
        if not productID then
            productID = ServicesData.getID(productName)
        end
        table.insert(productIDList, productID)
    end

    local totalHierarchy = {}
    for _, id in ipairs(productIDList) do
        totalHierarchy[id] = makeHierarchy(id)
    end

    local sortedCountingTable = countAndSort(totalHierarchy)

    return totalHierarchy, sortedCountingTable
end



return IngredientsHierarchy