Module:Timeline of release years

require('strict')
local p = {}

local function items(args, year, oldrange)
	local itemList = {}
	
	-- First loop through is to find the lowest year range, if any. If oldrange is supplied, the year range must also be greater than it.
	local range = 0;
	if args[year .. '_to'] or args[year .. 'a_to'] then
		local newrange = tonumber(args[year .. '_to'] or args[year .. 'a_to'])
		if newrange and (oldrange == nil or newrange > oldrange) then
			range = newrange;
		end
	end
	for asciiletter = 98, 106 do -- 98 > b, 106 > j
		if args[year .. string.char(asciiletter) .. '_to'] then
			local newrange = tonumber(args[year .. string.char(asciiletter) .. '_to'])
			if newrange and (oldrange == nil or newrange > oldrange) and (range == 0 or newrange < range) then
				range = newrange;
			end			
		end
	end

	-- Find items, filtered by range if available.
	if args[year] or args[year .. 'a'] then
		local thisrange = tonumber(args[year .. '_to'] or args[year .. 'a_to'])
		if (range == 0 and thisrange == nil) or (thisrange and thisrange == range) then
			table.insert(itemList, args[year] or args[year .. 'a'])
		end
	end
	for asciiletter = 98, 106 do -- 98 > b, 106 > j
		if args[year .. string.char(asciiletter)] then
			local thisrange = tonumber(args[year .. string.char(asciiletter) .. '_to'])
			if (range == 0 and thisrange == nil) or (thisrange and thisrange == range) then
				table.insert(itemList, args[year .. string.char(asciiletter)])
			end
		end
	end
	return table.maxn(itemList), itemList, range
end

local function color(args, year, itemNum, to_range)

	if args[year .. '_color'] then
		return args[year .. '_color'] 
	end
	
	if to_range and args[year .. '_to_' .. to_range .. '_color'] then
		return args[year .. '_to_' .. to_range .. '_color']
	end

	for yearrange = 1, 10 do
		if args['range' .. yearrange] and args['range' .. yearrange .. '_color'] then
			local _, _, beginyear, endyear = string.find( args['range' .. yearrange], '^(%d%d%d%d)%D+(%d%d%d%d)$' )

			local year = tonumber(year) or 9999 -- For year == 'TBA'
			beginyear = tonumber(beginyear) or 0
			endyear =  tonumber(endyear) or 9999

			if year >= beginyear and year <= endyear then
				local _, _, color1, color2 = string.find( args['range' .. yearrange .. '_color'], '^(%S*)%s*(%S*)$' )
				color2 = string.find(color2, '^#?%w+$') and color2 or color1
				return itemNum > 0 and color1 or color2
			end
		end
	end

	return itemNum > 0 and '#0BDA51' or '#228B22'
end

local function left(builder, args, year, itemNum, range)
	builder = builder:tag('th')
		:attr('scope', 'row')
		:css('border-right', '1.4em solid ' .. color(args, year, itemNum, range))
	if itemNum > 1 then
		builder = builder:attr('rowspan', itemNum)
	end
	if year == 'TBA' then
		builder:tag('abbr'):attr('title', 'To be announced'):wikitext('TBA')
	else
		builder:wikitext(range ~= 0 and year .. '–' .. range or year)
	end
end

local function right(builder, itemNum, itemList)
	if itemNum == 0 then return end

	if itemNum == 1 then
		builder:tag('td')
			:wikitext(itemList[1])
		return
	end

	-- if itemNum >= 2
	builder:tag('td')
		:addClass('rt-first')
		:wikitext(itemList[1])

	for key = 2, itemNum - 1 do
		builder = builder:tag('tr')
			:tag('td')
			:addClass('rt-next')
			:wikitext(itemList[key])
	end

	builder = builder:tag('tr')
		:tag('td')
		:addClass('rt-last')
		:wikitext(itemList[itemNum])

end

local function row(builder, args, year, emptyyear, lastyear, highrange)
	local oldrange = nil
	
	repeat
		local itemNum, itemList, range = items(args, year, oldrange)
		
		-- Now check for a new high range and catch it. We need to know what highrange was prior to update though.
		local oldhighrange = nil
		if(range > 0 and (highrange == nil or range > highrange)) then
			oldhighrange = (highrange or range)
			highrange = range
		end
		oldhighrange = (oldhighrange or highrange)

		-- If compressempty is set, check for empty items, track empty years and high ranges, and
		-- put out a compressed range when next year is found.
		if args.compressempty and oldrange == nil then
			-- If we're compressing and there's no items, return this year for tracking.
			if #itemList < 1 then
				return year, highrange
			end

			-- If emptyyear is below or equal the highrange, we need to make adjustments.
			if(emptyyear and oldhighrange and emptyyear <= oldhighrange) then
				-- If the current year is highrange or highrange +1, suppress empty row output entirely.
				-- If the current year is highrange+2 or more, adjust the emptyyear to be above highrange)				
				if(year <= (oldhighrange+1)) then
					emptyyear = nil
				elseif(year > (oldhighrange+1)) then
					emptyyear = oldhighrange+1
				end
			end

			-- If we have items but are tracking an empty year, output compressed range row.
			if emptyyear ~= nil then
				builder = builder:tag('tr')
				if year == 'TBA' then
					left(builder, args, emptyyear, 0, lastyear)
				else
					if year-1 == emptyyear then
						left(builder, args, emptyyear, 0, 0)
					else
						left(builder, args, emptyyear, 0, year-1)
					end
				end
			end
		end
		
		-- We can break out if this is the case. This means we have looped through more than once, but there were no more items remaining.
		if range == 0 and oldrange and #itemList < 1 then
			break
		end

		builder = builder:tag('tr')
		left(builder, args, year, itemNum, range)
		right(builder, itemNum, itemList)

		if range ~= 0 then
			oldrange = range
		end
	until range == 0
	
	return nil, highrange
end

function p._main(args)
	-- Main module code goes here.
	local currentyear = os.date('%Y')

	local ret
	local firstyear, lastyear
	local TBA = items(args, 'TBA') > 0 and true or false

	ret = mw.html.create( 'table' )
		:addClass('release-timeline wikitable')
		:addClass(args.align == 'left' and 'rt-left' or nil)

	ret:tag('caption')
		:wikitext((args.title or 'Release timeline') ..
			(args.subtitle and ('<div class="rt-subtitle">'..args.subtitle..'</div>') or ''))

	if tonumber(args.first) then
		firstyear = tonumber(args.first)
	else
		for i = 1, currentyear do
			if items(args, i) > 0 then
				firstyear = i
				break
			end
		end
		firstyear = firstyear or (currentyear + 3)
	end

	if tonumber(args.last) then
		lastyear = tonumber(args.last)
	else
		for i = currentyear + 3, TBA and currentyear or firstyear, -1 do
			if items(args, i) > 0 then
				lastyear = i
				break
			end
		end
		lastyear = lastyear or (currentyear - 1)
	end

	local emptyyear = nil
	local highrange = nil
	for year = firstyear, lastyear do
		local yearcheck, newhighrange = row(ret, args, year, emptyyear, lastyear, highrange)
		if (emptyyear == nil and yearcheck ~= nil) or (emptyyear ~= nil and yearcheck == nil) then
			emptyyear = yearcheck
		end
		highrange = newhighrange		
	end

	if TBA then
		row(ret, args, 'TBA')
	end

	return mw.getCurrentFrame():extensionTag{
		name = 'templatestyles', args = { src = 'Module:Timeline of release years/styles.css'}
	} .. tostring(ret)
end

function p.main(frame)
	local args = require('Module:Arguments').getArgs(frame)
	return p._main(args)
end

return p