Module:MysteriesView

From Against the Storm Official Wiki

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

---
--- Serves the Mysteries searching template by providing markup to the controller for display on the page.
---
---@module MysteriesView

local MysteriesView = {}

--region Dependencies

local StyleUtils = require("Module:StyleUtils")

--endregion

--region Private constants

local DEFAULT_CAPTION = "Forest Mysteries"

local CLASS_MYSTERIES_TABLE = "wikitable sortable mw-collapsible"
local CLASS_UNSORTABLE = "unsortable"

local MYSTERY_LINK_ICON_SIZE = StyleUtils.IMG_L()

local HEADER_ID = "ID"
local HEADER_NAME = "Name"
local HEADER_DESCRIPTION = "Description"
local HEADER_SEVERITY = "Severity"
local HEADER_HOSTILITY_LEVEL = "Hostility Level"
local HEADER_SEASON = "Season"
local HEADER_CONDITIONS = "Conditions"

local HEADER_ABBR_HOUSING = tostring(mw.html.create("abbr"):attr({ title="Housing Need Satisfied" }):wikitext("Hs"))
local HEADER_ABBR_FOOD = tostring(mw.html.create("abbr"):attr({ title="Complex Food Need Satisfied" }):wikitext("CF"))
local HEADER_ABBR_SERVICES = tostring(mw.html.create("abbr"):attr({ title="Services Need Satisfied" }):wikitext("Sv"))

local RED_X = tostring(mw.html.create("span"):attr({ style="color: #FF2A00;" }):wikitext("×"))

local REPLACEMENT_FILENAME = "Question_mark.png"

--endregion

--region Public sub-class

--- This subclass should never be created directly, but only via the constructFromTemplateFrame method, to ensure data integrity and minimize chance for errors.
MysteriesView.ViewParameters = {}

-- Indexes
local ARG_SKIP_CONDITIONS = "skip_conditions"
local ARG_SHOW_ID = "show_id"
local ARG_SHOW_DESCRIPTION = "show_description"
local ARG_SHOW_SEVERITY = "show_severity"
local ARG_SHOW_HOSTILITY_LEVEL = "show_hostility"
local ARG_SHOW_SEASON = "show_season"
local ARG_SHOW_CONDITION_HOUSING = "show_condition_housing"
local ARG_SHOW_CONDITION_FOOD = "show_condition_food"
local ARG_SHOW_CONDITION_SERVICES = "show_condition_services"
-- Flags
local ARG_SKIP_FLAG_VALUE = "skip"
local ARG_SHOW_FLAG_VALUE = "show"

function MysteriesView.ViewParameters.isShowingID(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_ID]
end
function MysteriesView.ViewParameters.isShowingName()
    return true
end
function MysteriesView.ViewParameters.isShowingDescription(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_DESCRIPTION]
end
function MysteriesView.ViewParameters.isShowingSeverity(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_SEVERITY]
end
function MysteriesView.ViewParameters.isShowingHostilityLevel(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_HOSTILITY_LEVEL]
end
function MysteriesView.ViewParameters.isShowingSeason(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_SEASON]
end
function MysteriesView.ViewParameters.isShowingConditions(self)
    -- This comparison is different!
    return ARG_SKIP_FLAG_VALUE ~= self[ARG_SKIP_CONDITIONS]
end
function MysteriesView.ViewParameters.isShowingConditionHousing(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_CONDITION_HOUSING]
end
function MysteriesView.ViewParameters.isShowingConditionFood(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_CONDITION_FOOD]
end
function MysteriesView.ViewParameters.isShowingConditionServices(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_CONDITION_SERVICES]
end

function MysteriesView.ViewParameters.countShownConditionColumns(self)

    local count = 0
    if self:isShowingConditionHousing() then
        count = count + 1
    end
    if self:isShowingConditionFood() then
        count = count + 1
    end
    if self:isShowingConditionServices() then
        count = count + 1
    end

    -- No matter what, one column.
    if 0 == count then
        return 1
    else
        return count
    end
end

local checkerSwitch = {
    [HEADER_ID] = MysteriesView.ViewParameters.isShowingID,
    [HEADER_NAME] = MysteriesView.ViewParameters.isShowingName,
    [HEADER_DESCRIPTION] = MysteriesView.ViewParameters.isShowingDescription,
    [HEADER_SEVERITY] = MysteriesView.ViewParameters.isShowingSeverity,
    [HEADER_HOSTILITY_LEVEL] = MysteriesView.ViewParameters.isShowingHostilityLevel,
    [HEADER_SEASON] = MysteriesView.ViewParameters.isShowingSeason,
    [HEADER_CONDITIONS] = MysteriesView.ViewParameters.isShowingConditions,
    [HEADER_ABBR_HOUSING] = MysteriesView.ViewParameters.isShowingConditionHousing,
    [HEADER_ABBR_FOOD] = MysteriesView.ViewParameters.isShowingConditionFood,
    [HEADER_ABBR_SERVICES] = MysteriesView.ViewParameters.isShowingConditionServices
}

---getCheckerMethod
---@param headerLabel string the label displayed on the table
---@return function the function to call to see whether that column should be shown
function MysteriesView.ViewParameters.getCheckerMethod(headerLabel)
    return checkerSwitch[headerLabel]
end

---constructViewParametersFromTemplateFrame
---@param frame table the mediawiki template's calling frame
---@return table an instance of the ViewParameters class
function MysteriesView.constructViewParametersFromTemplateFrame(frame)
    local newInstance = {}
    newInstance[ARG_SKIP_CONDITIONS] = frame.args[ARG_SKIP_CONDITIONS] or "not by default"
    newInstance[ARG_SHOW_ID] = frame.args[ARG_SHOW_ID] or "not by default"
    newInstance[ARG_SHOW_DESCRIPTION] = frame.args[ARG_SHOW_DESCRIPTION] or ARG_SHOW_FLAG_VALUE
    newInstance[ARG_SHOW_SEVERITY] = frame.args[ARG_SHOW_SEVERITY] or ARG_SHOW_FLAG_VALUE
    newInstance[ARG_SHOW_HOSTILITY_LEVEL] = frame.args[ARG_SHOW_HOSTILITY_LEVEL] or ARG_SHOW_FLAG_VALUE
    newInstance[ARG_SHOW_SEASON] = frame.args[ARG_SHOW_SEASON] or ARG_SHOW_FLAG_VALUE
    newInstance[ARG_SHOW_CONDITION_HOUSING] = frame.args[ARG_SHOW_CONDITION_HOUSING] or ARG_SHOW_FLAG_VALUE
    newInstance[ARG_SHOW_CONDITION_FOOD] = frame.args[ARG_SHOW_CONDITION_FOOD] or ARG_SHOW_FLAG_VALUE
    newInstance[ARG_SHOW_CONDITION_SERVICES] = frame.args[ARG_SHOW_CONDITION_SERVICES] or ARG_SHOW_FLAG_VALUE

    -- Attach methods to the instance
    setmetatable(newInstance, { __index = MysteriesView.ViewParameters })

    return newInstance
end

--endregion

--region Private member variables

local htmlTable

--endregion

--region Private methods

---createCaptionNode
---@param captionText string the desired caption
---@return table the html node of the caption tags
local function createCaptionNode(captionText)

    captionNode = mw.html.create("caption")

    if null == captionText or "" == captionText then
        captionNode:wikitext(DEFAULT_CAPTION)
    else
        captionNode:wikitext(captionText)
    end

    return captionNode
end

---openTable
---@param caption string the desired caption
---@return table the complete html node, htmlTable, the class variable
local function openTable(caption)

    htmlTable = mw.html.create("table")
    htmlTable:addClass(CLASS_MYSTERIES_TABLE):newline()
    htmlTable:node(createCaptionNode(caption)):newline()

    return htmlTable
end

---createHeaderCell
---@param label string the label to put in the header cell
---@return table a new html table header cell
local function createHeaderCell(label)

    local cell = mw.html.create("th")
    cell:wikitext(label)

    -- Assign the methods so they can be chained
    cell.spanTwoRows = function(self)
        self:attr({ rowspan="2" })
        return self
    end
    cell.makeUnsortable = function(self)
        self:addClass(CLASS_UNSORTABLE)
        return self
    end
    cell.spanMultipleColumns = function(self, columnsToSpan)
        self:attr({ colspan=columnsToSpan })
        return self
    end

    return cell
end

---loopThroughHeaderCells
---@param row table the html table row node to use
---@param preconfiguredRowToUse table the row already setup to choose from
---@param viewParameters table the ViewParameters specified by the author
---@return table the same row, now with more stuff in it
local function addAllCellsThatShouldBeShowing(row, preconfiguredRowToUse, viewParameters)

    for _, rowItem in ipairs(preconfiguredRowToUse) do
        local isShowingIn = MysteriesView.ViewParameters.getCheckerMethod(rowItem.label)
        if isShowingIn(viewParameters) then
            row:node(rowItem.cell):newline()
        end
    end
end

---addHeaderRow adds header rows to the member variable htmlTable and returns what was added.
---
---@param viewParameters table a ViewParameters initialized with its constructor
---@return table, table the header row added, sometimes a second sub-header row
local function addHeaderRow(viewParameters)

    -- Configure the options; we'll figure out which to create in a moment. Separating the setup from the logic makes this significantly easier to read and understand. These cannot be key-value pairs, like [label] = cell, because we have to use numerical indexes to preserve their order.
    local headersInOrderWithoutConditions = {
        { label = HEADER_ID, cell = createHeaderCell(HEADER_ID) },
        { label = HEADER_NAME, cell = createHeaderCell(HEADER_NAME) },
        { label = HEADER_DESCRIPTION, cell = createHeaderCell(HEADER_DESCRIPTION):makeUnsortable() },
        { label = HEADER_SEVERITY, cell = createHeaderCell(HEADER_SEVERITY) },
        { label = HEADER_HOSTILITY_LEVEL, cell = createHeaderCell(HEADER_HOSTILITY_LEVEL) },
        { label = HEADER_SEASON, cell = createHeaderCell(HEADER_SEASON) },

    }
    local headersInOrderWithConditions = {
        { label = HEADER_ID, cell = createHeaderCell(HEADER_ID):spanTwoRows() },
        { label = HEADER_NAME, cell = createHeaderCell(HEADER_NAME):spanTwoRows() },
        { label = HEADER_DESCRIPTION, cell = createHeaderCell(HEADER_DESCRIPTION):spanTwoRows():makeUnsortable() },
        { label = HEADER_SEVERITY, cell = createHeaderCell(HEADER_SEVERITY):spanTwoRows() },
        { label = HEADER_HOSTILITY_LEVEL, cell = createHeaderCell(HEADER_HOSTILITY_LEVEL):spanTwoRows() },
        { label = HEADER_SEASON, cell = createHeaderCell(HEADER_SEASON):spanTwoRows() },
        { label = HEADER_CONDITIONS, cell = createHeaderCell(HEADER_CONDITIONS):spanMultipleColumns(viewParameters:countShownConditionColumns()) },
    }
    local subheadersInOrderUnderConditions = {
        { label = HEADER_ABBR_HOUSING, cell = createHeaderCell(HEADER_ABBR_HOUSING) },
        { label = HEADER_ABBR_FOOD, cell = createHeaderCell(HEADER_ABBR_FOOD) },
        { label = HEADER_ABBR_SERVICES, cell = createHeaderCell(HEADER_ABBR_SERVICES) },
    }

    -- If there are subheaders, populate two rows then add them to the main htmlTable
    if viewParameters:isShowingConditions() then

        local headerRow = mw.html.create("tr")
        headerRow:newline()
        addAllCellsThatShouldBeShowing(headerRow, headersInOrderWithConditions, viewParameters)

        local subheaderRow = mw.html.create("tr")
        subheaderRow:newline()
        addAllCellsThatShouldBeShowing(subheaderRow, subheadersInOrderUnderConditions, viewParameters)

        htmlTable:node(headerRow):newline()
        htmlTable:node(subheaderRow):newline()

        return headerRow, subheaderRow

    else
        -- No subheaders, just populate one row then add it to the main htmlTable.
        local headerRow = mw.html.create("tr")
        headerRow:newline()
        addAllCellsThatShouldBeShowing(headerRow, headersInOrderWithoutConditions, viewParameters)

        htmlTable:node(headerRow):newline()

        return headerRow
    end
end

---createDataCell
---@param label string the label to put in the cell
---@return table a new html table data cell
local function createDataCell(label)

    local cell = mw.html.create("td")
    cell:wikitext(label)
    return cell
end

---addDataRow
---@param data table a table of data configured with keys to match the labels in cellsInOrder in this method
---@param viewParameters table a ViewParameters initialized with its constructor
---@return table the data row added, sometimes a second sub-header row
local function addDataRow(data, viewParameters)

    -- Configure the table; we'll figure out which to actually show in a moment. Separating the setup from the logic makes this significantly easier to read and understand. These cannot be key-value pairs, like [label] = cell, because we have to use numerical indexes to preserve their order.
    local cellsInOrderWithoutConditions = {
        { label = HEADER_ID, cell = createDataCell(data[HEADER_ID] or "—") },
        { label = HEADER_NAME, cell = createDataCell(data[HEADER_NAME] or "—") },
        { label = HEADER_DESCRIPTION, cell = createDataCell(data[HEADER_DESCRIPTION] or "—") },
        { label = HEADER_SEVERITY, cell = createDataCell(data[HEADER_SEVERITY] or "—") },
        { label = HEADER_HOSTILITY_LEVEL, cell = createDataCell(data[HEADER_HOSTILITY_LEVEL] or "—") },
        { label = HEADER_SEASON, cell = createDataCell(data[HEADER_SEASON] or "—") }
    }
    local cellsInOrderWithConditions = {
        { label = HEADER_ID, cell = createDataCell(data[HEADER_ID] or "—") },
        { label = HEADER_NAME, cell = createDataCell(data[HEADER_NAME] or "—") },
        { label = HEADER_DESCRIPTION, cell = createDataCell(data[HEADER_DESCRIPTION] or "—") },
        { label = HEADER_SEVERITY, cell = createDataCell(data[HEADER_SEVERITY] or "—") },
        { label = HEADER_HOSTILITY_LEVEL, cell = createDataCell(data[HEADER_HOSTILITY_LEVEL] or "—") },
        { label = HEADER_SEASON, cell = createDataCell(data[HEADER_SEASON] or "—") },
        { label = HEADER_ABBR_HOUSING, cell = createDataCell(data[HEADER_ABBR_HOUSING] and HEADER_ABBR_HOUSING or RED_X) },
        { label = HEADER_ABBR_FOOD, cell = createDataCell(data[HEADER_ABBR_FOOD] and HEADER_ABBR_FOOD or RED_X) },
        { label = HEADER_ABBR_SERVICES, cell = createDataCell(data[HEADER_ABBR_SERVICES] and HEADER_ABBR_SERVICES or RED_X) },
    }

    -- Skipping the sources altogether overrides individual column visibility.
    if viewParameters:isShowingConditions() then

        local dataRow = mw.html.create("tr")
        dataRow:newline()
        addAllCellsThatShouldBeShowing(dataRow, cellsInOrderWithConditions, viewParameters)

        htmlTable:node(dataRow):newline()

        return dataRow

    else
        local dataRow = mw.html.create("tr")
        dataRow:newline()
        addAllCellsThatShouldBeShowing(dataRow, cellsInOrderWithoutConditions, viewParameters)

        htmlTable:node(dataRow):newline()

        return dataRow
    end
end


---validateStringData
---@param stringData string a string to display
---@return string the same string, or nil if it's empty
local function validateStringData(stringData)

    if stringData == "" or stringData == " " then
        return nil
    else
        return stringData
    end
end

---validateBooleanData
---@param booleanData boolean a boolean to display
---@return boolean the same boolean, or false if it's anything besides a true value
local function validateBooleanData(booleanData)

    if booleanData == true or booleanData == "true" then
        return true
    else
        return nil
    end
end

---validateNumberData
---@param numberData number a number to display
---@return number the same number, or nil if it was invalid or 0
local function validateNumberData(numberData)

    if type(numberData) ~= "number" then
        return tonumber(numberData)
    else
        if numberData < 0 then
            return nil
        else
            return numberData
        end
    end
end

---createPageLink
---@param name string the name of the forest mystery
---@param iconFilename string the filename, excluding the extension
---@return string html markup representing the link to the page, with its icon
local function createMysteryPageLink(name, iconFilename)

    if not iconFilename or iconFilename == "" then
        iconFilename = REPLACEMENT_FILENAME
    else
        iconFilename = iconFilename .. ".png"
    end

    local iconPart = string.format("[[File:%s|%s|link=%s|alt=%s|%s]] ", iconFilename, MYSTERY_LINK_ICON_SIZE, name, name, name)
    local pagePart = "[[" .. name .. "]]"

    return mw.getCurrentFrame():preprocess(iconPart .. pagePart)
end

--endregion

--region Public methods

---startTable
---@param caption string the desired caption
---@param viewParameters table a ViewParameters initialized with its constructor
---@return table an html node, the htmlTable member variable
function MysteriesView.startTable(caption, viewParameters)

    openTable(caption)
    addHeaderRow(viewParameters)

    return htmlTable
end

---addRow
---@param id string the ID of the mystery
---@param name string the name of the mystery
---@param icon string the icon filename of the mystery, excluding the extension
---@param description string the description of the mystery
---@param severity string the severity of the mystery, for example, "Beneficial" or "Harsh"
---@param hostilityLevel number the hostility when the mystery kicks in
---@param season number the season when the mystery kicks in every year
---@param isConditionHousing boolean whether the mystery requires housing need to be satisfied to avoid
---@param isConditionFood boolean whether the mystery requires complex food need to be satisfied to avoid
---@param isConditionServices boolean whether the mystery requires services need to be satisfied to avoid
function MysteriesView.addRow(id, name, icon, description, severity, hostilityLevel, season, isConditionHousing, isConditionFood, isConditionServices, viewParameters)

    local data = {
        [HEADER_ID] = validateStringData(id),
        [HEADER_NAME] = createMysteryPageLink(name, icon),
        [HEADER_DESCRIPTION] = validateStringData(description),
        [HEADER_SEVERITY] = validateStringData(severity),
        [HEADER_HOSTILITY_LEVEL] = validateNumberData(hostilityLevel),
        [HEADER_SEASON] = validateNumberData(season),
        [HEADER_ABBR_HOUSING] = validateBooleanData(isConditionHousing),
        [HEADER_ABBR_FOOD] = validateBooleanData(isConditionFood),
        [HEADER_ABBR_SERVICES] = validateBooleanData(isConditionServices)
    }

    addDataRow(data, viewParameters)

    return htmlTable
end

---finalize
---@return table the complete html table node, fully rendered
function MysteriesView.finalize()

    return htmlTable
end

--endregion

return MysteriesView