Документация
local p = {}

local colorContrastModule = require('Модуль:Color contrast')
local htmlColor = mw.loadData('Модуль:Color contrast/colors')
local delinkModule = require('Модуль:Delink')
local mathModule = require('Модуль:Math')
local tableEmptyCellModule = require('Модуль:Пустая ячейка таблицы')
local yesNoModule = require('Модуль:Yesno')

local row
local nonNilParams = 0
local cellValueTBA = false
local trackingCategories = ""

local trackingCategoryList = {
	["air_dates"] = "[[Категория:Википедия:Списки эпизодов с неоформленной датой показа]]",
	["alt_air_dates"] = "[[Категория:Википедия:Списки эпизодов с неверно оформленной датой показа]]",
	["faulty_line_colors"] = "[[Категория:Википедия:Списки эпизодов с неверным цветом границ]]",
	["non_compliant_line_colors"] = "[[Категория:Википедия:Списки эпизодов с неверным цветом границ]]",
	["default_line_colors"] = "[[Категория:Википедия:Списки эпизодов с неверным цветом границ]]",
	["row_deviations"] = "[[Категория:Википедия:Списки эпизодов с отклонениями строк]]",
	["invalid_top_colors"] = "[[Категория:Википедия:Потенциально нечитаемые таблицы эпизодов]]",
	["tba_values"] = "[[Категория:Википедия:Списки эпизодов с незаполненными ячейками]]",
	["nonmatching_numbered_parameters"] = "[[Категория:Википедия:Списки эпизодов с неверными параметрами]]",
	["raw_unformatted_storyteleplay"] = "[[Категория:Википедия:Списки эпизодов с неоформленными сюжетом или телесценарием]]"
}

local cellNameList = {
	'Aux1','Столбец1',
	'DirectedBy','Режиссер',
	'WrittenBy','Сценарист',
	'Aux2','Столбец2',
	'Aux3','Столбец3',
	'OriginalAirDate','ДатаПоказа',
	'AltDate','ДатаПоказа2',
	'Guests','Гости',
	'MusicalGuests','Персоны',
	'ProdCode','ПродКод',
	'Viewers','Зрителей',
	'Aux4', 'Столбец4'
}

-- Список взаимоисключающих параметров
local excludeList = {
	['Guests'] = 'Aux1',['Гости'] = 'Столбец2',
	['MusicalGuests'] = 'Aux2',['Персоны'] = 'Столбец3'
}

-- Список ячеек, имеющих группы параметров
local parameterGroupCells = {}
local firstParameterGroupCell

-- Список параметров заголовка для списков с несколькими названиями.
local titleList = {
	'Title','Название',
	'RTitle','НазваниеПрим',
	'AltTitle','Название2',
	'RAltTitle','Название2Прим',
	'NativeTitle','НазваниеОригинал',
	'TranslitTitle','НазваниеТранслит'
}

-- Локальная функция для получения номера эпизода или производственного кода,
-- без какого-либо дополнительного текста.
local function idTrim(val, search)
	local valFind = string.find(val, search)

	if (valFind == nil) then
		return val
	else
		return string.sub(val, 0, valFind-1)
	end
end

-- Локальная функция для проверки того, что параметр имеет значение.
local function hasValue(param)
	if (param ~= nil and param ~= "") then
		return true
	else
		return false
	end
end

-- Локальная функция для создания транслитерации.
local function langSpan(translitNativeTitle, langCode)
	local line = mw.html.create('span')
	if hasValue(langCode) then
		line
			:attr('lang', langCode)
			:attr('xml:lang', langCode)
			:css('font-style','normal')
			:wikitext(translitNativeTitle)
	end
	return tostring(line)
end

-- Локальная функция для создания аббревиатуры
local function abbr(text,title)
	local abbr = mw.html.create('abbr')
		:attr('title',title)
		:wikitext(text)
	return tostring(abbr)
end

-- Локальная функция для создания ячейки Таблицы эпизодов.
local function createTableData(text, rowSpan, textAlign)
	if (rowSpan ~= nil and tonumber(rowSpan) > 1) then
		row:tag('td')
			:attr('rowspan', rowSpan)
			:wikitext(text)
	else
		row:tag('td')
			:css('text-align', textAlign)
			:wikitext(text)
	end
end

-- Локальная функция для добавления отслеживающей категории  на страницу.
local function addTrackingCategory(category)
	trackingCategories = trackingCategories .. category
end

-- Локальная функция для создания Краткого Содержания.
local function createShortSummaryRow(args, lineColor, linetop)
	-- исправление Краткого Содержания
	local shortSummaryText = args.ShortSummary or args['КраткоеСодержание']

	if hasValue(shortSummaryText) and (shortSummaryText:match('^[*:;#]') or shortSummaryText:match('^{|')) then
		shortSummaryText = '<span></span>\n' .. shortSummaryText
	end

	if hasValue(shortSummaryText) and (shortSummaryText:match('\n[*:;#]')) then
		shortSummaryText = shortSummaryText .. '\n<span></span>'
	end
	
	if (linetop) then
		shortSummaryText = nil
	end

	local shortSummaryCell = mw.html.create('td')
				:addClass('description')
				:css('border-bottom', 'solid 3px ' .. lineColor)
				:attr('colspan', nonNilParams)
				:newline()
				:wikitext(shortSummaryText)

	local cell = mw.html.create('tr')
					:addClass('expand-child')
					:node(shortSummaryCell)
	
	if (linetop) then
		local len = mw.ustring.len(cell)
		cell = mw.ustring.sub(cell, 1, len-10 )
	end
	
	return tostring(cell)
end

-- Локальная функция для добавления отслеживающей категории при проблемах с цветом.
local function addTopColorTrackingCategories(args)
	if (hasValue(args.TopColor or args['ЦветСтроки']) or '#eaecf0') then
		addTrackingCategory(trackingCategoryList["row_deviations"])

		-- Проверка контрастности в соответствии с [[ВП:ЦВЕТ]].
		local textContrastRatio = colorContrastModule._ratio{args.TopColor or args['ЦветСтроки'], 'black', ['error'] = 0}
		local linkContrastRatio = colorContrastModule._ratio{args.TopColor or args['ЦветСтроки'], '#0B0080', ['error'] = 0}
		local visitedLinkContrastRatio = colorContrastModule._ratio{args.TopColor or args['ЦветСтроки'], '#0645AD', ['error'] = 0}

		if (textContrastRatio < 4.5 or linkContrastRatio < 3 or visitedLinkContrastRatio < 3) then
			addTrackingCategory(trackingCategoryList["invalid_top_colors"])
		end
	end
end

-- Локальная функция для добавления отслеживающей категории при проблемах с цветом границы.
local function addLineColorTrackingCategories(args)
	if (hasValue(args.LineColor or args['ЦветГраницы']) or '#a2a9b1') then
		local blackContrastRatio = colorContrastModule._ratio{args.LineColor or args['ЦветГраницы'], 'black', ['error'] = 0}
		local whiteContrastRatio = colorContrastModule._ratio{'white', args.LineColor or args['ЦветГраницы'], ['error'] = 0}
		
		if (args.LineColor or args['ЦветГраницы']) == '' then
			addTrackingCategory(trackingCategoryList["faulty_line_colors"])
		elseif (blackContrastRatio < 3 and whiteContrastRatio < 3) then
			addTrackingCategory(trackingCategoryList["non_compliant_line_colors"])
		end
	else
		addTrackingCategory(trackingCategoryList["default_line_colors"])
	end
end

-- Локальная функция для удаления вики-ссылок из повторяющейся информации в ячейках.
local function removeWikilinks(args, v)
	return delinkModule._delink{args[v]}
end

-- Локальная функция для установки текста пустой ячейки либо "TBD" - to be announced, либо  "н/д" - нет данных.
local function setTBDStatus(args, awaitingText, expiredText, weeks)
	if args.OriginalAirDate == nil or args['ДатаПоказа'] == nil or args.OriginalAirDate == '' or args['ДатаПоказа'] == '' then
		return tableEmptyCellModule._main({alt_text = awaitingText, title_text = awaitingText})
	end
	
	local month, day, year = args.OriginalAirDate:gsub("&nbsp;", " "):match("(%a+) (%d+), (%d+)") or args['ДатаПоказа']:gsub("&nbsp;", " "):match("(%a+) (%d+), (%d+)")

	if (month == nil) then
		day, month, year = args.OriginalAirDate:gsub("&nbsp;", " "):match("(%d+) (%a+) (%d+)") or args['ДатаПоказа']:gsub("&nbsp;", " "):match("(%d+) (%a+) (%d+)")
	end

	if (day == nil) then
		return tableEmptyCellModule._main({alt_text = "TBD"})
	else
		-- Список месяцев. 
		local monthList = {
			['января'] = 1, ['январь'] = 1, ['January'] = 1,
			['февраля'] = 2, ['февраль'] = 2, ['February'] = 2,
			['марта'] = 3, ['март'] = 3, ['March'] = 3,
			['апреля'] = 4, ['апрель'] = 4, ['April'] = 4,
			['мая'] = 5, ['май'] = 5, ['May'] = 5,
			['июня'] = 6, ['июнь'] = 6, ['June'] = 6,
			['июля'] = 7, ['июль'] = 7, ['July'] = 7,
			['августа'] = 8, ['август'] = 8, ['August'] = 8,
			['сентября'] = 9, ['сентябрь'] = 9, ['September'] = 9,
			['октября'] = 10, ['октябрь'] = 10, ['October'] = 10,
			['ноября'] = 11, ['ноябрь'] = 11, ['November'] = 11,
			['декабря'] = 12, ['декабрь'] = 12, ['December'] = 12
		}
		
		if not monthList[month] then
			error('Неверный месяц ' .. month)
		end

		local seconds = os.time() - os.time({year = year, month = monthList[month], day = day, hour = 0, min = 0, sec = 0})

		if (seconds >= 60 * 60 * 24 * 7 * weeks) then
			return tableEmptyCellModule._main({alt_text = expiredText, title_text = expiredText})
		else
			return tableEmptyCellModule._main({alt_text = awaitingText, title_text = awaitingText})
		end
	end
end

-- Local function which is used to create an empty cell.
local function createEmptyCell(args, v, unsetParameterGroup)
	if (unsetParameterGroup) then
		args[v] = tableEmptyCellModule._main({alt_text = "н/д"})
	elseif (v == 'Viewers' or v == 'Зрителей' and hasValue(args.OriginalAirDate or args['ДатаПоказа'])) then
		args[v] = setTBDStatus(args, "TBD", "н/д", 4)
	elseif (v == 'DirectedBy' and v == 'Режиссер' or v == 'WrittenBy' and v == 'Сценарист') then
		args[v] = setTBDStatus(args, "TBA", "Неизвестно", 4)
	else
		args[v] = tableEmptyCellModule._main({})
	end
end

-- Air dates that don't use {{Start date}}
local function checkUsageOfDateTemplates(args, v, onInitialPage, title)
	if (v == 'OriginalAirDate' or v == ' ДатаПоказа'
		and args[v] ~= ''
		and string.match(args[v], '%d%d%d%d') ~= nil
		and string.match(args[v], '2C2C2C') == nil
		and string.find(args[v], 'dtstart') == nil
		and onInitialPage
		and title.namespace == 0)
	then
		addTrackingCategory(trackingCategoryList["air_dates"])
	end

	-- Alternate air dates that do use {{Start date}}
	if (v == 'AltDate' or v == 'ДатаПоказа2' and args[v] ~= '' and string.find(args[v], 'dtstart') ~= nil and onInitialPage and title.namespace == 0) then
		addTrackingCategory(trackingCategoryList["alt_air_dates"])
	end
end

-- Local function which is used to create a Production Code cell.
local function createProductionCodeCell(args, v)
	if (hasValue(args.ProdCode or args['ПродКод']) and string.find(args.ProdCode or args['ПродКод'], 'TBA') == nil) then
		row:tag('td')
			:attr('id', 'pc' .. idTrim(idTrim(args.ProdCode or args['ПродКод'], ' ----'), '<'))
			:css('text-align', 'center')
			:wikitext(args.ProdCode or args['ПродКод'])
	elseif ((args.ProdCode == '' or args['ПродКод'] == '') and string.find(args.ProdCode or args['ПродКод'] or '', 'TBA') ~= nil) then
		createEmptyCell(args, v, false)
		createTableData(args.ProdCode or args['ПродКод'], 1, "center")
	else
		-- ProductionCode parameter not used; Do nothing.
	end

	nonNilParams = nonNilParams + 1
end

--[[
Local function which is used to extract data
from the numbered serial parameters (Title1, Aux1, etc.), and then convert them to
use the non-numbered parameter names (Title, Aux).

The function returns the args as non-numbered prameter names.
]]--
local function extractDataFromNumberedSerialArgs(args, i, numberOfParameterGroups, title)
	for _, v in ipairs(cellNameList) do
		local parameter = v
		local numberedParameter = v .. "_" .. i
		local excludeParameter = excludeList[parameter] or 'NULL' .. parameter
		local excludeNumberParameter = (excludeList[numberedParameter] or 'NULL' .. parameter) .. "_" .. i
		if (not hasValue(args[numberedParameter]) and not hasValue(args[excludeNumberParameter])
			and hasValue(parameterGroupCells[parameter]) and not hasValue(args[excludeParameter])) then
			if (v ~= 'ProdCode' and v ~= 'ПродКод') then
				createEmptyCell(args, parameter, true)
			else
				args[parameter] = ''
			end
			if (title.namespace == 0) then
				addTrackingCategory(trackingCategoryList["nonmatching_numbered_parameters"])
			end
		elseif (hasValue(args[numberedParameter]) and not  hasValue(args[excludeNumberParameter])) then
			args[parameter] = args[numberedParameter]
		end
	end

	return args
end

--[[
Local function which is used to create column cells.

EpisodeNumber, EpisodeNumber2 and Title are created in different functions
as they require some various if checks.

See:
	-- createEpisodeNumberCell()
	-- createEpisodeNumberCellSecondary()
	-- createTitleCell()
]]--
local function createCells(args, isSerial, currentRow, onInitialPage, title, numberOfParameterGroups)
	for k, v in ipairs(cellNameList) do
		if (v == 'ProdCode' and v == 'ПродКод') then
			createProductionCodeCell(args, v)
		elseif (args[v]) then
			-- Set empty cells to TBA/TBD
			if (args[v] == '') then
				createEmptyCell(args, v, false)
			elseif (v == 'WrittenBy' or v == 'Сценарист' and title.namespace == 0) then
				if ((string.find(args[v], "''Сюжет") ~= nil  or string.find(args[v], "''Сценарий") ~= nil or string.find(args[v], "''Телесценарий") ~= nil  or string.find(args[v], "''Телепередача") ~= nil  or string.find(args[v], "''Передача") ~= nil) and string.find(args[v], "8202") == nil) then
					-- &#8202; is the hairspace added through {{StoryTeleplay}}
					addTrackingCategory(trackingCategoryList["raw_unformatted_storyteleplay"])
				end
			end

			-- If serial titles need to be centered and not left, then this should be removed.
			local textAlign = "center"
			if (v == 'Aux1' or v == 'Столбец1' and isSerial) then
				textAlign = "left"
			end

			-- Remove wikilinks from links in serial rowspans rows after the first.
			-- if (currentRow > 1) then
				-- args[v] = removeWikilinks(args, v)
			-- end

			local thisRowspan
			if (firstParameterGroupCell and k < firstParameterGroupCell) then
				thisRowspan = numberOfParameterGroups
			else
				thisRowspan = 1
			end

			if (currentRow == 1 or (currentRow > 1 and k >= (firstParameterGroupCell or 0))) then
				createTableData(args[v], thisRowspan, textAlign)
			end
			nonNilParams = nonNilParams + 1
			checkUsageOfDateTemplates(args, v, onInitialPage, title)
		end

		if (args[v] == "TBA") then
			cellValueTBA = true
		end
	end
end

--[[
Local function which is used to create the Title cell text.

The title text will be handled in the following way:
	Line 1: <Title><RTitle> (with no space between)
	Line 2: <AltTitle><RAltTitle> (with no space between) OR
	Line 2: Transcription: <TranslitTitle> (<Language>: <NativeTitle>)<RAltTitle> (with space between first two parameters)

	If <Title> or <RTitle> are empty,
	then the values of line 2 will be placed on line 1 instead.

--]]
local function createTitleText(args)
	local titleString = ''
	local isCellPresent = false
	local useSecondLine = false
	local lineBreakUsed = false

	-- Surround the Title with quotes; No quotes if empty.
	if (args.Title ~= nil or args['Название'] ~= nil) then
		-- Название с подрезкой лишних кавычек
		local mainTitle = args.Title or args['Название']
		
		if (args.Title == "" or args['Название'] == "") then
			isCellPresent = true
		else
			titleString = "&#171;'''" .. mainTitle .. "'''&#187;"
			useSecondLine = true
			isCellPresent = true
		end
	end

	if (args.RTitle ~= nil or args['НазваниеПрим'] ~= nil) then
		if (args.RTitle == "" or args['НазваниеПрим'] == "") then
			isCellPresent = true
		else
			titleString = titleString .. (args.RTitle or args['НазваниеПрим']) 
			useSecondLine = true
			isCellPresent = true
		end
	end

	-- Surround the AltTitle/TranslitTitle with quotes; No quotes if empty.
	if (args.AltTitle or args['Название2'] or args.TranslitTitle or args['НазваниеТранслит']) then

		isCellPresent = true

		if (useSecondLine) then
			titleString = titleString .. "<br>"
			lineBreakUsed = true
		end
		
		if (hasValue(args.AltTitle or args['Название2'])) then
			-- Название2 с подрезкой лишних кавычек и исправление конфликта с <ref>
			local altTitle
			
			if (string.find(args.AltTitle or args['Название2'], 'ref') ~= nil) then
				altTitle = mw.ustring.gsub(args.AltTitle or args['Название2'], '<ref>^[*.]$', '')
			else
				altTitle = mw.ustring.gsub(args.AltTitle or args['Название2'], "'",'')
			end
			
			titleString = titleString .. "&#171;''" .. altTitle .. "''&#187;"
		elseif (hasValue(args.TranslitTitle or args['НазваниеТранслит'])) then
			if (hasValue(args.NativeTitleLangCode or args['КодЯзыка'])) then
				titleString = titleString .. abbr('транскр','транскрипция') .. '.: &#171;' .. langSpan(args.TranslitTitle or args['НазваниеТранслит'], args.NativeTitleLangCode or args['КодЯзыка'])  .. '&#187;'
			else
				titleString = titleString .. abbr('транскр','транскрипция') .. '.: &#171;' .. (args.TranslitTitle or args['НазваниеТранслит']) .. '&#187;'
			end
		end
	end

	if (args.NativeTitle ~= nil or args['НазваниеОригинал'] ~= nil) then
		if (args.NativeTitle == "" or args['НазваниеОригинал'] == "") then
			isCellPresent = true
		else
			isCellPresent = true

			if (useSecondLine and lineBreakUsed == false) then
				titleString = titleString .. "<br>"
			end

			if (hasValue(args.NativeTitleLangCode or args['КодЯзыка'])) then
				local languageCode = "Lang-" .. (args.NativeTitleLangCode or args['КодЯзыка'])
				titleString = titleString .. " (" .. abbr('ориг','оригинал') .. ".: " .. langSpan(args.NativeTitle or args['НазваниеОригинал'], args.NativeTitleLangCode or args['КодЯзыка']) .. ")"
			else
				titleString = titleString .. " (" .. abbr('ориг','оригинал') .. ".: " .. (args.NativeTitle or args['НазваниеОригинал']) .. ")"
			end
		end
	end

	if (args.RAltTitle ~= nil or args['Название2Прим'] ~= nil) then
		if (args.RAltTitle == "" or args['Название2Прим'] == "") then
			isCellPresent = true
		else
			isCellPresent = true

			if (useSecondLine and lineBreakUsed == false) then
				titleString = titleString .. "<br>"
			end

			titleString = titleString .. (args.RAltTitle or args['Название2Прим'])
		end
	end

	return titleString, isCellPresent
end

--[[
Local function which is used to extract data
from the numbered title parameters (Title1, RTitle2, etc.), and then convert them to
use the non-numbered prameter names (Title, RTitle).

The function returns two results:
	-- The args parameter table.
	-- A boolean indicating if the title group has data.
]]--
local function extractDataFromNumberedTitleArgs(args, i)
	local nextGroupValid = false

	for _, v in ipairs(titleList) do
		local parameter = v
		local numberedParameter = v .. "_" .. i
		args[parameter] = args[numberedParameter]

		if (nextGroupValid == false and hasValue(args[numberedParameter])) then
			nextGroupValid = true
		end
	end

	return args, nextGroupValid
end

-- Local function which is used to process the multi title list.
local function processMultiTitleList(args, numberOfParameterGroups)
	local nativeTitleLangCode = args.NativeTitleLangCode or args['КодЯзыка']
	local titleText = ""
	local isCellPresent = false
	local isFirstTitleGroup = true -- Making sure that the title cell is created at least once and isn't created again if other #N titles are empty.

	for i = 1, numberOfParameterGroups do
		local args, nextGroupValid = extractDataFromNumberedTitleArgs(args, i)
		if (nextGroupValid) then
			if (isFirstTitleGroup == false) then
				titleText = titleText .. "<hr>"
			end

			local titleTextRow
			titleTextRow = createTitleText(args)
			titleText = titleText .. titleTextRow
			isFirstTitleGroup = false
		else
			if (isFirstTitleGroup) then
				titleText, isCellPresent = createTitleText(args)
			end

			-- Valid titles have to be in succession (#1, #2, #3 and not #1, #4 #5), so exit for loop if next group is empty.
			return titleText, isCellPresent
		end
	end

	return titleText
end

-- Local function which is used to create a Title cell.
local function createTitleCell(args, numberOfParameterGroups, multiTitleListEnabled, isSerial)
	local titleText
	local isCellPresent

	if (multiTitleListEnabled) then
		titleText, isCellPresent = processMultiTitleList(args, numberOfParameterGroups)
	else
		titleText, isCellPresent = createTitleText(args)
	end

	if (isCellPresent == false) then
		return nil
	end

	local textAlign = "left"

	-- If Title is blank, then set Raw Title to TBA
	if (hasValue(titleText) == false) then
		titleText = tableEmptyCellModule._main({})
		textAlign = "left"
	end

	-- If title is the first cell, create it with a !scope="row"
	if (nonNilParams == 0) then
		if (isSerial) then
			row:tag('th')
				:addClass('summary')
				:attr('scope', 'row')
				:attr('rowspan', numberOfParameterGroups)
				:css('text-align', textAlign)
				:wikitext(titleText)
		else
			row:tag('th')
				:addClass('summary')
				:attr('scope', 'row')
				:css('text-align', textAlign)
				:wikitext(titleText)
		end
	else
		if (isSerial) then
			row:tag('td')
				:addClass('summary')
				:attr('rowspan', numberOfParameterGroups)
				:css('text-align', textAlign)
				:wikitext(titleText)
		else
			row:tag('td')
				:addClass('summary')
				:css('text-align', textAlign)
				:wikitext(titleText)
		end
	end

	nonNilParams = nonNilParams + 1
end

-- Local function which is used to create a table row header for either the
-- EpisodeNumber or EpisodeNumber2 column cells.
local function createTableRowEpisodeNumberHeader(episodeNumber, numberOfParameterGroups, episodeText)
		local epID = string.match(episodeNumber, "^%w+")
		row:tag('th')
			:attr('scope', 'row')
			:attr('rowspan', numberOfParameterGroups)
			:attr('id', epID and 'ep' .. epID or '')
			:css('text-align', 'center')
			:wikitext(episodeText)
end

--[[
Local function which is used to extract the text from the EpisodeNumber or EpisodeNumber2
parameters and format them into a correct MoS compliant version.

Styles supported:
	-- A number range of two numbers, indicating the start and end of the range,
	seperated by an en-dash (–) with no spaces in between.
		Example: "1 - 2" -> "1–2"; "1-2-3" -> "1–3".
	-- An alphanumeric or letter range, similar to the above.
		Example: "A - B" -> "A–B"; "A-B-C" -> "A–C".
		Example: "A1 - B1" -> "A1–B1"; "A1-B1-C1" -> "A1–C1".
	-- A number range of two numbers, indicating the start and end of the range,
	seperated by a visual <hr> (divider line).
	-- An alphanumeric or letter range, similar to the above.
]]--
local function getEpisodeText(episodeNumber)
	if (episodeNumber == '') then
		return tableEmptyCellModule._main({})
	else

		local episodeNumber1
		local episodeNumber2

		-- Used for double episodes that need a visual "–"" or "<hr>"" added.
		local divider

		episodeNumber = episodeNumber:gsub('%s*<br%s*/?%s*>%s*', '<hr>')

		if (episodeNumber:match('^(%w+)%s*<hr */%s*>%s*(%w+)$')) then
			episodeNumber1, episodeNumber2 = episodeNumber:match('^(%w+)%s*<hr */%s*>%s*(%w+)$')
			divider = "<hr>"
		elseif (episodeNumber:match('^(%w+)%s*<hr */%s*>.-<hr */%s*>%s*(%w+)$')) then  -- 3 or more elements
			episodeNumber1, episodeNumber2 = episodeNumber:match('^(%w+)%s*<hr */%s*>.-<hr */%s*>%s*(%w+)$')
			divider = "<hr>"
		elseif (mw.ustring.match(episodeNumber, '^(%w+)%s*[%s%-–/&]%s*(%w+)$')) then
			episodeNumber1, episodeNumber2 = mw.ustring.match(episodeNumber, '^(%w+)%s*[%s%-–/&]%s*(%w+)$')
			divider = "–"
		else
			episodeNumber1, episodeNumber2 = mw.ustring.match(episodeNumber, '^(%w+)%s*[%s%-–/&].-[%s%-–/&]%s*(%w+)$')  -- 3 or more elements
			divider = "–"
		end

		if (not episodeNumber1) then
			return episodeNumber
		elseif (not episodeNumber2) then
			return string.match(episodeNumber, '%w+')
		else
			return episodeNumber1 .. divider .. episodeNumber2
		end
	end
end

-- Local function which is used to create EpisodeNumber2 and EpisodeNumber3 cells.
local function _createEpisodeNumberCellSecondary(episodeValue, numberOfParameterGroups)
	if (episodeValue) then
		local episodeText = getEpisodeText(episodeValue)

		if (nonNilParams == 0) then
			createTableRowEpisodeNumberHeader(episodeValue, numberOfParameterGroups, episodeText)
		else
			createTableData(episodeText, numberOfParameterGroups, "center")
		end

		nonNilParams = nonNilParams + 1

	end
end

-- Local function which is used to create seconday episode number cells.
local function createEpisodeNumberCellSecondary(args, numberOfParameterGroups)
	_createEpisodeNumberCellSecondary(args.EpisodeNumber2 or args['НомерЭпизода2'], numberOfParameterGroups)
	_createEpisodeNumberCellSecondary(args.EpisodeNumber3 or args['НомерЭпизода3'], numberOfParameterGroups)
end

-- Local function which is used to create an EpisodeNumber cell.
local function createEpisodeNumberCell(args, numberOfParameterGroups)
	if (args.EpisodeNumber or args['НомерЭпизода']) then
		local episodeText = getEpisodeText(args.EpisodeNumber or args['НомерЭпизода'])
		createTableRowEpisodeNumberHeader(args.EpisodeNumber or args['НомерЭпизода'], numberOfParameterGroups, episodeText)
		nonNilParams = nonNilParams + 1
	end
end

-- Local function which is used to create a single row of cells.
-- This is the standard function called.
local function createSingleRowCells(args, numberOfParameterGroups, multiTitleListEnabled, onInitialPage, title)
	createEpisodeNumberCell(args, 1)
	createEpisodeNumberCellSecondary(args, 1)
	createTitleCell(args, numberOfParameterGroups, multiTitleListEnabled, false)
	createCells(args, false, 1, onInitialPage, title, numberOfParameterGroups)
end

-- Local function which is used to create a multiple row of cells.
-- This function is called when part of the row is rowspaned.
-- Current use is for Doctor Who serials.
local function createMultiRowCells(args, numberOfParameterGroups, onInitialPage, title, topColor)
	createEpisodeNumberCell(args, numberOfParameterGroups)
	createEpisodeNumberCellSecondary(args, numberOfParameterGroups)
	createTitleCell(args, numberOfParameterGroups, false, true)

	for i = 1, numberOfParameterGroups do
		args = extractDataFromNumberedSerialArgs(args, i, numberOfParameterGroups, title)
		createCells(args, true, i, onInitialPage, title, numberOfParameterGroups)
		if (i ~= numberOfParameterGroups) then
			row = row:done()  -- Use done() to close the 'tr' tag in rowspaned rows.
				:tag('tr')
				:css('background', topColor)
		end
	end
end

-- Local function which is used to retrieve the NumParts value.
local function getnumberOfParameterGroups(args)
	for k, v in ipairs(cellNameList) do
		local numberedParameter = v .. "_" .. 1
		if (args[numberedParameter]) then
			parameterGroupCells[v] = true
			if not firstParameterGroupCell then
				firstParameterGroupCell = k
			end
		end
	end

	if (hasValue(args.NumParts or args['НомерЧасти'])) then
		return (args.NumParts or args['НомерЧасти']), true
	else
		return 1, false
	end
end

-- Local function which is used to retrieve the remainder value after x has been divided by y
local function mathMod(x, y)
	local ret = x % y
	if not (0 <= ret and ret < y) then
		ret = 0
	end
	return ret
end

-- Local function which is used to retrieve the Top Color value.
local function getTopColor(args, rowColorEnabled, onInitialPage)
	local episodeNumber = mathModule._cleanNumber(args.EpisodeNumber or args['НомерЭпизода']) or 1
	if (args.TopColor or args['ЦветСтроки']) then
		if (string.find(args.TopColor or args['ЦветСтроки'], "#")) then
			return args.TopColor or args['ЦветСтроки']
		else
			return '#' .. (args.TopColor or args['ЦветСтроки'])
		end
	elseif (rowColorEnabled and onInitialPage and mathMod(episodeNumber, 2) == 0) then
		return '#E9E9E9'
	elseif (onInitialPage and (args.ShortSummary or args['КраткоеСодержание'])) then
		return '#F2F2F2'
	else
		return 'inherit'
	end
end

-- Local function which is used to retrieve the Row Color value.
local function isRowColorEnabled(args)
	local rowColorEnabled = yesNoModule(args.RowColor, false)

	if (args.RowColor  or args['ОбщийЦвет'] and string.lower(args.RowColor or args['ОбщийЦвет']) == 'on' or 'да' or 'вкл' or '') then
		rowColorEnabled = true
	end

	return rowColorEnabled
end

-- Local function which is used to retrieve the Line Color value.
local function getLineColor(args)
	-- Default color to light blue
	local lineColor = args.LineColor or args['ЦветГраницы'] or 'CCCCFF'

	-- Add # to color if necessary, and set to default color if invalid
	if (htmlColor[lineColor] == nil) then
		lineColor = '#' .. (mw.ustring.match(lineColor, '^[%s#]*([a-fA-F0-9]*)[%s]*$') or '')
		if (lineColor == '#') then
			lineColor = '#CCCCFF'
		end
	end

	return lineColor
end

-- Local function which is used to check if the table is located on the page
-- currently viewed, or on a transcluded page instead.
-- If it is on a transcluded page, the episode summary should not be shown.
local function isOnInitialPage(args, sublist, pageTitle, initiallistTitle)
	-- This should be the only check needed, however, it was previously implemented with two templates
	-- with one of them not requiring an article name, so for backward compatability, the whole sequence is kept.
	local onInitialPage
	
	-- Only sublist had anything about hiding, so only it needs to even check
	if (sublist) then
		local pageTitleMatch = mw.ustring.match(pageTitle, 'Список эпизодов', 1) -- Результат: Список эпизодов или nil
		
		if (not hasValue(initiallistTitle) and pageTitleMatch == 'Список эпизодов') then -- Автоопределение страницы "Список эпизодов ..."
			onInitialPage = true
		elseif hasValue(initiallistTitle) then
			onInitialPage = mw.uri.anchorEncode(pageTitle) == mw.uri.anchorEncode(initiallistTitle)
		end
	else
		onInitialPage = false
	end

	return onInitialPage
end

-- Local function which does the actual main process.
local function _main(args, sublist, linetop)
	local title = mw.title.getCurrentTitle()
	local pageTitle = title.text
	local initiallistTitle = args['1'] or ''

	-- Is this list on the same page as the page directly calling the template?
	local onInitialPage = isOnInitialPage(args, sublist, pageTitle, initiallistTitle)

	-- Need just this parameter removed if blank, no others
	if (hasValue(args.ShortSummary or args['КраткоеСодержание']) == false) then
		args.ShortSummary = nil
		args['КраткоеСодержание'] = nil
	end

	local lineColor = getLineColor(args)
	local rowColorEnabled = isRowColorEnabled(args)
	local topColor = getTopColor(args, rowColorEnabled, onInitialPage)

	local root = mw.html.create()							-- Create the root mw.html object to return
	row = root:tag('tr')									-- Create the table row and store it globally
				:addClass('vevent')
				:css('text-align', 'center')
				:css('background', topColor)

	local numberOfParameterGroups, multiTitleListEnabled = getnumberOfParameterGroups(args)

	if (multiTitleListEnabled and (not args.Title_2 or args['Название_2'])) then
		createMultiRowCells(args, numberOfParameterGroups, onInitialPage, title, topColor)
	else
		createSingleRowCells(args, numberOfParameterGroups, multiTitleListEnabled, onInitialPage, title)
	end

	-- add these categories only in the mainspace and only if they are on the page where the template is used
	if (onInitialPage and title.namespace == 0) then
		addLineColorTrackingCategories(args)
		addTopColorTrackingCategories(args)
	end

	if (cellValueTBA == true and title.namespace == 0) then
		addTrackingCategory(trackingCategoryList["tba_values"])
	end
	
	local pageTitleMatch = mw.ustring.match(pageTitle, 'Список эпизодов', 1)
	if (pageTitleMatch == 'Список эпизодов' or title.namespace ~= 0) then
		trackingCategories = ''
	end
	
	-- Do not show the summary if this is being transcluded on the initial list page
	-- Do include it on all other lists
	if (not onInitialPage and (args.ShortSummary or args['КраткоеСодержание'])) then
		local bottomWrapper = createShortSummaryRow(args, lineColor)
		return tostring(root) .. tostring(bottomWrapper) .. trackingCategories
	elseif (linetop) then
		local bottomWrapper = createShortSummaryRow(args, lineColor)
		local bottomWrapperLen = mw.ustring.len(bottomWrapper)
		local bottomWrapperEnd = mw.ustring.sub(bottomWrapper, 1, bottomWrapperLen-10)
		return tostring(root) .. tostring(bottomWrapperEnd) .. trackingCategories
	else
		return tostring(root) .. trackingCategories
	end
end

-- Local function which handles both module entry points.
local function main(frame, sublist, linetop)
	local getArgs = require('Модуль:Arguments').getArgs
	local args

	-- Most parameters should still display when blank, so don't remove blanks
	if (sublist) then
		args = getArgs(frame, {removeBlanks = false, wrappers = 'Шаблон:Список серий/песочница/sublist'})
	elseif (linetop) then
		args = getArgs(frame, {removeBlanks = false, wrappers = 'Шаблон:Список серий/песочница/шапка'})
	else
		args = getArgs(frame, {removeBlanks = false, wrappers = 'Шаблон:Список серий'})
	end

	-- args['1'] = mw.getCurrentFrame():getParent():getTitle()
	return _main(args, sublist, linetop, frame)
end

--------------------------------------------------------------------------------
-- Экспорт
--------------------------------------------------------------------------------

function p.sublist(frame)
	return main(frame, true, false)
end

function p.linetop(frame)
	return main(frame, false, true)
end

function p.list(frame)
	return main(frame, false, false)
end

return p