Module:CollectorsData

From Against the Storm Official Wiki
Revision as of 22:18, 5 December 2023 by Aeredor (talk | contribs) (Fixed pattern matching for grade)

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

---
--- Module for compiling collectors information from wiki data sources.
--- Restructures the flat data tables that are produced from CsvUtils to make
--- them more conducive to Lua methods that need to display the information.
----
--- The standard way of using this module is a call like the following:
---
--- area = CollectorsData.getCollectorArea(collectorName)
---
--- This will return the working radius of the collector. It is preferable to
--- call the getter methods with the name of the collector rather than
--- retrieving the entire data record for the collector. This way your code
--- stays protected from variations in this module. These getter methods are
--- called with the plain-language display name of the collector, as spelled
--- in the game, including punctuation.
---
--- * longDescription = CollectorsData.getCollectorDescription(collectorName)
--- * constructionCategory = CollectorsData.getCollectorCategory(collectorName)
--- * sizeX, sizeY = CollectorsData.getCollectorSize(collectorName)
--- * requiredGoodID, stackSize = CollectorsData.getCollectorRequiredGood(collectorName, requirementIndex)
--- * timeSeconds = CollectorsData.getCollectorConstructionTime(collectorName)
--- * cityScore = CollectorsData.getCollectorCityScore(collectorName)
--- * isMovable = CollectorsData.isCollectorMovable(collectorName)
--- * isInitiallyEssential = CollectorsData.isCollectorInitiallyEssential(collectorName)
--- * radius = CollectorsData.getCollectorArea(collectorName)
--- * storageCapacity = CollectorsData.getCollectorStorage(collectorName)
--- * workplaces = CollectorsData.getCollectorNumberOfWorkplaces(collectorName)
--- * goodID = CollectorsData.getCollectorRecipeProduct(collectorName, recipeIndex)
--- * gradeStars, gatheringSeconds = CollectorsData.getCollectorRecipeStats(collectorName, recipeIndex)
--- * iconFilename = CollectorsData.getCollectorIcon(collectorName)
---
--- And the following getter methods are all called with the collector's ID.
--- You will have an ID instead of a display name when dealing with other data
--- like recipes, species, etc.
---
--- * name = CollectorsData.getCollectorNameByID(id)
--- * iconFilename = CollectorsData.getCollectorIconByID(id)
---
--- You can retrieve all collectors that produce a certain good (with any grade of
--- efficiency) with the following getter:
---
--- collectorNamesTable = CollectorsData.getCollectorNamesWithRecipeProductID(productID)
---
--- There are methods to retrieve the lists of required goods, workplaces, and
--- recipes, but it is advised to instead use the getter methods
--- getCollectorRequiredGood, getCollectorNumberOfWorkplaces, and
--- getCollectorRecipe with an index desired, which is good for loops. If
--- you must get the tables, there are three "getAll" methods:
---
--- * requiredGoodsTable = CollectorsData.getAllCollectorRequiredGoods(collectorName)
--- * workplacesTable = CollectorsData.getAllCollectorWorkplaces(collectorName)
--- * recipesTable = CollectorsData.getAllCollectorRecipes(collectorName)
---
--- As a last resort, or if you need to transform the data structure, you can
--- call the method getAllDataForCollector(collectorName) or
--- getAllDataForCollectorByID(id). These return a whole record
--- from the data table corresponding to the required display name or ID.
---
--- The data table for collectors has the following structure:
---
--- collectorsTable = {
--- 	["collector1_ID"] = {
--- 		["id"] = "collector1_ID",
--- 		["displayName"] = "Plain Language Name",
--- 		["description"] = "A long string with some HTML entities too.",
--- 		["category"] = "Construction toolbar category",
--- 		["sizeX"] = 9, size in tiles
--- 		["sizeY"] = 9, size in tiles
--- 		["requiredGoods"] = {
--- 			[1] = { ["stackSize"] = 99, ["goodID"] = "good1_ID" },
--- 			[2] = { ["stackSize"] = 99, ["goodID"] = "good2_ID" },
--- 			[3] = { ... } or missing if fewer
--- 		},
--- 		["constructionTime"] = 99, number of seconds
--- 		["cityScore"] = 99, points?
--- 		["movable"] = true or false, if it can be moved at any cost
--- 		["initiallyEssential"] = true or false, if a new-game-starting blueprint
--- 		["area"] = 99, tile radius
--- 		["storage"] = 99, capacity
--- 		["workplaces"] = {
--- 			[1] = "Any",
--- 			[2] = "Any",
--- 			[3] = "Any",
--- 			[4] = ... representing the number of workplaces, or missing if fewer
--- 		},
--- 		["recipes"] = {
--- 			[1] = {
--- 				"goodID" = "good1_ID",
--- 				"grade" = 9, number of stars,
--- 				"gatheringTime" = 99, seconds,
--- 			}
--- 			[2] = { ... }
--- 			[3] = ... or missing if fewer
--- 		}
--- 	},
--- 	["collector2_ID"] = {
--- 		...
--- 	},
--- 	["collector3_ID"] = {
--- 		...
--- 	},
--- 	...
--- }
---
--- @module CollectorsData
local CollectorsData = {}



local CsvUtils = require("Module:CsvUtils")



--region Private member variables

--- Main data tables, like this: table[ID] = table containing data for that ID
local collectorsTable

--- Supporting table, list of names table[i] = name.
local collectorsNames

--- Lookup map. Built once and reused on subsequent calls within this session,
--- like this: table[display name] = id
local mapNamesToIDs
--- Lookup map. Built once and reused on subsequent calls within this session,
--- like this: table[goodID] = { name1, name2, ... }
local mapRecipeGoodIDsToCollectorNames

--endregion



--region Private constants

local DATA_TEMPLATE_NAME = "Template:Collectors_csv"

local INDEX_ID = "id"
local INDEX_NAME = "displayName"
local INDEX_DESCRIPTION = "description"
local INDEX_CATEGORY = "category"
local INDEX_SIZE_X = "sizeX"
local INDEX_SIZE_Y = "sizeY"
local INDEX_CONSTRUCTION_TIME = "constructionTime"
local INDEX_CITY_SCORE = "cityScore"
local INDEX_MOVABLE = "movable"
local INDEX_INITIALLY_ESSENTIAL = "initiallyEssential"
local INDEX_STORAGE = "storage"
local INDEX_REQUIRED_GOODS = "requiredGoods" -- table
local INDEX_WORKPLACES = "workplaces" -- table
local INDEX_RECIPES = "recipes" -- table

local INDEX_CONSTRUCTION_GOODS_STACK_SIZE = "stackSize"
local INDEX_CONSTRUCTION_GOODS_GOOD_ID = "goodID"

local INDEX_RECIPE_GOOD_ID = "goodID"
local INDEX_RECIPE_STACK_SIZE = "stackSize"
local INDEX_RECIPE_GRADE = "grade"
local INDEX_RECIPE_PRODUCTION_TIME = "productionTime"

local PATTERN_SPLIT_STACK_AND_ID = "(%d+)%s([%[%]%s%a]+)"
local PATTERN_CAPTURE_END_NUMBER = "(%d)$"

--endregion



--region Private methods

---
--- Creates a new subtable containing the construction goods required for the
--- specified building.
---
--- @param originalBuilding table data record from which to make subtable
--- @param headerLookup table header lookup built from the CSV data
--- @return table subtable with the required construction goods
local function makeRequiredGoodsSubtable(originalBuilding, headerLookup)

    -- A constant we'll need only within this function.
    local REQ_GOOD_HEADER_BASE_STRING = "requiredGood"

    -- Copy the originals directly into a subtable.
    local requiredIndex1 = headerLookup[REQ_GOOD_HEADER_BASE_STRING .. "1"]
    local requiredIndex2 = headerLookup[REQ_GOOD_HEADER_BASE_STRING .. "2"]
    local requiredIndex3 = headerLookup[REQ_GOOD_HEADER_BASE_STRING .. "3"]

    local number1, id1 = originalBuilding[requiredIndex1]:match(PATTERN_SPLIT_STACK_AND_ID)
    local number2, id2 = originalBuilding[requiredIndex2]:match(PATTERN_SPLIT_STACK_AND_ID)
    local number3, id3 = originalBuilding[requiredIndex3]:match(PATTERN_SPLIT_STACK_AND_ID)

    -- don't add subtables that would just contain nils
    local requiredGoods = {
        number1 and id1 and { [INDEX_CONSTRUCTION_GOODS_STACK_SIZE] = tonumber(number1), [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id1 } or nil,
        number2 and id2 and { [INDEX_CONSTRUCTION_GOODS_STACK_SIZE] = tonumber(number2), [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id2 } or nil,
        number3 and id3 and { [INDEX_CONSTRUCTION_GOODS_STACK_SIZE] = tonumber(number3), [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id3 } or nil
    }

    return requiredGoods
end



---
--- Creates a new subtable containing the workplaces available in the specified
--- building.
---
--- @param originalBuilding table data record from which to make subtable
--- @param headerLookup table header lookup built from the CSV data
--- @return table subtable with the workplaces
local function makeWorkplacesSubtable(originalBuilding, headerLookup)

    -- A constant we'll need only within this function.
    local WORKPLACE_HEADER_BASE_STRING = "workplace"

    -- Copy the originals directly into a subtable.
    local workplaceIndex1 = headerLookup[WORKPLACE_HEADER_BASE_STRING .. "1"]
    local workplaceIndex2 = headerLookup[WORKPLACE_HEADER_BASE_STRING .. "2"]
    local workplaceIndex3 = headerLookup[WORKPLACE_HEADER_BASE_STRING .. "3"]
    local workplaceIndex4 = headerLookup[WORKPLACE_HEADER_BASE_STRING .. "4"]

    local workplace1 = originalBuilding[workplaceIndex1]
    local workplace2 = originalBuilding[workplaceIndex2]
    local workplace3 = originalBuilding[workplaceIndex3]
    local workplace4 = originalBuilding[workplaceIndex4]

    -- if it's not an empty string, then save that to the table, otherwise nil
    local workplaces = {
        (workplace1 ~= "" and workplace1) or nil,
        (workplace2 ~= "" and workplace2) or nil,
        (workplace3 ~= "" and workplace3) or nil,
        (workplace4 ~= "" and workplace4) or nil
    }

    return workplaces
end



---
--- Creates a new subtable containing the recipes available in the specified
--- building.
---
--- @param buildingName string the display name of the building
--- @param originalBuilding table data record from which to make subtable
--- @param headerLookup table header lookup built from the CSV data
--- @return table subtable with the recipe IDs
local function makeRecipesSubtable(buildingName, originalBuilding, headerLookup)

    -- A constant we'll need only within this function.
    local MAX_RECIPES = 3
    local LOOKUP_RECIPE_BASE_STRING = "recipe"
    local LOOKUP_GOOD_SUFFIX_STRING = "Good"
    local LOOKUP_GRADE_SUFFIX_STRING = "Grade"
    local LOOKUP_TIME_SUFFIX_STRING = "ProductionTime"

    if not mapRecipeGoodIDsToCollectorNames then
        mapRecipeGoodIDsToCollectorNames = {}
    end

    -- Copy the originals directly into a subtable.
    local recipes = {}
    for i = 1, MAX_RECIPES do

        -- A subtable for this recipe
        newRecipe = {}
        local originalGoodIndex = headerLookup[LOOKUP_RECIPE_BASE_STRING .. i
                .. LOOKUP_GOOD_SUFFIX_STRING]
        local originalGradeIndex = headerLookup[LOOKUP_RECIPE_BASE_STRING .. i
                .. LOOKUP_GRADE_SUFFIX_STRING]
        local originalTimeIndex = headerLookup[LOOKUP_RECIPE_BASE_STRING .. i
                .. LOOKUP_TIME_SUFFIX_STRING]

        local originalStackSize, originalGoodID = originalBuilding[originalGoodIndex]:match(PATTERN_SPLIT_STACK_AND_ID)

        -- Skip blank recipes.
        if originalGoodID and originalGoodID ~= "" then
            newRecipe[INDEX_RECIPE_GOOD_ID] = originalGoodID
            newRecipe[INDEX_RECIPE_STACK_SIZE] = tonumber(originalStackSize)
            newRecipe[INDEX_RECIPE_GRADE] = tonumber(originalBuilding[originalGradeIndex]:match(PATTERN_CAPTURE_END_NUMBER))
            newRecipe[INDEX_RECIPE_PRODUCTION_TIME] = tonumber(originalBuilding[originalTimeIndex])

            table.insert(recipes,newRecipe)

            if not mapRecipeGoodIDsToCollectorNames[originalGoodID] then
                mapRecipeGoodIDsToCollectorNames[originalGoodID] = {}
            end
            table.insert(mapRecipeGoodIDsToCollectorNames[originalGoodID], buildingName)
        end
    end

    return recipes
end



---
--- Transforms the originalCollectorsTable returned from CSV processing to be
--- more conducive to member functions looking up data. Converts text strings
--- into subtables. Converts header row into field keys on every record and
--- stores records by id rather than arbitrary integer.
---
--- @param originalCollectorsTable table of CSV-based data, with header row, data rows
--- @param headerLookup table lookup table of headers to get indexes
--- @return table better structured with IDs as keys
local function restructureCollectors(originalCollectorsTable, headerLookup)

    -- A few constants we need only in this function.
    local DATA_ROWS = 2
    local INDEX_ORIGINAL_ID = 1
    local INDEX_ORIGINAL_NAME = 2
    local INDEX_ORIGINAL_DESCRIPTION = 3
    local INDEX_ORIGINAL_CATEGORY = 4
    local INDEX_ORIGINAL_SIZE_X = 5
    local INDEX_ORIGINAL_SIZE_Y = 6
    -- Required goods are indexes 7, 8, 9
    local INDEX_ORIGINAL_CONSTRUCTION_TIME = 10
    local INDEX_ORIGINAL_CITY_SCORE = 11
    local INDEX_ORIGINAL_MOVABLE = 12
    local INDEX_ORIGINAL_ESSENTIAL = 13
    local INDEX_ORIGINAL_STORAGE = 14

    mapNamesToIDs = {}
    collectorsNames = {}

    local newCollectorsTable = {}
    for _, originalCollector in ipairs(originalCollectorsTable[DATA_ROWS]) do

        -- Copy over the content, mapping unhelpful indexes into headers keys.
        local newCollector = {}
        newCollector[INDEX_ID] = originalCollector[INDEX_ORIGINAL_ID]
        newCollector[INDEX_NAME] = originalCollector[INDEX_ORIGINAL_NAME]
        newCollector[INDEX_DESCRIPTION] = originalCollector[INDEX_ORIGINAL_DESCRIPTION]
        newCollector[INDEX_CATEGORY] = originalCollector[INDEX_ORIGINAL_CATEGORY]
        newCollector[INDEX_SIZE_X] = tonumber(originalCollector[INDEX_ORIGINAL_SIZE_X])
        newCollector[INDEX_SIZE_Y] = tonumber(originalCollector[INDEX_ORIGINAL_SIZE_Y])
        newCollector[INDEX_CITY_SCORE] = tonumber(originalCollector[INDEX_ORIGINAL_CITY_SCORE])
        newCollector[INDEX_MOVABLE] = originalCollector[INDEX_ORIGINAL_MOVABLE] == "True"
        newCollector[INDEX_INITIALLY_ESSENTIAL] = originalCollector[INDEX_ORIGINAL_ESSENTIAL] == "True"
        newCollector[INDEX_STORAGE] = tonumber(originalCollector[INDEX_ORIGINAL_STORAGE])
        newCollector[INDEX_CONSTRUCTION_TIME] = tonumber(originalCollector[INDEX_ORIGINAL_CONSTRUCTION_TIME])

        newCollector[INDEX_REQUIRED_GOODS] = makeRequiredGoodsSubtable(originalCollector, headerLookup)
        newCollector[INDEX_WORKPLACES] = makeWorkplacesSubtable(originalCollector, headerLookup)
        newCollector[INDEX_RECIPES] = makeRecipesSubtable(newCollector[INDEX_NAME], originalCollector, headerLookup)

        newCollectorsTable[ newCollector[INDEX_ID] ] = newCollector

        table.insert(collectorsNames, newCollector[INDEX_NAME])

        -- Also populate the map for looking up IDs with display names
        mapNamesToIDs[ newCollector[INDEX_NAME] ] = newCollector[INDEX_ID]
    end

    return newCollectorsTable
end



---
--- Data loader function that uses the utility module and restructures the data
--- to be easier to access for invoking methods and later method calls. This
--- method is automatically called by all public member functions if the main
--- data table has not yet been populated in the current session.
local function loadData()

    -- Utility module retrieves the data as basic, flat lua tables.
    local originalCollectorsTable, headerLookup = CsvUtils.extractTables(DATA_TEMPLATE_NAME)

    -- Now restructure to be more conducive.
    collectorsTable = restructureCollectors(originalCollectorsTable, headerLookup)
end



---
--- Uses the display name, which people are more familiar with, to find the
--- encoded ID of the building. Useful for retrieving the  data that is
--- indexed by ID.
---
--- Returns nil if the building with the specified name is not found.
---
--- @param displayName string plain-language name of the building to find
--- @return string ID of the building found, or nil if not found
local function findIDByName(displayName)

    -- At runtime, this should never be nil or empty, so throw an error.
    if not displayName or displayName == "" then
        error("Parameter is nil or empty for name: " .. displayName .. ".")
    end

    if not collectorsTable then
        loadData()
    end

    return mapNamesToIDs[displayName]
end

--endregion



--region Public methods

--- Retrieve the whole table of data for the specified collector. Instead of
--- this, you should probably be calling the individual getter methods.
---
--- Throws an error if called with nil or empty string. Returns nil if the
--- specified collector cannot be found.
---
--- @param id string ID of the collector
--- @return table containing the data with key-value pairs, or nil if not found
function CollectorsData.getAllDataForCollectorByID(id)

    -- At runtime, this should never be nil or empty.
    if not id or id == "" then
        error("Parameter is nil or empty for the id.")
    end

    if not collectorsTable then
        loadData()
    end

    return collectorsTable[id]
end



--- Retrieve the whole table of data for the specified collector. Instead of
--- this, you should probably be calling the individual getter methods.
---
--- Throws an error if called with nil or empty string. Returns nil if the
--- specified collector cannot be found.
---
--- @param displayName string plain language name of the collector
--- @return table containing the data with key-value pairs, or nil if not found
function CollectorsData.getAllDataForCollector(displayName)

    -- At runtime, this should never be nil or empty.
    if not displayName or displayName == "" then
        error("Parameter is nil or empty for the name.")
    end

    if not collectorsTable then
        loadData()
    end

    local id = findIDByName(displayName)
    if not id then
        return nil
    end

    return collectorsTable[id]
end



---
--- Retrieves the ID for the collector specified by its plain language
--- display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return string the ID of the specified collector
function CollectorsData.getCollectorID(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_ID]
end



---
--- Retrieves the description for the collector specified by its plain
--- language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return string the in-game description of the specified collector
function CollectorsData.getCollectorDescription(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_DESCRIPTION]
end



---
--- Retrieves the construction toolbar category for the collector specified
--- by its plain language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return string the category of the specified collector
function CollectorsData.getCollectorCategory(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_CATEGORY]
end



---
--- Retrieves the 2x2 size for the collector specified by its plain language
--- display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return number the X-size of the collector
---@return number the Y-size of the collector
function CollectorsData.getCollectorSize(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_SIZE_X], collector[INDEX_SIZE_Y]
end



---
--- Retrieves the goods required for construction for the collector specified
--- by its plain language display name, in a table that looks like this:
---
--- ["requiredGoods"] = {
---    [1] = { ["stackSize"] = 99, ["goodID"] = "good1_ID" },
---    [2] = { ["stackSize"] = 99, ["goodID"] = "good2_ID" },
---    [3] = { ... } or missing if fewer
--- }
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return table of required goods
function CollectorsData.getAllCollectorRequiredGoods(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_REQUIRED_GOODS]
end



---
--- Retrieves the specified required construction good for the collector
--- specified by its plain language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@param requirementIndex number which construction good to retrieve
---@return string the ID of the good that is required, or nil if none
---@return number the stack size of that good, or nil if none
function CollectorsData.getCollectorRequiredGood(displayName, requirementIndex)

    local requiredGoods = CollectorsData.getAllCollectorRequiredGoods(displayName)

    if not requiredGoods then
        return nil
    end

    local requirement = requiredGoods[requirementIndex]
    if not requirement then
        return nil
    end

    return requirement[INDEX_CONSTRUCTION_GOODS_GOOD_ID], requirement[INDEX_CONSTRUCTION_GOODS_STACK_SIZE]
end



---
--- Retrieves the construction time for the collector specified by its plain
--- language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return number of seconds it takes to construct the collector
function CollectorsData.getCollectorConstructionTime(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_CONSTRUCTION_TIME]
end



---
--- Retrieves the city score awarded for the collector specified by its plain
--- language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return number of points for city score from having the collector
function CollectorsData.getCollectorCityScore(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_CITY_SCORE]
end



---
--- Retrieves whether the collector specified by its plain language display
--- name can be moved, at any cost.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return boolean of whether the collector can be moved
function CollectorsData.isCollectorMovable(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_MOVABLE]
end



---
--- Retrieves whether the collector specified by its plain language display
--- name has an essential starting blueprint for a new game profile.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return boolean of whether the collector's blueprint is essential
function CollectorsData.isCollectorInitiallyEssential(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_INITIALLY_ESSENTIAL]
end



---
--- Retrieves the storage capacity of the collector specified by its plain
--- language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return number of storage capacity at the collector
function CollectorsData.getCollectorStorage(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_STORAGE]
end



---
--- Retrieves the table of workplaces for the collector specified by its
--- plain language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return table of workplaces
function CollectorsData.getAllCollectorWorkplaces(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_WORKPLACES]
end



---
--- Retrieves the number of workplaces for the collector specified by its
--- plain language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return number of workplaces
function CollectorsData.getCollectorNumberOfWorkplaces(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return #collector[INDEX_WORKPLACES]
end



---
--- Retrieves the table of recipes for the collector specified by its
--- plain language display name, in a table that looks like this:
---
--- ["recipes"] = {
--- 	[1] = "recipe1_ID",
--- 	[2] = "recipe2_ID",
--- 	[3] = "recipe3_ID",
--- 	[4] = ... or missing if fewer
--- }
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return table of recipe IDs
function CollectorsData.getAllCollectorRecipes(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_RECIPES]
end



---
--- Retrieves the number of recipes for the collector specified by its
--- plain language display name, in a table that looks like this:
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return number of recipes at the collector
function CollectorsData.getCollectorNumberOfRecipes(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return 0
    end

    local recipes = collector[INDEX_RECIPES]
    if not recipes then
        return 0
    end

    return #recipes
end



---
--- Retrieves the product's ID and the stack size produced by the specified
--- recipe at the collector specified by its plain language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@param recipeIndex number the index of the recipe ID
---@return string the ID of the good produced by the specified recipe
function CollectorsData.getCollectorRecipeProduct(displayName, recipeIndex)

    local recipes = CollectorsData.getAllCollectorRecipes(displayName)

    if not recipes then
        return nil
    end

    local recipe = recipes[recipeIndex]
    if not recipe then
        return nil
    end

    return recipe[INDEX_RECIPE_GOOD_ID], recipe[INDEX_RECIPE_STACK_SIZE]
end



---
--- Retrieves the other stats for specified recipe at the collector specified by
--- its plain language display name: the efficiency grade and the gathering
--- time.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@param recipeIndex number the index of the recipe ID
---@return number of stars, the grade of the specified recipe
---@return number of seconds for gathering
function CollectorsData.getCollectorRecipeStats(displayName, recipeIndex)

    local recipes = CollectorsData.getAllCollectorRecipes(displayName)

    if not recipes then
        return nil
    end

    local recipe = recipes[recipeIndex]
    if not recipe then
        return nil
    end

    return recipe[INDEX_RECIPE_GRADE], recipe[INDEX_RECIPE_PRODUCTION_TIME]
end



---
--- Retrieves the icon filename for the collector specified by its plain
--- language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return string the collector's icon filename, including the extension
function CollectorsData.getCollectorIcon(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    -- the base string of the icon is the ID. It has to be not nil to
    -- concatenate
    if collector[INDEX_ID] then
        return collector[INDEX_ID] .. "_icon.png"
    else
        return nil
    end
end



---
--- Retrieves the plain language display name for the collector specified by
--- its ID.
---
--- Returns nil if the collector named was not found.
---
---@param id string the ID of the collector
---@return string the collector's name as seen in-game
function CollectorsData.getCollectorNameByID(id)

    local collector = CollectorsData.getAllDataForCollectorByID(id)

    if not collector then
        return nil
    end

    return collector[INDEX_NAME]
end



---
--- Retrieves the icon filename for the the collector specified by its ID.
---
--- Returns nil if the collector named was not found.
---
---@param id string the ID of the collector
---@return string the collector's icon filename, including the extension
function CollectorsData.getCollectorIconByID(id)

    local collector = CollectorsData.getAllDataForCollectorByID(id)

    if not collector then
        return nil
    end

    -- the base string of the icon is the ID. It has to be not nil to
    -- concatenate
    if collector[INDEX_ID] then
        return collector[INDEX_ID] .. "_icon.png"
    else
        return nil
    end
end



---
--- Retrieve all display names of collectors that have a recipe that produces the
--- specified product ID.
---
---@param productID string the ID of the good to produce
---@return table of collector names that produce the specified good
function CollectorsData.getCollectorNamesWithRecipeProductID(productID)

    -- At runtime, this should never be nil or empty.
    if not productID or productID == "" then
        error("Parameter is nil or empty for product ID.")
    end

    if not collectorsTable then
        loadData()
    end

    return mapRecipeGoodIDsToCollectorNames[productID]
end



---
--- Retrieves the name of the collector at the specified index.
---
---@param index number index
---@return string name of the building at the specified index
function CollectorsData.getCollectorNameFromDatabase(index)

    if not index then
        return nil
    end

    if not collectorsTable then
        loadData()
    end

    return collectorsNames[index]
end



---
--- Returns the number of buildings that can be retrieved from this
--- module's database.
---
---@return number of records
function CollectorsData.getNumberOfCollectorsInDatabase()

    if not collectorsTable then
        loadData()
    end

    return #collectorsNames
end

--endregion

return CollectorsData