Questo modulo serve a realizzare dei tabelloni per i tornei sportivi.

(legenda colori)

{{#invoke:TeamBracket|teamBracket
|compact = yes/no [default: yes]
|rounds =
|maxround = deve essere inferiore a rounds
|headings = valorizzare con no per non farli apparire
|RD-shade = colore di sfondo dell'intestazione del round (se diverso da quello di default)
|RDn-RDn+1-path = valorizzare con 0 per non far apparire i collegamenti fra i due round selezionati
|byes = valorizzare con il numero di turni in cui un incontro potrebbe non verificarsi (bye, walkover, ecc.)
|seeds = valorizzare con yes per farli apparire in ogni turno
|autoseeds = yes/no [default: no]
|sets = alternativo a legs (se variano da turno a turno, separare con /)
|legs = alternativo a sets (se variano da turno a turno, separare con /)
|hideomittedscores = yes/no [default: no]
|aggregate = valorizzare con yes, sets o legs
|boldwinner = yes/no [default: no]
|seed-width = [default: 28px]
|team-width = [default: 170px]
|score-width = [default: 28px]
|RDn = intestazione del round (se diverso dal nome di default)
|RD-finalina = intestazione della finale per il 3º posto (se diverso dal nome di default)
|RDn-groupm =
|RDn-seedm = seed della squadra o del partecipante
|RDn-teamm = squadra o partecipante
|RDn-scorem = punteggio della squadra o del partecipante
|RDn-scorem-s = punteggio della squadra o del partecipante (se l'incontro prevede più di una gara o di un set)
|RDn-shadem-s = colore di sfondo del risultato (se diverso da quello di default)
|RDn-scorem-agg = sovrascrive il valore automatico
}}


--
-- This module will implement {{TeamBracket}}
--
require('Module:No globals')
local getArgs = require('Module:Arguments').getArgs

local p = {}
local yes_replies = { 'yes', 'y', 's', 'si', 'sì'}
local no_replies = { 'n', 'no'}
local debug = false

local style = {
	table = "border-style:none;font-size:90%;margin:1em 2em 1em 1em;border-collapse:separate;border-spacing:0",
	seed_cell = "background-color:#f2f2f2;border:1px solid #aaa;text-align:center;",
	team_cell = "background-color:#f2f2f2;border:1px solid #aaa;text-align:left;", --padding:0 2px
	score_cell ="background-color:#f9f9f9;border:1px solid #aaa;text-align:center;",
	header_third_place = "text-align:center;border:1px solid #aaa;background-color:#f2f2f2",
	header_cell = "text-align:center;border:1px solid #aaa;background-color:#f2f2f2",
	path1 = "border:0 solid black;border-right-width:2px",
	path2 = "border:0 solid black",
	group = "text-align:center", 
	winner = {'font-weight' , 'bold'},
	first_place = {'background-color', 'gold'},
	second_place = {'background-color', 'silver'},
	third_place = {'background-color', 'bronze'},
	row_height = "3px", 
	buffer_sx_width = "5px",
	seed_width = "25px",
	team_width = "150px",
	score_width = "25px",
	row_width = "10px"
}

if debug then
	style.group = style.group .. ";background-color:green"
	style.path1 = style.path1 .. ";background-color:#F08080"
	style.path2 = style.path2 .. ";background-color:#F08080"
end

--[[ ===============================================================================
Ritorna true se needle è lista haystack, altrimenti ritorna false
	===============================================================================]]
local function in_array( needle, haystack )
	if needle == nil then return false; end
	for n,v in ipairs( haystack ) do
		if v == needle then return true; end
	end
	return false;
end

--[[ ===============================================================================
Oggetto per generare un TeamBracket
- args: array dei parametri passati al modulo
===============================================================================]]
local TeamBracket = {}

function TeamBracket:new(args)

	local self = {}
	setmetatable(self, { __index = TeamBracket,
						 __tostring = function(t) return self:__tostring() end })

	self.args = args
	self.rounds = tonumber(self.args['rounds']) or 2
	self.teams = math.pow(2, self.rounds)
	self.compact = in_array(self.args['compact'], yes_replies)
	self.hideSeeds = in_array(self.args['seeds'], no_replies)
	self.showSeeds = in_array(self.args['seeds'], yes_replies)
	local padding = '%0' .. ((self.teams < 10) and 1 or 2) .. 'd'
	self.argname_pattern = 'RD%d-%s' .. padding
	self.scorename_pattern = 'RD%d-score' .. padding
	self.scores = {}
	-- load number of scores for each round
	local scores_raw = args['scores'] or '1'
	local max_scores = 1
	for i,score_value in ipairs(mw.text.split(scores_raw, ',')) do
		self.scores[i] = tonumber(score_value) or 1
		if self.scores[i] > max_scores then max_scores = self.scores[i] end
	end
	local last_scores = self.scores[#self.scores]
	for i =#self.scores+1, self.rounds do
		self.scores[i] = last_scores
	end
	if max_scores > 1 then
		self.scorename_pattern = self.scorename_pattern .. "-%d"
	end
	-- set default seeds for round 1
	if not(self.hideSeeds) then
		local seeds = self:getSeeds()
		local argname
		for i = 1, table.getn(seeds) do
			argname = self:getTeamArgName(1, 'seed', i)
			if not args[argname] then
				args[argname] = seeds[i]
			end
		end
	end
	self.tbl = mw.html.create('table'):cssText(style.table)
	if in_array(args['nowrap'], yes_replies) then
		self.tbl:css('white-space', 'nowrap')
	end
	self.rows = { }
	if self.compact then self.tbl:attr('cellpadding', '0') end
	self.last_element = {}
	self.rows = {}
	self.current_col = 0
	self.dump = {}
	self.not_draw_top = false
	self:renderHeading()
	self:renderTree()
	return self
end

--[[ ===============================================================================
-- collassa l'oggetto in una stringa contenente il wikicodice per generare l'albero
===============================================================================]]
function TeamBracket:__tostring()
	return tostring(self.tbl)
end


--[[ ===============================================================================
Genera i valori di default dei seeds
===============================================================================]]
function TeamBracket:getSeeds()
	local seeds = {1, 2}
	local count = 2
	local before = false
	for r = 2, self.rounds do
		local max = math.pow(2, r)
		for i = 1, count do
			local pos = i * 2
			if before then pos = pos - 1 end
			table.insert(seeds, pos, max - seeds[i * 2 - 1] + 1)
			before = not before
		end
		count = count * 2
	end
	return seeds
end

--[[ ===============================================================================
Se fino alla riga row, colonna col-1 non sono ancora stati aggiunti elementi aggiunte
una cella vuota dall'ultima colonna usta dalla riga. 
Aggiorna quindi il conteggio delle colonne usate alla posizione colonna +width 
Il valore di default di width è 0 
=============================================================================== --]]
function TeamBracket:addGap(row, col, width, debug_text)
	width = width or 0
	if self.last_element[row] + 1 < col  then
		local gap = tostring(col - self.last_element[row] - 1)
		self.rows[row]:tag('td'):css('background-color', (debug and '#F5F5DC') or ''):attr('colspan', gap)
		--:wikitext(self.last_element[row] .. " c:".. col .. " w gap:" .. gap .. " w add:" .. width .. " n col:" ..  col + width)
	end
	self.last_element[row] = col + width
end

--[[ ===============================================================================
Aggiunge le righe grafiche di congiunzione delle celle per la riga index del
turno round. top e left indicano se deve essere generata rispettivamente la riga
sinistra o quella superiore
- row1 = prima riga del match superiore da cui esce il path
- row2 = prima riga del match inferiore da cui esce il path
- row_gap = ampiezza del path
- col = colonna in cui inizia il path
row1 nullo significa che il path parte solo da row2 e viceversa. Se entrambi sono
nulli la funzione non disegna niente.
===============================================================================]]
function TeamBracket:addPath(row1, row2, row_gap, col)
	if  not (row1 or row2) then return end
	local start_row = row1 and row1 + 2
	local end_row = row2
	local middle_row
	if start_row then
		if not end_row then
			row_gap = row_gap / 2
			middle_row = start_row + row_gap - 1
		else
			middle_row = start_row + row_gap/2 - 1
		end
	else
		row_gap = row_gap / 2 
		middle_row = end_row - row_gap + 2
	end
	if start_row then 
		for i = start_row, middle_row -1 do self:addGap(i, col) end
	end
	self:addGap(middle_row, col, 1)
	if end_row then 
		for i =  middle_row +1, end_row+1 do self:addGap(i, col) end
	end

	local cell = self.rows[start_row or middle_row]
		:tag('td')
		:attr('rowspan', row_gap)
		:cssText(style.path1)
	if start_row then  cell:css('border-top-width', '2px') end
	if end_row then cell:css('border-bottom-width', '2px') end
	local cell2 = self.rows[middle_row]:tag('td'):cssText(style.path2)
	if start_row then 
		cell2:css('border-bottom-width', '2px')
	else
		cell2:css('border-top-width', '2px')
	end
end

function TeamBracket:getWidth(param, default)
	local arg = self.args[param .. '-width']
	if not arg or arg == '' then arg = default end
	if tonumber(arg) ~= nil then
		arg = arg .. 'px'
	end
	return arg
end

function TeamBracket:getTeamArgName(round, argname, team)
	return string.format(self.argname_pattern, round, argname, team)
end

function TeamBracket:getTeamArg(round, argname, team)
	return self.args[self:getTeamArgName(round, argname, team)]
end

function TeamBracket:getScoreArg(round, team, score)
	return self.args[string.format(self.scorename_pattern, round, team, score)]
end

function TeamBracket:getRoundName(round)
	local name = self.args['RD' .. round]
	if name then return name end
	local round_names = {"Finale", "Semifinali", "Quarti", "Ottavi"}
	local roundFromLast = self.rounds - round + 1
	if roundFromLast < 5 then
		return round_names[roundFromLast]
	else
		return tostring(round) .. "° turno"
	end
end

function TeamBracket:renderTeam(round, team_name, team_number, row, show_seed, top, winner, score_results, debug_info)

	self:addGap(row, self.current_col, self.current_width)
	self:addGap(row+1, self.current_col, self.current_width)
	-- seed cell
	local seedCell
	local seedArg = self:getTeamArg(round, 'seed', team_number)
	if show_seed and (not self.hideSeeds) then
		seedCell = self.rows[row]:tag('td'):attr('rowspan', '2'):cssText(style.seed_cell):wikitext(seedArg):newline()
		if self.not_draw_top then seedCell:css('border-top-width', '0' ) end	
		if winner then seedCell:css(style.winner[1], style.winner[2]) end
	end
	-- team cell
	local teamCell = self.rows[row]:tag('td'):attr('rowspan', '2'):cssText(style.team_cell):wikitext(team_name):newline()
	if not show_seed and (not self.hideSeeds) then
		teamCell:attr('colspan', '2')
	end
	if self.not_draw_top then
		teamCell:css('border-top-width', '0' )
	end
	if winner then teamCell:css(style.winner[1], style.winner[2]) end
	if round == self.rounds then
		if winner then 
			teamCell:css(style.first_place[1], style.first_place[2])
		else
			teamCell:css(style.second_place[1], style.second_place[2])
		end
	end
	-- scores cells
	for i = 1, self.scores[round] do
		local scoreCell = self.rows[row]
			:tag('td')
			:attr('rowspan', '2')
			:cssText(style.score_cell)
			:wikitext(self:getScoreArg(round, team_number, i))
			:newline()
		if self.not_draw_top then scoreCell:css('border-top-width', '0') end
		if score_results[i] then scoreCell:css(style.winner[1], style.winner[2]) end
	end
	if round == 1 then self.not_draw_top = not self.not_draw_top end
end

function TeamBracket:getScore(round, team_number, i)
	return tonumber(self:getScoreArg(round, team_number, i)) or 0
end

function TeamBracket:getWinner(round, first_team_number)
	local victory1 = 0
	local victory2 = 0
	local winners = {}
	for i = 1, self.scores[round] do
		local score1 = self:getScore(round, first_team_number, i)
		local score2 = self:getScore(round, first_team_number+1, i)
		if score1 > score2 then
			victory1 = victory1 + 1
			winners[i] = 1
		else 
			victory2 = victory2 + 1
			winners[i] = 2
		end
	end
	if victory1 == victory2 then return 0, winners end
	if victory1 > victory2 then return 1, winners end
	return 2, winners
end

function TeamBracket:renderMatch(round, match_number, row_base, debug_info)
	local team_number2 = match_number * 2 
	local team_number1 = team_number2 - 1
	local team_name1 = self:getTeamArg(round, 'team', team_number1)
	local team_name2 = self:getTeamArg(round, 'team', team_number2)
	if team_name1 == 'bye' or team_name2 == 'bye' then
		self.not_draw_top = false
		return nil
	end
	local seedArg1 = self:getTeamArg(round, 'seed', team_number1)
	local seedArg2 = self:getTeamArg(round, 'seed', team_number2)
	local showSeed = self.showSeeds
		or (seedArg1 and seedArg1 == '-')
		or (seedArg2 and seedArg2 == '-')
	local winner, score_results = self:getWinner(round, team_number1)
	self:renderTeam(round, team_name1, team_number1, row_base, showSeed, true, winner == 1, score_results, debug_info)
	self:renderTeam(round, team_name2, team_number2, row_base+2, showSeed, false, winner ==2, score_results, debug_info)
	return row_base
end

function TeamBracket:render_final_3(row)
	local team_name1 = self:getTeamArg(self.rounds, 'team', 3)
	local team_name2 = self:getTeamArg(self.rounds, 'team', 4)
	if team_name1 == 'bye' or team_name2 == 'bye' then
		return nil
	end
	local seedArg1 = self:getTeamArg(self.rounds, 'seed', 3)
	local seedArg2 = self:getTeamArg(self.rounds, 'seed', 4)
	local showSeed = self.showSeeds
		or (seedArg1 and seedArg1 == '-')
		or (seedArg2 and seedArg2 == '-')
	local winner, score_results = self:getWinner(self.rounds, 3)
	local seedArg1 = self:getTeamArg(self.rounds, 'seed', 3)
	local seedArg2 = self:getTeamArg(self.rounds, 'seed', 4)
	self:addGap(row, self.current_col, self.current_width)
	self.rows[row]:tag('td')
		:cssText(style.header_third_place)
		:attr('colspan', self.current_width + 1)
		:wikitext("Finale 3° posto")
		:newline()
	self:renderTeam(self.rounds, team_name1, 3, row+2, showSeed, true, winner==1, score_results)
	self:renderTeam(self.rounds, team_name2, 4, row+4, showSeed, true, winner==1, score_results)
end

function TeamBracket:AddGroup(round, row, group_number)
	local name = self.args[string.format('RD%d-group%d', round, group_number)]
	if name then
		local span =  self.current_col + self.current_width - 1
		self:addGap(row, self.current_col, self.current_col + span)
		self:addGap(row+1, self.current_col, self.current_col + span)
		self.rows[row]:tag('td')
			:attr('rowspan', '2')
			:attr('colspan', span)
			:cssText(style.group)
			:wikitext(name)
			:newline()
	end
end

function TeamBracket:renderTree()
	local function normal_gap(row_count, match_number)
		return (row_count + 2 - match_number * 4) / match_number
	end

	local function compact_gap(row_count, match_number)
		return (row_count - 4 * match_number) / match_number
	end

	-- create 3 or 1 rows for every team
	local row_count =  self.teams * 2 + (self.compact and 0 or (self.teams - 2))
	local gap_function = (self.compact and compact_gap) or normal_gap
	for i = 1, row_count do
		self.rows[i] = mw.html.create('tr')
		self.rows[i]:tag('td'):css('height', style.row_height):wikitext(debug and i or '')
		self.last_element[i] = 1
	end
	self.current_col = 2
	for round =1, self.rounds do
		self.current_width = (self.hideSeeds and 2 or 1) + self.scores[round]
		local match_number = math.pow(2, self.rounds - round)
		local gap = gap_function(row_count, match_number)
		local row_base = gap / 2 + (self.compact and 1 or 0)
		local group_number = 1
		for n = 1, match_number, 2 do
			local match1 = self:renderMatch(round, n, row_base)
			if round < self.rounds then
				local match2 = self:renderMatch(round, n+1, row_base + gap + 4)
				if not self.compact and round % 4 == 1 then
					self:AddGroup(round, row_base+4, group_number)
					group_number = group_number + 1
				end
				self:addPath(match1, match2, gap + 4, self.current_col + self.current_width + 1)
				row_base = row_base + 2 * gap + 8
			end
		end
		self.current_col = self.current_col + self.current_width + 3
	end
	local third_place = self:getTeamArg(self.rounds, 'team', 3)
	--if true then return third_place end
	if third_place then -- semifinale 3° e 4° posto
		self.current_col = self.current_col - self.current_width - 3
		local offset = gap_function(row_count, 1) / 2 + (self.compact and 1 or 0) + 6
		if offset+5 > row_count then
			for i=row_count+1, offset+5 do
				self.rows[i] = mw.html.create('tr')
				self.rows[i]:tag('td'):css('height', style.row_height):wikitext(debug and i or '')
				self.last_element[i] = 1
			end
		end
		self:render_final_3(offset)
	end			
	for _,row in ipairs(self.rows) do
		self.tbl:node(row)
	end
end

function TeamBracket:renderHeading()
	local titleRow = self.tbl:tag('tr')
	local widthRow = self.tbl:tag('tr')
	local blank_text = self.compact and '' or '&nbsp;'
	titleRow:tag('td')
	local row_count = 1
	widthRow:tag('td')
		:css('width', style.buffer_sx_width)
		:css('height', '5px')
		:wikitext(debug and row_count or '')
	row_count = row_count + 1
	for round = 1, self.rounds do
		local colspan = tostring((self.hideSeeds and 1 or 2) + self.scores[round])
		local teamCell = titleRow:tag('td')
			:cssText(style.header_cell)
			:attr('colspan', colspan)
			:wikitext(self:getRoundName(round))
			:newline()
		if not self.hideSeeds then
			widthRow:tag('td'):css('width', self:getWidth('seed', style.score_width)):wikitext(debug and row_count or blank_text)
			row_count = row_count + 1
		end
		teamCell = widthRow:tag('td'):css('width', self:getWidth('team', style.team_width)):wikitext(debug and row_count or blank_text)
		row_count = row_count + 1
		for i = 1, self.scores[round] do
			widthRow:tag('td'):css('width', self:getWidth('score', style.score_width)):wikitext(debug and row_count or blank_text)
			row_count = row_count + 1
		end
		if round < self.rounds then
			titleRow:tag('td'):attr('colspan', '2')
			widthRow:tag('td'):css('width', style.row_width):wikitext(debug and row_count or blank_text)
			row_count = row_count + 1
			widthRow:tag('td'):css('width', style.row_width):wikitext(debug and row_count or blank_text)
			row_count = row_count + 1
		end
	end
end

function p.teamBracket(frame)
	local args = getArgs(frame, {
		-- se l'argomento è un seed lo ritorna così com'è anche se blank, tutti gli altri argomenti sono puliti
		valueFunc = function (key, value)
			if not value then return nil end
			if key:find("^RD%d%d?-seed") then return value end
			value = mw.text.trim(value)
			if value == '' then return nil end 
			return value
		end
		}
	)
	--return  TeamBracket:new(args)
	debug = in_array(args['debug'], yes_replies) or debug
	local team_bracket = TeamBracket:new(args)
	return tostring(team_bracket)
end

return p