Module:Adnoam/ItemList

From Fallen London Wiki

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

local p = {}

local Hat = mw.loadData('Module:ItemList/Hats')
local Clothing = mw.loadData('Module:ItemList/Clothing')
local Gloves = mw.loadData('Module:ItemList/Gloves')
local Weapon = mw.loadData('Module:ItemList/Weapons')
local Boots = mw.loadData('Module:ItemList/Boots')
local Companion = mw.loadData('Module:ItemList/Companions')
local Destiny = mw.loadData('Module:ItemList/Destinies')
local Affiliation = mw.loadData('Module:ItemList/Affiliations')
local Transport = mw.loadData('Module:ItemList/Transport')
local HomeComfort = mw.loadData('Module:ItemList/Home Comforts')
local Ship = mw.loadData('Module:ItemList/Ships')
local Spouse = mw.loadData('Module:ItemList/Spouses')
local Club = mw.loadData('Module:ItemList/Clubs')

local item_class = {
    ["Hat"] = Hat,
    ["Clothing"] = Clothing,
    ["Gloves"] = Gloves,
    ["Weapon"] = Weapon,
    ["Boots"] = Boots,
    ["Companion"] = Companion,
    ["Destiny"] = Destiny,
    ["Affiliation"] = Affiliation,
    ["Transport"] = Transport,
    ["Home Comfort"] = HomeComfort,
    ["Ship"] = Ship,
    ["Spouse"] = Spouse,
    ["Club"] = Club,
}

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

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

--[[
Detect and deal with negative qualities (i.e. menaces).
For the purpose of sorting, flip the value's sign
]]
local function deal_with_menace(quality, value)
    return menace[quality] and flip_sign(value) or value
end

local regular_q = {
  ["Watchful"] = true,
  ["Shadowy"] = true,
  ["Persuasive"] = true,
  ["Dangerous"] = true,
  ["Nightmares"] = true,
  ["Scandal"] = true,
  ["Suspicion"] = true,
  ["Wounds"] = 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,
  ["Zeefaring"] = true
}

--[[
Check whether an item's effects include *any* regular qualities.
Returns 'true' only if no regular effects are listed for the item.

"Regular" qualities unclude main attributes, main menaces, BDR and the parabolan attributes.
]]
local function no_regular_qualities(effects)
    if (effects) then
        for e_name, e_value in pairs(effects) do
            if (regular_q[e_name]) then
                return false;
            end
        end
    end

    return true
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:
    q = 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.v == b.v) then
        if (menace[a.q] and not menace[b.q]) then
            return false
        elseif (menace[b.q] and not menace[a.q]) then
            return true
        end
        return (a.q < b.q)
    else
        return (a.v > b.v)
    end
end

local special_fonts = {
    ["Fate"] = "FontFate",
    ["Retired"] = "FontRetired",
    ["Rose"] = "FontRose",
    ["Christmas"] = "FontChristmas",
    ["Hallowmas"] = "FontHallowmas",
    ["Whitsun"] = "FontWhitsun",
    ["Fruits"] = "FontFruits",
    ["Election"] = "FontElection",
    ["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 .. "]] [[Renown Items (Guide)|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,
    ["Retired"] = special_font,
    ["Rose"] = special_font,
    ["Christmas"] = special_font,
    ["Hallowmas"] = special_font,
    ["Whitsun"] = special_font,
    ["Fruits"] = special_font,
    ["Election"] = 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,
}

--[[
This function formats an item's name for display
]]
local function display_item_name(name, fancy)
    local frame = mw.getCurrentFrame()
    if (frame == nil) then
        return "'''no frame: " .. name .. "'''"
    end
    local 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*%(.*%)", "")
    if (appear == name) then appear = nil end

    if (appear) then
        return "'''" .. frame:expandTemplate{ title = "IL", args = { name, Size="40px", Alignment=align, Appearance=appear } } .. "'''"
    else
        return "'''" .. frame:expandTemplate{ title = "IL", args = { name, Size="40px", Alignment=align } } .. "'''"
    end
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: { q="quality", v=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 i, e in pairs(effects) do
        local found = false
        -- We assume not many qualities, otherwise we'd build a look-up table
        
        for p, q in ipairs(qualities) do  
            if i == q then
                found = p
                break
            end
        end
        
        ismenace = menace[i]
        
        if ismenace then
            if e > 0 then
                e = (e)
                if i then e = e end
                if found then
                    -- Use a placeholder value to get correct sort order
                    e = e + 1000
                end
                table.insert(menaceincrease, {q=i, v=e})
            end
            if e < 0 then
                e = (e)
                if i then e = e end
                if found then
                    -- Use a placeholder value to get correct sort order
                    e = e + 1000
                end
                table.insert(menacereduce, {q=i, v=e}) 
            end
        else
            if e > 0 then
                e = (e)
                if i then e = e end
                if found then
                    -- Use a placeholder value to get correct sort order
                    e = e + 1000
                end
                table.insert(benefits, {q=i,v=e}) 
            end
            if e < 0 then
                if found then
                    -- Use a placeholder value to get correct sort order
                    e = e + 1000
                end
                table.insert(detriments, {q=i, v=e}) 
            end
        end
        
    end
    
    table.insert(sorted, {q="buff", v=2000})
    table.sort(benefits, compare_effects)
    table.sort(menacereduce, compare_effects)
    table.sort(detriments, compare_effects)
    table.sort(menaceincrease, compare_effects)
    
    for k,v in pairs(benefits) do sorted[#sorted+1] = v end
    for k,v in pairs(menacereduce) do sorted[#sorted+1] = v end
    for k,v in pairs(detriments) do sorted[#sorted+1] = v end
    for k,v in pairs(menaceincrease) do sorted[#sorted+1] = v end
 
    -- Now that the effects list is sorted, fix the values for the selected
    -- qualities
    
    for idx, q in ipairs(sorted) do
        if idx ~= 1 and q.v > 500 then
            value = value + effects[q.q]
            sorted[idx].v = effects[q.q]
        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].v = value

    -- now start creating the item text line itself
    local item_line = display_item_name(name, fancy)
    if (item.effects) then
        item_line = item_line .. " ("
        
        local transformed = {}
        for i, vv in ipairs(sorted) do
            if i ~= 1 then  -- Skip artificial buff line
                transformed[i-1] = vv.q .. " " .. qvalue(
                    (vv.v))
            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

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

    local effects = item.effects
    if not effects then effects = {} end
    
    -- first, create a sorted list of the item's effects
    for i, e in pairs(effects) do
        local found = false
        -- We assume not many qualities, otherwise we'd build a look-up table
        for p, q in ipairs(qualities) do  
            if i == q then
                found = p
                break
            end
        end
        e = (e)
        
        if i then e = e end
        if found then
            -- Use a placeholder value to get correct sort order
            e = e + 1000
        end
        table.insert(sorted, {q=i, v=e})
    end
    -- Add a "total buff" placeholder which will sort first
    table.insert(sorted, {q="buff", v=2000})
    
    table.sort(sorted, compare_effects)

    -- Now that the effects list is sorted, fix the values for the selected
    -- qualities and the "total buff" placeholder.
    local value = 0
    for idx, q in ipairs(sorted) do
        if idx ~= 1 and q.v > 500 then
            value = value + effects[q.q]
            sorted[idx].v = effects[q.q]
        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].v = value

    -- now start creating the item text line itself
    local item_line = display_item_name(name, fancy)
    if (item.effects) then
        item_line = item_line .. " ("
        
        local transformed = {}
        for i, vv in ipairs(sorted) do
            if i ~= 1 then  -- Skip artificial buff line
                transformed[i-1] = vv.q .. " " .. qvalue(
                    (vv.v))
            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: { q="quality", v=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].v > 0) then
                return true
            else
                return false
            end
        end
        local first = a.sort_key[i]
        local second = b.sort_key[i]
        if (first.v > second.v) then
            return true
        elseif (first.v < second.v) then
            return false
        elseif first.q > second.q then
            return true
        elseif first.q < second.q 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].v > 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

--[[
Return 'true' if an effects table has the desired quality, or 'false' otherwise.

Parameters:
    @effects: and item's effects table
    @quality: quality to look for among the effects, possibly a comma-separated list
    @neg: specified quality must have a negative value
]]
local function quality_match(effects, quality, neg)
    if (quality == "Other") then
        return no_regular_qualities(effects)
    end
        
    if (effects == nil) then
        return false
    end
    
    for q in mw.text.gsplit(quality, ",", true) do
        if (effects[q] and ((not neg) or effects[q] < 0)) then
            return true
        end
    end
    return false
end

local function create_list(class, quality, fancy, neg)
    local qualities = mw.text.split(quality, ",", true)
    local sorted_lines = {}
    for name, item in pairs(class) do
        if (quality_match(item.effects, quality, neg)) then
            table.insert(sorted_lines, create_item_line(name, item, qualities, fancy, neg))
        end
    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
                line = line .. string.format([[| rowspan="%d" |<big><big>%+d</big></big>
]], rowspan[i], vv.display_value)
            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

--[[
Determine whether a specified item has a specific quality in its effects.

Input parameters:
    @item: specific item table (containing effects and qualifiers)
    @quality: quality to look for among the effects (e.g. "Watchful")
    @neg: specified quality must have a negative value

If @item is nil, return false.
Otehrewise, if @quality is nil, return true.
]]
local function has_quality(item, quality, neg)
    if (item) then
        if (quality) then
            return quality_match(item.effects, quality, neg)
        else
            return true
        end
    end

    return false
end

--[[
Find an item in a class table.

Input parameters:
    @class: item class table (e.g. Hat)
    @quality: optional name of quality to search (e.g. "Watchful")
    @name: optional name of item to search
    @neg: specified quality must have a negative value

If both @quality and @name were specified, return the item's name if it exists
in the specified class and its effects include the quality.

If only @name was specified, return the item's name if it exists in the specified class.

If only @quality was specified, return an arbitrary item's name which belongs to the class
and has the quality has an effect.
]]
local function find_item_in_class(class, quality, name, neg)
    if (class == nil) then
        return nil
    end
 
    if (name) then
        if has_quality(class[name], quality, neg) then
            return name
        end
    elseif (quality) then
        for name, item in pairs(class) do
            if has_quality(item, quality, neg) then
                return name
            end
        end
    end
 
    return nil
end


local seasonal = {
    ["Rose"] = true,
    ["Christmas"] = true,
    ["Hallowmas"] = true,
    ["Fruits"] = true,
    ["Election"] = 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(name, item, quality, restrictions)
    if (not quality_match(item.effects, quality, false)) then
        return false
    end
    
    -- special case: ignore. Companion can never be used while in London
    if (name == "The Imperturbable Patroness") then
        return false
    end

    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(item_class[class]) do
        if (item_match(name, item, quality, 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 returnes 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
    }
 
    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", "Destiny",
                        "Affiliation", "Transport", "Home Comfort", "Ship", "Spouse", "Club"}

    -- 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 and item_class[class]) then
        full_list = create_list(item_class[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.list_size_body(class, quality)
    if (class == "") then class = nil end
    if (quality == "") then quality = nil end

    local count = 0
    if (class and quality and item_class[class]) then
        for i, v in pairs(item_class[class]) do
            if (quality == "Other") then
                if (no_regular_qualities(v.effects)) then
                    count = count + 1
                end
            else
                if (v.effects and v.effects[quality]) then
                    count = count + 1
                end
            end
        end
    end

  if (count > 0) then
      return count
  else
      return ""
  end
end

function p.list_size(frame)
    local class = frame.args[1] or frame:getParent().args[1]
    local quality = frame.args[2] or frame:getParent().args[2]
    return p.list_size_body(class, quality)
end

function p.items_exist_body(class, quality, name, 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
    
    local found = nil
    
    if (class) then
        local c = item_class[class]
        if (not quality and not name) then
            found = c and class or nil
        else
            found = find_item_in_class(c, quality, name, neg)
        end
    else
        -- no class specified
        for _, class in pairs(item_class) do
            found = find_item_in_class(class, quality, name, neg)
            if (found) then
                break
            end
        end
    end

    return found or ""
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 name = frame.args.name or frame:getParent().args.name
    local neg = frame.args.neg or frame:getParent().args.neg
    
    return p.items_exist_body(class, quality, name, neg)
end

function p.verify_item_body(class, name)
    if (class == "") then class = nil end
    if (name == "") then name = nil end

    local count = 0
    if (name and class) then
        if (item_class[class]) then
            if (item_class[class][name]) then
                return "" -- item found
            end
        else
            return "[[Category:" .. "Module ItemList Parameter Errors]]" -- nonexistent class
        end
    else
        return "" -- empty input, silently ignore
    end

    return "[[Category:" .. "Items without Module ItemList data]]"
end

function p.verify_item(frame)
    local class = frame.args.class or frame:getParent().args.class
    local name = frame.args.name or frame:getParent().args.name
    return p.verify_item_body(class, name)
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: Optional item class. Can be nil or an empty string to search them all.
    @name: Item name. Must be specified.

The function returns true if the item is marked as Retired, and flase it it is not.
Returns nil if no item of that class/name can be found.
]]
function p.is_retired(class, name)
    if (name == nil) then return nil end
    
    item = nil
    if (class and 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 nil end
    
    if (item.qualifiers) then
        for _, q in ipairs(item.qualifiers) do
            if (q == "Retired") then
                return true
            end
        end
    end
    
    return false
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_max(class, quality)
    if (class == nil or item_class[class] == nil or quality == nil) then
        return nil
    end

    local max_values = {}
    max_values.value = 0
    max_values.count = 0
    max_values.no_mood = 0
    max_values.no_mood_count = 0
    max_values.no_fate = 0
    max_values.no_fate_count = 0
    for name, item in pairs(item_class[class]) do
        if (name ~= "The Imperturbable Patroness" and
            has_quality(item, quality, false)) then

            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 (value > max_values.value) then
                    max_values.value = value
                    max_values.count = 1
           	    elseif (value == max_values.value) then
                    max_values.count = max_values.count + 1
                end

                if (non_mood_item) then
       	            if (value > max_values.no_mood) then
       	                max_values.no_mood = value
                        max_values.no_mood_count = 1
       	            elseif (value == max_values.no_mood) then
    	                max_values.no_mood_count = max_values.no_mood_count + 1
                    end
           	    end
            
           	    if (non_fate_item and non_mood_item) then
       	            if (value > max_values.no_fate) then
   	                    max_values.no_fate = value
	                    max_values.no_fate_count = 1
       	            elseif (value == max_values.no_fate) then
    	                max_values.no_fate_count = max_values.no_fate_count + 1
                    end
               	end
           	end
        end
    end
	
    return max_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",
        ["Fate"] = "Fate Items",
        ["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
            if (regular_q[q]) then
                cat = cat .. "[[Category:" .. q .. " Items]]"
            end
        end
    end
 
    return cat
end

function p.categories(frame)
    local name = get_arg(frame, "name")
    local class = get_arg(frame, "class")
    return p.categories_body(name, class)
end

return p