Module:Bracket

To edit the documentation or categories for this module, click here.


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

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 sep = '%s*,%s*'

local h = {}

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
			local r, m, val, team = k:match('R(%d+)M(%d+)_(.*)_(%d+)')
			r = tonumber(r)
			m = tonumber(m)
			h.initializeMatch(args, r, m)
			if val == 'team' then
				args[r][m]['team' .. team][val] = m_team.teamlinkname(v)
			else
				args[r][m]['team' .. team][val] = v
			end
		elseif k:find('R%d+M%d+_.*') then
			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] = v
		elseif k:find('R%d+_') then
			local r, val = k:match('R(%d+)_(.*)')
			r = tonumber(r)
			h.initializeMatch(args, r)
			args[r][val] = v
		else
			args[k] = v
		end
	end
	return args
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.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 not next(data) then
		return
	end
	local processed = h.processCargoData(data)
	h.addProcessedToArgs(args, settings, processed, overviewPage)
end

function h.doCargoQuery(page)
	local query = {
		tables = 'MatchSchedule',
		fields = {
			'Team1',
			'Team2',
			'Team1Final',
			'Team2Final',
			'Winner',
			'FF',
			'Team1Score',
			'Team2Score',
			'Tab',
			'N_MatchInTab',
			'UniqueMatch'
		},
		where = ('OverviewPage="%s"'):format(page),
		types = {
			-- keep winner as a string since that's what's expected from args sigh
			Team1Score = 'number',
			Team2Score = 'number',
			FF = 'number'
		},
		limit = 9999
	}
	return util_cargo.queryAndCast(query)
end

function h.processCargoData(data)
	local processed = {}
	for _, row in ipairs(data) do
		h.sortFF(row)
		processed[('%s_%s'):format(row.Tab,row.N_MatchInTab)] = {
			winner = row.Winner,
			team1 = { score = row.Team1Score, team = row.Team1, teamfinal = row.Team1Final },
			team2 = { score = row.Team2Score, team = row.Team2, teamfinal = row.Team2Final },
		}
	end
	return processed
end

function h.sortFF(row)
	if row.FF == 1 then
		row.Team1Score = 'FF'
		row.Team2Score = 'W'
	elseif row.FF == 2 then
		row.Team1Score = 'W'
		row.Team2Score = 'FF'
	end
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 uniquematch does NOT include page number in it
				local uniquematch = ('%s_%s'):format(title, m)
				if not argmatch then
					h.initializeMatch(args, r, m)
					argmatch = args[r][m]
				end
				h.addMatchCargoToMatch(argmatch, processed[uniquematch])
			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 round, col in ipairs(settings) do
		h.addLinesColumn(tbl, col.lines, round, not args.notitle)
		h.addMatchesColumn(tbl, args, col.matches, round, not args.notitle, togglers[round])
	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 roundwidth then
		return h.parseWidth(roundwidth)
	else
		return ROUNDWIDTH
	end
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, r, addtitle)
	local roundname = 'round' .. (r - 1)
	if not lineData then
		return
	end
	for m, row in ipairs(lineData) do
		if m == 1 and addtitle then
			h.addBracketLine(tbl, roundname, row, 2)
		else
			h.addBracketLine(tbl, roundname, row, 0)
		end
	end
	return
end

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

function h.addMatchesColumn(tbl, args, data, r, addtitle, toggler)
	local roundname = 'round' .. r
	if addtitle then
		local title = args[r] and args[r].title or data.title or ''
		h.makeTitle(tbl, roundname, 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.addSpacer(tbl, roundname, row.above)
		end
		if row.display == 'match' then
			h.makeMatch(tbl, game, roundname, not args.nolabels and row.label, args.teamstyle)
		elseif row.display == 'hline' then
			h.makeHorizontalCell(tbl, roundname)
		end
	end
	return
end

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

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

function h.makeMatch(tbl, game, roundname, label, teamstyle)
	if game.label then label = game.label end
	h.addSpacer(tbl, roundname, nil, label)
	h.makeTeam(tbl, roundname, game, game.team1, '1', teamstyle)
	h.makeTeam(tbl, roundname, game, game.team2, '2', teamstyle)
	h.addSpacer(tbl, roundname)
	return
end

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

function h.makeTeam(tbl, roundname, game, data, n, teamstyle)
	local isWinner = game.winner == n
	local isbye = util_args.castAsBool(data.bye)
	local line = tbl:tag('div')
		:addClass('bracket-team')
		:addClass(roundname)
		:addClass(game.class)
	util_esports.addTeamHighlighter(line, data.teamfinal or data.playerlink or data.player or data.team)
	if 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 isbye then
		team:wikitext('BYE')
		line:addClass('bracket-bye')
	else
		bracket_wiki.teamDisplay(team, data, teamstyle)
	end
	h.makeScore(line, data.score, isbye, game.winners, n)
end

function h.makeScore(line, score, isbye, winners, n)
	local tbl = util_text.split(tostring(score or ''),sep)
	tbl_win = winners and util_text.split(winners, sep) or {}
	for k, v in ipairs(tbl) do
		local div = line:tag('div')
			:addClass('bracket-team-points')
			:wikitext(v or (isbye and '-') or '')
		if tbl_win[k] == n then
			div:addClass('bracket-score-winner')
		elseif tbl_win[k] then
			div:addClass('bracket-score-loser')
		end
	end
end

local p = {}

function p.main(frame)
	local tpl_args = util_args.merge(true)
	-- 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('No bracket definition provided!')
	elseif pcall(assignBracket) then
		-- pass
	else
		error(('Bracket %s is not a valid input'):format(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

return p