Module:Build bracket/Params

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

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.