Przejdź do zawartości

Moduł:Build bracket/Params

Z Wikipedii, wolnej encyklopedii
 Dokumentacja modułu[stwórz] • [odśwież]
local Params = {}

-- =========================
-- 1) MODULE BINDINGS
-- =========================
-- Upvalues bound per call
local state, config, Helpers, StateChecks

-- Stdlib aliases
local str_format = string.format
local t_insert = table.insert
local t_sort = table.sort

-- Locals filled on bind (with safe fallbacks)
local isempty, notempty, bargs, getPArg, getFArg, toChar, split

local function _toCharFallback(n)
    n = tonumber(n or 0) or 0
    if n >= 1 and n <= 26 then
        return string.char(96 + n) -- 'a'..'z'
    end
    return tostring(n)
end

local function bind(_state, _config, _Helpers, _StateChecks)
    state, config, Helpers, StateChecks = _state, _config, _Helpers, _StateChecks
    isempty = Helpers and Helpers.isempty or function(v)
            return v == nil or v == ""
        end
    notempty = Helpers and Helpers.notempty or function(v)
            return v ~= nil and v ~= ""
        end
    bargs = Helpers and Helpers.bargs
    getPArg = Helpers and Helpers.getPArg
    getFArg = Helpers and Helpers.getFArg
    toChar = (Helpers and Helpers.toChar) or _toCharFallback
    split = Helpers and Helpers.split
end

-- Try both RDj[a]-X and RDj[A]-X; falls back to "" if not present
local function readPerHeaderArg(j, k, suffix)
    local ch = toChar(k) -- e.g. 'a' with Helpers.toChar
    local key1 = "RD" .. j .. ch .. suffix
    local v = bargs(key1)
    if v ~= nil and v ~= "" then
        return v
    end
    local key2 = "RD" .. j .. string.upper(ch) .. suffix
    v = bargs(key2)
    if v ~= nil and v ~= "" then
        return v
    end
    return ""
end

-- =========================
-- 2) ARG HELPERS
-- =========================
-- Trim + split a comma list frame arg (returns {} on blank)
local function readCsvArg(name)
    local raw = (getFArg(name) or ""):gsub("%s+", "")
    return split(raw, {","}, true)
end

-- ========================================
-- 3) BUILD SKELETON (formerly getCells)
-- ========================================
local function buildSkeleton()
    local DEFAULT_TPM = 2
    local maxrow = 1
    local colentry = {}
    local hasNoHeaders = true

    -- ensure containers
    state.entries = state.entries or {}
    state.shift = state.shift or {}
    state.teamsPerMatch = state.teamsPerMatch or {}
    state.maxtpm = state.maxtpm or 0

    local Cmin, Cmax = config.minc, config.c

    -- Phase 1: Determine header presence and teamsPerMatch
    for j = Cmin, Cmax do
        if notempty(getFArg("col" .. j .. "-headers")) then
            hasNoHeaders = false
        end
        local tpm =
            tonumber(getFArg("RD" .. j .. "-teams-per-match")) or tonumber(getFArg("col" .. j .. "-teams-per-match")) or
            tonumber(getFArg("teams-per-match")) or
            DEFAULT_TPM
        state.teamsPerMatch[j] = tpm
        if tpm > state.maxtpm then
            state.maxtpm = tpm
        end
    end

    -- Phase 2: Build colentry for each column
    for j = Cmin, Cmax do
        state.entries[j] = {}
        state.shift[j] = tonumber(bargs("RD" .. j .. "-shift")) or tonumber(bargs("shift")) or 0

        colentry[j] = {
            readCsvArg("col" .. j .. "-headers"),
            readCsvArg("col" .. j .. "-matches"),
            readCsvArg("col" .. j .. "-lines"),
            readCsvArg("col" .. j .. "-text"),
            readCsvArg("col" .. j .. "-groups") -- reserved for user-specified groups
        }

        -- inject a default header if none were specified anywhere (unless noheaders=y/yes)
        local noheaders = (getFArg("noheaders") or ""):lower()
        if hasNoHeaders and (noheaders ~= "y" and noheaders ~= "yes") then
            t_insert(colentry[j][1], 1)
        end
    end

    -- Ctype mapping for colentry positions
    local CTYPE_MAP = {"header", "team", "line", "text", "group"}

    -- Helpers to populate entries (preserve legacy shapes)
    local function populateTeam(j, rowIndex, n)
        local TPM = state.teamsPerMatch[j]
        -- scaffold a text row above when needed (legacy behavior)
        if state.entries[j][rowIndex - 1] == nil and state.entries[j][rowIndex - 2] == nil then
            state.entries[j][rowIndex - 2] = {ctype = "text", index = n}
            state.entries[j][rowIndex - 1] = {ctype = "blank"}
        end
        -- first team (top)
        state.entries[j][rowIndex] = {ctype = "team", index = TPM * n - (TPM - 1), position = "top"}
        state.entries[j][rowIndex + 1] = {ctype = "blank"}
        -- remaining teams in the match (every 2 rows)
        for m = 2, TPM do
            local idx = TPM * n - (TPM - m)
            local r = rowIndex + 2 * (m - 1)
            state.entries[j][r] = {ctype = "team", index = idx}
            state.entries[j][r + 1] = {ctype = "blank"}
        end
    end

    local function populateText(j, rowIndex, index)
        state.entries[j][rowIndex] = {ctype = "text", index = index}
        state.entries[j][rowIndex + 1] = {ctype = "blank"}
    end

    local function populateLine(j, rowIndex)
        -- first segment draws its bottom edge
        state.entries[j][rowIndex] = {ctype = "line", border = "bottom"}
        state.entries[j][rowIndex + 1] = {ctype = "blank"}
        -- second segment draws its top edge
        state.entries[j][rowIndex + 2] = {ctype = "line", border = "top"}
        state.entries[j][rowIndex + 3] = {ctype = "blank"}
    end

    local function populateGroup(j, rowIndex, n)
        state.entries[j][rowIndex] = {ctype = "group", index = n}
        state.entries[j][rowIndex + 1] = {ctype = "blank"}
    end

    local function populateDefault(j, rowIndex, n)
        state.entries[j][rowIndex] = {ctype = "header", index = n, position = "top"}
        state.entries[j][rowIndex + 1] = {ctype = "blank"}
    end

    -- Phase 3: Populate entries for each column
    for j = Cmin, Cmax do
        local textindex = 0
        local TPM = state.teamsPerMatch[j]
        local shiftJ = state.shift[j]

        for k, positions in ipairs(colentry[j]) do
            t_sort(positions)
            local ctype = CTYPE_MAP[k]

            for n = 1, #positions do
                if shiftJ ~= 0 and positions[n] > 1 then
                    positions[n] = positions[n] + shiftJ
                end
                local rowIndex = 2 * positions[n] - 1
                local lastRow = rowIndex + 2 * TPM - 1
                if lastRow > maxrow then
                    maxrow = lastRow
                end

                if ctype == "team" then
                    populateTeam(j, rowIndex, n)
                    textindex = n
                elseif ctype == "text" then
                    populateText(j, rowIndex, textindex + n)
                elseif ctype == "line" then
                    populateLine(j, rowIndex)
                elseif ctype == "group" then
                    populateGroup(j, rowIndex, n)
                else
                    populateDefault(j, rowIndex, n)
                end
            end
        end
    end

    if isempty(config.r) then
        config.r = maxrow
    end
end

-- ========================================
-- 4) NAME RESOLUTION HELPERS
-- ========================================
local function paramNames(cname, j, i, l)
    local function getArg(key)
        return bargs(key) or ""
    end
    local function getP(key)
        return getPArg(key) or ""
    end

    local e = state.entries[j][i]
    local RD = "RD" .. j
    local hidx = e.headerindex
    local hchar = toChar(hidx)
    local RDh = RD .. hchar

    local SYNONYMS = {legs = {"legs", "sets"}}

    -- Try a list of names against base+index(+suffix); returns first non-empty
    local function tryAny(base, names, idx, suffix)
        suffix = suffix or ""
        for _, nm in ipairs(names) do
            local a = bargs(base .. "-" .. nm .. idx .. suffix) or ""
            if isempty(a) then
                a = bargs(base .. "-" .. nm .. string.format("%02d", idx) .. suffix) or ""
            end
            if notempty(a) then
                return a
            end
        end
        return ""
    end

    local function tryBoth(base, name, idx, suffix)
        suffix = suffix or ""
        local a = getArg(base .. "-" .. name .. idx .. suffix)
        if isempty(a) then
            a = getArg(base .. "-" .. name .. str_format("%02d", idx) .. suffix)
        end
        return a
    end

    -- Round names (prefer altname when present)
    local RDlabel = getArg(RD .. "-altname") or RD
    local RDhlabel = getArg(RDh .. "-altname") or RDh

    local rname = {{RD, RDlabel}, {RDh, RDhlabel}}
    local name = {cname, getArg(cname .. "-altname") or cname}
    local nameKeys = SYNONYMS[cname] or {name[1]}
    local index = {e.index, e.altindex}
    local result = {}

    if cname == "header" then
        if hidx == 1 then
            for _, base in ipairs({rname[1], rname[2]}) do
                for k = 2, 1, -1 do
                    result[#result + 1] = getArg(base[k])
                end
            end
        else
            for k = 2, 1, -1 do
                result[#result + 1] = getArg(rname[2][k])
            end
        end
    elseif cname == "pheader" then
        if hidx == 1 then
            for _, base in ipairs({rname[1], rname[2]}) do
                for k = 2, 1, -1 do
                    result[#result + 1] = getP(base[k])
                end
            end
        else
            for k = 2, 1, -1 do
                result[#result + 1] = getP(rname[2][k])
            end
        end
    elseif cname == "score" then
        local bases = {rname[2][2], rname[2][1], rname[1][2], rname[1][1]}
        local idxs = {index[2], index[2], index[1], index[1]}
        for n = 1, 4 do
            if l == 1 then
                result[#result + 1] = tryAny(bases[n], nameKeys, idxs[n])
            end
            result[#result + 1] = tryAny(bases[n], nameKeys, idxs[n], "-" .. l)
        end
    elseif cname == "shade" then
        for k = 2, 1, -1 do
            local base = (hidx == 1) and rname[1][k] or rname[2][k]
            result[#result + 1] = getArg(base .. "-" .. name[1])
        end
        result[#result + 1] = getArg("RD-shade")
        result[#result + 1] = (config.COLORS and config.COLORS.cell_bg_dark) or "#eaecf0"
    elseif cname == "text" then
        local bases = {rname[2][2], rname[2][1], rname[1][2], rname[1][1]}
        local idxs = {index[2], index[2], index[1], index[1]}
        local names = {name[2], name[1]}
        for ni = 1, 2 do
            for n = 1, 4 do
                result[#result + 1] = tryBoth(bases[n], names[ni], idxs[n])
            end
        end
    else
        local bases = {rname[2][2], rname[2][1], rname[1][2], rname[1][1]}
        local idxs = {index[2], index[2], index[1], index[1]}
        for n = 1, 4 do
            result[#result + 1] = tryAny(bases[n], nameKeys, idxs[n])
        end
    end

    for _, val in ipairs(result) do
        if notempty(val) then
            return val
        end
    end
    return ""
end

-- ========================================
-- 5) NUMBERED PARAM MODE
-- ========================================
local masterindex = 1

local function numberedParams(j)
    local row = state.entries[j]
    if not row then
        return
    end

    local function nextArg()
        local v = bargs(tostring(masterindex)) or ""
        masterindex = masterindex + 1
        return v
    end

    local R = config.r
    for i = 1, R do
        local e = row[i]
        if e then
            local ct = e.ctype
            if ct == "team" then
                local legs = state.rlegs[j]

                if config.forceseeds then
                    e.seed = nextArg()
                end

                e.team = nextArg()
                e.legs = paramNames("legs", j, i)
                e.score = {weight = {}}
                e.weight = "normal"

                if notempty(e.legs) then
                    legs = tonumber(e.legs) or legs
                end

                for l = 1, legs do
                    e.score[l] = nextArg()
                    e.score.weight[l] = "normal"
                end

                if config.aggregate and legs > 1 then
                    e.score.agg = nextArg()
                    e.score.weight.agg = "normal"
                end
            elseif ct == "header" then
                e.header = paramNames("header", j, i)
                e.pheader = paramNames("pheader", j, i)
                e.shade = paramNames("shade", j, i)
            elseif ct == "text" then
                e.text = nextArg()
            elseif ct == "group" then
                e.group = nextArg()
            elseif ct == "line" and e.hastext == true then
                e.text = nextArg()
            end
        end
    end
end

-- ========================================
-- 6) NAMED MODE ASSIGNERS (per-ctype)
-- ========================================
local function cellHasMeaningfulContent(e)
    if not e then
        return false
    end
    if e.ctype == "team" then
        return notempty(e.team)
    end
    if e.ctype == "text" then
        return notempty(e.text)
    end
    if e.ctype == "group" then
        return notempty(e.group)
    end
    if e.ctype == "line" and e.hastext == true then
        return notempty(e.text)
    end
    return false
end

local function enforceContentUnhide(j)
    if not (state.hide and state.hide[j]) then
        return
    end
    local explicit = (state._hideExplicit and state._hideExplicit[j]) or {}
    local R = config.r

    -- If master hid a header, but we later discover content in that header,
    -- flip it visible unless there was an EXPLICIT per-header hide.
    for i = 1, R do
        local e = state.entries[j][i]
        if e and e.headerindex then
            local h = e.headerindex
            if state.hide[j][h] and cellHasMeaningfulContent(e) then
                state.hide[j][h] = false
            end
        end
    end
end

local function assignTeamParams(j, i)
    local legs = state.rlegs[j]
    local e = state.entries[j][i]

    e.seed = paramNames("seed", j, i)
    e.team = paramNames("team", j, i)
    e.legs = paramNames("legs", j, i)
    e.score = {weight = {}}
    e.weight = "normal"

    if notempty(e.legs) then
        legs = tonumber(e.legs) or legs
    end

    if config.autolegs then
        local l = 1
        repeat
            e.score[l] = paramNames("score", j, i, l)
            e.score.weight[l] = "normal"
            l = l + 1
        until isempty(paramNames("score", j, i, l))
        legs = l - 1
    else
        for l = 1, legs do
            e.score[l] = paramNames("score", j, i, l)
            e.score.weight[l] = "normal"
        end
    end

    if config.aggregate and legs > 1 then
        e.score.agg = paramNames("score", j, i, "agg")
        e.score.weight.agg = "normal"
    end
end

local function assignHeaderParams(j, i)
    local e = state.entries[j][i]
    e.header = paramNames("header", j, i)
    e.pheader = paramNames("pheader", j, i)
    e.shade = paramNames("shade", j, i)

    -- Did shade originate from an RD*-shade param?
    local hchar = toChar(e.headerindex)
    local rdNames = {
        "RD" .. j .. "-shade",
        "RD" .. j .. hchar .. "-shade",
        "RD-shade"
    }
    e.shade_is_rd = false
    for _, pname in ipairs(rdNames) do
        local v = bargs(pname)
        if notempty(v) and e.shade == v then
            e.shade_is_rd = true
            break
        end
    end
end

local function assignTextParams(j, i)
    state.entries[j][i].text = paramNames("text", j, i)
end

local function assignGroupParams(j, i)
    state.entries[j][i].group = paramNames("group", j, i)
end

local function assignLineTextParams(j, i)
    state.entries[j][i].text = paramNames("text", j, i)
end

-- ========================================
-- 7) TABLE-WIDE ASSIGNMENT PASS
-- ========================================
local function getScalarRoundParam(j, bases) -- e.g. {"legs","sets"}
    -- prefer per-round keys, then global
    for _, base in ipairs(bases) do
        local v = bargs("RD" .. j .. "-" .. base)
        if notempty(v) then
            return v
        end
    end
    for _, base in ipairs(bases) do
        local v = bargs(base)
        if notempty(v) then
            return v
        end
    end
    return ""
end

local function assignParams()
    masterindex = 1
    local maxcol = 1

    local Cmin, Cmax, R = config.minc, config.c, config.r

    for j = Cmin, Cmax do
        -- prepare per-round containers
        state.hide[j] = state.hide[j] or {}
        state.byes[j] = state.byes[j] or {}

        -- Set legs for this column
        local vlegs = getScalarRoundParam(j, {"legs", "sets"})
        state.rlegs[j] = tonumber(vlegs) or 1
        if notempty(vlegs) then
            config.autolegs = false
        end

        -- assign params
        if config.paramstyle == "numbered" then
            numberedParams(j)
        else
            local col = state.entries[j]
            for i = 1, R do
                local cell = col[i]
                if cell ~= nil then
                    local ct = cell.ctype
                    if ct == "team" then
                        assignTeamParams(j, i)
                    elseif ct == "header" then
                        assignHeaderParams(j, i)
                    elseif ct == "text" then
                        assignTextParams(j, i)
                    elseif ct == "group" then
                        assignGroupParams(j, i)
                    elseif ct == "line" and cell.hastext == true then
                        assignLineTextParams(j, i)
                    end
                end
                if config.autocol and not StateChecks.isBlankEntry(j, i) and j > maxcol then
                    maxcol = j
                end
            end
        end

        enforceContentUnhide(j)

        -- parent header text forces visible
        for i = 1, R do
            local e = state.entries[j][i]
            if e and e.ctype == "header" then
                local hidx = e.headerindex
                if (Helpers.notempty and Helpers.notempty(e.pheader)) then
                    state.hide[j][hidx] = false
                end
            end
        end
    end

    if config.autocol then
        config.c = maxcol
    end
end

-- ========================================
-- 8) STRUCTURE DISCOVERY (hide/byes/indices)
-- ========================================
local function getHide(j)
    state.hide[j] = {}
    state._hideExplicit = state._hideExplicit or {}
    state._hideExplicit[j] = {}

    -- master round-level hide flag: RD{j}-hide
    local masterRaw = bargs("RD" .. j .. "-hide") or ""
    local masterOn = (Helpers and Helpers.yes and Helpers.yes(masterRaw)) or false

    for k = 1, state.headerindex[j] do
        state.hide[j][k] = masterOn

        -- per-header override
        local rh = readPerHeaderArg(j, k, "-hide")
        if rh ~= "" then
            if Helpers and Helpers.yes and Helpers.yes(rh) then
                state.hide[j][k] = true
                state._hideExplicit[j][k] = true
            elseif Helpers and Helpers.no and Helpers.no(rh) then
                state.hide[j][k] = false
                state._hideExplicit[j][k] = true
            end
        end
    end
end

local function getByes(j)
    state.byes[j] = {}
    for k = 1, state.headerindex[j] do
        -- global byes
        local byes = (bargs("byes") or ""):lower()
        if (Helpers.yes and Helpers.yes(byes)) then
            state.byes[j][k] = true
        elseif tonumber(byes) then
            state.byes[j][k] = (j <= tonumber(byes))
        else
            state.byes[j][k] = false
        end
        -- per-round byes
        local r = (bargs("RD" .. j .. "-byes") or ""):lower()
        if (Helpers.yes and Helpers.yes(r)) then
            state.byes[j][k] = true
        elseif r == "no" or r == "n" then
            state.byes[j][k] = false
        end
        -- per-header byes
        local rh = (readPerHeaderArg(j, k, "-byes") or ""):lower()
        if (Helpers.yes and Helpers.yes(rh)) then
            state.byes[j][k] = true
        elseif rh == "no" or rh == "n" then
            state.byes[j][k] = false
        end
    end
end

local function getAltIndices()
    local Cmin, Cmax, R = config.minc, config.c, config.r

    for j = Cmin, Cmax do
        state.headerindex[j] = 0

        -- per-round counters
        local teamindex, textindex, groupindex = 1, 1, 1
        local row = state.entries[j]

        -- if the very first cell is nil, bump headerindex once (legacy quirk)
        if row and row[1] == nil then
            state.headerindex[j] = state.headerindex[j] + 1
        end

        -- walk rows in the round
        for i = 1, R do
            local e = row and row[i] or nil
            if e then
                local ct = e.ctype
                if ct == "header" then
                    e.altindex = state.headerindex[j]
                    teamindex, textindex = 1, 1
                    state.headerindex[j] = state.headerindex[j] + 1
                elseif ct == "team" then
                    e.altindex = teamindex
                    teamindex = teamindex + 1
                elseif ct == "text" or (ct == "line" and e.hastext == true) then
                    e.altindex = textindex
                    textindex = textindex + 1
                elseif ct == "group" then
                    e.altindex = groupindex
                    groupindex = groupindex + 1
                end
                e.headerindex = state.headerindex[j]
            end
        end

        getByes(j)
        getHide(j)
    end
end

-- ========================================
-- 9) PUBLIC API
-- ========================================
function Params.buildSkeleton(_state, _config, _Helpers, _StateChecks)
    bind(_state, _config, _Helpers, _StateChecks)
    buildSkeleton()
end

function Params.scanStructure(_state, _config, _Helpers, _StateChecks)
    bind(_state, _config, _Helpers, _StateChecks)
    getAltIndices()
end

function Params.assign(_state, _config, _Helpers, _StateChecks)
    bind(_state, _config, _Helpers, _StateChecks)
    assignParams()
end

-- ========================================
-- 10) SLICING FOR MIN ROUND (base offset)
-- ========================================
local function shiftCols(tbl, base, c)
    if not tbl then
        return {}
    end
    local out = {}
    for j = base + 1, c do
        out[j - base] = tbl[j]
    end
    return out
end

function Params.sliceForMinround(_state, _config)
    local base = _config.base or 0
    if base <= 0 then
        return
    end

    local oldC = _config.c
    local newC = oldC - base
    if newC < 1 then
        newC = 1
    end

    -- Shift all column-indexed tables
    _state.entries = shiftCols(_state.entries, base, oldC)
    _state.headerindex = shiftCols(_state.headerindex, base, oldC)
    _state.rlegs = shiftCols(_state.rlegs, base, oldC)
    _state.maxlegs = {} -- recompute later
    _state.hascross = {} -- rebuild later
    _state.crossCell = {} -- rebuild later
    _state.pathCell = {} -- rebuild later
    _state.skipPath = {} -- rebuild later
    _state.hide = shiftCols(_state.hide, base, oldC)
    _state.byes = shiftCols(_state.byes, base, oldC)
    _state.teamsPerMatch = shiftCols(_state.teamsPerMatch, base, oldC)
    _state.matchgroup = {} -- recompute

    -- Update view range: now we render 1..newC
    _config.c = newC
    _config.minc = 1
end

return Params