Module:PerksData: Difference between revisions

From Against the Storm Official Wiki
m (For perks with multiple sources, the sources table would have whitespace in it for sources beside the first. This is a quick and dirty fix.)
(introducing a new unique key that's a lowercase version of the id for more robust lookup)
 
(8 intermediate revisions by the same user not shown)
Line 114: Line 114:
PerksData.RARITY_EPIC = "Epic"
PerksData.RARITY_EPIC = "Epic"
PerksData.RARITY_LEGENDARY = "Legendary"
PerksData.RARITY_LEGENDARY = "Legendary"
PerksData.RARITY_MYTHIC = "Mythic"
PerksData.NONE = "None"
PerksData.NONE = "None"
PerksData.SOURCE_ALTAR = "Altar"
PerksData.SOURCE_CORNERSTONE = "Cornerstone"
PerksData.SOURCE_ORDER = "Order"
PerksData.SOURCE_RELIC = "Relic"
PerksData.SOURCE_TRADER = "Trader"


--endregion
--endregion
Line 126: Line 133:
local VALUE_DIVISOR = 10
local VALUE_DIVISOR = 10


local INDEX_KEY = "key"
local INDEX_ID = "id"
local INDEX_ID = "id"
local INDEX_NAME = "displayName"
local INDEX_NAME = "displayName"
Line 133: Line 141:
local INDEX_RARITY = "rarity"
local INDEX_RARITY = "rarity"
local INDEX_PRICE = "price"
local INDEX_PRICE = "price"
local SOURCE_CORNERSTONE = "Cornerstone"
local SOURCE_ORDER = "Order"
local SOURCE_RELIC = "Relic"
local SOURCE_TRADER = "Trader"


local SWITCH_RARITIES_CHECK = {
local SWITCH_RARITIES_CHECK = {
Line 143: Line 146:
[PerksData.RARITY_RARE] = true,
[PerksData.RARITY_RARE] = true,
[PerksData.RARITY_EPIC] = true,
[PerksData.RARITY_EPIC] = true,
[PerksData.RARITY_LEGENDARY] = true
[PerksData.RARITY_LEGENDARY] = true,
[PerksData.RARITY_MYTHIC] = true,
}
 
local NORMALIZED_TEXT_RARITIES = {
["uncommon"] = PerksData.RARITY_UNCOMMON,
["rare"] = PerksData.RARITY_RARE,
["epic"] = PerksData.RARITY_EPIC,
["legendary"] = PerksData.RARITY_LEGENDARY,
["mythic"] = PerksData.RARITY_MYTHIC
}
 
local RARITIES_SORT_ORDER = {
[PerksData.RARITY_UNCOMMON] = 1,
[PerksData.RARITY_RARE] = 2,
[PerksData.RARITY_EPIC] = 3,
[PerksData.RARITY_LEGENDARY] = 4,
[PerksData.RARITY_MYTHIC] = 5,
}
 
local SWITCH_SOURCES_CHECK = {
[PerksData.SOURCE_ALTAR] = true,
[PerksData.SOURCE_CORNERSTONE] = true,
[PerksData.SOURCE_ORDER] = true,
[PerksData.SOURCE_RELIC] = true,
[PerksData.SOURCE_TRADER] = true,
}
 
local NORMALIZED_TEXT_SOURCES = {
["altar"] = PerksData.SOURCE_ALTAR,
["cornerstone"] = PerksData.SOURCE_CORNERSTONE,
["order"] = PerksData.SOURCE_ORDER,
["relic"] = PerksData.SOURCE_RELIC,
["trader"] = PerksData.SOURCE_TRADER,
}
}
local ERROR_MESSAGE_ID_MISSING = "PerksData cannot compare IDs when one or more of the IDs were missing."
local ERROR_PREFIX_NAME_NOT_FOUND = "PerksData attempted to retrieve the display name of a Perk or Cornerstones, but it was not found. Double-check that the data is up-to-date and loaded correctly:"


--endregion
--endregion
Line 159: Line 199:
local function cleanDescription(originalPerkDescription)
local function cleanDescription(originalPerkDescription)


-- remove the <sprite> tags completely
-- replace any duplicated quotes
originalPerkDescription = originalPerkDescription:gsub("<sprite[^>]+>%s?", "")
originalPerkDescription = originalPerkDescription:gsub("&quot;&quot;", '"')
 
-- remove the HTML entities from the beginning and end
originalPerkDescription = originalPerkDescription:gsub("^&quot;(.*)&quot;$", "%1")


-- swap any last duplicated quotes
-- remove any leftover quotes
return originalPerkDescription:gsub("&quot;&quot;", "&quot;")
return originalPerkDescription:gsub("&quot;", '"')
end
end


Line 187: Line 224:
-- Split on commas. We can use the + here because there are no blanks.
-- Split on commas. We can use the + here because there are no blanks.
for sourceName in originalPerkSources:gmatch("([^,]+)") do
for sourceName in originalPerkSources:gmatch("([^,]+)") do
sourceName = sourceName:gsub("%s+", "")
sourceName = sourceName:gsub("%s+", "")
table.insert(sources, sourceName)
table.insert(sources, sourceName)
Line 224: Line 260:
-- Copy over the content, mapping unhelpful indexes into header keys
-- Copy over the content, mapping unhelpful indexes into header keys
local newPerk = {}
local newPerk = {}
newPerk[INDEX_KEY] = string.lower(originalPerk[INDEX_OLD_ID])
newPerk[INDEX_ID] = originalPerk[INDEX_OLD_ID]
newPerk[INDEX_ID] = originalPerk[INDEX_OLD_ID]
newPerk[INDEX_NAME] = originalPerk[INDEX_OLD_NAME]
newPerk[INDEX_NAME] = originalPerk[INDEX_OLD_NAME]
Line 233: Line 270:
newPerk[INDEX_SOURCES] = makeSourcesSubtable(originalPerk[INDEX_OLD_SOURCES])
newPerk[INDEX_SOURCES] = makeSourcesSubtable(originalPerk[INDEX_OLD_SOURCES])


newPerksTable[ newPerk[INDEX_ID] ] = newPerk
newPerksTable[ newPerk[INDEX_KEY] ] = newPerk


table.insert(perksIDs, newPerk[INDEX_ID])
table.insert(perksIDs, newPerk[INDEX_ID])
Line 269: Line 306:


--region Public methods
--region Public methods
---isPerkIDValid
---@param perkID string the unique identifier of a Perk or Cornerstone
---@return boolean true if there is a record with the specified ID
function PerksData.isPerkIDValid(perkID)
if not perkID or "" == perkID then
return false
else
local record = PerksData.getAllDataForPerkByID(perkID)
return record ~= nil
end
end


---
---
Line 290: Line 340:
end
end


return perksTable[id]
return perksTable[string.lower(id)]
end
end


Line 448: Line 498:




---isFromAltarByID
---@param perkID string the ID of the perk
---@return boolean whether the perk comes from the Forsaken Altar
function PerksData.isFromAltarByID(perkID)
return PerksData.isAnySource(perkID, PerksData.SOURCE_ALTAR)
end




Line 459: Line 519:
function PerksData.isFromCornerstoneByID(perkID)
function PerksData.isFromCornerstoneByID(perkID)


return PerksData.isAnySource(perkID, SOURCE_CORNERSTONE)
return PerksData.isAnySource(perkID, PerksData.SOURCE_CORNERSTONE)
end
end


Line 473: Line 533:
function PerksData.isFromOrderByID(perkID)
function PerksData.isFromOrderByID(perkID)


return PerksData.isAnySource(perkID, SOURCE_ORDER)
return PerksData.isAnySource(perkID, PerksData.SOURCE_ORDER)
end
end


Line 487: Line 547:
function PerksData.isFromEventByID(perkID)
function PerksData.isFromEventByID(perkID)


return PerksData.isAnySource(perkID, SOURCE_RELIC)
return PerksData.isAnySource(perkID, PerksData.SOURCE_RELIC)
end
end


Line 501: Line 561:
function PerksData.isFromTraderByID(perkID)
function PerksData.isFromTraderByID(perkID)


return PerksData.isAnySource(perkID, SOURCE_TRADER)
return PerksData.isAnySource(perkID, PerksData.SOURCE_TRADER)
end
end


Line 535: Line 595:
---@return boolean whether the perk has the specified rarity
---@return boolean whether the perk has the specified rarity
function PerksData.isRarityMatchByID(perkID, rarityToCheck)
function PerksData.isRarityMatchByID(perkID, rarityToCheck)
-- Do some basic verification on the rarity specified and throw an error
-- if it is wrong at runtime.
if not SWITCH_RARITIES_CHECK[rarityToCheck] then
error("Specified rarity is not valid.")
end


local perk = PerksData.getAllDataForPerkByID(perkID)
local perk = PerksData.getAllDataForPerkByID(perkID)
Line 642: Line 696:


local list = {}
local list = {}
for perkID, perk in pairs(perksTable) do
for _, perk in pairs(perksTable) do


if string.lower(perk[INDEX_NAME]):find(string.lower(searchTerm)) then
if string.lower(perk[INDEX_NAME]):find(string.lower(searchTerm)) then
table.insert(list, perkID)
table.insert(list, perk[INDEX_ID])
end
end
end
end
Line 671: Line 725:


local list = {}
local list = {}
for perkID, perk in pairs(perksTable) do
for _, perk in pairs(perksTable) do


if string.lower(perk[INDEX_DESCRIPTION]):find(string.lower(searchTerm)) then
if string.lower(perk[INDEX_DESCRIPTION]):find(string.lower(searchTerm)) then
table.insert(list, perkID)
table.insert(list, perk[INDEX_ID])
end
end
end
end


return list
return list
end
function PerksData.getAllPerkIDs()
if not perksTable then
loadData()
end
return perksIDs
end
--- Gets all IDs for all perks, but filtered to the specified rarity and source.
---
---@param rarityToFilter string the rarity to filter to, should be normalized already
---@param sourceToFilter string the source to filter to, should be normalized already
---@return table an array of filtered IDs
function PerksData.getAllPerkIDsFilteredByRarityAndSource(rarityToFilter, sourceToFilter)
if not perksTable then
loadData()
end
-- Don't use the internal functions, because we need to return an empty list without any errors if one or the other wasn't provided.
local filteredIDs = {}
for _, perk in pairs(perksTable) do
if not rarityToFilter or "" == rarityToFilter or perk[INDEX_RARITY] == rarityToFilter then
for _, perkSource in ipairs(perk[INDEX_SOURCES]) do
if not sourceToFilter or "" == sourceToFilter or perkSource == sourceToFilter then
table.insert(filteredIDs, perk[INDEX_ID])
break
end
end
end
end
return filteredIDs
end
--- A custom sorting function so that perks can be listed alphabetically via table.sort(yourTable, PerksData.compareNames). Public so controllers and other data models might reuse this.
---
--- Throws errors if the IDs do not correspond to perks in the data.
---
---@param perkID1 string the ID of the first perk to compare
---@param perkID2 string the ID of the second perk to compare
---@return boolean true if the first perk should come first in the list
function PerksData.compareNames(perkID1, perkID2)
if not perkID1 or "" == perkID1 or not perkID2 or "" == perkID2 then
error(ERROR_MESSAGE_ID_MISSING)
end
if not perksTable then
loadData()
end
local perk1 = perksTable[string.lower(perkID1)]
local perk2 = perksTable[string.lower(perkID2)]
if not perk1 then
error(ERROR_PREFIX_NAME_NOT_FOUND .. " id=" .. perkID1 .. ".")
end
if not perk2 then
error(ERROR_PREFIX_NAME_NOT_FOUND .. " id=" .. perkID2 .. ".")
end
local name1 = perk1[INDEX_NAME]
local name2 = perk2[INDEX_NAME]
if not name1 then
error(ERROR_PREFIX_NAME_NOT_FOUND .. " id=" .. perkID1 .. ".")
end
if not name2 then
error(ERROR_PREFIX_NAME_NOT_FOUND .. " id=" .. perkID2 .. ".")
end
-- Sort by name, then rarity, then their ids (so it catches when we have Oil +2 and Oil +1 that are both Uncommon)
if name1 == name2 then
local rarity1 = RARITIES_SORT_ORDER[perk1[INDEX_RARITY]]
local rarity2 = RARITIES_SORT_ORDER[perk2[INDEX_RARITY]]
if rarity1 == rarity2 then
return perk1[INDEX_ID] < perk2[INDEX_ID]
else
return rarity1 < rarity2
end
else
return name1 < name2
end
end
--- Standardizes the display of a string name of one of Perks' rarities.
---
--- If the provided rarity is nil or not an accepted term (for example misspelled), this will return nil.
---
---@param rarityString string the descriptive name of the rarity of a perk or cornerstone
---@return string that same rarity, normalized to title case
function PerksData.normalizeRarityText(rarityString)
if not rarityString or "" == rarityString then
return nil
end
local rarity = string.lower(rarityString)
if not rarity or SWITCH_RARITIES_CHECK[rarity] then
return nil
end
return NORMALIZED_TEXT_RARITIES[rarity]
end
--- Standardizes the display of a string name of one of Perks' sources.
---
--- If the provided source is nil or not an accepted term (for example misspelled), this will return nil.
---
---@param sourceString string the descriptive name of the source of a perk or cornerstone
---@return string that same source, normalized to title case
function PerksData.normalizeSourceText(sourceString)
if not sourceString or "" == sourceString then
return nil
end
local source = string.lower(sourceString)
if not source or SWITCH_SOURCES_CHECK[source] then
return nil
end
return NORMALIZED_TEXT_SOURCES[source]
end
end



Latest revision as of 18:59, 10 May 2024

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

---
--- Module for compiling perks 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:
---
--- iconFilename = PerksData.getIconFilename(perkID)
---
--- This will return a string corresponding to the filename of the icon of the
--- perk, including the .png extension. It is preferable to call getter methods
--- with the ID of the perk rather than retrieving the entire record for the
--- perk, so that your code stays protected from variations in this module.
--- The getter methods are all called with the ID of the perk, not their
--- plain-language display name, so there are many duplicates.
---
--- * plainLanguageName = PerksData.getNameByID(id)
--- * longDescription = PerksData.getDescriptionByID(id)
--- * iconFilename = PerksData.getIconByID(id)
--- * isFromCornerstone = PerksData.isFromCornerstoneByID(id)
--- * isFromTrader = PerksData.isFromTraderByID(id)
--- * isFromOrder = PerksData.isFromOrderByID(id)
--- * isFromEvent = PerksData.isFromEventByID(id)
--- * rarityConstant = PerksData.getRarityByID(id)
--- * isMatch = PerksData.isRarityMatchByID(id, rarity)
--- * isUncommon = PerksData.isRarityUncommonByID(id)
--- * isRare = PerksData.isRarityRareByID(id)
--- * isEpic = PerksData.isRarityEpicByID(id)
--- * isLegendary = PerksData.isRarityLegendaryByID(id)
--- * price = PerksData.getPriceByID(id)
---
--- Note that display names, or plain language names, are very likely to have
--- duplicates in the list of perks.
---
--- You can also use this method, although it is more likely to break.
---
--- * enumValue = PerksData.getRarity(id)
---
--- The rarity constants are public and may be used. You are encouraged to
--- refer directly to the constants themselves rather than comparing them to
--- what might seem to be appropriate string literals, like `== "Epic"`. For
--- example, use this kind of statement instead.
---
--- if PerksData.getRarity(perkID) == PerksData.RARITY_EPIC then
---
--- Then render your own string literal to display. This guidance may change
--- in the future.
---
--- There is a method to retrieve the table of sources, but it is advised to
--- instead use the getter methods isFromTrader, isFromOrder, and isFromEvent
--- instead. If you must get the table, there is a getAll method:
---
--- * sourcesTable = PerksData.getAllSources(id)
---
--- As a last resort, or if you need to transform the data structure, you can
--- call the method getAllDataForPerkByID(id). This returns a whole record from
--- the data table.
---
--- The data table for perks has the following structure:
---
--- perksTable = {
--- 	["perk1_ID"] = {
--- 		["id"] = "perk1_ID",
--- 		["displayName"] = "Plain Language Name",
--- 		["description"] = "A long string with some HTML entities too.",
--- 		["iconFilename"] = "Icon_filename.png",
--- 		["sources"] = {
--- 			[1] = SOURCE_ORDER, for examples
--- 			[2] = SOURCE_RELIC,
--- 			[3] = SOURCE_CORNERSTONE,
--- 			[4] = ... or missing if fewer
--- 		},
--- 		["rarity"] = PerksData.RARITY_EPIC, for example
--- 		["price"] = 99
--- 	},
--- 	["perk2_ID"] = {
--- 		...
--- 	},
--- 	["perk3_ID"] = {
--- 		...
--- 	},
--- 	...
--- }
---
--- @module PerksData
local PerksData = {}



local CsvUtils = require("Module:CsvUtils")



--region Private member variables

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

--- Supporting table, list of names table[index] = ID.
local perksIDs

--- Lookup maps. Built once and reused on all subsequent calls within this
--- session, like this: table[displayName] = ID
local mapNamesToIDs

--endregion



--region Public enums

PerksData.RARITY_UNCOMMON = "Uncommon"
PerksData.RARITY_RARE = "Rare"
PerksData.RARITY_EPIC = "Epic"
PerksData.RARITY_LEGENDARY = "Legendary"
PerksData.RARITY_MYTHIC = "Mythic"
PerksData.NONE = "None"

PerksData.SOURCE_ALTAR = "Altar"
PerksData.SOURCE_CORNERSTONE = "Cornerstone"
PerksData.SOURCE_ORDER = "Order"
PerksData.SOURCE_RELIC = "Relic"
PerksData.SOURCE_TRADER = "Trader"

--endregion



--region Private constants

local DATA_TEMPLATE_NAME = "Template:Perks_csv"

local VALUE_DIVISOR = 10

local INDEX_KEY = "key"
local INDEX_ID = "id"
local INDEX_NAME = "displayName"
local INDEX_DESCRIPTION = "description"
local INDEX_ICON_FILENAME = "iconFilename"
local INDEX_SOURCES = "sources"
local INDEX_RARITY = "rarity"
local INDEX_PRICE = "price"

local SWITCH_RARITIES_CHECK = {
	[PerksData.RARITY_UNCOMMON] = true,
	[PerksData.RARITY_RARE] = true,
	[PerksData.RARITY_EPIC] = true,
	[PerksData.RARITY_LEGENDARY] = true,
	[PerksData.RARITY_MYTHIC] = true,
}

local NORMALIZED_TEXT_RARITIES = {
	["uncommon"] = PerksData.RARITY_UNCOMMON,
	["rare"] = PerksData.RARITY_RARE,
	["epic"] = PerksData.RARITY_EPIC,
	["legendary"] = PerksData.RARITY_LEGENDARY,
	["mythic"] = PerksData.RARITY_MYTHIC
}

local RARITIES_SORT_ORDER = {
	[PerksData.RARITY_UNCOMMON] = 1,
	[PerksData.RARITY_RARE] = 2,
	[PerksData.RARITY_EPIC] = 3,
	[PerksData.RARITY_LEGENDARY] = 4,
	[PerksData.RARITY_MYTHIC] = 5,
}

local SWITCH_SOURCES_CHECK = {
	[PerksData.SOURCE_ALTAR] = true,
	[PerksData.SOURCE_CORNERSTONE] = true,
	[PerksData.SOURCE_ORDER] = true,
	[PerksData.SOURCE_RELIC] = true,
	[PerksData.SOURCE_TRADER] = true,
}

local NORMALIZED_TEXT_SOURCES = {
	["altar"] = PerksData.SOURCE_ALTAR,
	["cornerstone"] = PerksData.SOURCE_CORNERSTONE,
	["order"] = PerksData.SOURCE_ORDER,
	["relic"] = PerksData.SOURCE_RELIC,
	["trader"] = PerksData.SOURCE_TRADER,
}

local ERROR_MESSAGE_ID_MISSING = "PerksData cannot compare IDs when one or more of the IDs were missing."
local ERROR_PREFIX_NAME_NOT_FOUND = "PerksData attempted to retrieve the display name of a Perk or Cornerstones, but it was not found. Double-check that the data is up-to-date and loaded correctly:"


--endregion



--region Private methods

---
--- Removes some markup and processing leftovers from the CSV extraction.
---
---@param originalPerkDescription string the perk's description
---@return string the cleaned description
local function cleanDescription(originalPerkDescription)

	-- replace any duplicated quotes
	originalPerkDescription = originalPerkDescription:gsub("&quot;&quot;", '"')

	-- remove any leftover quotes
	return originalPerkDescription:gsub("&quot;", '"')
end


---
--- Creates a new subtable containing the sources of the specified perk.
---
---@param originalPerkSources string list from which to extract sources
---@return table subtable with the sources
local function makeSourcesSubtable(originalPerkSources)

	local sources = {}

	-- Sub in any HTML entities for actual commas. This was originally critical
	-- to have the CSV data correctly parsed, but now we have to undo it to
	-- correctly split up these values.
	originalPerkSources = originalPerkSources:gsub("&comma;", ",")
	originalPerkSources = originalPerkSources:gsub("&quot;", "")

	-- Split on commas. We can use the + here because there are no blanks.
	for sourceName in originalPerkSources:gmatch("([^,]+)") do
		sourceName = sourceName:gsub("%s+", "")
		table.insert(sources, sourceName)
	end

	return sources
end



---
--- Transforms the oldPerksTable returned from CSV processing to be more
--- conducive to member functions looking up data. Essentially, we convert the
--- text strings into tables with the same base name and an index.
---
---@param originalPerksTable table of CSV-based table, with a header row, data rows
---@return table better structured with IDs as keys
local function restructurePerksTable(originalPerksTable)

	-- A few constants we'll need only within this function.
	local DATA_ROWS = 2
	local INDEX_OLD_ID = 1
	local INDEX_OLD_NAME = 2
	local INDEX_OLD_DESCRIPTION = 3
	local INDEX_OLD_ICON_FILENAME = 4
	local INDEX_OLD_SOURCES = 5
	local INDEX_OLD_RARITY = 6
	local INDEX_OLD_PRICE = 7

	perksIDs = {}
	mapNamesToIDs = {}

	local newPerksTable = {}
	for _, originalPerk in ipairs(originalPerksTable[DATA_ROWS]) do

		-- Copy over the content, mapping unhelpful indexes into header keys
		local newPerk = {}
		newPerk[INDEX_KEY] = string.lower(originalPerk[INDEX_OLD_ID])
		newPerk[INDEX_ID] = originalPerk[INDEX_OLD_ID]
		newPerk[INDEX_NAME] = originalPerk[INDEX_OLD_NAME]
		newPerk[INDEX_DESCRIPTION] = cleanDescription(originalPerk[INDEX_OLD_DESCRIPTION])
		newPerk[INDEX_ICON_FILENAME] = originalPerk[INDEX_OLD_ICON_FILENAME] .. ".png"
		newPerk[INDEX_RARITY] = originalPerk[INDEX_OLD_RARITY]
		newPerk[INDEX_PRICE] = originalPerk[INDEX_OLD_PRICE] / VALUE_DIVISOR

		newPerk[INDEX_SOURCES] = makeSourcesSubtable(originalPerk[INDEX_OLD_SOURCES])

		newPerksTable[ newPerk[INDEX_KEY] ] = newPerk

		table.insert(perksIDs, newPerk[INDEX_ID])

		-- Also populate the map for looking up IDs with display names
		if not mapNamesToIDs[ newPerk[INDEX_NAME] ] then
			mapNamesToIDs[ newPerk[INDEX_NAME] ] = {}
		end
		table.insert(mapNamesToIDs[ newPerk[INDEX_NAME] ], newPerk[INDEX_ID])
	end

	return newPerksTable
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.
	-- Doesn't use the header lookup.
	local originalPerksTable, _ = CsvUtils.luaTableFromCSV(CsvUtils.extractCSV(DATA_TEMPLATE_NAME))

	-- Now restructure to be more conducive.
	perksTable = restructurePerksTable(originalPerksTable)
end

--endregion



--region Public methods

---isPerkIDValid
---@param perkID string the unique identifier of a Perk or Cornerstone
---@return boolean true if there is a record with the specified ID
function PerksData.isPerkIDValid(perkID)

	if not perkID or "" == perkID then
		return false
	else
		local record = PerksData.getAllDataForPerkByID(perkID)
		return record ~= nil
	end
end

---
--- Retrieve the whole table of data for the specified perk. 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 perk cannot be found.
---
--- @param id string ID of the perk
--- @return table containing the data for the specified perk with key-value pairs, or nil if not found
function PerksData.getAllDataForPerkByID(id)

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

	if not perksTable then
		loadData()
	end

	return perksTable[string.lower(id)]
end



---
--- Get the IDs of all the Perks that are named the same thing.
---
--- Returns a table of IDs, since it's unknown how many there could be.
---
--- @param displayName string plain-language name of the perk
--- @return table of IDs of the Perk found, or an empty table if none found
function PerksData.getAllPerkIDsByName(displayName)

	if not perksTable then
		loadData()
	end

	local foundPerkIDs = mapNamesToIDs[displayName]

	if not foundPerkIDs or #foundPerkIDs == 0 then
		return {}
	end

	return foundPerkIDs
end



---
--- Retrieves the display name of the perk specified by its ID.
---
--- Returns nil if the perk was not found
---
---@param perkID string the ID of the perk
---@return string the name of the perk
function PerksData.getNameByID(perkID)

	local perk = PerksData.getAllDataForPerkByID(perkID)

	if not perk then
		return nil
	end

	return perk[INDEX_NAME]
end



---
--- Retrieves the description of the perk specified by its ID.
---
--- Returns nil if the perk was not found
---
---@param perkID string the ID of the perk
---@return string the description of the perk
function PerksData.getDescriptionByID(perkID)

	local perk = PerksData.getAllDataForPerkByID(perkID)

	if not perk then
		return nil
	end

	return perk[INDEX_DESCRIPTION]
end



---
--- Retrieves the display name of the perk specified by its ID.
---
--- Returns nil if the perk was not found
---
---@param perkID string the ID of the perk
---@return string the icon filename of the perk, including the .png extension
function PerksData.getIconByID(perkID)

	local perk = PerksData.getAllDataForPerkByID(perkID)

	if not perk then
		return nil
	end

	return perk[INDEX_ICON_FILENAME]
end



---
--- Retrieves all the sources of the perk specified by its ID.
---
--- Returns nil if the perk was not found.
---
---@param perkID string the ID of the perk
---@return table of sources, strings
function PerksData.getAllSourcesByID(perkID)

	local perk = PerksData.getAllDataForPerkByID(perkID)

	if not perk then
		return nil
	end

	return perk[INDEX_SOURCES]
end



---
--- Retrieves the number of sources of the perk specified by its ID.
---
--- Returns nil if the perk was not found.
---
---@param perkID string the ID of the perk
---@return number of sources for this perk
function PerksData.getNumberOfSourcesByID(perkID)

	local perk = PerksData.getAllDataForPerkByID(perkID)

	if not perk then
		return nil
	end

	local sourcesTable = perk[INDEX_SOURCES]

	if not sourcesTable then
		return nil
	end

	return #sourcesTable
end



---
--- Checks all the sources to see whether the specified source is found.
---
---@param
function PerksData.isAnySource(perkID, sourceToCheck)

	local perk = PerksData.getAllDataForPerkByID(perkID)

	if not perk then
		return nil
	end

	for _, source in ipairs(perk[INDEX_SOURCES]) do

		if source == sourceToCheck then
			return true
		end
	end

	return false
end





---isFromAltarByID
---@param perkID string the ID of the perk
---@return boolean whether the perk comes from the Forsaken Altar
function PerksData.isFromAltarByID(perkID)

	return PerksData.isAnySource(perkID, PerksData.SOURCE_ALTAR)
end


---
--- Retrieves whether the perk specified by its ID comes from a Cornerstone.
---
--- Returns nil if the perk was not found.
---
---@param perkID string the ID of the perk
---@return boolean whether the perk comes from a Cornerstone
function PerksData.isFromCornerstoneByID(perkID)

	return PerksData.isAnySource(perkID, PerksData.SOURCE_CORNERSTONE)
end



---
--- Retrieves whether the perk specified by its ID comes from a Order.
---
--- Returns nil if the perk was not found
---
---@param perkID string the ID of the perk
---@return boolean whether the perk comes from a Order
function PerksData.isFromOrderByID(perkID)

	return PerksData.isAnySource(perkID, PerksData.SOURCE_ORDER)
end



---
--- Retrieves whether the perk specified by its ID comes from an Event.
---
--- Returns nil if the perk was not found
---
---@param perkID string the ID of the perk
---@return boolean whether the perk comes from an Event
function PerksData.isFromEventByID(perkID)

	return PerksData.isAnySource(perkID, PerksData.SOURCE_RELIC)
end



---
--- Retrieves whether the perk specified by its ID comes from a Trader.
---
--- Returns nil if the perk was not found
---
---@param perkID string the ID of the perk
---@return boolean whether the perk comes from a Trader
function PerksData.isFromTraderByID(perkID)

	return PerksData.isAnySource(perkID, PerksData.SOURCE_TRADER)
end



---
--- Retrieves the rarity of the perk specified by its ID.
---
--- Returns nil if the perk was not found.
---
---@param perkID string the ID of the perk
---@return string one of the rarity constants
function PerksData.getRarityByID(perkID)

	local perk = PerksData.getAllDataForPerkByID(perkID)

	if not perk then
		return nil
	end

	return perk[INDEX_RARITY]
end



---
--- Retrieves whether the perk specified by its ID is of the given rarity.
---
--- Returns nil if the perk was not found
---
---@param perkID string the ID of the perk
---@param rarityToCheck string of the rarity constants
---@return boolean whether the perk has the specified rarity
function PerksData.isRarityMatchByID(perkID, rarityToCheck)

	local perk = PerksData.getAllDataForPerkByID(perkID)

	if not perk then
		return nil
	end

	return perk[INDEX_RARITY] == rarityToCheck
end


---
--- Retrieves whether the perk specified by its ID is of Uncommon rarity.
---
--- Returns nil if the perk was not found
---
---@param perkID string the ID of the perk
---@return boolean whether the perk is Uncommon
function PerksData.isRarityUncommonByID(perkID)

	return PerksData.isRarityMatchByID(perkID, PerksData.RARITY_UNCOMMON)
end


---
--- Retrieves whether the perk specified by its ID is of Rare rarity.
---
--- Returns nil if the perk was not found
---
---@param perkID string the ID of the perk
---@return boolean whether the perk is Rare
function PerksData.isRarityRareByID(perkID)

	return PerksData.isRarityMatchByID(perkID, PerksData.RARITY_RARE)
end


---
--- Retrieves whether the perk specified by its ID is of Epic rarity.
---
--- Returns nil if the perk was not found
---
---@param perkID string the ID of the perk
---@return boolean whether the perk is Epic
function PerksData.isRarityEpicByID(perkID)

	return PerksData.isRarityMatchByID(perkID, PerksData.RARITY_EPIC)
end


---
--- Retrieves whether the perk specified by its ID is of Legendary rarity.
---
--- Returns nil if the perk was not found
---
---@param perkID string the ID of the perk
---@return boolean whether the perk is Legendary
function PerksData.isRarityLegendaryByID(perkID)

	return PerksData.isRarityMatchByID(perkID, PerksData.RARITY_LEGENDARY)
end



---
--- Retrieves the price of the perk specified by its ID.
---
--- Returns nil if the perk was not found.
---
---@param perkID string the ID of the perk
---@return number the price of the perk from a trader
function PerksData.getPriceByID(perkID)

	local perk = PerksData.getAllDataForPerkByID(perkID)

	if not perk then
		return nil
	end

	return perk[INDEX_PRICE]
end



---
--- Loop through all perks and return those whose names contain the
--- specified search term.
---
---@param searchTerm string terms to search for
---@return table list of perkIDs with a match in their description
function PerksData.getAllPerkIDsWhereName(searchTerm)

	if not searchTerm then
		return {}
	end

	if not perksTable then
		loadData()
	end

	local list = {}
	for _, perk in pairs(perksTable) do

		if string.lower(perk[INDEX_NAME]):find(string.lower(searchTerm)) then
			table.insert(list, perk[INDEX_ID])
		end
	end

	return list
end



---
--- Loop through all perks and return those whose descriptions contain the
--- specified search term.
---
---@param searchTerm string terms to search for
---@return table list of perkIDs with a match in their description
function PerksData.getAllPerkIDsWhereDescription(searchTerm)

	if not searchTerm then
		return {}
	end

	if not perksTable then
		loadData()
	end

	local list = {}
	for _, perk in pairs(perksTable) do

		if string.lower(perk[INDEX_DESCRIPTION]):find(string.lower(searchTerm)) then
			table.insert(list, perk[INDEX_ID])
		end
	end

	return list
end



function PerksData.getAllPerkIDs()

	if not perksTable then
		loadData()
	end

	return perksIDs
end



--- Gets all IDs for all perks, but filtered to the specified rarity and source.
---
---@param rarityToFilter string the rarity to filter to, should be normalized already
---@param sourceToFilter string the source to filter to, should be normalized already
---@return table an array of filtered IDs
function PerksData.getAllPerkIDsFilteredByRarityAndSource(rarityToFilter, sourceToFilter)

	if not perksTable then
		loadData()
	end

	-- Don't use the internal functions, because we need to return an empty list without any errors if one or the other wasn't provided.
	local filteredIDs = {}
	for _, perk in pairs(perksTable) do
		if not rarityToFilter or "" == rarityToFilter or perk[INDEX_RARITY] == rarityToFilter then
			for _, perkSource in ipairs(perk[INDEX_SOURCES]) do
				if not sourceToFilter or "" == sourceToFilter or perkSource == sourceToFilter then
					table.insert(filteredIDs, perk[INDEX_ID])
					break
				end
			end
		end
	end

	return filteredIDs
end



--- A custom sorting function so that perks can be listed alphabetically via table.sort(yourTable, PerksData.compareNames). Public so controllers and other data models might reuse this.
---
--- Throws errors if the IDs do not correspond to perks in the data.
---
---@param perkID1 string the ID of the first perk to compare
---@param perkID2 string the ID of the second perk to compare
---@return boolean true if the first perk should come first in the list
function PerksData.compareNames(perkID1, perkID2)

	if not perkID1 or "" == perkID1 or not perkID2 or "" == perkID2 then
		error(ERROR_MESSAGE_ID_MISSING)
	end

	if not perksTable then
		loadData()
	end

	local perk1 = perksTable[string.lower(perkID1)]
	local perk2 = perksTable[string.lower(perkID2)]
	if not perk1 then
		error(ERROR_PREFIX_NAME_NOT_FOUND .. " id=" .. perkID1 .. ".")
	end
	if not perk2 then
		error(ERROR_PREFIX_NAME_NOT_FOUND .. " id=" .. perkID2 .. ".")
	end

	local name1 = perk1[INDEX_NAME]
	local name2 = perk2[INDEX_NAME]
	if not name1 then
		error(ERROR_PREFIX_NAME_NOT_FOUND .. " id=" .. perkID1 .. ".")
	end
	if not name2 then
		error(ERROR_PREFIX_NAME_NOT_FOUND .. " id=" .. perkID2 .. ".")
	end

	-- Sort by name, then rarity, then their ids (so it catches when we have Oil +2 and Oil +1 that are both Uncommon)
	if name1 == name2 then
		local rarity1 = RARITIES_SORT_ORDER[perk1[INDEX_RARITY]]
		local rarity2 = RARITIES_SORT_ORDER[perk2[INDEX_RARITY]]

		if rarity1 == rarity2 then
			return perk1[INDEX_ID] < perk2[INDEX_ID]
		else
			return rarity1 < rarity2
		end
	else
		return name1 < name2
	end
end



--- Standardizes the display of a string name of one of Perks' rarities.
---
--- If the provided rarity is nil or not an accepted term (for example misspelled), this will return nil.
---
---@param rarityString string the descriptive name of the rarity of a perk or cornerstone
---@return string that same rarity, normalized to title case
function PerksData.normalizeRarityText(rarityString)

	if not rarityString or "" == rarityString then
		return nil
	end

	local rarity = string.lower(rarityString)
	if not rarity or SWITCH_RARITIES_CHECK[rarity] then
		return nil
	end

	return NORMALIZED_TEXT_RARITIES[rarity]
end

--- Standardizes the display of a string name of one of Perks' sources.
---
--- If the provided source is nil or not an accepted term (for example misspelled), this will return nil.
---
---@param sourceString string the descriptive name of the source of a perk or cornerstone
---@return string that same source, normalized to title case
function PerksData.normalizeSourceText(sourceString)

	if not sourceString or "" == sourceString then
		return nil
	end

	local source = string.lower(sourceString)
	if not source or SWITCH_SOURCES_CHECK[source] then
		return nil
	end

	return NORMALIZED_TEXT_SOURCES[source]
end

--endregion

return PerksData