Module:Pieces

From Valheim Wiki
Revision as of 15:54, 1 January 2023 by Mave (talk | contribs)

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

local item_link = require('Module:Item').go
local is_crafting_station = require('Module:Item').is_crafting_station
local trim = mw.text.trim
local cargo = mw.ext.cargo
local cache = require 'mw.ext.LuaCache'

local currentFrame -- global cache for current frame object.
local inputArgs -- global args cache.
local lang -- cache current lang.

local pieceanchor

local l10n = function(key)
    return key
end

local extCols_stationBefore = nil
local extCols_stationAfter = nil

local extCols_A = nil
local extCols_B = nil
local extCols_C = nil
local extCols_D = nil

function getArg(key)
    local v = trim(inputArgs[key] or '')
    if v=='' then
        return nil
    else
        return v
    end
end

local itemLink = (function()
    local cache = {}
    return function(name, args)
        local key = name.."|"
        if args then
            for k, v in pairs(args) do
                key = key..k..'='..tostring(v)..'|'
            end
        end
        if not cache[key] then
            local args = args and mw.clone(args) or {}
            args[1] = name
            if (not args[2]) or args[2]=='' then
                args[2] = currentFrame:expandTemplate{ title = 'tr', args = {name, lang=lang} }
            end
            args['small'] = 'y'
            args['lang'] = lang or 'en'
            args['nolink'] = args['nolink'] and 'y' or nil
            local mode = args['mode'] or nil
            if mode == nil and name:lower() == 'by hand' then
                mode = 'noimage'
            end
            if mode ~= nil then
                args['mode'] = mode
            end
            cache[key] = item_link(currentFrame, args)
        end
        return cache[key]
    end
end)()

-- credit: http://richard.warburton.it
-- this version is with trim.
local explode = function(div,str)
    if (div=='') then return false end
    local pos,arr = 0,{}
    -- for each divider found
    for st,sp in function() return string.find(str,div,pos,true) end do
        table.insert(arr, trim(string.sub(str,pos,st-1))) -- Attach chars left of current divider
        pos = sp + 1 -- Jump past current divider
    end
    table.insert(arr, trim(string.sub(str,pos))) -- Attach chars right of last divider
    return arr
end

-- return an array of itemname, split xxx/yyy to item1=xxx, item2=yyy. If it's something like "Lead/Iron Bar", it will normalize as item1 = Iron Bar, item2 = Lead Bar.
local split = (function()
    local metals = {
        ['Copper/Tin'] = 1,
        ['Tin/Copper'] = 2,
    }
    return function(name)
        local count = select(2, name:gsub("/", "/", 2))
        if count == 0 then
            -- only 1 item
            return { trim(name) }
        elseif count == 1 then
            -- 2 items
            local item1a, item1b, item2a, item2b = name:match("^%s*(%S+)%s*(.-)/%s*(%S+)%s*(.-)$")
            local x = metals[item1a..'/'..item2a]
            if tostring(item1b) == '' and x then
                item1b = item2b
            end
            if x == 2 then
                return {trim(item2a..' '..item2b), trim(item1a..' '..item1b)}
            else
                return {trim(item1a..' '..item1b), trim(item2a..' '..item2b)}
            end
        else
            -- 3 or more items
            return explode('/', name)
        end
    end
end)()

-- return 1 or 2 value(s), when input is name[note], return item, note.
local itemname = function(str)
    local item, note = str:match("^(.-)(%b[])$")
    if item then
        return item, note
    else
        return str
    end
end

-- normalize ingredient name input, Lead Bar=>¦Lead Bar¦, Iron/Lead Bar => ¦Iron Bar¦Lead Bar¦, Lead/Iron Bar => ¦Iron Bar¦Lead Bar¦ ....
local normalize = function(name)
    local piece = '¦'
    for k, v in ipairs(split(name)) do
        piece = piece .. itemname(v) .. '¦'
    end
    return piece
end

local escape = function(str)
    return str:gsub("'", "\\'"):gsub("'", "\\'")
end
local enclose = function(str)
    return "'" .. escape(str) .. "'"
end

local getItemGroupName = function(item)
    if item == 'Wood' or item == 'Wood2' or item == 'Wood3' then
        return 'Any Wood'
    elseif item == 'Copper Bar' or item == 'Tin Bar' then
        return 'Any Bar'
    end
end

local normalizeStation = function(station)
    return station
end

local normalizeVersion = function(_version)
    return _version
end

local criStr = function(args)
    local constraints = {}
    -- station = ? and station != ?
    local _station = trim(args['station'] or '')
    local _stationnot = trim(args['stationnot'] or '')
    local str = ''
    if _station ~= '' then
        for _, v in ipairs(explode('/', _station)) do
            if str ~= '' then
                str = str .. ' OR '
            end
            str = str .. "station = " .. enclose(normalizeStation(v))
        end
    end
    if _stationnot ~= '' then
        if str ~= '' then
            str = '(' .. str .. ')'
        end
        for _, v in ipairs(explode('/', _stationnot)) do
            if str ~= '' then
                str = str .. ' AND '
            end
            str = str .. 'station <> ' .. enclose(normalizeStation(v))
        end
    end
    constraints['station'] = str
    local _piece = trim(args['piece'] or '')
    local _piecenot = trim(args['piecenot'] or '')
    local str = ''
    if _piece ~= '' then
        for _, v in ipairs(explode('/', _piece)) do
            if str ~= '' then
                str = str .. ' OR '
            end
            if mw.ustring.sub(v, 1, 5) == 'LIKE ' then
                str = str .. "piece LIKE " .. enclose(trim(mw.ustring.sub(v, 6)))
            else
                str = str .. 'piece=' .. enclose(v)
            end
        end
    end
    if _piecenot ~= '' then
        if str ~= '' then
            str = '(' .. str .. ')'
        end
        for _, v in ipairs(explode('/', _piecenot)) do
            if str ~= '' then
                str = str .. ' AND '
            end
            if mw.ustring.sub(v, 1, 5) == 'LIKE ' then
                str = str .. "piece NOT LIKE " .. enclose(trim(mw.ustring.sub(v, 6)))
            else
                str = str .. 'piece <> ' .. enclose(v)
            end
        end
    end
    if str ~= '' then
        constraints['piece'] = str
    end
    -- ingredient = ?
    local _ingredient = trim(args['ingredient'] or '')
    if _ingredient ~= '' then
        local str = ''
        for _, v in ipairs(explode('/', _ingredient)) do
            if str ~= '' then
                str = str .. ' OR '
            end
            if mw.ustring.sub(v, 1, 1) == '#' then
                str = str .. "ingredients HOLDS LIKE '%¦" .. escape(mw.ustring.sub(v, 2)) .. "¦%'"
            elseif mw.ustring.sub(v, 1, 5) == 'LIKE ' then
                str = str .. "ingredients HOLDS LIKE '%¦" .. escape(trim(mw.ustring.sub(v, 6))) .. "¦%'"
            else
                str = str .. "ingredients HOLDS LIKE '%¦" .. escape(v) .. "¦%'"
                -- any xxx
                local group = getItemGroupName(v)
                if group then
                    str = str .. " OR ingredients HOLDS LIKE '%¦" .. escape(group) .. "¦%'"
                end
            end
        end
        constraints['ingredient'] = str
    end

    --versions
    local _version = normalizeVersion(args['version'] or args['versions'] or '')
    if _version ~= '' then
        constraints['version'] = 'version = '..enclose(_version)
    end

    local where = ''
    if constraints['station'] then
        where = constraints['station']
    end
    if constraints['piece'] then
        if where ~= '' then
            where = where .. ' AND '
        end
        where = where .. '(' .. constraints['piece'] .. ')'
    end
    if constraints['ingredient'] then
        if where ~= '' then
            where = where .. ' AND '
        end
        where = where .. '(' .. constraints['ingredient'] .. ')'
    end
    if constraints['version'] then
        if where ~= '' then
            where = where .. ' AND '
        end
        where = where .. '(' .. constraints['version'] .. ')'
    end
    return where
end

local pieceCell = function(row, showPieceId, needLink, noVersion, template)
    local piece, pieceid, token = row['piece'], row['pieceid'], row['token']
    local str = ''
    local args = {anchor = pieceanchor, nolink = not needLink, class='multi-line'}
    if showPieceId then
        args['id'] = pieceid
    end
    if token then
        args[2] = token
    end
    if version ~= '' then
        args['icons'] = 'n'
    end
    str = str .. itemLink(piece, args)
    if not noVersion and version ~= nil and version ~= '' then
        -- {{version icons}} is a slow template, so cache its result:
        local vstr = cache.get(lang..':recipes:versionicons:'..version) -- cache for current lang
        if not vstr then
            vstr = ' (' ..currentFrame:expandTemplate{ title = 'version icons', args = {version} }..')'
            cache.set(lang..':recipes:versionicons:'..version, vstr, 3600*24) -- cache 24hr.
        end
        str = str .. vstr
    end
    if template then
        local template_str = currentFrame:expandTemplate{ title = template, args = {
            link = needLink, showid = showPieceId, noversion = noVersion,
            pieceid=pieceid, token=token,
            piece=piece,
        } }
        str = template_str:gsub('@@@@', str)
    end
    return str
end

local ingredientsCell = function(args)
    local str = '<ul>'
    for _, v in ipairs(explode('^', args)) do
        str = str .. '<li>'
        local item, amount = v:match('^(.-)¦(.-)$')
        local s
        for _, itemname in ipairs(split(item or '')) do
            if s then
                s = s .. l10n('ingredients_sep') .. itemLink(itemname)
            else
                s = itemLink(itemname)
            end
        end
        str = str .. s
        if amount ~= '1' then
            str = str .. ' <span class="note-text">('..amount..')</span>'
        end
        str = str .. '</li>'
    end
    str = str .. '</ul>'
    return str
end

local stationLevelLink = function(station, level)
    return '<div class="station-level-container" title="' .. l10n('Required station level') .. '">'
            .. '[[File:Crafting Station Level Star.png|link=' .. station .. ']]'
            .. '<span>' .. level .. '</span>'
            .. '</div>'
end

local stationCell = function(station, level, options)
    options = options or {wrap = 'y', suffixLinkWithItemTag = false}
    if station == 'By Hand' then
        return l10n('By Hand')
    elseif true == is_crafting_station(station) then
        -- station == 'Workbench' or station == 'Forge' or station == 'Cauldron' or station == 'Fermenter' then
        local linkItem = itemLink(station, options)
        if level ~= '' then
            linkItem = linkItem .. '<br>' .. stationLevelLink(station, level)
        end

        return linkItem
        -- return itemLink(station, options)
    elseif station == "Station One and Station Two" then
        return itemLink("Station One", options) .. l10n('And').. itemLink('Station Two', {mode = 'text'})
    else
        return station
    end
end
-- for extract.
local compactStation = function(station)
    if station == 'By Hand' then
        return ''
    else
        return l10n('compact_before') .. station .. l10n('compact_after')
    end
end

local getFlags = function(args)
    local needCate = 1
    local needLink = true
    local _cate = trim(args['cate'] or '')
    if _cate == 'force' or _cate == 'all' then
        needCate = 2
    elseif _cate == 'n' or _cate == 'no' then
        needCate = nil
    end
    local _link = trim(args['link'] or '')
    if _link == 'y' or _link == 'yes' or _link == 'force' then
        needLink = true
    elseif _link == 'n' or _link == 'no' then
        needLink = false
    end
    return needCate, needLink
end

local addCate, cateStr -- for table body. init in p.query

local tableStart = function(title, withStation)
    local header_
    local str = '<div class="crafts '.. (getArg('class') or '')
    local _id = (getArg('id') or '')
    if _id ~= '' then
        str = str .. '" id="'.. _id
    end
    local _css = (getArg('css') or getArg('style') or '')
    if _css ~= '' then
        str = str .. '" style="'.. _css
    end
    str = str .. '"><div class="wrap"><table '
    if (getArg('sortable') or 'y'):sub(1,1) ~= 'n' then
        str = str .. 'class="sortable" '
    end
    str = str .. 'cellpadding="0" cellspacing="0">'
    if title ~= '' then
        str = str .. '<caption>' .. title .. '</caption>'
    end

    local _i, _field
    str = str .. '<tr>'
    _i = 1
    _field = 'col-A-1'
    while getArg(_field) do
        if not extCols_A then
            extCols_A = {}
        end
        table.insert(extCols_A, _field)
        str = str .. '<th>'.. getArg(_field) ..'</th>'
        _i = _i + 1
        _field = 'col-A-' .. _i
    end
    str = str .. '<th class="result">' .. (getArg('header-result') or l10n('Piece')) .. '</th>'
    _i = 1
    _field = 'col-B-1'
    while getArg(_field) do
        if not extCols_B then
            extCols_B = {}
        end
        table.insert(extCols_B, _field)
        str = str .. '<th>'.. getArg(_field) ..'</th>'
        _i = _i + 1
        _field = 'col-B-' .. _i
    end
    str = str .. '<th class="ingredients">' .. (getArg('header-ingredients') or l10n('Ingredients')) .. '</th>'
    _i = 1
    _field = 'col-C-1'
    while getArg(_field) do
        if not extCols_C then
            extCols_C = {}
        end
        table.insert(extCols_C, _field)
        str = str .. '<th>'.. getArg(_field) ..'</th>'
        _i = _i + 1
        _field = 'col-C-' .. _i
    end
    if withStation then
        _i = 1
        _field = 'station-col-before-1'
        while getArg(_field) do
            if not extCols_stationBefore then
                extCols_stationBefore = {}
            end
            table.insert(extCols_stationBefore, _field)
            str = str .. '<th class="station">'.. getArg(_field) ..'</th>'
            _i = _i + 1
            _field = 'station-col-before-' .. _i
        end
        str = str .. '<th class="station">' .. (getArg('header-station') or l10n('Crafting Station')) .. '</th>'
        _i = 1
        _field = 'station-col-after-1'
        while getArg(_field) do
            if not extCols_stationAfter then
                extCols_stationAfter = {}
            end
            table.insert(extCols_stationAfter, _field)
            str = str .. '<th class="station">'.. getArg(_field) ..'</th>'
            _i = _i + 1
            _field = 'station-col-after-' .. _i
        end
    end
    _i = 1
    _field = 'col-D-1'
    while getArg(_field) do
        if not extCols_D then
            extCols_D = {}
        end
        table.insert(extCols_D, _field)
        str = str .. '<th>'.. getArg(_field) ..'</th>'
        _i = _i + 1
        _field = 'col-D-' .. _i
    end
    str = str .. '</tr>'
    return str
end

local tableEnd = function(rows_count, expectedrows)
    local str = '</table><div style="display: none">total: '..rows_count..' row(s)</div></div></div>'
    if expectedrows and rows_count ~= expectedrows then
        str = str .. '[[Category:'.. l10n('cate_unexpected_rows_count') .. ']]'
    end
    if not expectedrows and rows_count == 0 then
        str = str .. '[[Category:'.. l10n('cate_no_row') .. ']]'
    end
    return str
end

local tableRow = function(str, row, current_station, station_count, rows_count, showPieceId, withStation, needCate, needLink, needGroup, current_piece, piece_count, current_piece_ext, piece_ext_count, template, stationGroup)
    local str_w = '' -- before piece col
    local str_x = '' -- between piece and ingredients cols
    local str_y = '' -- between ingredients and station cols
    local str_z = '' -- after station
    local str_pieceCell = ''

    local piece_index = getArg('piece-index-#'..rows_count) or getArg('piece-index-'..row['piece'])

    str = str .. '<tr data-rowid="'..tostring(rows_count)..'">'

    if needGroup then
        local piece = row['piece']..'|'..(row['pieceid'] or '')
        -- grouping piece col
        if current_piece == piece then -- is same group ??
            piece_count = piece_count + 1
        else
            --new group:
            -- rowspan value for prev group, if needed.
            if piece_count then
                str = str:gsub("yyyrowspanyyy", tostring(piece_count))
            end
            -- begin this group
            current_piece = piece
            piece_count = 1
            str_pieceCell = '<td class="piece" rowspan="yyyrowspanyyy">'.. pieceCell(row, showPieceId, needLink, false, template).. '</td>'
        end
        -- grouping ext cols
        if piece_index and (current_piece_ext == piece_index) then -- is same group ??
            piece_ext_count = piece_ext_count + 1
        else
            --new group:
            -- rowspan value for prev group, if needed.
            if piece_ext_count then
                str = str:gsub("zzzrowspanzzz", tostring(piece_ext_count))
            end
            -- begin this group
            current_piece_ext = piece_index
            piece_ext_count = 1
            if extCols_A then
                for _, v in ipairs(extCols_A) do
                    if piece_index then
                        str_w = str_w .. '<td class="'..v..'" rowspan="zzzrowspanzzz">' .. (getArg(piece_index .. '-row-' .. v) or '') .. '</td>'
                    else
                        str_w = str_w .. '<td class="'..v..'" rowspan="zzzrowspanzzz"></td>'
                    end
                end
            end
            if extCols_B then
                for _, v in ipairs(extCols_B) do
                    if piece_index then
                        str_x = str_x .. '<td class="'..v..'" rowspan="zzzrowspanzzz">' .. (getArg(piece_index .. '-row-' .. v) or '') .. '</td>'
                    else
                        str_x = str_x .. '<td class="'..v..'" rowspan="zzzrowspanzzz"></td>'
                    end
                end
            end
            -- extCols_C = { col-C-1 }
            -- for _, v in ipairs(extCols_C) = col-C-1
            -- piece_index = 'piece-index-'..row['piece'] = getArg('piece-index-Grilled neck tail') = a
            -- value = piece_index .. '-row-'' .. col-C-1) = getArg('a-row-col-C-1') = 20s
            if extCols_C then
                for _, v in ipairs(extCols_C) do
                    if piece_index then
                        str_y = str_y .. '<td class="'..v..'" rowspan="zzzrowspanzzz">' .. (getArg(piece_index .. '-row-' .. v) or '') .. '</td>'
                    else
                        str_y = str_y .. '<td class="'..v..'" rowspan="zzzrowspanzzz"></td>'
                    end
                end
            end
            if extCols_D then
                for _, v in ipairs(extCols_D) do
                    if piece_index then
                        str_z = str_z .. '<td class="'..v..'" rowspan="zzzrowspanzzz">' .. (getArg(piece_index .. '-row-' .. v) or '') .. '</td>'
                    else
                        str_z = str_z .. '<td class="'..v..'" rowspan="zzzrowspanzzz"></td>'
                    end
                end
            end
        end
    else
        if extCols_A then
            for _, v in ipairs(extCols_A) do
                if piece_index then
                    str_w = str_w .. '<td class="'..v..'">' .. (getArg(piece_index .. '-row-' .. v) or '') .. '</td>'
                else
                    str_w = str_w .. '<td class="'..v..'"></td>'
                end
            end
        end
        if extCols_B then
            for _, v in ipairs(extCols_B) do
                if piece_index then
                    str_x = str_x .. '<td class="'..v..'">' .. (getArg(piece_index .. '-row-' .. v) or '') .. '</td>'
                else
                    str_x = str_x .. '<td class="'..v..'"></td>'
                end
            end
        end
        if extCols_C then
            for _, v in ipairs(extCols_C) do
                if piece_index then
                    str_y = str_y .. '<td class="'..v..'">' .. (getArg(piece_index .. '-row-' .. v) or '') .. '</td>'
                else
                    str_y = str_y .. '<td class="'..v..'"></td>'
                end
            end
        end
        if extCols_D then
            for _, v in ipairs(extCols_D) do
                if piece_index then
                    str_z = str_z .. '<td class="'..v..'">' .. (getArg(piece_index .. '-row-' .. v) or '') .. '</td>'
                else
                    str_z = str_z .. '<td class="'..v..'"></td>'
                end
            end
        end
        str_pieceCell = '<td class="piece">'.. pieceCell(row, showPieceId, needLink, false, template).. '</td>'
    end

    str = str .. str_w .. str_pieceCell .. str_x .. '<td class="ingredients">' .. ingredientsCell(row['args'] or '').. '</td>' .. str_y

    if withStation then
        local stationName = row['station'] or ''
        local stationLevel = row['stationlevel'] or ''
        local station = stationName .. stationLevel -- @TODO: Mave

        if stationGroup then
            if current_station == station then -- is same group ??
                station_count = station_count + 1
            else
                --new group:
                -- rowspan value for prev group, if needed.
                if station_count then
                    str = str:gsub("xxxrowspanxxx", tostring(station_count))
                end
                -- begin this group
                current_station = station
                station_count = 1
                local station_index = getArg('station-index-'..station)
                -- station before:
                if extCols_stationBefore then
                    for _, v in ipairs(extCols_stationBefore) do
                        if station_index then
                            str = str .. '<td class="station '..v..'" rowspan="xxxrowspanxxx">' .. (getArg(station_index .. '-row-' .. v) or '') .. '</td>'
                        else
                            str = str .. '<td class="station '..v..'" rowspan="xxxrowspanxxx"></td>'
                        end
                    end
                end
                str = str .. '<td class="station" data-station="' .. stationName .. '" data-stationlevel="' .. stationLevel .. '" rowspan="xxxrowspanxxx">'.. stationCell(stationName, stationLevel) ..'</td>'
                -- station after:
                if extCols_stationAfter then
                    for _, v in ipairs(extCols_stationAfter) do
                        if station_index then
                            str = str .. '<td class="station '..v..'" rowspan="xxxrowspanxxx">' .. (getArg(station_index .. '-row-' .. v) or '') .. '</td>'
                        else
                            str = str .. '<td class="station '..v..'" rowspan="xxxrowspanxxx"></td>'
                        end
                    end
                end
            end
        else
            if current_station == station then -- is same group ??
                station_count = station_count + 1
            else
                current_station = station
                station_count = 1
            end
            local station_index = getArg('station-index-'..station)
            -- station before:
            if extCols_stationBefore then
                for _, v in ipairs(extCols_stationBefore) do
                    if station_index then
                        str = str .. '<td class="station '..v..'">' .. (getArg(station_index .. '-row-' .. v) or '') .. '</td>'
                    else
                        str = str .. '<td class="station '..v..'"></td>'
                    end
                end
            end
            str = str .. '<td class="station">'.. stationCell(stationName, '') ..'</td>'
            -- station after:
            if extCols_stationAfter then
                for _, v in ipairs(extCols_stationAfter) do
                    if station_index then
                        str = str .. '<td class="station '..v..'">' .. (getArg(station_index .. '-row-' .. v) or '') .. '</td>'
                    else
                        str = str .. '<td class="station '..v..'"></td>'
                    end
                end
            end
        end
    end

    str = str .. str_z ..'</tr>'
    return str, current_station, station_count, current_piece, piece_count, current_piece_ext, piece_ext_count
end

local extRows = function(withStation, isTop)
    local prefix
    if isTop then
        prefix = 'topextrow-'
    else
        prefix = 'extrow-'
    end
    local returnstr = ''
    local valid = true
    local p
    local str
    local _i = 1
    local temp
    while valid do
        local i = tostring(_i) .. '-'
        p = prefix .. i
        valid = false
        str = '<tr data-'..prefix..'id="'..tostring(_i)..'">'
        if extCols_A then
            for _, v in ipairs(extCols_A) do
                temp = getArg(p..v)
                if temp then
                    valid = true
                    str = str .. '<td class="'..v..'">' .. temp .. '</td>'
                else
                    str = str .. '<td class="'..v..'"></td>'
                end
            end
        end
        temp = getArg(p..'col-piece')
        if temp then
            valid = true
            str = str .. '<td class="result">' .. temp .. '</td>'
        else
            str = str .. '<td class="result"></td>'
        end
        if extCols_B then
            for _, v in ipairs(extCols_B) do
                temp = getArg(p..v)
                if temp then
                    valid = true
                    str = str .. '<td class="'..v..'">' .. temp .. '</td>'
                else
                    str = str .. '<td class="'..v..'"></td>'
                end
            end
        end
        temp = getArg(p..'col-ingredients')
        if temp then
            valid = true
            str = str .. '<td class="ingredients">' .. temp .. '</td>'
        else
            str = str .. '<td class="ingredients"></td>'
        end
        if extCols_C then
            for _, v in ipairs(extCols_C) do
                temp = getArg(p..v)
                if temp then
                    valid = true
                    str = str .. '<td class="'..v..'">' .. temp .. '</td>'
                else
                    str = str .. '<td class="'..v..'"></td>'
                end
            end
        end
        if withStation then
            -- station before:
            if extCols_stationBefore then
                for _, v in ipairs(extCols_stationBefore) do
                    temp = getArg(p..v)
                    if temp then
                        valid = true
                        str = str .. '<td class="station '..v..'">' .. temp .. '</td>'
                    else
                        str = str .. '<td class="station '..v..'"></td>'
                    end
                end
            end
            temp = getArg(p..'col-station')
            if temp then
                valid = true
                str = str .. '<td class="station">' .. temp .. '</td>'
            else
                str = str .. '<td class="station"></td>'
            end
            -- station after:
            if extCols_stationAfter then
                for _, v in ipairs(extCols_stationAfter) do
                    temp = getArg(p..v)
                    if temp then
                        valid = true
                        str = str .. '<td class="station '..v..'">' .. temp .. '</td>'
                    else
                        str = str .. '<td class="station '..v..'"></td>'
                    end
                end
            end
        end
        if extCols_D then
            for _, v in ipairs(extCols_D) do
                temp = getArg(p..v)
                if temp then
                    valid = true
                    str = str .. '<td class="'..v..'">' .. temp .. '</td>'
                else
                    str = str .. '<td class="'..v..'"></td>'
                end
            end
        end
        str = str .. '</tr>'

        if valid then
            _i = _i + 1
            returnstr = returnstr .. str
        end
    end
    return returnstr
end


local tableBody = function(noPiecesText, piece, showPieceId, withStation, needGroup, needCate, needLink, rootpagename, title, expectedrows, template, stationGroup)
    if next(piece) == nil then
        return "''" .. (noPiecesText or 'No pieces') .. "''"
    end

    local str = tableStart(title, withStation)
    -- top ext rows:
    str = str .. extRows(withStation, true)
    -- main rows:
    local current_station
    local station_count
    local rows_count = 0
    local current_piece
    local piece_count
    local current_piece_ext
    local piece_ext_count
    for _, row in ipairs(piece) do
        rows_count = rows_count + 1
        -- table row:
        str, current_station, station_count, current_piece, piece_count, current_piece_ext, piece_ext_count = tableRow(str, row, current_station, station_count, rows_count, showPieceId, withStation, needCate, needLink, needGroup, current_piece, piece_count, current_piece_ext, piece_ext_count, template, stationGroup)
        -- cate:
        if needCate then
            if needCate == 2 or rootpagename == currentFrame:expandTemplate{ title = 'tr', args = {row['piece'], lang=lang} } then
                addCate(row['station'])
            end
        end
    end
    -- rowspan value for last station group and piece group
    if withStation and station_count and stationGroup then
        str = str:gsub("xxxrowspanxxx", tostring(station_count))
    end
    if needGroup then
        str = str:gsub("yyyrowspanyyy", tostring(piece_count))
        str = str:gsub("zzzrowspanzzz", tostring(piece_ext_count))
    end
    -- ext rows:
    str = str .. extRows(withStation)
    -- table end
    str = str .. tableEnd(rows_count, expectedrows)

    -- cate
    if needCate then
        str = str .. cateStr()
    end

    return str
end
-----------------------------------------------------------------

local p = {}

-- for {{pieces/register}}
p.register = function(frame)
    local args = frame:getParent().args

    -- {{{ingredients}}}
    local ingredients = {} -- list of {index, itemname, amount}
    for k, v in pairs(args) do
        if(type(k) == 'number') then
            if k % 2 == 1 then  -- 2n-1, nth item
                local index, item, amount = (k+1)/2, trim(v), trim(args[k+1])
                ingredients[index] = {item, amount}
            end
        end
    end

    local serialized = '' -- serialized ingredients list
    for _, v in ipairs(ingredients) do
        serialized = serialized .. '^' .. v[1] .. '¦' .. v[2]
    end
    serialized = mw.ustring.sub(serialized, 2)

    table.sort(ingredients, function(a , b) return a[1] < b[1] end) -- sort by ingredient item name
    local ingredients_string = ''
    local ingredients_string_full = ''
    for _, v in ipairs(ingredients) do
        local name, amount = unpack(v)
        local ingstr = normalize(name)
        ingredients_string = ingredients_string .. '^' .. ingstr
        ingredients_string_full = ingredients_string_full .. '^' .. ingstr .. amount
    end

    --{{{version}}}, normalize
    version = normalizeVersion(args['version'] or '')

    --store
    frame:callParserFunction('#cargo_store:_table=Pieces',{
        piece = trim(args['piece'] or ''),
        pieceid = trim(args['pieceprefabname'] or ''),
        token = trim(args['token'] or ''),
        description = trim(args['description'] or ''),
        station = normalizeStation(trim(args['station'] or '')),
        ingredients = mw.ustring.sub(ingredients_string, 2),
        ings = mw.ustring.sub(ingredients_string_full, 2),
        args = serialized,
    })
end -- p.register

-- for {{pieces}}
p.query = function(frame)
    currentFrame = frame -- global frame cache
    local args = frame:getParent().args
    inputArgs = args

    lang = frame.args['lang'] or 'en'

    pieceanchor = trim(args['pieceanchor'] or '')

    addCate, cateStr = (function()
        local cate = l10n('station_cate')
        local cateCache = {}
        local addCate = function(station)
            cateCache[station] = true
        end
        local cateStr = function()
            local str = ''
            for station, _ in pairs(cateCache) do
                str = str .. '[[Category:'..(cate[station] or frame:expandTemplate{ title = 'tr', args = {station, lang=lang, link='y'}})..']]'
            end
            if str ~= '' then
                str = '[[Category:'.. l10n('cate_craftable').. ']]' .. str
            end
            return str
        end
        return addCate, cateStr
    end)()

    local where = trim(args['where'] or '')
    if where == '' then
        where = criStr(args)
    end

    -- no constraint no result.
    if where == '' then
        return '<span style="color:red;font-weight:bold;">Pieces: No constraint</span>'
    end

    -- format:
    local needCate, needLink = getFlags(args)
    local needGroup = true
    if (getArg('grouping') or 'y'):sub(1,1) == 'n' then
        needGroup = false
    end
    local showPieceId = false
    if trim(args['showpieceid'] or '') ~= '' then
        showPieceId = true
    end
    local _title = trim(args['title'] or '')
    local _expectedrows = trim(args['expectedrows'] or '')
    if _expectedrows ~= '' then
        _expectedrows = tonumber(_expectedrows)
    else
        _expectedrows = nil
    end
    local rootpagename = mw.title.getCurrentTitle().rootText

    local noPiecesText = 'No pieces'
    if args['piece'] or '' ~= '' then
        noPiecesText = 'This piece cannot be built'
    end
    if args['ingredient'] or '' ~= '' then
        noPiecesText = 'This item is not used in any building pieces'
    end

    if trim(args['nostation'] or '') ~= '' then
        -- no station
        -- query, still need contain station field for cate.
        local piece = mw.ext.cargo.query('Pieces', 'piece, pieceid, token, station, args', {
            where = where,
            groupBy = "pieceid, piece, ings",
            orderBy = "piece", -- Don't order by station
            limit = 2000,
        })
        return tableBody(noPiecesText, piece, showPieceId, false, needGroup, needCate, needLink, rootpagename, _title, _expectedrows, getArg('piecetemplate'), false)
    else
        -- with station
        local stationGroup = true
        if (getArg('stationgrouping') or 'y'):sub(1,1) == 'n' then
            stationGroup = false
        end
        -- query
        local piece = mw.ext.cargo.query('Pieces', 'piece, pieceid, token, station, args', {
            where = where,
            groupBy = "pieceid, piece, ings",
            orderBy = "station, piece, ings", -- order by station first for station grouping.
            limit = 2000,
        })
        return tableBody(noPiecesText, piece, showPieceId, true, needGroup, needCate, needLink, rootpagename, _title, _expectedrows, getArg('piecetemplate'), stationGroup)
    end
end -- p.query

-- for {{pieces/extract}}
p.extract = function(frame)
    currentFrame = frame -- global frame cache

    local args = frame:getParent().args
    inputArgs = args

    lang = frame.args['lang'] or 'en'
    --l10n_table = l10n_info[lang] or l10n_info['en']

    local where = trim(args['where'] or '')
    if where == '' then
        where = criStr(args)
    end

    -- no constraint no piece.
    if where == '' then
        return '<span style="color:red;font-weight:bold;">Pieces/extract: No constraint</span>'
    end

    -- query:
    local piece = mw.ext.cargo.query('Pieces', 'piece, pieceid, token, description, station, args', {
        where = where,
        groupBy = "pieceid, piece, ings",
        orderBy = "piece", -- Don't order by station
        limit = 20, -- enough.
    })

    -- output
    local mode = getArg('mode')
    local sep = getArg('sep') or getArg('seperator')
    if not mode or mode =='compact' or mode == '' then
        --default mode = compact
        local sep = sep or l10n('default_sep_compact')
        local withPiece = getArg('withpiece')
        local withStation = not getArg('nostation')
        local withVersion = not getArg('noversion')
        local str = nil
        for _, row in ipairs(piece) do
            if str then
                str = str .. sep
            else
                str = ''
            end
            str = str .. '<span class="recipe compact">'
            if withVersion then
                if row['version'] ~= '' then
                    str = str ..currentFrame:expandTemplate{ title = 'version icons', args = {row['version']} }..': '
                end
            end
            local ingFlag = nil
            for _, v in ipairs(explode('^', row['args'])) do
                if ingFlag then
                    str = str .. ' + '
                else
                    ingFlag = true
                end
                local item, amount = v:match('^(.-)¦(.-)$')
                if amount ~= '1' then
                    str = str .. amount .. ' '
                end
                local s
                for _, itemname in ipairs(split(item)) do
                    if s then
                        s = s .. "&thinsp;/&thinsp;" .. itemLink(itemname, {mode='image'})
                    else
                        s = itemLink(itemname, {mode='image'})
                    end
                end
                str = str .. s
            end
            if withPiece then
                str = str .. ' = '
                if row['amount'] ~= '1' then
                    str = str .. row['amount'] .. ' '
                end
                local args = {mode='image'}
                str = str .. itemLink(row['piece'], args)
            end
            if withStation then
                str = str .. compactStation(row['station'])
            end
            str = str..'</span>'
        end
        return str
    elseif mode == 'ingredients' then
        local sep = sep or l10n('default_sep_ingredients')
        local str = ''
        for _, row in ipairs(piece) do
            if str ~= '' then
                str = str .. sep
            end
            str = str .. ingredientsCell(row['args'] or '')
        end
        return '<div class="crafting-ingredients">'..str..'</div>'
    elseif mode == 'station' then
        -- only return first row.
        for _, row in ipairs(piece) do
            return stationCell(row['station'], '', {})
        end
    elseif mode == 'piece' then
        -- only return first row.
        local needCate, needLink = getFlags(args)
        for _, row in ipairs(piece) do
            return pieceCell(row, getArg('showpieceid'), needLink, true, getArg('piecetemplate'))
        end
    elseif mode == 'ingredients-buy' then
        -- only process first row.
        for _, row in ipairs(piece) do
            local value = 0
            for _, v in ipairs(explode('^', row['args'])) do
                local item, amount = v:match('^(.-)¦(.-)$')
                value = value + require('Module:Iteminfo').getItemStat( tonumber(currentFrame:expandTemplate{ title = 'itemIdFromName', args = {item, lang='en'} }) or 0, 'value' ) * amount
            end
            return value
        end
    elseif mode == 'ingredients-sell' then
        -- only process first row.
        for _, row in ipairs(piece) do
            local value = 0
            for _, v in ipairs(explode('^', row['args'])) do
                local item, amount = v:match('^(.-)¦(.-)$')
                value = value + math.floor(require('Module:Iteminfo').getItemStat( tonumber(currentFrame:expandTemplate{ title = 'itemIdFromName', args = {item, lang='en'} }) or 0, 'value' )/5) * amount
            end
            return value
        end
    else
        return '<span style="color:red;font-weight:bold;">Pieces/extract: Invalid mode</span>'
    end
end -- p.extract

-- count
p.count = function(frame)
    local args = frame:getParent().args
    local where = trim(args['where'] or '')
    if where == '' then
        where = criStr(args)
    end
    -- no constraint no result.
    if where == '' then
        return
    end
    -- query: since we must use group by to eliminate duplicates, so we can not use COUNT() to get row count directly.
    local piece = mw.ext.cargo.query('Pieces', 'piece, pieceid, token, station, args', {
        where = where,
        groupBy = "pieceid, piece, ings",
        limit = 2000,
    })
    -- count
    local count = 0
    for _, row in ipairs(piece) do
        count = count + 1
    end
    return count
end -- p.count

-- return "yes" or ""
p.exist = function(frame)
    local args = frame:getParent().args
    local where = trim(args['where'] or '')
    if where == '' then
        where = criStr(args)
    end
    -- no constraint no result.
    if where == '' then
        return
    end
    -- query:
    local piece = mw.ext.cargo.query('Pieces', 'piece', {
        where = where,
        limit = 1, -- enough.
    })
    -- output
    for _, row in ipairs(piece) do
        return 'yes'
    end
end -- p.exist

return p