Module:ItemList

From Fallen London Wiki

This module creates a list of Items belonging to a certain equip-able Class (e.g. "Hat"), and with specific effects (e.g. "Persuasive"). The qualities can be a comma-separated list (without spaces, e.g. "Bizarre,Dreaded,Respectable"), in which case their effects are combined.

Exposed functions[edit]

  • items_exist(class, quality, neg): check if at least one such item exists on the wiki.
 Returns the number of items that belong to the given class and modify the given quality. If no match, return an empty string.
 
 Both class and quality are optional, although at least one should be given. If either are omitted, then items are filtered only for their class or for the quality they modify. By default items are counted if they either increase or decrease the given quality, but if neg is truthy, then only items with negative modifiers are considered.
   
 Example usage:
     {{#invoke:ItemList|items_exist|name=Overgoat|class=Companion}}
     {{#invoke:ItemList|items_exist|class=Hat|quality=Scandal}}
     {{#invoke:ItemList|items_exist|quality=Watchful}}


  • create_list(class, quality, fancy, neg): returns the formated and sorted list of all items with the specified class and quality effect.
 If the specified quality is "Other", then the list is made of all items in the class which have no "regular" effects (main attributes/menaces/BDR).
 If fancy is true, then the result is a table instead of a list, with fancier formatting.
 If neg is true, then only items with negative stats will be returned, sorted with the most negative stats first.
   
 Example usage:
     {{#invoke:ItemList|create_list|class=Hat|quality=Persuasive}}
     {{#invoke:ItemList|create_list|class=Companion|quality=Other}}


  • best_in_slot(Quality): returns a best-in-slot items table for the specified quality.
 The following optional parameters can be set to "no" to exclude from the results items with certain properties: "Fate", "Mood", "Seasonal", "Profession", "Faction", "Ambition," "SMEN", "Retired".
   
 Example usage:
     {{#invoke:ItemList|best_in_slot|Quality=Persuasive}}
     {{#invoke:ItemList|best_in_slot|Quality=Dangerous|Fate=no|Retired=no}}
  • find_minmax(class, quality, minmax, name): Returns information about the best-in-slot bonus for a given class and quality.
 The minmax parameter must be either "max" or "min," and directs whether to return the best-in-slot or worst-in-slot values. The name parameter is the name of the item being compared against, which will be excluded from the returned results. 
 
 The function returns a table with the following keys:
   * value = max (or min) effect value possible for the specified quality in this item class
   * count = number of items found with this value
   * no_mood = max (or min) value possible when disregarding Moods
   * no_mood_count = number of non-Mood items found with this value
   * no_fate = max (or min) value possible when disregarding both Fate and Mood items
   * no_fate_count = number of non-Fate non-Mood items found with this value

Using the Module[edit]

Modules are not called directly from Wiki pages, but are invoked indirectly via a Template. In this case, the Module is invoked via {{ItemList}}.

The Module creates the desired list by parsing arguments passed to the {{Item}} on the pages for individual available equip-able items.

Module table structure[edit]

Lists of items returned by this Module contains a list of items, each in the format:

 ["item name"] = { <item info> },
   

Each <item info> is itself a table, which can hold two types of key/value pairs:

  • effects = { <effects table> }
  • qualifiers = { <qualifiers list> }
 * Both of the above are optional, e.g. if an item has no effects and no qualifiers.
 * The order of items, effects, and qualifiers is not guaranteed.


If the item has effects, the <effects table> will list them all, each with its own key/value pairs. For example:

  • effects = { ["Persuasive"] = 2, ["Scandal"] = -1 }

Notes:

 * Quality names are case-sensitive.
 * The order of effects is irrelevant. For each item, the selected quality will be printed
   first, followed by all the others, sorted according to their effect level (menaces last).

If the item has qualifiers, the <qualifiers list> will list them as a flat list. For example:

  • qualifiers = { "Rose", "Retired", "Fate" }

Notes:

 * Qualifier names are also case-sensitive.
 * "Fate" and "Retired" will always be printed last. 
 * If the qualifier is not of a value known by the module, it will be printed as-is.
 * Known qualifiers will be printed with special formatting. The list is below,
   under "local display_qualifier". The values basically include all Profession names,
   all Ambition names, all Faction names, all Seasonal event names, all Kickstarter
   reward names, as well as "Fate", "Retired", "Mood", "Protege", "Connected Pet",
   "Knife & Candle", "SMEN", and "Mysteries".

local p = {}

local menace = {
  ["Nightmares"] = true,
  ["Scandal"] = true,
  ["Suspicion"] = true,
  ["Wounds"] = true,
  ["Plagued by a Popular Song"] = true,
  ["Troubled Waters"] = true
}

local menace_msg = {
	[-2] = "Greatly reduces",
	[-1] = "Reduces",
	[1] = "Increases",
	[2] = "Greatly increases"
}

local function flip_sign(value)
    return value*(-1)
end

--[[
This function formats an effect's value for display.
For positive numbers, '+' is added up front
]]
local function qvalue(v)
    local str = ""
    if (v > 0) then
        str = "+"
    end
    return str .. v
end

--[[
This function is used to compare two effects to determine which should be
displayed first.

Each input effect (a or b) is a table with:
    qual = quality name (e.g. "Persuasive")
    v = the quality's value (e.g. 2)
    
If the two effects have the same value, then:
- Menaces will appear after other qualities.
- If both are menaces (or both are not), sort lexicographically by
  effect name.
]]
local function compare_effects(a, b)
    if (a.val == b.val) then
        if (menace[a.qual] and not menace[b.qual]) then
            return false
        elseif (menace[b.qual] and not menace[a.qual]) then
            return true
        end
        return (a.qual < b.qual)
    else
        return (a.val > b.val)
    end
end

local special_fonts = {
    ["Fate"] = "FontFate",
    ["Exceptional Friendship"] = "FontExceptional",
    ["Retired"] = "FontRetired",
    ["Rose"] = "FontRose",
    ["Christmas"] = "FontChristmas",
    ["Hallowmas"] = "FontHallowmas",
    ["Whitsun"] = "FontWhitsun",
    ["Fruits"] = "FontFruits",
    ["Election"] = "FontElection",
    ["Grand Clearing-Out"] = "FontClearing",
    ["Prelapsarian Exhibition"] = "FontExhibition",
    ["Horticultural Show"] = "FontHorticultural",
    ["Summer festival"] = "FontSummer",
    ["World Quality"] = "FontWorld",
    ["Sunless Sea"] = "FontSunless",
    ["Sunless Skies"] = "FontSkies",
    ["Mask of the Rose"] = "FontMask",
    ["Silver Tree"] = "FontSilver",
    ["Mysteries"] = "FontMysteries",
    ["SMEN"] = "FontSMEN"
}

local function special_font(id)
    local frame = mw.getCurrentFrame()
    
    if (special_fonts[id] == nil or frame == nil) then
        return id .. "[[Category:" .. "Module ItemList Internal Errors]]"
    end
        
    --return special_fonts[id]
    return frame:expandTemplate{ title = special_fonts[id] } 
end


local function display_mood()
    return "[[Mood]]"
end

local function display_pet()
    return "[[Connected Pet]]"
end

local function display_kc()
    return "[[Knife-and-Candle (Guide)|Knife & Candle]]"
end

local function display_ambition(ambition)
    return ("[[Ambition: " .. ambition .. "]] Item")
end

local function display_profession(profession)
    return ("Profession: [[" .. profession .. "]] Item")
end

local function display_protege()
    return ("[[The Protégé of a Mysterious Benefactor]] Item")
end

local function display_faction(faction)
    return ("[[Faction: " .. faction .. "|" .. faction .. "]] [[Factions (Guide)#Renown Items|Faction Item]]")
end


--[[
A hash table for the proper function(s) which know how to create
the display text for each possible qualifier.
]]
local display_qualifier = {
    ["Fate"] = special_font,
    ["Exceptional Friendship"] = special_font,
    ["Retired"] = special_font,
    ["Rose"] = special_font,
    ["Christmas"] = special_font,
    ["Hallowmas"] = special_font,
    ["Whitsun"] = special_font,
    ["Fruits"] = special_font,
    ["Election"] = special_font,
    ["Grand Clearing-Out"] = special_font,
    ["Prelapsarian Exhibition"] = special_font,
    ["Horticultural Show"] = special_font,
    ["Summer festival"] = special_font,
    ["World Quality"] = special_font,
    ["Mood"] = display_mood,
    ["Connected Pet"] = display_pet,
    ["Knife & Candle"] = display_kc,
    ["Sunless Sea"] = special_font,
    ["Sunless Skies"] = special_font,
    ["Mask of the Rose"] = special_font,
    ["Silver Tree"] = special_font,
    ["Mysteries"] = special_font,
    ["SMEN"] = special_font,
    ["Nemesis"] = display_ambition,
    ["Bag a Legend!"] = display_ambition,
    ["Heart's Desire!"] = display_ambition,
    ["Light Fingers!"] = display_ambition,
    ["Campaigner"] = display_profession,
    ["Mystic"] = display_profession,
    ["Silverer"] = display_profession,
    ["Enforcer"] = display_profession,
    ["Murderer"] = display_profession,
    ["Licentiate"] = display_profession,
    ["Journalist"] = display_profession,
    ["Author"] = display_profession,
    ["Correspondent"] = display_profession,
    ["Rat-Catcher"] = display_profession,
    ["Stalker"] = display_profession,
    ["Monster-Hunter"] = display_profession,
    ["Trickster"] = display_profession,
    ["Conjurer"] = display_profession,
    ["Crooked-Cross"] = display_profession,
    ["Watcher"] = display_profession,
    ["Agent"] = display_profession,
    ["Midnighter"] = display_profession,
    ["Protege"] = display_protege,
    ["Bohemians"] = display_faction,
    ["Constables"] = display_faction,
    ["Criminals"] = display_faction,
    ["Hell"] = display_faction,
    ["Revolutionaries"] = display_faction,
    ["Rubbery Men"] = display_faction,
    ["Society"] = display_faction,
    ["The Church"] = display_faction,
    ["The Docks"] = display_faction,
    ["The Great Game"] = display_faction,
    ["Tomb-Colonies"] = display_faction,
    ["Urchins"] = display_faction,
}

local il = nil
--[[
This function formats an item's name for display
]]
local function display_item_name(name, fancy, icon)

    -- repeated calls to frame:expandTemplate{} was slowing down page due to FFI
    -- instead we call {{IL}} once, cache it, and sub out the values
    -- item icon is part of item details obtained from SMW
    if not il then
        local frame = mw.getCurrentFrame()
        if (frame == nil) then
            return "'''no frame: " .. name .. "'''"
        end
        -- the <brackets> prevent overlaps with real values, e.g. "icon" appears in "Helicon"
        il = frame:expandTemplate{title = "IL", args = {"<item>", Size = "40px", Image = "<icon.png>", Alignment = "<align>", Appearance = "<alias>"}}
    end

    local link = string.gsub(il, "(<item>)", name)
    link = string.gsub(link, "(File:<icon%.png>)", icon)
    link = string.gsub(link, "(<align>)", fancy and "left" or "")
    -- If the item's name ends with "(something)" in parenthesis, strip them
    -- for the Appearance param of the IL template.
    local appear = string.gsub(name, "%s*%(.*%)", "")
    link = string.gsub(link, "(<alias>)", appear)

    return link
end

--[[
Format for display the list of optional qualifiers which are to be added to the
item line, after all the effects.

The optional "FATE", followed by the optional "RETIRED" are to be shown last.
]]
local function display_qualifiers(qualifiers)
    local qualifier_list = ""
    local is_fate = false
    local is_retired = false
    local is_first = true
    
    for _, vv in ipairs(qualifiers) do
        if (vv == "Fate") then is_fate = true
        elseif (vv == "Retired") then is_retired = true;
        else
            local new_item = display_qualifier[vv] and display_qualifier[vv](vv) or vv
            qualifier_list = qualifier_list .. " " .. new_item
        end
    end

    if (is_fate) then
        local new_item = special_font("Fate")
        qualifier_list = qualifier_list .. " " .. new_item
    end

    if (is_retired) then
        local new_item = special_font("Retired")
        qualifier_list = qualifier_list .. " " .. new_item
    end

    return qualifier_list
end

--[[
This function is used to create the display line for a given item, with the
list of its effects, and optional qualifiers at the end.

Input parameters:
    @name: the item's name
    @item: the table holding the item's qualities (effects and qualifiers)
    @qualities: the qualities to place first in the effects list
    @fancy: use fancy format style
    @neg: specified quality must have a negative value

Example created line: "Lemurian's Mask (Bizarre +1) FEAST OF THE ROSE

The function returns a table with the following members:
    @name: the item's name
    @line: the generated line of item effect list and qualifiers
    @display_value: the value to be displayed in the first column in "fancy"
                    mode.
    @sort_key: a sorted array holding all of the items effects.
               The first effect is a synthetic "buff" effect which is the
               summed value of the specified qualities (if provided). This
               is followed by the specified qualities, then the remainder.
               Each element of this sorted array is itself a pair
               of the format: { qual="quality", val=value) }
]]

local function create_item_line(name, item, qualities, fancy, neg)
    local sorted = {}

    local effects = item.effects
    if not effects then effects = {} end
    
    local detriments = {}
    local benefits = {}
    local menacereduce = {}
    local menaceincrease = {}
    
    local value = 0
    
    -- first, create a sorted list of the item's effects
    for effectquality, effectvalue in pairs(effects) do
        local found = false
        -- We assume not many qualities, otherwise we'd build a look-up table
        
        for p, desiredquality in ipairs(qualities) do  
            if effectquality == desiredquality then
                found = p
                break
            end
        end
        
        local ismenace = menace[effectquality]
        
        if ismenace then
            if effectvalue > 0 then
                effectvalue = (effectvalue)
                if effectquality then effectvalue = effectvalue end
                if found then
                    -- Use a placeholder value to get correct sort order
                    effectvalue = effectvalue + 1000
                end
                table.insert(menaceincrease, {qual=effectquality, val=effectvalue})
            end
            if effectvalue < 0 then
                effectvalue = (effectvalue)
                if effectquality then effectvalue = effectvalue end
                if found then
                    -- Use a placeholder value to get correct sort order
                    effectvalue = effectvalue + 1000
                end
                table.insert(menacereduce, {qual=effectquality, val=effectvalue}) 
            end
        else
            if effectvalue > 0 then
                effectvalue = (effectvalue)
                if effectquality then effectvalue = effectvalue end
                if found then
                    -- Use a placeholder value to get correct sort order
                    effectvalue = effectvalue + 1000
                end
                table.insert(benefits, {qual=effectquality,val=effectvalue}) 
            end
            if effectvalue < 0 then
                if found then
                    -- Use a placeholder value to get correct sort order
                    effectvalue = effectvalue + 1000
                end
                table.insert(detriments, {qual=effectquality, val=effectvalue}) 
            end
        end
        
    end
    
    table.insert(sorted, {qual="buff", val=2000})
    table.sort(benefits, compare_effects)
    table.sort(menacereduce, compare_effects)
    table.sort(detriments, compare_effects)
    table.sort(menaceincrease, compare_effects)
    
    for k,entry in pairs(benefits) do sorted[#sorted+1] = entry end
    for k,entry in pairs(menacereduce) do sorted[#sorted+1] = entry end
    for k,entry in pairs(detriments) do sorted[#sorted+1] = entry end
    for k,entry in pairs(menaceincrease) do sorted[#sorted+1] = entry end
 
    -- Now that the effects list is sorted, fix the values for the selected
    -- qualities
    
    for idx, entry in ipairs(sorted) do
        if idx ~= 1 and entry.val > 500 then
            value = value + effects[entry.qual]
            sorted[idx].val = effects[entry.qual]
        end
    end

    local display_value = value
    if #qualities == 1 then
        -- This is quite the hack. Menaces get sorted in reverse order, and
        -- this is accomplished by negating their values higher up. This means
        -- the total value will also be negative, which is good for sorting,
        -- but bad for display. *However*, if we're adding multiple qualities
        -- together, things get too complicated - what if we're adding menace
        -- and non-menace qualities? So we only try to undo the negation in
        -- the case where there's only one quality involved.
        display_value = (value) 
    end
    -- When we're selecting only negative items, we also want to sort in
    -- reverse order, so negate the buff value. But the display value needs to
    -- remain untouched.
    if (neg) then value = flip_sign(value) end
    sorted[1].val = value

    -- now start creating the item text line itself
    local item_line = display_item_name(name, fancy, item.icon)
    if (item.effects) then
        item_line = item_line .. " ("
        
        local transformed = {}
        for i, entry in ipairs(sorted) do
            if i ~= 1 then  -- Skip artificial buff line
            	if menace[entry.qual] then
            		transformed[i-1] = menace_msg[entry.val] .. ' ' .. entry.qual .. ' ' .. 'build up.'
            	else
                	transformed[i-1] = entry.qual .. " " .. qvalue(
                    	(entry.val))
                end
            end
        end
        item_line = item_line .. table.concat(transformed, ", ") .. ")"
    end
    
    if (item.qualifiers) then
        if (fancy) then
            item_line = item_line .. "<br/>"
        end
        item_line = item_line .. display_qualifiers(item.qualifiers)
    end
    if (fancy) then
        item_line = item_line .. "<br clear=all/>"
    end
    
    return {name=name, line=item_line, display_value=display_value,
            sort_key=sorted}
end

--[[
This function is used to compare two item lines, for the purpose of sorting.
Each input item (a or b) is a table with:
    name = the item's name
    line = the item line (e.g. "Mask of the Rose (Persuasive +1)")
    sort_key = sorting key, which is itself a sorted array of item effects.
        Each array element of 'k' is a pair: { qual="quality", val=value) }
        
The function compares each of the input items' sorting key array elements,
one by one. So, for example, if one item's highest quality is +10, and the
other's highest quality is +9, the first item will be sorted first. If both
have the same level for their highest quality, the second highest quality is
and so on. If the lists of effect levels for both items are identical, they
are sorted according to the item's name.
]]
local function compare_lines(a, b)
    -- Go over item a's sorting key's effects one by one, and compare each
    -- to b's corresponding sorting key's effect
    for i = 1, #a.sort_key do
        if (i > #b.sort_key) then
            -- No more effectes listed for item b.
            -- Just check if what's left for 'a' is positive or negative.
            if (a.sort_key[i].val > 0) then
                return true
            else
                return false
            end
        end
        local first = a.sort_key[i]
        local second = b.sort_key[i]
        if (first.val > second.val) then
            return true
        elseif (first.val < second.val) then
            return false
        end
    end

    -- We've checked all of a's listed effects. If there are more effects listed
    -- for item 'b', then we'll just check if they are positive or negative.
    if (#b.sort_key > #a.sort_key) then
        if (b.sort_key[#a.sort_key + 1].val > 0) then
            return false
        else
            return true
        end
    end
    
    -- Both of the items' effect levels are equivallent.
    -- Sort lexicographically by item name.
    return a.name < b.name
end

--[[
Adapter function retrieves item data stored in SMW.

Input parameters:
    @header: A string containing the Selection part of the SMW query, using Ask syntax

The returned list contains item detail tables as described in the top-level documentation for this module.
]]
local function smw_fetch_items(header)
    local extendedResult = mw.smw.getQueryResult(header .. [===[ 
        [[!The Imperturbable Patroness]]
        [[:+]]
        |?=Item
        |?Has icon=Icon
        |?Has effect#-=Effects
        |?Category:Fate Items=Fate
        |?-Has renown item=Faction
        |?-Has profession item=Profession
        |?Category:Mood
        |?Category:SMEN
        |?Category:Knife-and-Candle Medals=Knife & Candle
        |?Category:Connected Pet
        |?Category:Ambition: Light Fingers! Items=Light Fingers!
        |?Category:Ambition: Heart's Desire! Items=Heart's Desire!
        |?Category:Ambition: Nemesis Items=Nemesis
        |?Category:Ambition: Bag a Legend! Items=Bag a Legend!
        |?Category:Retired Items=Retired
        |?Category:Hallowmas Equipment=Hallowmas
        |?Category:Feast of the Rose Equipment=Rose
        |?Category:Christmas Equipment=Christmas
        |?Category:Election Equipment=Election
        |?Category:Grand Clearing-Out Equipment=Grand Clearing-Out
        |?Category:Prelapsarian Exhibition Equipment=Prelapsarian Exhibition
        |?Category:Horticultural Show Equipment=Horticultural Show
        |?Category:Summer festival Equipment=Summer festival
        |?Category:Fruits of the Zee Equipment=Fruits
        |?Category:Whitsun Equipment=Whitsun
        |?Category:World Quality Equipment=World Quality
        |?Category:Sunless Sea Items=Sunless Sea
        |?Category:Sunless Skies Items=Sunless Skies
        |?Category:Mask of the Rose Items=Mask of the Rose
        |?Category:Protégé Items=Protege
        |?Category:Fallen London Mysteries=Mysteries
        |?Category:Exceptional Friendship Equipment=Exceptional Friendship
        |limit=1000
    ]===]);
    local results = {};
    for i, item in ipairs(extendedResult.results) do 
        local name = item.fulltext;
        local quals = {};
        for q, b in pairs(item.printouts) do
            if b[1] == "t" then 
                table.insert(quals, q); 
            elseif q == "Faction" and #b == 1 then
                table.insert(quals, string.sub(b[1].fulltext, 10)); -- strip "Faction: "
            elseif q == "Profession" and #b == 1 then
                table.insert(quals, b[1].fulltext);
            end
        end
        local effs = {};
        for j, e in ipairs(item.printouts.Effects) do
            effs[e["Modifies quality"].item[1].fulltext] = e["Modifies by"].item[1];
        end
        local icon
        if item.printouts.Icon ~= nil then
        	icon = item.printouts.Icon[1].fulltext
        else
        	icon = 'Question'
        end
        results[name] = {effects = effs, qualifiers = quals, icon = icon};
    end

    return results;
end

--[[
Queries the SMW database for a list of the items that can be equipped in a given slot and provide a bonus (or malus) to the given quality

Input parameters:
    @class: The name of the equipment slot
    @quality: The name of the quality being filtered

The returned list contains item detail tables and described in the top-level documentation for this module.
]]
local function fetch_items(class, quality, neg, other)
    local header = "[[Equips in slot::" .. class .. "]]"
    header = header .. "[[Has effect::" .. string.gsub(quality,",","||")
    if neg then
        header = header .. ";<0]]"
    else
        header = header .. ";?]]"
    end
    if other then header = header .. other end
    return smw_fetch_items(header);
end

local function create_list(class, quality, fancy, neg)
    local qualities = mw.text.split(quality, ",", true)
    local sorted_lines = {}
    for name, item in pairs(fetch_items(class, quality, neg)) do
        table.insert(sorted_lines, create_item_line(name, item, qualities, fancy, neg))
    end

    table.sort(sorted_lines, compare_lines)
    
    if fancy then
        local rowspan = {}
        local current_buff = 0
        local last_i = 0
        for i, vv in ipairs(sorted_lines) do
            local buff = vv.display_value
            if buff ~= current_buff then
                if last_i ~= 0 then
                    rowspan[last_i] = i - last_i
                end
                last_i = i
                current_buff = buff
            end
        end
        rowspan[last_i] = #sorted_lines + 1 - last_i
        
        local result = '{| class="article-table"\n|-\n'
        if last_i ~= 0 then  -- If we have *any* buff columns
            result = result .. '! scope="col" |Value\n'
        end
        result = result .. '! scope="col" |Item\n'
        
        local out_lines = {}
        for i, vv in ipairs(sorted_lines) do
            local line = "|-\n"
            if rowspan[i] then
            	if menace[quality] then
            		line = line .. string.format([[| rowspan="%d" |<big>%s</big><br><big>build up.</big>
]], rowspan[i], menace_msg[vv.display_value])
            	else
                line = line .. string.format([[| rowspan="%d" |<big><big>%+d</big></big>
]], rowspan[i], vv.display_value)
				end
            end
            out_lines[i] = line .. "|" .. vv.line .. "\n"
        end
        return result .. table.concat(out_lines) .. "|}\n"
    else
        local out_lines = {}
        for i, vv in ipairs(sorted_lines) do
            out_lines[i] = "* " .. vv.line .. "\n"
        end
        return table.concat(out_lines)
    end
end

local seasonal = {
    ["Rose"] = true,
    ["Christmas"] = true,
    ["Hallowmas"] = true,
    ["Fruits"] = true,
    ["Election"] = true,
    ["Grand Clearing-Out"] = true,
    ["Prelapsarian Exhibition"] = true,
    ["Horticultural Show"] = true,
    ["Summer festival"] = true,
    ["Whitsun"] = true,
}
 
--[[
Returns the combined bonuses of an item

Input parameters:
    @effects: Item effects table of the form "quality" --> value (e.g. "Dreaded" --> 1)
    @quality: The quality to search, possibly a comma-separated string
]]
local function sum_value(effects, quality)
    if (effects == nil) then
        return 0
    end
    
    local value = 0
    for q in mw.text.gsplit(quality, ",", true) do
        if (effects[q]) then
            value = value + effects[q]
        end
    end
    
    return value
end
 
--[[
Check if an item had the desired quality

Input parameters:
    @name: item name
    @item: item structure (containing optional "effects" and "qualifiers" sub-tables)
    @quality: the quality to search (e.g. "Watchful")
    @restrictions: option restrictions of which items not to include


]]
local function item_match(item, restrictions)
    if (restrictions and item.qualifiers) then
        -- scan item's qualifiers list and create a table which can be easily compared
        -- to the restrictions table provided.
        local qualifier_table = {}
        for _, q in pairs(item.qualifiers) do
            if (seasonal[q]) then
                qualifier_table["Seasonal"] = true
            elseif (display_qualifier[q] == display_profession) then
                qualifier_table["Profession"] = q
            elseif (display_qualifier[q] == display_faction) then
                qualifier_table["Faction"] = true
            elseif (display_qualifier[q] == display_ambition) then
                qualifier_table["Ambition"] = q
            else
                qualifier_table[q] = true
            end
        end
 
        for k, v in pairs(restrictions) do
            if (qualifier_table[k]) then
                if v == true or qualifier_table[k] ~= v then
                    return false
                end
            end
        end
    end
 
   return true
end

--[[
Check if a list of items are all profession items

Input parameters:
    @items: a table ("name" --> item structure (with its own effects and qualifiers))

For each item provided, scan all its qualifiers to see if it's a profession item.
Return 'true' iff all items are profession items.
]]
local function all_professions(items)
    local item_count = 0
    local profession_count = 0
  
    for name, item in pairs(items) do
        item_count = item_count + 1
        if (item.qualifiers) then
            for __, q in ipairs(item.qualifiers) do
                if (display_qualifier[q] == display_profession) then
                    profession_count = profession_count +1
                end
            end
        end
    end
    
    if (item_count == 0) then
        return true
    end
    
    return (item_count == profession_count)
end

local function create_entry(found, value)
    local entry = {}
    entry.value = value
    entry.count = found[value].count
    entry.items = found[value].items
    return entry
end

local function find_items(class, quality, restrictions)
    local found = {}
    local count = 0
    local none_found = true
    
    for name, item in pairs(fetch_items(class, quality)) do
        if (item_match(item, restrictions)) then
            none_found = false
            local value = sum_value(item.effects, quality)
            if (found[value] == nil) then
                found[value] = {}
                found[value].count = 0
                found[value].items = {}
            end
        
        
            found[value].items[name] =  item
            found[value].count = found[value].count + 1
        end
    end

    if (none_found) then
        return found
    end
    
    sorted = {}
    for value, items in pairs(found) do
        table.insert(sorted, value)
    end
    table.sort(sorted, function(a,b) return (a > b) end)
    
    local top = sorted[1]

    -- If the best item found has no positive impact on the quality we don't want it here
    if (top <= 0) then
        return {}
    end
    
    local relevant = {}
    table.insert(relevant, create_entry(found, sorted[1]))
    if (all_professions(found[top].items)) then
        if (sorted[2]) then
            table.insert(relevant, create_entry(found, sorted[2]))
        end
    end
    found = relevant
    
    return found
end

--[[
Create the table rows of a best-in-slot table for a given item class.

Input parameters:
    @class: item class (e.g. "Hats", etc.)
    @entry: an array wth either one or two objects in it. Each of them is a table with the keys:
            "count" = number of items in this object
            "value" = the bonus value of the sought after quality
            "items" = items table ("item name" --> effects and qualifiers)

The function returns the values:
    @section: 
    @value: The max value of the quality for the given item class (to be used by the calling
            function to agregate the total bonus to the uqality from all classes)
    @compensate: optional value. If specified, the calling function should add this to the total,
                 but only once for all classes. This will only occur when there are multiple
                 options for this and for another class.
]]
local function add_class_to_table(class, entry)
    local sect = ""
    local count = 0
    for i=1,#entry do
        count = count + entry[i].count
    end
    
    if (count > 1) then
        sect = sect .. "| rowspan=\"".. count .."\" "
    end
    sect = sect .. "| [[".. class .."]]\n"
    
    if (count == 0) then
        sect = sect .. "|\n"
        sect = sect .. "|\n"
        sect = sect .. "|\n"
        sect = sect .. "|-\n"
        
        return sect, 0, 0
    end

    for i=1,#entry do
        if (entry[i].count > 1) then
            sect = sect .. "| rowspan=\"".. entry[i].count .."\" "
        end
        sect = sect .. "|"
        if (entry[i].value > 0) then
            sect = sect .. " +" .. entry[i].value
        end
        
        if (#entry > 1 and i == 1) then
            sect = sect .. " [*]"
        end

        sect = sect .. "\n"
        
        for name, item in pairs(entry[i].items) do
            sect = sect .. "| [[" .. name .. "]]\n"
            sect = sect .. "|"
            if (item.qualifiers) then
                sect = sect .. display_qualifiers(item.qualifiers)
            end
            sect = sect .. "\n"
            sect = sect .. "|-\n"
        end
    end
    
    local compensate_for_total = 0
    if (#entry > 1) then
        compensate_for_total = entry[1].value - entry[2].value
    end
    
    return sect, entry[#entry].value, compensate_for_total
end 

--[[
Create the quality's best-in-slot wiki table

Input parameters:
    @quality: the desired quality (e.g. "Persuasive")
    @restrictions: option restrictions on the items to take into acount

The function returns a formatted wiki code table.
]]
function p.create_table(quality, restrictions)
    local total = 0
 
    local item_page_list = {
        ["Watchful"] = true,
        ["Shadowy"] = true,
        ["Persuasive"] = true,
        ["Dangerous"] = true,
        ["Bizarre"] = true,
        ["Dreaded"] = true,
        ["Respectable"] = true,
        ["A Player of Chess"] = true,
        ["Artisan of the Red Science"] = true,
        ["Glasswork"] = true,
        ["Kataleptic Toxicology"] = true,
        ["Mithridacy"] = true,
        ["Monstrous Anatomy"] = true,
        ["Shapeling Arts"] = true,
        ["Steward of the Discordance"] = true,        
        ["Neathproofed"] = true,
        ["Chthonosophy"] = true,
        ["Insubstantial"] = true,
        ["Inerrant"] = true,
    }
 
    local item_page = "Item"
 
    if (item_page_list[quality]) then
        item_page = "[[" .. quality .. " Items|Item]]"
    elseif (quality == "BDR") then
        item_page = "[[Bizarre, Dreaded, Respectable (Guide)|Item]]"
        quality = "Bizarre,Dreaded,Respectable"
    end
 
    local t = "{| class=\"article-table\" border=\"0\" cellspacing=\"1\" cellpadding=\"1\"\n"
    t = t .. "|-\n"
    t = t .. "! scope=\"col\" |Slot\n"
    t = t .. "! scope=\"col\" |Bonus\n"
    t = t .. "! scope=\"col\" |" .. item_page .. "\n"
    t = t .. "! scope=\"col\" |Notes\n"
    t = t .. "|-\n"
    
    local class_list = {"Hat", "Clothing", "Gloves", "Weapon", "Boots",
    	                "Companion", "Treasure", "Destiny", "Affiliation",
    	                "Transport", "Home Comfort", "Ship", "Spouse", "Club", 
    	                "Tools of the Trade", "Boon", "Burden", "Adornment", 
    	                "Luggage", "Crew", "Airship"
    }

    -- populate class table
    local class_items = {}
    local max_prof_adv = 0
    local solo_prof_class = nil
    for i, class in ipairs(class_list) do
        class_items[class] = find_items(class, quality, restrictions)
        if (#class_items[class] > 1) then
            local prof_adv = class_items[class][1].value - class_items[class][2].value
            if (prof_adv > max_prof_adv) then
                max_prof_adv = prof_adv
                solo_prof_class = class
            elseif (prof_adv == max_prof_adv) then
                solo_prof_class = nil
            end
        end
    end

    -- for classes with the top items coming from professions,
    -- remove the second choices except when the profession item
    -- advantage is the max
    for i, class in ipairs(class_list) do
        if (#class_items[class] > 1) then
            local prof_adv = class_items[class][1].value - class_items[class][2].value
            if (prof_adv < max_prof_adv) then
                class_items[class][1] = class_items[class][2]
                class_items[class][2] = nil
            end
        end
    end

    -- if only one class has the top profession item, trim its runner ups
    if (solo_prof_class) then
        class_items[solo_prof_class][2] = nil
    end

    local compensate_total = 0
    for i, class in ipairs(class_list) do
        local sect, max_v, compensate = add_class_to_table(class, class_items[class])
        if (compensate > 0) then
            compensate_total = compensate
        end
        total = total + max_v
        t = t .. sect
    end
    total = total + compensate_total
 
    t = t:sub(1, -2) -- remove last "\n"
    t = t .. "style=\"border-top:3px solid grey;\"\n"
    t = t .. "| Total\n"
    t = t .. "| +" .. total.. "\n"
    t = t .. "| \n"
    t = t .. "| \n"
    
    t = t .. "|}"
 
    if (compensate_total > 0) then
        t = t .. " [*] Profession items are mutually exclusive"
    end
    
    return t
end

--[[
Get an argument passed to the module.

Input parameters:
    @frame: frame object
    @name: parameter name

If the parameter was no specified, or if it an empty string, return nil.
]]
local function get_arg(frame, name)
    local arg = frame.args[name] or frame:getParent().args[name]
    
    if (arg == "") then arg = nil end
    
    return arg
end

function p.create_list_body(class, quality, fancy, neg)
    if (class == "") then class = nil end
    if (quality == "") then quality = nil end
    if (fancy == "") then fancy = nil end
    if (neg == "") then neg = nil end
    
    if (class and quality) then
        full_list = create_list(class, quality, fancy, neg)
    else
        return "[[Category:" .. "Module ItemList Parameter Errors]]"
    end

    return full_list
end

function p.create_list(frame)
    local full_list = ""
    local class = frame.args.class or frame:getParent().args.class
    local quality = frame.args.quality or frame:getParent().args.quality
    local fancy = frame.args.fancy or frame:getParent().args.fancy
    local neg = frame.args.neg or frame:getParent().args.neg

    return p.create_list_body(class, quality, fancy, neg)
end

function p.items_exist_body(class, quality, neg)
    if (class == "") then class = nil end
    if (quality == "") then quality = nil end
    if (name == "") then name = nil end
    if (neg == "") then neg = nil end
    
    quality = string.gsub(quality, ",", "||")

    local query = {}
    if class then table.insert(query, "[[Equips in slot::" .. class .. "]]") end
    if quality then
        if neg then
            table.insert(query, "[[Has effect::" .. quality .. ";<0]]")
        else
            table.insert(query, "[[Has effect.Modifies quality::" .. quality .. "]]")
        end
    end

    local count = mw.smw.ask(table.concat(query, " "), "format=count")
    if count ~= "0" then return count else return "" end
end

function p.items_exist(frame)
    local class = frame.args.class or frame:getParent().args.class
    local quality = frame.args.quality or frame:getParent().args.quality
    local neg = frame.args.neg or frame:getParent().args.neg
    
    return p.items_exist_body(class, quality, neg)
end

function p.best_in_slot(frame)
    local quality = get_arg(frame, "Quality")
    local possible_restrictions = {
        "Fate",
        "Mood",
        "Seasonal",
        "Faction",
        "Profession",
        "Ambition",
        "SMEN",
        "Retired",
    }
    
    local restrictions = {}
    for _, v in ipairs(possible_restrictions) do
        local arg = get_arg(frame, v)
        if (arg == "no" or arg == "none") then
            restrictions[v] = true
        elseif (arg ~= "yes" and arg ~= "all") then
            restrictions[v] = arg
        end
    end
    
    if (quality) then
        return p.create_table(quality, restrictions)
    else
        return "[[Category:" .. "Module ItemList Parameter Errors]]"
    end
end

--[[
Return the max value of a quality in a given class

Input parameters:
    @class: e.g. "Hat"
    @quality: e.g. "Watchful". Can also be a comma-separated list of qualities
    @mood: true if Mood items should be included, false otherwise

The function returns a table with the following keys:
    * value = max effect value possible for the specified quality in this item class
    * count = number of items found with this max value
    * no_mood = max value possible when disregarding Moods
    * no_mood_count = number of non-Mood items found with this max value
    * no_fate = max value possible when disregarding both Fate and Mood items
    * no_fate_count = number of non-Fate non-Mood items found with this max value

If the parameters were not specified, or an invalid class name, return nil.

Note: All Mood items are already best-in-slot and already non-Fate. Therefore
      the "no_fate" value/counter returned are for those non-Fate items which
      are also not Moods.
]]
function p.find_minmax(class, quality, minmax, name)

    local extreme_values = {}
    extreme_values.value = 0
    extreme_values.count = 0
    extreme_values.no_mood = 0
    extreme_values.no_mood_count = 0
    extreme_values.no_fate = 0
    extreme_values.no_fate_count = 0
    -- Exclude self, to avoid returning stale data
    for name, item in pairs(fetch_items(class, quality, nil, "[[!" .. name .. "]]")) do

        local non_fate_item = true
        local non_mood_item = true
        local non_retired = true
        if (item.qualifiers) then
            for _, q in ipairs(item.qualifiers) do
                if (q == "Retired") then
                    non_retired = false
                    break
                end
                if (q == "Fate") then
                    non_fate_item = false
                elseif (q == "Mood") then
                    non_mood_item = false
                end
            end
        end

        -- Skip comparison to Retired items            
        if (non_retired) then
            local value = sum_value(item.effects, quality)
       	    if ((minmax == "max" and value > extreme_values.value) or (minmax == "min" and value < extreme_values.value)) then
                extreme_values.value = value
                extreme_values.count = 1
       	    elseif (value == extreme_values.value) then
                extreme_values.count = extreme_values.count + 1
            end

            if (non_mood_item) then
                if ((minmax == "max" and value > extreme_values.no_mood) or (minmax == "min" and value < extreme_values.no_mood)) then
                    extreme_values.no_mood = value
                    extreme_values.no_mood_count = 1
                elseif (value == extreme_values.no_mood) then
                 extreme_values.no_mood_count = extreme_values.no_mood_count + 1
                end
       	    end
            
       	    if (non_fate_item and non_mood_item) then
                if ((minmax == "max" and value > extreme_values.no_fate) or (minmax == "min" and value < extreme_values.no_fate)) then
                    extreme_values.no_fate = value
	            extreme_values.no_fate_count = 1
       	        elseif (value == extreme_values.no_fate) then
    	            extreme_values.no_fate_count = extreme_values.no_fate_count + 1
                end
            end
        end
    end
	
    return extreme_values
end

function p.categories_body(name, class)
    local cat = ""
    local item = nil
 
    if (name == nil) then return "" end
 
    if (class) then
        if (item_class[class]) then
            item = item_class[class][name]
        end
    else
        for _, class in pairs(item_class) do
            item = class[name]
            if (item) then
                break
            end
        end
    end
 
    if (item == nil) then return "" end
 
    local cat_names = {
        ["Rose"] = "Feast of the Rose Equipment",
        ["Christmas"] = "Christmas Equipment",
        ["Hallowmas"] = "Hallowmas Equipment",
        ["Whitsun"] = "Whitsun Equipment",
        ["Fruits"] = "Fruits of the Zee Equipment",
        ["Election"] = "Election Equipment",
        ["Grand Clearing-Out"] = "Grand Clearing-Out Equipment",
        ["Prelapsarian Exhibition"] = "Prelapsarian Exhibition Equipment",
        ["Horticultural Show"] = "Horticultural Show Equipment",
        ["Summer festival"] = "Summer festival Equipment",
        ["World Quality"] = "World Quality Equipment",
        ["Fate"] = "Fate Items",
        ["Exceptional Friendship"] = "Exceptional Friendship Equipment",
        ["Retired"] = "Retired Items",
        ["Mysteries"] = "Retired Items",
        ["[[Impossible!]]"] = "Retired Items",
        ["Exotica"] = "Retired Items",
    }
 
    if (item.qualifiers) then
        for _, q in ipairs(item.qualifiers) do
            if (cat_names[q]) then
                cat = cat .. "[[Category:" .. cat_names[q] .. "]]"
            elseif (display_qualifier[q] == display_faction) then
                cat = cat .. "[[Category:Renown " .. "Items]]"
                cat = cat .. "[[Category:Faction: " .. q .. "]]"
            elseif (display_qualifier[q] == display_profession) then
                cat = cat .. "[[Category:Profession " .. "Items]]"
                cat = cat .. "[[Category:" .. q .. "]]"
            end
        end
    end
 
    if (item.effects) then
        for q, _ in pairs(item.effects) do
            cat = cat .. "[[Category:" .. q .. " Items]]"
        end
    end
 
    return cat
end

return p