Module:Bracket

local util_args = require('Module:ArgsUtil') local util_cargo = require('Module:CargoUtil') local util_esports = require('Module:EsportsUtil') local util_map = require("Module:MapUtil") local util_table = require('Module:TableUtil') local util_text = require('Module:TextUtil') local util_tournament = require("Module:TournamentUtil") local util_vars = require('Module:VarsUtil') local bracket_wiki = require('Module:Bracket/Wiki') -- wiki localization per game local i18n = require('Module:i18nUtil')

local m_team = require('Module:Team') local lang = mw.getLanguage('en')

local ROWS_PER_TEAM = 6 local ROWS_PER_TITLE = 2 local ROWS_PER_HLINE = 1 local ROUNDWIDTH = 12 local LINEWIDTH = '3em' local SCOREWIDTH = 2

local TEAM_STYLE = nil -- constant from args local SHOW_BESTOF = false local CURRENT_ROUND = nil -- index used in too many places to pass in every function signature

local p = {} local h = {}

function p.main(frame) local tpl_args = util_args.merge i18n.init('Bracket') h.setConstants(tpl_args) -- use require instead of loadData so that we can use next and # local settings local function assignBracket settings = require('Module:Bracket/'.. tpl_args[1]) end if not tpl_args[1] then error(i18n.print('error_noDefinition')) elseif pcall(assignBracket) then -- pass else error(i18n.print('error_invalidInput', tpl_args[1])) end local args = h.processArgs(tpl_args) h.processSettings(settings, args) if util_args.castAsBool(args.cargo) then h.addCargoData(args, settings) end return h.makeOutput(args, settings) end

function h.setConstants(tpl_args) TEAM_STYLE = tpl_args.teamstyle SHOW_BESTOF = util_args.castAsBool(tpl_args.show_bestof) end

function h.processArgs(tpl_args) -- format tpl_args local args = {} for k, v in pairs(tpl_args) do		if type(k) ~= 'string' then -- pass elseif k:find('R%d+M%d+_.*_') then -- team-specific arg local r, m, val, team = k:match('R(%d+)M(%d+)_(.*)_(%d+)') r = tonumber(r) m = tonumber(m) h.initializeMatch(args, r, m) args[r][m]['team' .. team][val] = h.castArg(val, v)		elseif k:find('R%d+M%d+_.*') then -- match-specific arg local r, m, val = k:match('R(%d+)M(%d+)_(.*)') r = tonumber(r) m = tonumber(m) h.initializeMatch(args, r, m)			args[r][m][val] = h.castArg(val, v)		elseif k:find('R%d+_') then -- round-specific arg local r, val = k:match('R(%d+)_(.*)') r = tonumber(r) h.initializeMatch(args, r)			args[r][val] = v		else -- global arg args[k] = v		end end h.addImpliedArgs(args) return args end

function h.castArg(val, v)	if val == 'team' then return m_team.teamlinkname(v) end if val == 'bye' then return util_args.castAsBool(v) end if val == 'winner' or val == 'bestof' then return tonumber(v) end if val == 'winners' then return h.castWinnersArg(v) end if val == 'score' then return h.castScoreArg(v) end return v end

function h.castWinnersArg(v) return util_map.split(v, nil, tonumber) end

function h.castScoreArg(v) return util_map.split(v, nil, h.castPartScoreArg) end

function h.castPartScoreArg(str) return tonumber(str) or str end

function h.initializeMatch(args, r, m)	if not args[r] then args[r] = {} end if not args[r][m] and m then args[r][m] = { team1 = {}, team2 = {} } end end

function h.addImpliedArgs(args) for round, roundData in pairs(args) do		if type(round) == 'number' then for match, matchData in pairs(roundData) do				if type(match) == 'number' then h.addImpliedArgsToMatch(matchData) end end end end end

function h.addImpliedArgsToMatch(matchData) if matchData.class == 'qualified' then matchData.label = i18n.print('qualMatch') elseif matchData.class == 'relegated' then matchData.label = i18n.print('relMatch') end for i, v in ipairs({ 'team1', 'team2' }) do		if matchData[v] then h.addImpliedArgsToTeam(matchData[v], matchData, i)		end end end

function h.addImpliedArgsToTeam(team, matchData, i)	team.teamfinal = team.teamfinal or team.team team.iswinner = matchData.winner == i	team.bestof = matchData.bestof local function mapWinners(winner) return winner == i	end if matchData.winners then team.iswinners = util_map.copy(matchData.winners, mapWinners) end end

function h.processSettings(settings, args) -- in theory this could be done in the settings module before returning but -- this way the code is a bit more hidden from users editing stuff -- and also this makes the settings module closer to a read-only table that you -- import (and clone) here which i guess is nice? -- tbh im not sure if this was the right way to do it tho for r, col in ipairs(settings) do		local m = #col.matches while m >= 1 do			-- need to iterate backwards bc we'll delete third-place matches if hidden local match = col.matches[m] local lines = col.lines and col.lines[m] if lines and lines.reseed then lines.class = lines.class:format(lang:lc(args.reseed or 'reseeding')) end if match.argtoshow then if not util_args.castAsBool(args[match.argtoshow]) then if col.matches[m+1] then col.matches[m+1].above = (col.matches[m+1].above or 0) + (match.above or 0) + 6 end table.remove(col.matches,m) end end m = m - 1 end end end

-- cargo function h.addCargoData(args, settings) local overviewPage = util_esports.getOverviewPage(args.page) local data = h.doCargoQuery(overviewPage) if #data == 0 then return end local processed = h.processCargoData(data) h.addProcessedToArgs(args, settings, processed, overviewPage) end

function h.doCargoQuery(page) local query = { tables = { 'MatchSchedule=MS', 'Teams=Teams1', 'TournamentRosters=Ros1', 'Teams=Teams2', 'TournamentRosters=Ros2', },		join = { 'MS.Team1=Teams1._pageName', 'MS.PageAndTeam1=Ros1.PageAndTeam', 'MS.Team2=Teams2._pageName', 'MS.PageAndTeam2=Ros2.PageAndTeam', },		fields = h.getFields, where = ('MS.OverviewPage="%s"'):format(page), }	return util_cargo.queryAndCast(query) end

function h.getFields local fields = { 'MS.Team1', 'MS.Team2', 'MS.Team1Final', 'MS.Team2Final', 'MS.Winner [number]', 'MS.Player1', 'MS.Player2', 'MS.FF [number]', 'MS.Team1Score [number]', 'MS.Team2Score [number]', 'MS.Tab', 'MS.N_MatchInTab', 'MS.MatchId', -- idk what this is doing here 'MS.BestOf [number]', }	if util_tournament.isInternational then fields[#fields+1] = 'COALESCE(Ros1.Region, Teams1.Region)=Team1Region' fields[#fields+1] = 'COALESCE(Ros2.Region,Teams2.Region)=Team2Region' end return fields end

function h.processCargoData(data) local processed = {} for _, row in ipairs(data) do		util_esports.setScoreDisplays(row) if not row.Tab then error(i18n.print('error_noTabDefined', row.MatchId)) elseif not row.MatchId then error(i18n.print('error_noMatchIdDefined')) end processed[('%s_%s'):format(row.Tab,row.N_MatchInTab)] = { winner = row.Winner, team1 = h.getTeamCargoFromRow(row, 1), team2 = h.getTeamCargoFromRow(row, 2), }	end return processed end

function h.getTeamCargoFromRow(row, i)	local function field(field) return row['Team' .. i .. field] end local ret = { score = field('ScoreDisplay') and { field('ScoreDisplay') }, team = field(''), teamfinal = field('Final'), player = row['Player' .. i], iswinner = row.Winner == i,		bestof = row.BestOf, region = field('Region'), }	return ret end

function h.addProcessedToArgs(args, settings, processed, overviewPage) for r, col in ipairs(settings) do		h.initializeMatch(args, r)		local title = args[r] and args[r].title or col.matches.title or '' for m, _ in ipairs(col.matches) do			h.initializeMatch(args, r, m)			local argmatch = args[r] and args[r][m] if argmatch and argmatch.cargomatch then h.addMatchCargoToMatch(argmatch, processed[argmatch.cargomatch]) else -- the matchid does NOT include page number in it				local matchid = ('%s_%s'):format(title, m)				if not argmatch then h.initializeMatch(args, r, m)					argmatch = args[r][m] end h.addMatchCargoToMatch(argmatch, processed[matchid]) end end end end

function h.addMatchCargoToMatch(argMatch, cargoDataMatch) if not cargoDataMatch then return end -- allow arg data to overwrite cargo data always if applicable argMatch.winner = argMatch.winner or cargoDataMatch.winner for _, team in ipairs({ 'team1', 'team2' }) do		for k, v in pairs(cargoDataMatch[team]) do			argMatch[team][k] = argMatch[team][k] or v		end end end

-- print function h.makeOutput(args, settings) local output = mw.html.create if settings.togglers then h.printAllBrackets(args, settings, output) else h.printBracket(args, settings, output:tag('div'), {}) end return output end

function h.printAllBrackets(args, settings, output) local toggleN = util_vars.setGlobalIndex('BracketToggler') local togglers = h.makeTogglerButtons(settings.togglers, toggleN) local tblRound1 = h.printNextBracketDiv(output, toggleN, 1) h.printBracket(args, settings, tblRound1, togglers) local tableList = { tblRound1 } for i, toggle in ipairs(settings.togglers) do		h.setupNextToggle(settings, args, togglers, toggle, i)		local tbl = h.printNextBracketDiv(output, toggleN, i + 1) h.printBracket(args, toggle.bracket, tbl, togglers) tableList[#tableList+1] = tbl end h.setTableHidden(tableList, args.initround) end

function h.setupNextToggle(settings, args, togglers, toggle, i)	h.fixColumnLabelsForToggle(settings, toggle.bracket, i)	table.remove(args, 1) table.remove(togglers, 1) h.processSettings(toggle.bracket, args) end

function h.fixColumnLabelsForToggle(settings, bracket, i)	for k, col in ipairs(bracket) do		col.matches.title = settings[k + i].matches.title end end

function h.printNextBracketDiv(output, toggleN, i)	local div = output:tag('div') :addClass(h.allToggleClass(toggleN, false)) :addClass(h.roundToggleClass(toggleN, i, false)) return div end

function h.allToggleClass(n, isAttr) local dot = isAttr and '.' or '' return ('%sbracket-toggle-allrounds-%s'):format(dot, n) end

function h.roundToggleClass(n, i, isAttr) local dot = isAttr and '.' or '' return ('%sbracket-toggle-round-%s-%s'):format(dot, n, i) end

function h.makeTogglerButtons(togglers, n)	local tbl = {} tbl[1] = h.makeToggler(n, 1) for i, _ in ipairs(togglers) do		if i == #togglers then tbl[#tbl+1] = h.makeLastToggler(n) else -- first add 1 because we already did 1 from the default bracket tbl[#tbl+1] = h.makeToggler(n, i + 1) end end return tbl end

function h.makeToggler(n, i)	local div = mw.html.create('div') :addClass('bracket-toggler') :wikitext('[') div:tag('span') :addClass('alwaysactive-toggler') :attr('data-toggler-hide', h.allToggleClass(n, true)) :attr('data-toggler-show', h.roundToggleClass(n, i + 1, true)) :wikitext('x') div:wikitext(']') return div end

function h.makeLastToggler(n) local div = mw.html.create('div') :addClass('bracket-toggler') div:tag('span') :addClass('alwaysactive-toggler') :attr('data-toggler-hide', h.allToggleClass(n, true)) :attr('data-toggler-show', h.roundToggleClass(n, 1, true)) :wikitext('<<') return div end

function h.setTableHidden(tableList, initround) initround = tonumber(initround or 1) or 1 for k, tbl in ipairs(tableList) do		if k ~= initround then tbl:addClass('toggle-section-hidden') end end end

function h.printBracket(args, settings, tbl, togglers) tbl:addClass('bracket-grid') :css({			['grid-template-columns'] = h.getGTC(settings, args),			['grid-template-rows'] = h.getGTR(settings, args.notitle)		}) for r, col in ipairs(settings) do CURRENT_ROUND = 'round' .. (r - 1) h.addLinesColumn(tbl, col.lines, not args.notitle) CURRENT_ROUND = 'round' .. r		h.addMatchesColumn(tbl, args, col.matches, r, not args.notitle, togglers[r]) end return tbl end

function h.getGTC(settings, args) local scores = {} for round, col in ipairs(settings) do		scores[round] = args[round] and tonumber(args[round].extendedseries or '') or col.extendedseries or 1 end local firstcol = settings[1].lines and next(settings[1].lines) local firstwidth = firstcol and LINEWIDTH or '0' return h.getCustomGTC(scores, args.roundwidth, args.roundminwidth, firstwidth) end

function h.getCustomGTC(scores, roundwidth, minwidth, firstwidth) local linewidth = minwidth and ' minmax(2em,3em) ' or ' 3em ' roundwidth = h.getRoundwidth(roundwidth) minwidth = h.parseWidth(minwidth) or roundwidth local widths = {} for k, v in ipairs(scores) do		local min = (SCOREWIDTH * (v - 1) + minwidth) local max = (SCOREWIDTH * (v - 1) + roundwidth) widths[#widths+1] = ('minmax(%sem, %sem)'):format(min, max) end return firstwidth .. ' ' .. table.concat(widths, linewidth) end

function h.getRoundwidth(roundwidth) if not roundwidth then return ROUNDWIDTH end return h.parseWidth(roundwidth) end

function h.parseWidth(width) if not width then return nil end return tonumber(width:gsub('em',) or ) end

function h.getGTR(settings, notitle) local max = 0 for _, col in ipairs(settings) do		local total = 0 for _, match in ipairs(col.matches) do			total = total + (match.above or 0) if match.display == 'match' then total = total + ROWS_PER_TEAM elseif match.display == 'hline' then total = total + ROWS_PER_HLINE end end if total > max then max = total end end if not notitle then max = max + ROWS_PER_TITLE end return ('repeat(%s,var(--grid-row-height))'):format(max) end

function h.addLinesColumn(tbl, lineData, addtitle) if not lineData then return end for m, row in ipairs(lineData) do		if m == 1 and addtitle then h.addBracketLine(tbl, row, 2) else h.addBracketLine(tbl, row, 0) end end end

function h.addBracketLine(tbl, linerow, extra) if linerow.above + extra > 0 then local div = tbl:tag('div') :addClass('bracket-line') :addClass(CURRENT_ROUND) :cssText(('grid-row:span %s;'):format(linerow.above + extra)) end tbl:tag('div') :addClass('bracket-line') :addClass(linerow.class) :addClass(CURRENT_ROUND) :cssText(('grid-row:span %s;'):format(linerow.height)) end

function h.addMatchesColumn(tbl, args, data, r, addtitle, toggler) if addtitle then local title = args[r] and args[r].title or data.title or '' h.makeTitle(tbl, title, toggler) end for m, row in ipairs(data) do		local game = args[r] and args[r][m] or { team1 = {}, team2 = {} } if row.above then h.printSpacer(tbl, row.above) end if row.display == 'match' then h.makeMatch(tbl, game, not args.nolabels and row.label) elseif row.display == 'hline' then h.makeHorizontalCell(tbl) end end end

function h.makeTitle(tbl, text, toggler) local outerdiv = tbl:tag('div') :addClass('bracket-grid-header') :addClass(CURRENT_ROUND) local innerdiv = outerdiv:tag('div') :addClass('bracket-header-content') :wikitext(text) if toggler then innerdiv:node(toggler) end end

function h.makeHorizontalCell(tbl) tbl:tag('div') :addClass('bracket-spacer') :addClass('horizontal') :addClass(CURRENT_ROUND) end

function h.makeMatch(tbl, game, label) if game.label then label = game.label end h.printSpacer(tbl, nil, label) h.printTeam(tbl, game, game.team1) h.printTeam(tbl, game, game.team2) h.printSpacer(tbl, nil, nil) end

function h.printSpacer(tbl, above, label) local div = tbl:tag('div') :addClass('bracket-spacer') :addClass(CURRENT_ROUND) if label then div:wikitext(label) end if above then div:cssText(('grid-row:span %s;'):format(above)) end end

function h.printTeam(tbl, game, data) local line = tbl:tag('div') :addClass('bracket-team') :addClass(CURRENT_ROUND) :addClass(game.class) if not data.bye then util_esports.addTeamHighlighter(line, data.player or data.teamfinal or data.team) end if data.iswinner then line:addClass('bracket-winner') end local team = line:tag('div') :addClass('bracket-team-name') if data.free then team:wikitext(data.free) elseif data.bye then team:wikitext('BYE') line:addClass('bracket-bye') else bracket_wiki.teamDisplay(team, data, TEAM_STYLE) end h.printScore(line, data) end

function h.printScore(line, data) if SHOW_BESTOF and not data.score then h.printBestof(line, data) return end for i, v in ipairs(data.score or { '' }) do		local div = line:tag('div') :addClass('bracket-team-points') :wikitext(v or (data.bye and '-') or '') if data.iswinner then div:addClass('bracket-score-winner') elseif data.iswinners and data.iswinners[i] then div:addClass('bracket-score-loser') end end end

function h.printBestof(line, data) if not SHOW_BESTOF then return end if data.bye then return end local div = line:tag('div') :addClass('bracket-team-points') :addClass('bracket-team-bestof') if data.bestof then div:wikitext(i18n.print('bestof', data.bestof)) end end

return p