Module:Modifier table

From Path of Exile Wiki
Revision as of 10:41, 1 June 2018 by >OmegaK2 (put elder/shape side by side ...)
Jump to navigation Jump to search
Module documentation[view] [edit] [history] [purge]


Implements {{Modifier table}}.

--[[
Module responsible for displaying modifiers in various ways.

]]

local m_util = require('Module:Util')
local getArgs = require('Module:Arguments').getArgs
local m_game = require('Module:Game')
local f_item_link = require('Module:Item link').item_link

local cargo = mw.ext.cargo

local p = {}

-- ----------------------------------------------------------------------------
-- Strings
-- ----------------------------------------------------------------------------
-- This section contains strings used by this module.
-- Add new strings here instead of in-code directly, this will help other
-- people to correct spelling mistakes easier and help with translation to
-- other PoE wikis.

local i18n = {
    mod_table = {
        name = m_util.html.abbr('Name', 'Name of the modifier if available or its internal identifier instead'),
        mod_group = m_util.html.abbr('Group', 'Only one modifier from the specified group can appear at a time under normal circumstances'),
        mod_type = 'Type',
        domain = '[[Modifiers#Mod_Domain|Domain]]',
        generation_type = '[[Modifiers#Mod_Generation_Type|Generation Type]]',
        required_level = '[[Image:Level_up_icon_small.png|link=|For generated item/monster modifiers the minimum item/monster level respectively. Some generation types may not require this condition to be met, however item level restrictions may be raised to 80% of this value.]]',
        labyrinth = '[[The Lord\'s Labyrinth|Labyrinth]]',
        stat_text = m_util.html.abbr('Stats', 'Stats of the modifier and the range they can roll in (if applicable)'),
        buff = m_util.html.abbr('Buff', 'ID of the buff granted and the values associated'),
        granted_skill = m_util.html.abbr('Skill', 'ID of the skill granted'),
        tags = '[[Tags]]',
        
        iiq = m_util.html.abbr('IIQ', 'increased Quantity of Items found in this Area'),
        iir = m_util.html.abbr('IIR', 'increased Rarity of Items found in this Area'),
        pack_size = m_util.html.abbr('Pack<br>Size', 'Monster pack size'),
        
        spawn_weights = m_util.html.abbr('Spawn Weighting', 'List of applicable tags and their values for weighting (i.e. calculating the chances) of the spawning of this modifier'),
        generation_weights = m_util.html.abbr('Generation Weighting', 'List of applicable tags and their values for weighting (i.e. calculating the chances) of the spawning of this modifier'),
        
        normal_labyrinth = 'Normal Labyrinth',
        cruel_labyrinth = 'Cruel Labyrinth',
        merciless_labyrinth = 'Merciless Labyrinth',
        eternal_labyrinth = 'Eternal Labyrinth',
    },
    
    drop_down_table = {
        collapse_all = 'Collapse all',
        expand_all = 'Expand all',
        table_intro = 'The table below displays the available [[modifiers]] for [[item]]s such as',
        prefix = 'Prefix',
        suffix = 'Suffix',
        corrupted = 'Corrupted',     
        enchant = 'Enchantment',
        elder_prefix = 'Elder prefix',
        elder_suffix = 'Elder suffix',
        shaper_prefix = 'Shaper prefix',
        shaper_suffix = 'Shaper suffix',
        mod_group = 'Mod group:',
    },
    
    errors = {
        --
        -- Mod template
        --
        sell_price_duplicate_name = 'Do not specify a sell price item name multiple times. Adjust the amount instead.',
        sell_price_missing_argument = 'Both %s and %s must be specified',
        
        --
        -- Modifier link template
        --
        undefined_statid = 'Please define any of these stat ids: %s',
        incorrect_modid = 'Please change the name from "%s" to any of these modifier ids:<br>%s',
        multiple_results = 'Please choose only one of these modifier ids:<br>%s',
        no_results = 'No results found.',
    },
}


--
-- Helper/Utility functions
--

local h = {}

function h.header(str)
    --[[
    This function replaces specific numbers with a generic #. 
    ]]
    
    local s = table.concat(m_util.string.split(str, '%(%d+%.*%d*%-%d+%.*%d*%)'), '#')
    s = table.concat(m_util.string.split(s, '%d+%.*%d*'), '#')
    s = table.concat(m_util.string.split(s, '<br>'), ', ')
    s = table.concat(m_util.string.split(s, '<br />'), ' ')
   
   return s
end

function h.query_weights(table_name, page_ids)
    results = cargo.query(
        string.format('mods,%s', table_name),
        string.format('mods._pageID,%s.tag,%s.weight', table_name, table_name),
        {
            where=page_ids,
            join=string.format('mods._pageID=%s._pageID', table_name),
            orderBy=string.format('mods.id ASC,%s.ordinal ASC', table_name),
            limit=5000,
        }
    )
    if #results == 5000 then
        error('Hit maximum cargo results')
    end
    
    return m_util.cargo.map_results_to_id{results=results,field='mods._pageID'}
end

function h.disambiguate_mod_name(results)
    --[[
        Disambiguates results from a mods query.
    ]]
    local str = {}
    for i,v in pairs(results) do
        str[#str+1] = string.format(
            '%s - %s ([[%s|page]])', 
            v['mods.id'] or v['mods._pageName'],
            string.gsub(
                v['mods.stat_text_raw'] or 'N/A', 
                '<br>', 
                ', '
            ) or '',
            v['mods._pageName']
        )
    end 
    return table.concat(str, '<br>')
end

function h.cargo_query(tpl_args)
    --[[
    Returns a Cargo query of all the results.
    
    tpl_args to be added to the cargo table needs to prefixed with q_.
    * tpl_args.q_*
    
    ]]
    
    -- Parse query arguments
    local query = {
        limit = 5000,
        offset = 0,
    }
    for key, value in pairs(tpl_args) do 
        if string.sub(key, 0, 2) == 'q_' then
            query[string.sub(key, 3)] = value
        end
    end
    
    -- Query cargo table. If there are too many results then repeat, 
    -- offset, re-query and add the remaining results:
    local results = {}      
    repeat
        local result = cargo.query(
            query.tables,
            query.fields,
            query
        )
        query.offset = query.offset + #result

        for _,v in ipairs(result) do
            results[#results + 1] = v
        end
    until #result < query.limit 
    
    return results
end

-- ----------------------------------------------------------------------------
-- Template: Mod table
-- ----------------------------------------------------------------------------

local mod_table = {}
mod_table.data = {
    {
        arg = nil,
        header = i18n.mod_table.name,
        fields = {'mods._pageName', 'mods.id', 'mods.name'},
        options = {
            [3] = {
                optional=true,
           },
        },
        display = function(tpl_args, frame, tr, data)
            local name
            if data['mods.name'] then
                name = data['mods.name']
            else
                name = data['mods.id']
            end
            tr
                :tag('td')
                    :wikitext(string.format('[[%s|%s]]', data['mods._pageName'], name))
        end,
        order = 1000,
        sort_type = 'text',
    },
    {
        arg = 'domain',
        header = i18n.mod_table.domain,
        fields = {'mods.domain'},
        display = function(tpl_args, frame, tr, data)
            local value = data['mods.domain']
            tr
                :tag('td')
                    :attr('data-sort-value', value)
                    :wikitext(m_game.constants.mod.domains[tonumber(value)]['short_upper'])
        end,
        order = 2000,
        sort_type = 'text',
    },
    {
        arg = 'generation_type',
        header = i18n.mod_table.generation_type,
        fields = {'mods.generation_type'},
        display = function(tpl_args, frame, tr, data)
            local value = data['mods.generation_type']
            tr
                :tag('td')
                    :attr('data-sort-value', value)
                    :wikitext(m_game.constants.mod.generation_types[tonumber(value)]['short_upper'])
        end,
        order = 2001,
        sort_type = 'text',
    },
    {
        arg = {'group', 'mod_group'},
        header = i18n.mod_table.mod_group,
        fields = {'mods.mod_group'},
        display = function(tpl_args, frame, tr, data)
            tr
                :tag('td')
                    :wikitext(data['mods.mod_group'])
        end,
        order = 2002,
        sort_type = 'text',
    },
    {
        arg = {'mod_type'},
        header = i18n.mod_table.mod_type,
        fields = {'mods.mod_type'},
        display = function(tpl_args, frame, tr, data)
            tr
                :tag('td')
                    :wikitext(data['mods.mod_type'])
        end,
        order = 2003,
        sort_type = 'text',
    },
    {
        arg = {'level', 'required_level'},
        header = i18n.mod_table.required_level,
        fields = {'mods.required_level'},
        display = function(tpl_args, frame, tr, data)
            tr
                :tag('td')
                    :wikitext(data['mods.required_level'])
        end,
        order = 2004,
    },
    {
        arg = {'enchantment', 'labyrinth'},
        header = i18n.mod_table.labyrinth,
        fields = {string.format([[
CONCAT(
    CASE mods.required_level 
        WHEN 32 THEN "%s" 
        WHEN 53 THEN "%s" 
        WHEN 66 THEN "%s" 
        WHEN 75 THEN "%s" 
    END
)=labyrinth_text
            ]], i18n.mod_table.normal_labyrinth, i18n.mod_table.cruel_labyrinth, i18n.mod_table.merciless_labyrinth, i18n.mod_table.eternal_labyrinth)},
        display = function(tpl_args, frame, tr, data)
            tr
                :tag('td')
                    :wikitext(data['labyrinth_text'])
        end,
        order = 2005,
    },
    {
        arg = {'stat_text'},
        header = i18n.mod_table.stat_text,
        fields = {'mods.stat_text'},
        display = function(tpl_args, frame, tr, data)
            local text
            -- map display type shows this in another column, remove this text to avoid clogging up the list
            if tpl_args.type == 'map' then
                local texts = m_util.string.split(data['mods.stat_text'], '<br>')
                local out = {}
                
                local valid
                for _, v in ipairs(texts) do
                    valid = true
                    for _, data in pairs(mod_table.stat_ids) do
                        if string.find(v, data.pattern) ~= nil then
                            valid = false
                            break
                        end
                    end
                    
                    if valid then
                        table.insert(out, v)
                    end
                end
                
                text = table.concat(out, '<br>')
            else
                text = data['mods.stat_text']
            end
            
            tr
                :tag('td')
                    :wikitext(text)
        end,
        order = 3000,
        sort_type = 'text',
    },

    {
        arg = 'buff',
        header = i18n.mod_table.buff,
        fields = {'mods.granted_buff_id', 'mods.granted_buff_value'},
        display = function(tpl_args, frame, tr, data)
            tr
                :tag('td')
                    :wikitext(string.format('%s %s', data['mods.granted_buff_id'], data['mods.granted_buff_value']))
        end,
        order = 4000,
        sort_type = 'text',
    },
    {
        arg = {'skill', 'granted_skill'},
        header = i18n.mod_table.granted_skill,
        fields = {'mods.granted_skill'},
        display = function(tpl_args, frame, tr, data)
            tr
                :tag('td')
                    :wikitext(data['mods.granted_skill'])
        end,
        order = 4001,
        sort_type = 'text',
    },
    {
        arg = {'tags'},
        header = i18n.mod_table.tags,
        fields = {'mods.tags'},
        display = function(tpl_args, frame, tr, data)
            tr
                :tag('td')
                    :wikitext(table.concat(m_util.string.split(data['mods.tags'], ','), ', '))
        end,
        order = 5000,
        sort_type = 'text',
    },
}
mod_table.stat_ids = {
    ['map_item_drop_quantity_+%'] = {
        header = i18n.mod_table.iiq,
        pattern = '%d+%% increased Quantity of Items found in this Area',
    },
    ['map_item_drop_rarity_+%'] = {
        header = i18n.mod_table.iir,
        pattern = '%d+%% increased Rarity of Items found in this Area',
    },
    ['map_pack_size_+%'] = {
        header = i18n.mod_table.pack_size,
        pattern = '%+%d+%% Monster pack size',
    }
}
mod_table.weights = {'spawn_weights', 'generation_weights'}

function p.mod_table(frame)
    tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
    
    for _, key in ipairs(mod_table.weights) do
        tpl_args[key] = m_util.cast.boolean(tpl_args[key])
    end
    
    if string.find(tpl_args.q_where, '%[%[') ~= nil then
        error('SMW leftover in where clause')
    end
    
    local row_infos = {}
    for _, row_info in ipairs(mod_table.data) do
        local enabled = false
        if row_info.arg == nil then
            enabled = true
        elseif type(row_info.arg) == 'string' and m_util.cast.boolean(tpl_args[row_info.arg]) then
            enabled = true
        elseif type(row_info.arg) == 'table' then 
            for _, argument in ipairs(row_info.arg) do
                if m_util.cast.boolean(tpl_args[argument]) then
                    enabled = true
                    break
                end
            end
        end
        
        if enabled then
            row_info.options = row_info.options or {}
            row_infos[#row_infos+1] = row_info
        end
    end
    
    -- sort the rows
    table.sort(row_infos, function (a, b)
        return (a.order or 0) < (b.order or 0)
    end)
    
    -- Set tables
    local tables = 'mods' 
    if tpl_args.q_tables then
        tables = tables .. ',' .. tpl_args.q_tables
    end
    
    
    -- Set required fields
    local fields = {
        'mods._pageID',
    }
    for _, rowinfo in ipairs(row_infos) do
        if type(rowinfo.fields) == 'function' then
            rowinfo.fields = rowinfo.fields()
        end
        for index, field in ipairs(rowinfo.fields) do
            rowinfo.options[index] = rowinfo.options[index] or {}
            fields[#fields+1] = field
        end
    end
    
    -- Parse query arguments
    local query = {
        -- Workaround: fix duplicates
        groupBy='mods._pageID',
    }
    for key, value in pairs(tpl_args) do 
        if string.sub(key, 0, 2) == 'q_' then
            query[string.sub(key, 3)] = value
        end
    end
    
    
    fields = table.concat(fields, ',')
    if tpl_args.q_fields then
        fields = fields .. ' ,' .. tpl_args.q_fields
    end
    
    local results = cargo.query(
        tables,
        fields,
        query
    )
    
    if #results == 0 then
        if tpl_args.default ~= nil then
            return tpl_args.default
        else
            return 'No results found'
        end
    end
    
    -- this might be needed in other queries, currently not checking if it's actually needed
    -- because performance impact should be neglible
    local page_ids = {}
    for _, row in ipairs(results) do
        page_ids[#page_ids+1] = string.format('mods._pageID="%s"', row['mods._pageID'])
    end
    page_ids = table.concat(page_ids, ' OR ')
    
    local weights = {}
    for _, key in ipairs(mod_table.weights) do
        if tpl_args[key] then
            weights[key] = h.query_weights(key, page_ids)
        end
    end
    
    local stats
    if tpl_args.type == 'map' then
        local query_stat_ids = {}
        for k, _ in pairs(mod_table.stat_ids) do
            query_stat_ids[#query_stat_ids+1] = string.format('mod_stats.id="%s"', k)
        end
        
        local stat_results = cargo.query(
            'mods,mod_stats',
            'mods._pageID,mod_stats.id,mod_stats.min,mod_stats.max',
            {
                where=string.format('(%s) AND (%s)', page_ids, table.concat(query_stat_ids, ' OR ')),
                join='mods._pageID=mod_stats._pageID',
                orderBy='mods.id ASC',
                limit=5000,
            }
        )
        if #stat_results == 5000 then
            error('Hit maximum cargo results')
        end
        
        stats = m_util.cargo.map_results_to_id{results=stat_results, field='mods._pageID'}
        -- In addition map stats to stat <-> min/max pairs
        for page_id, rows in pairs(stats) do
            local stat_id_map = {}
            for _, row in ipairs(rows) do
                stat_id_map[row['mod_stats.id']] = {min=tonumber(row['mod_stats.min']), max=tonumber(row['mod_stats.max'])}
            end
            stats[page_id] = stat_id_map
        end
    end
    
    --
    -- Display
    --
    
    -- Preformance optimization
    if tpl_args.q_fields then
        tpl_args._extra_fields = m_util.string.split(tpl_args.q_fields, ',')
        for index, field in ipairs(tpl_args._extra_fields) do
            field = m_util.string.split(field, '=')
            -- field[2] will be nil if there is no alias
            tpl_args._extra_fields[index] = field[2] or field[1]
        end
    else
        tpl_args._extra_fields = {}
    end
    
    local tbl = mw.html.create('table')
    tbl:attr('class', 'wikitable sortable modifier-table')
    -- Header
    
    local tr = tbl:tag('tr')
    for _, row_info in ipairs(row_infos) do
        tr
            :tag('th')
                :attr('data-sort-type', row_info.sort_type or 'number')
                :wikitext(row_info.header)
                :done()
    end
    
    if tpl_args.type == 'map' then
        for stat_id, data in pairs(mod_table.stat_ids) do
            tr
                :tag('th')
                    :attr('data-sort-type', 'number')
                    :wikitext(data.header)
                    :done()
        end
    end
    
    for _, key in ipairs(mod_table.weights) do
        if tpl_args[key] then
            tr
                :tag('th')
                    :wikitext(i18n.mod_table[key])
        end
    end
    
    for _, field in ipairs(tpl_args._extra_fields) do
        tr
            :tag('th')
                :wikitext(field)
    end
    
    -- Body
    
    for _, row in ipairs(results) do
        tr = tbl:tag('tr')
                
        for _, rowinfo in ipairs(row_infos) do
            -- this has been cast from a function in an earlier step
            local display = true
            for index, field in ipairs(rowinfo.fields) do
                -- this will bet set to an empty value not nil confusingly
                if row[field] == '' then
                    if rowinfo.options[index].optional ~= true then
                        display = false
                        break
                    else
                        row[field] = nil
                    end
                end
            end
            if display then
                rowinfo.display(tpl_args, frame, tr, row, rowinfo.fields)
            else
                tr:wikitext(m_util.html.td.na())
            end
        end
        
        if tpl_args.type == 'map' then
            for stat_id, data in pairs(mod_table.stat_ids) do
                local stat_data = stats[row['mods._pageID']]
                if stat_data and stat_data[stat_id] then
                    local v = stat_data[stat_id]
                    local text
                    if v.min == v.max then
                        text = v.min
                    else
                        text = string.format('(%s to %s)', v.min, v.max)
                    end
                    tr
                        :tag('td')
                            :attr('data-sort-value', (v.min+v.max)/2)
                            :wikitext(string.format('%s%%', text))
                            :done()
                else
                    tr:wikitext(m_util.html.td.na())
                end
            end
        end
        
        for _, key in ipairs(mod_table.weights) do
            if tpl_args[key] then
                local weight_out = {}
                for _, wrow in ipairs(weights[key][row['mods._pageID']]) do
                    weight_out[#weight_out+1] = string.format('%s %s', wrow[key .. '.tag'], wrow[key .. '.weight'])
                end
                if #weight_out > 0 then
                    tr
                            :tag('td')
                                :wikitext(table.concat(weight_out, '<br>'))
                                :done()
                else    
                    tr:wikitext(m_util.html.td.na())
                end
            end
        end
        
        for _, field in ipairs(tpl_args._extra_fields) do
            if row[field] then
                tr
                    :tag('td')
                        :wikitext(row[field])
            else
                tr:wikitext(m_util.html.td.na())
            end
        end
    end
    
    return tostring(tbl)
end




-- ---------------------------------------------------------------------
-- Modifier link
-- ---------------------------------------------------------------------

function p.modifier_link(frame)
    --[[
    Finds and links to a modifier in formatted form.
    
    Examples: 
    = p.modifier_link{"Tyrannical"}
    = p.modifier_link{"Flaring"}
    = p.modifier_link{"Dictator's"}
    = p.modifier_link{"StrDexMaster%"}
    = p.modifier_link{"LocalIncreasedPhysicalDamagePercentAndAccuracyRating8", display='max', statid='local_physical_damage_+%'}
    
    ]]

    -- Get template args:
    local tpl_args = getArgs(frame, {parentFirst = true})
    local frame = m_util.misc.get_frame(frame)
        
    -- Aliases:
    tpl_args.modid = tpl_args.modid or tpl_args.id or tpl_args[1] or ''
    
    -- Define query arguments:
    local tables = {'mods', 'mod_stats', 'spawn_weights'}
    local fields = {'mods.name', 'mods.stat_text', 'mods.stat_text_raw', 'mods.generation_type', 'mods._pageName', 'mod_stats.max', 'mod_stats.min', 'spawn_weights.tag', 'spawn_weights.weight', 'mods.id', 'mod_stats.id'}
    local query = {
        join = 'mods._pageName=mod_stats._pageName, mods._pageName=spawn_weights._pageName',
        where = string.format(
            '(mods.name LIKE "%s" or mods.id LIKE "%s" or mods.stat_text LIKE "%s" or mods.stat_text_raw LIKE "%s") AND mod_stats.id LIKE "%%%s%%"', 
            tpl_args.modid, 
            tpl_args.modid,
            tpl_args.modid,
            tpl_args.modid,
            tpl_args.statid or '%' 
        ),
        -- groupBy = 'mods._pageID, mod_stats.id, spawn_weights.tag',
    }
    
    -- Query cargo rows:
    local results = m_util.cargo.query(tables, fields, query, args)
    
    -- Create own list for each cargo table and group by page name:
    tpl_args.tbl = {}
    for _,v in ipairs(tables) do 
        tpl_args.tbl[v] = {}
    end
    
    tpl_args.results_unique = {}
    local hash = {}
    for _,v in ipairs(results) do
        for ii, vv in pairs(tpl_args.tbl) do 
            if tpl_args.tbl[ii][v['mods._pageName']] == nil then 
                tpl_args.tbl[ii][v['mods._pageName']] = {}
            end
            local n = #tpl_args.tbl[ii][v['mods._pageName']] or 0
            tpl_args.tbl[ii][v['mods._pageName']][n+1] = v
            
            -- Get a sorted list that only has unique page names:
            if hash[v['mods._pageName']] ~= true then
                local m = #tpl_args.results_unique
                tpl_args.results_unique[m+1] = v
                hash[v['mods._pageName']] = true
            end
        end
    end 
    
    -- Helpful error handling:
    local err_tbl = {
        {
            bool = #results == 0,
            disp = {
                i18n.errors.no_results,
            }
        },
        {
            bool = #tpl_args.results_unique > 1,
            disp = {
                i18n.errors.multiple_results,
                h.disambiguate_mod_name(tpl_args.results_unique),
            },
        },
        {
            bool = tpl_args.modid ~= tpl_args.results_unique[1]['mods.id'],
            disp = {
                string.gsub(
                    i18n.errors.incorrect_modid, 
                    '%%s',
                    tpl_args.modid,
                    1
                ),
                h.disambiguate_mod_name(tpl_args.results_unique),
            },
        },
    }
    for _,v in ipairs(err_tbl) do 
        if v.bool then
            local cats = {'Pages with modifier link errors'}
            return m_util.html.error(
                {msg = string.format(v.disp[1], v.disp[2]) .. m_util.misc.add_category(cats)}
            )
        end
    end
    
    -- Display formats:
    local display = {
        abbr = {
            display = function(tpl_args, frame)
                local stat_text_colored = m_util.html.poe_color(
                    'mod', 
                    string.format(
                        '[[%s|%s]]', 
                        tpl_args.results_unique[1]['mods._pageName'], 
                        tpl_args.results_unique[1]['mods.name'] or tpl_args.results_unique[1]['mods.id']
                    )
                )
                
                local out = m_util.html.tooltip(
                    stat_text_colored, 
                    tpl_args.results_unique[1]['mods.stat_text_raw'], 
                    class
                )
                return out
            end,
        },
        verbose = {
            display = function(tpl_args, frame)
                return string.format(
                    '%s - %s (%s)',
                    m_util.html.poe_color(
                        'mod', 
                        string.format(
                            '[[%s|%s]]', 
                            tpl_args.results_unique[1]['mods._pageName'], 
                            tpl_args.results_unique[1]['mods.name'] or tpl_args.results_unique[1]['mods.id']
                        )
                    ),
                    m_util.html.poe_color(
                        'mod', 
                        string.gsub(
                            tpl_args.results_unique[1]['mods.stat_text'], 
                            '<br>', 
                            ', '
                        )
                    ),
                    m_game.constants.mod.generation_types[
                        tonumber(
                            tpl_args.results_unique[1]['mods.generation_type']
                        )
                    ].full
                )
            end,
        },
        stat_text = {
            display = function(tpl_args, frame)
                return m_util.html.poe_color(
                    'mod', 
                    tpl_args.results_unique[1]['mods.stat_text']
                )
            end,
        },
        max = {
            display = function(tpl_args, frame)
                local statid = {}
                for _,v in ipairs(tpl_args.tbl.mod_stats[tpl_args.results_unique[1]['mods._pageName']]) do 
                    statid[#statid+1] = v['mod_stats.id']
                    if tpl_args.statid == v['mod_stats.id'] then
                        return v['mod_stats.max']
                    end
                end
                
                if tpl_args.statid == nil then
                    return m_util.html.error(
                        {
                            msg = string.format(
                                i18n.errors.undefined_statid, 
                                table.concat(statid, ', ')
                            )
                        }
                    )
                end
            end
        },
        min = {
            display = function(tpl_args, frame)
                local statid = {}
                for _,v in ipairs(tpl_args.tbl.mod_stats[tpl_args.results_unique[1]['mods._pageName']]) do 
                    statid[#statid+1] = v['mod_stats.id']
                    if tpl_args.statid == v['mod_stats.id'] then
                        return v['mod_stats.min']
                    end
                end
                
                if tpl_args.statid == nil then
                    return m_util.html.error(
                        {
                            msg = string.format(
                                i18n.errors.undefined_statid, 
                                table.concat(statid, ', ')
                            )
                        }
                    )
                end
            end
        },
    }
    
    return display[tpl_args.display or 'abbr'].display(tpl_args, frame)
end




-- ---------------------------------------------------------------------
-- Drop down list
-- ---------------------------------------------------------------------
    
function h.get_mod_domain(cargo_query)
    --[[ 
	Gets the mod domain based on the item class.
    ]]
	
    local out = cargo_query
    local mod_domains = m_game.constants.mod.domains
    
    -- Set the item class as key and the corresponding mod domain as 
    -- value:
    local class_to_domain = {
        ['Life Flasks']=2,
        ['Mana Flasks']=2,
        ['Hybrid Flasks']=2,
        ['Utility Flasks']=2,
        ['Critical Utility Flasks']=2,
        ['Maps']=5,
        ['Jewel']=10,
        ['Leaguestones']=12,
        ['Abyss Jewel']=13,
    }
    
    for i,_ in ipairs(out) do
        -- Get the domain, if it's not defined in the table assume it's 
        -- in the item domain.
        out[i]['items.domain'] = class_to_domain[out[i]['items.class']] or 1
        
        -- Convert the mod domain number to understandable text:
        out[i]['items.domain_text'] = mod_domains[out[i]['items.domain']]['short_lower']
    end

    return out
end

function h.get_item_tags(frame)
    --[[ 
    This function queries for the tags of a specific item.
    ]]
    
    -- Get template arguments:
    local tpl_args = getArgs(frame, {parentFirst=true})
    local frame = m_util.misc.get_frame(frame)
    
    -- Format the cargo query:
    tpl_args.q_tables = 'items'
    tpl_args.q_fields = 'items.name, items.tags, items.class, items.inventory_icon, items.html, items._pageName'
    local tbl = {
        {tpl_args.page, 'items._pageName = "%s"'},
        {tpl_args.item, 'items.name = "%s"'},
    }
    for _,v in ipairs(tbl) do 
        if v[1] ~= nil then
            condition = string.format(v[2], v[1])
            break
        end
    end
    tpl_args.q_where = condition
    tpl_args.q_groupBy = 'items._pageName'
    tpl_args.q_orderBy = 'items.name'

    -- Query mods with cargo:
    results = h.cargo_query(tpl_args)
    
    -- Find out what mod domain an item class is in:
    results = h.get_mod_domain(results)

    return results
end

function h.get_spawn_chance(frame)
    --[[
    Calculates the spawn chance of a set of mods that all have a 
    spawn weight.
    
    ]]
	
    -- Get template arguments:
    local tpl_args = getArgs(frame, {parentFirst=true})
    local frame = m_util.misc.get_frame(frame)
    
    -- Get the table:
    local tbl = tpl_args['tbl']
    
    -- Probabilities affecting the result besides the spawn weight:
    local chance_multiplier = tonumber(tpl_args['chance_multiplier']) or 1 
    
    -- Total number of outcomes.
    local N = 0
    for i,_ in ipairs(tbl) do
        N = N + tbl[i]['spawn_weights.weight']
    end
    
    for i,_ in ipairs(tbl) do
        -- Number of ways it can happen:
        local n = tbl[i]['spawn_weights.weight']

        -- Truncated value:
        tbl[i]['spawn_weights.chance'] = string.format(
            "%0.2f%%", 
            n/N * chance_multiplier*100
        )
    end 
    
    return tbl
end
    
function p.drop_down_table(frame)
    --[[
    This function queries mods that can spawn on an item. It compares 
    the item tags and the spawn weight tags. If there's a match and 
    the spawn weight is larger than zero, then that mod is added to a 
    drop down list.
    
    To Do:
    * Add support to:
        * Forsaken masters
        * Bestiary
    * Add a proper expand/collapse toggle for the entire header row so 
        it reacts together with mw-collapsible. 
    * Show Mod group in a better way perhaps: 
        Mod group [Collapsible, default=Expanded]
            # to Damage [Collapsible, default=Collapsed]
                3 to Damage
                5 to Damage
    * Add a where condition that somehow filters out mods that obviously 
      wont match with the item. spawn_weights.weight>0 isn't enough due 
      to possible edge cases.
    * Consider moving the where variable to section table.
		
    
    Examples: 
    Weapons
    p.drop_down_table{item = 'Rusted Hatchet', header = 'One Handed Axes'}
    p.drop_down_table{item = 'Stone Axe', header = 'Two Handed Axes'}

    Accessories
    p.drop_down_table{item = 'Amber Amulet', header = 'Amulets'}
    
    Jewels
    p.drop_down_table{item = 'Cobalt Jewel', header = 'Jewels'}
    
    Armour
    p.drop_down_table{item = 'Plate Vest', header = 'Body armours'}
    
    Boots
    p.drop_down_table{item = 'Iron Greaves', header = 'Boots'}
    
    p.drop_down_table{
        item = 'Fishing Rod', 
        header = 'FISH PLEASE', 
        item_tags = 'fishing_rod', 
        extra_fields = 'Has spawn weight, Has spawn chance'
    }
    
    = p.drop_down_table{
        item = 'Fishing Rod',
        item_tags = 'axe, one_hand_weapon, onehand, weapon, default',
        extra_item_tags = 'fishing_rod'
    }
	
    = p.drop_down_table{
        item = 'Vaal Blade',
    }
        
    ]]

    -- Get template arguments:
    local tpl_args = getArgs(frame, {parentFirst=true})
    local frame = m_util.misc.get_frame(frame)
    
    -- Get the items tags:
    tpl_args.get_item_tags = h.get_item_tags(tpl_args)[1]
    
    -- For some reason cargo queried item tags, are not comma-space 
    -- separated any more.
    if tpl_args.item_tags ~= nil then
        tpl_args.item_tags = m_util.string.split(tpl_args.item_tags, ', ')
    else
        tpl_args.item_tags = m_util.string.split(tpl_args.get_item_tags['items.tags'], ',')
    end
    if tpl_args.extra_item_tags then
        local extra_item_tags = m_util.string.split(tpl_args.extra_item_tags, ', ')
        for _,v in ipairs(extra_item_tags) do 
            tpl_args.item_tags[#tpl_args.item_tags+1] = v
        end
    end
    
    -- Create drop down lists in these sections and query in these
    -- generation types.
    local section = {}
    section = {
        {
            header = i18n.drop_down_table.prefix,
            where = function(tpl_args, frame, value)
                local where = {}
                for _, item_tag in ipairs(tpl_args.item_tags) do 
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s)',
                        item_tag,
                        1,
                        tpl_args.get_item_tags['items.domain']
                    )
                end
                
                return table.concat(where, ' OR ') 
            end,
        },
        {
            header = i18n.drop_down_table.suffix,
            where = function(tpl_args, frame, value)
                local where = {}
                for _, item_tag in ipairs(tpl_args.item_tags) do 
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s)',
                        item_tag,
                        2,
                        tpl_args.get_item_tags['items.domain']
                    )
                end
                
                return table.concat(where, ' OR ') 
            end,
        },
        {
            header = i18n.drop_down_table.elder_prefix,
            where = function(tpl_args, frame, value)
                local suffix = '_elder'
                local where = {}
                for i, item_tag in ipairs(tpl_args.item_tags) do 
                    if not string.match(item_tag, suffix) then 
                        tpl_args.item_tags[i] = item_tag .. suffix
                    end
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s)',
                        tpl_args.item_tags[i],
                        1,
                        tpl_args.get_item_tags['items.domain']
                    )
                end
                
                return table.concat(where, ' OR ') 
            end,
        },
        {
            header = i18n.drop_down_table.elder_suffix,
            where = function(tpl_args, frame, value)
                local suffix = '_elder'
                local where = {}
                for i, item_tag in ipairs(tpl_args.item_tags) do 
                    if not string.match(item_tag, suffix) then 
                        tpl_args.item_tags[i] = item_tag .. suffix
                    end
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s)',
                        tpl_args.item_tags[i],
                        2,
                        tpl_args.get_item_tags['items.domain']
                    )
                end
                
                return table.concat(where, ' OR ') 
            end,
        },
        {
            header = i18n.drop_down_table.shaper_prefix,
            where = function(tpl_args, frame, value)
                local suffix = '_shaper'
                local where = {}
                for i, item_tag in ipairs(tpl_args.item_tags) do 
                    if not string.match(item_tag, suffix) then 
                        tpl_args.item_tags[i] = item_tag .. suffix
                    end
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s)',
                        tpl_args.item_tags[i],
                        1,
                        tpl_args.get_item_tags['items.domain']
                    )
                end
                
                return table.concat(where, ' OR ') 
            end,
        },
        {
            header = i18n.drop_down_table.shaper_suffix,
            where = function(tpl_args, frame, value)
                local suffix = '_shaper'
                local where = {}
                for i, item_tag in ipairs(tpl_args.item_tags) do 
                    if not string.match(item_tag, suffix) then 
                        tpl_args.item_tags[i] = item_tag .. suffix
                    end
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s)',
                        tpl_args.item_tags[i],
                        2,
                        tpl_args.get_item_tags['items.domain']
                    )
                end
                
                return table.concat(where, ' OR ') 
            end,
        },
        {
            header = i18n.drop_down_table.corrupted,
            where = function(tpl_args, frame, value)
                local where = {}
                for _, item_tag in ipairs(tpl_args.item_tags) do 
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s)',
                        item_tag,
                        5,
                        tpl_args.get_item_tags['items.domain']
                    )
                end
                
                return table.concat(where, ' OR ') 
            end,
            chance_multiplier = 1/4, -- See Vaal orb, for the 4 possible events. 
        },
        {
            header = i18n.drop_down_table.enchant,
            where = function(tpl_args, frame, value)
                local where = {}
                for _, item_tag in ipairs(tpl_args.item_tags) do 
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s)',
                        item_tag,
                        10,
                        tpl_args.get_item_tags['items.domain']
                    )
                end
                
                return table.concat(where, ' OR ') 
            end,
        },
    }
      
       
    -- Introductory text:
    local out = {}
    out[#out+1] = string.format(
        '==%s== \n', 
        tpl_args['header'] or table.concat(tpl_args.item_tags, ', ')
    )
    out[#out+1] = string.format(
        '<div style="float: right; text-align:center"><div class="mw-collapsible-collapse-all" style="cursor:pointer;">[%s]</div><hr><div class="mw-collapsible-expand-all" style="cursor:pointer;">[%s]</div></div>',
        i18n.drop_down_table.collapse_all,
        i18n.drop_down_table.expand_all
    )
    out[#out+1] = string.format('%s %s.<br><br><br>', 
        i18n.drop_down_table.table_intro, 
        f_item_link{
            page=tpl_args.get_item_tags['items._pageName'], 
            name=tpl_args.get_item_tags['items.name'],
            inventory_icon=tpl_args.get_item_tags['items.inventory_icon'] or '', 
            html=tpl_args.get_item_tags['items.html'] or '', 
            skip_query=true
        }
    )
    
    -- Save the original tag format:
    local item_tags_orig = {}
    for i,v in ipairs(tpl_args.item_tags) do
        item_tags_orig[i] = v
    end
    
    local item_mods = {}
    local mod_group_counter = {}
    mod_group_counter['all'] = {}
    local tableIndex = -1  
    for _, sctn in ipairs(section) do
        -- Reset to original tags:
        for i,v in ipairs(item_tags_orig) do
            tpl_args.item_tags[i] = v
        end
        
        -- Create html container:
        local container = mw.html.create('div')
            :attr('style', 'vertical-align:top; display:inline-block;')
        
        -- Cargo preparation:
        tpl_args.q_tables = 'mods, spawn_weights, mod_stats'
        tpl_args.q_fields = 'mods.name, mods.id, mods.required_level, mods.generation_type, mods.domain, mods.mod_group, mods.mod_type, mods.stat_text, mods._pageName, mod_stats.id, spawn_weights.tag, spawn_weights.weight, spawn_weights.ordinal, spawn_weights._pageName'
        tpl_args.q_join = 'mods._pageName=spawn_weights._pageName, mods._pageName=mod_stats._pageName'
        tpl_args.q_where = sctn['where'](tpl_args, frame, value)
        tpl_args.q_groupBy = 'mods._pageName, spawn_weights.tag, spawn_weights.weight'
        tpl_args.q_orderBy = 'mods.generation_type, mods.mod_group, mods.mod_type, mods._pageName, mods.required_level, spawn_weights.ordinal'
        
        local extra_fields = {}
        if tpl_args.extra_fields ~= nil then
            extra_fields = m_util.string.split(tpl_args.extra_fields, ', ')
            tpl_args.fields = string.format(
                '%s, %s', 
                tpl_args.fields, 
                table.concat(extra_fields, ', ')
            )
        end
        
        -- Query mods:
        results = h.cargo_query(tpl_args)
		
        -- Create own list for spawn weights and group by page name:
        local spawn_weights = {}
		local results_unique = {}
		local hash = {}
        for _,v in ipairs(results) do
            if spawn_weights[v['mods._pageName']] == nil then 
                spawn_weights[v['mods._pageName']] = {}
            end
            local n = #spawn_weights[v['mods._pageName']] or 0
            spawn_weights[v['mods._pageName']][n+1] = v
			
            -- Get a sorted list that only has unique page names:
            if hash[v['mods._pageName']] ~= true then
                results_unique[#results_unique+1] = v
                hash[v['mods._pageName']] = true
            end
        end
        
        if #results_unique > 0 then 
            item_mods[sctn['header']] = {}
            mod_group_counter[sctn['header']] = {}
                      
            -- Loop through all found modifiers:
            local last
            for _, v in ipairs(results_unique) do   
                local pagename = v['spawn_weights._pageName']
                
                -- Loop through all the modifier tags until they match 
                -- the item tags:
                local j = 0
                local tag_match_stop
                repeat 
                    j = j+1
                    local mod_tag = spawn_weights[pagename][j]['spawn_weights.tag']
                    local mod_tag_weight = tonumber(
                        spawn_weights[pagename][j]['spawn_weights.weight']
                    )

                    -- Loop through the item tags until it matches the 
                    -- spawn weight tag and the mod tag has a value larger than 
                    -- zero:
                    local y = 0
                    local tag_match_add = false
                    repeat     
                        y = y+1
                        tag_match_stop = ((mod_tag == tpl_args.item_tags[y]) and ((mod_tag_weight or -1) >= 0)) or (spawn_weights[pagename][j] == nil)
                        tag_match_add  =  (mod_tag == tpl_args.item_tags[y]) and ((mod_tag_weight or -1) > 0)
                    until tag_match_stop or y == #tpl_args.item_tags
                    
                    -- If there's a match then save that mod and other 
                    -- interesting information:
                    if tag_match_add then 
                        
                        -- Assume that the mod is global then go through 
                        -- all the stat ids and check if any of the 
                        -- stats are local:
                        local mod_scope = 'Global' 
                        for _, vv in ipairs(spawn_weights[pagename]) do 
                            if vv['mod_stats.id']:find('.*local.*') ~= nil then 
                                mod_scope = 'Local'
                            end
                        end 
                        
                        -- Save the matching modifier tag:
                        local a = #item_mods[sctn['header']]
                        item_mods[sctn['header']][a+1] = spawn_weights[pagename][j]
                        
                        -- Save other interesting fields:
                        item_mods[sctn['header']][a+1]['mods.scope'] = mod_scope     
                        item_mods[sctn['header']][a+1]['spawn_weight.idx_match'] = j                        
						item_mods[sctn['header']][a+1]['mods.add'] = tag_match_add     
						item_mods[sctn['header']][a+1]['mods.stop'] = tag_match_stop
                        -- Mod group counter:
                        local group = item_mods[sctn['header']][a+1]['mods.mod_group'] or 'nil_group'
                        mod_group_counter[sctn['header']][group] = 1 + (mod_group_counter[sctn['header']][group] or 0)
                        mod_group_counter['all'][group] = 1 + (mod_group_counter['all'][group] or 0)
                    end
                until tag_match_stop 
            end
            
            -- If the user wants to see the spawn chance then do the 
            -- calculations and save that result as well:
            if tpl_args.spawn_chance ~= nil then 
                extra_fields[#extra_fields+1] = 'spawn_weights.chance'
                item_mods[sctn['header']] = h.get_spawn_chance{
                    tbl = item_mods[sctn['header']], 
                    chance_multiplier = sctn['chance_multiplier']
                }
            end
            
            -- Create the drop down table with <table></table>:
            local headers = container 
            if #item_mods[sctn['header']] > 0 then 
                headers
                    :tag('h3')
                        :wikitext(string.format(
                            '%s', 
                            sctn['header']
                            )
                        )
                        :done()
                    :done()
            end
            
            local total_mod_groups = 0
            for k,v in pairs(mod_group_counter[sctn['header']]) do
                total_mod_groups = 1+total_mod_groups
            end
                
            -- Loop through and add all matching mods to the <table>. 
            local tbl, last_group, last_type
            for _, rows in ipairs(item_mods[sctn['header']]) do   
            
                -- If the last mod group is different to the current 
                -- mod group then assume the mod isn't related and start 
                -- a new drop down list, if there's only one mod group 
                -- then use mod type instead:
                if rows['mods.mod_group'] ~= last_group or (total_mod_groups == 1 and rows['mods.mod_type'] ~= last_type) then
                    -- Check through all the mods and see if there are 
                    -- multiple mod types within the same mod group:
                    local count = {}
                    for _, n in ipairs(item_mods[sctn['header']]) do 
                        
                        -- If the mod has the same mod group, then add 
                        -- the mod type to the counter. Only unique mod 
                        -- types matter so the number is just a dummy 
                        -- value:
                        if n['mods.mod_group'] == rows['mods.mod_group'] then
                            count[n['mods.mod_type']] = 1 
                        end
                    end
                    
                    -- Calculate how many unique mod types with the 
                    -- same mod group there are:
                    local number_of_mod_types = 0
                    for _ in pairs(count) do 
                        number_of_mod_types = number_of_mod_types + 1 
                    end
                    
                    -- If there are multiple unique mod types with the 
                    -- same mod group then change the style of the drop 
                    -- down list to indicate it, if there's only one 
                    -- mod group in the generation type then ignore it:
                    if number_of_mod_types > 1 and total_mod_groups > 1 then 
                        tbl_caption = string.format(
                            '%s', 
                            m_util.html.poe_color(
                                'mod', 
                                string.format(
                                    '%s %s', 
                                    i18n.drop_down_table.mod_group, 
                                    rows['mods.mod_group']
                                )
                            )
                        )

                    else
                        tbl_caption = string.format(
                            '%s (%s)', 
                            m_util.html.poe_color(
                                'mod', 
                                h.header(rows['mods.stat_text'])
                            ), 
                            rows['mods.scope']
                        )
                    end
                    
                    -- Add class and style to the <table>:
                    tableIndex = tableIndex+1
                    tbl = container:tag('table')
                    tbl
                        :attr('class', 'mw-collapsible mw-collapsed')
                        :attr('style', 
                            'text-align:left; line-height:1.60em; width:810px;'
                        )
                        :tag('th')
                            :attr('class', 
                                string.format(
                                    'mw-customtoggle-%s', 
                                    tableIndex
                                )
                            )
                            :attr('style', 
                                'text-align:left; line-height:1.40em; border-bottom:1pt solid dimgrey;'
                            )
                            :attr('colspan', '3' .. #extra_fields)
                            :wikitext(tbl_caption)
                            :done()
                        :done()    
                end
                
                -- If the mod has no name then use the mod type:
                local mod_name = rows['mods.name']
                if  mod_name == '' or mod_name == nil then 
                    mod_name = rows['mods.mod_type']
                end
                
                -- Check if there are any extra properties to show in 
                -- the drop down list and then add a cell for that, 
                -- add this node at the end of the table row:
                local td = mw.html.create('td')
                if extra_fields ~= nil then 
                    for _, extra_field in ipairs(extra_fields) do 
                        td
                            :attr('width', '*')
                            :wikitext(string.format(
                                '%s:&nbsp;%s ', 
                                extra_field, 
                                rows[extra_field]
                                )
                            )
                            :done()
                    end
                end
                
                -- Add a table row with the interesting properties that 
                -- modifier has:
                tbl
                    :tag('tr')
                        :attr('class', 'mw-collapsible mw-collapsed')
                        :attr(
                            'id', 
                            string.format(
                                'mw-customcollapsible-%s', 
                                tableIndex
                            )
                        )
                        :tag('td')
                            :attr('width', '160')
                            :wikitext(
                                string.format(
                                    '&nbsp;&nbsp;&nbsp;[[%s|%s]]', 
                                    rows['mods._pageName'], 
                                    mod_name:gsub('%s', '&nbsp;')
                                )
                            )
                            :done()
                        :tag('td')
                            :attr('width', '1')
                            :wikitext(
                                string.format(
                                    '%s&nbsp;%s',
                                    m_game.level_requirement['short_upper']
                                        :gsub('%s', '&nbsp;'), 
                                    rows['mods.required_level']
                                )
                            )
                            :done()        
                        :tag('td')
                            :attr('width', '*')
                            :wikitext(
                                string.format(
                                    '%s', 
                                    m_util.html.poe_color(
                                        'mod', 
                                        rows['mods.stat_text']
                                            :gsub('<br>', ', ')
                                            :gsub('<br />', ' ')
                                    )  
                                )
                            )
                            :done()
                        :node(td)
                        :done()
                    :done()
                
                -- Save the last mod group for later comparison:
                last_group = rows['mods.mod_group']
                last_type = rows['mods.mod_type']
            end
        end
        
        out[#out+1] = tostring(container)
    end
    
    return table.concat(out,'')
end

-- ----------------------------------------------------------------------------
-- Debug functions
-- ----------------------------------------------------------------------------

p.debug = {}
function p.debug.tbl_data(tbl)
    keys = {}
    for _, data in ipairs(mod_table.data) do
        if type(data.arg) == 'string' then 
            keys[data.arg] = 1
        elseif type(data.arg) == 'table' then
            for _, arg in ipairs(data.arg) do
                keys[arg] = 1
            end
        end
    end
    
    for _, key in ipairs(mod_table.weights) do
        keys[key] = 1
    end
    
    local out = {}
    for key, _ in pairs(keys) do
        out[#out+1] = string.format("['%s'] = '1'", key)
    end
    
    return table.concat(out, ', ')
end

-- ----------------------------------------------------------------------------
--
-- ----------------------------------------------------------------------------

return p