Dead by Daylight Wiki
Advertisement

local p = {}
local mathOps = require("Módulo:MathOps")
local str = require("Módulo:Strings")
local frame = mw.getCurrentFrame()
local _brigtnessTreshold = 0.36
local _isValidFileNameCounter = 0

local strings = {
	male = "Masculino",
	female = "Femenino",
	nonhuman = "No Aplicable (no humano)";
	undefined = "Indefinido",
	charNotFound = "Character not found!",
	patch = "Parche"
}

local months = {
	January = "Enero",
	February = "Febrero",
	March = "Marzo",
	April = "Abril",
	May = "Mayo",
	June = "Junio",
	July = "Julio",
	August = "Agosto",
	September = "Septiembre",
	October = "Octubre",
	November = "Noviembre",
	December = "Diciembre"
}

local days = {
	Monday = "Lunes",
	Tuesday = "Martes",
	Wednesday = "Miercoles",
	Thursday = "Jueves",
	Friday = "Viernes",
	Saturday = "Sábado",
	Sunday = "Domingo"
}
p.strings = strings
----------------------------------------------

p.timeFormat1 = "%d.%m.%Y" -- 5.9.2022
timeContants = {
	minute = 60, --in seconds
	hour = 3600,
	day = 86400,
	week = 604800
}

--If params will be passed {...} then index should equals 0, so this list will be passeb back: ((index == 0 and params.args) or params)
function p.resolveParameter(param, index, returnCanBeNil)
	local retArg
	if type(param) == "table" then retArg = (((index == 0 and param.args) or params) or (param.args and param.args[(index or 1)] and p.replaceSpecialCharacters(param.args[(index or 1)]))) end --if parameter is passed from wiki, not other function
	if retArg == nil and type(param) == "table" and param.args ~= nil and next(param.args) == nil and not returnCanBeNil then retArg = getParamOrPageName() end --param.args ~= nil and next(param.args): this means that params.args is not nil but empty table
	if retArg == nil and not returnCanBeNil then retArg = getParamOrPageName(param) end --if parameter was passed directly or not at all
	if retArg == nil and type(param) == "string" and not returnCanBeNil then retArg = param end --simply pass string parameter, and proccess it (with special char removal).
	if retArg ~= nil and type(param) == "string" then retArg = p.replaceSpecialCharacters(retArg) end --final processing
	return retArg
end

function getParamOrPageName(param)
	if param and 
		(type(param) ~= types.table or 
		(type(param) == types.table and not param.args)) 
	then 
		return param
	else
		local pageName = mw.title.getCurrentTitle().text
		return p.split(pageName, '/')[1] -- remove page suffixes such as language codes
	end
end

--translates string containing #1# #2# ... to individual strings (resp. words) contained in dyntable
function p.getDynamicString(dynTable, templateString)
	--dynTable = {"Testing Chapter", nil, "DLC Str World", nil, 999};
	--templateString = "Retracted #1# #2# #3# #4# #5# #6# #7# #8#"
	local skipList = {skip = {}, spans = {}}
	local spanRegex = "(<span .-</span>)" --mostly clr case
	local skipRegex = "#(.-)#" --skip anything inside hashtags
	if type(dynTable) == types.string then --is parameter is string then just split it
		dynTable = string.split(dynTable)
	elseif type(dynTable) == types.table then
		local tmp = {}
		local skipCounter = 1
		local spanCounter = 1
		for i, el in pairs(dynTable) do
			--finding any strings need to be first as span regex WILL create elements matching this regex
			if el:find(skipRegex) then --if you find any string to skip then ...
				for m in el:gmatch(skipRegex) do
					table.insert(skipList.skip, m)
					el = el:gsub(skip(m:gsub('%W','%%%1')), "#skip" .. skipCounter) --We have to escape special characters as the string is being used as a pattern and for an instance "[[ ]]" are magic chars for LUA patterns
					skipCounter = skipCounter + 1
				end
			end
			
			if el:find(spanRegex) then --if you find any spans to skip then ...
				for m in el:gmatch(spanRegex) do --let's find spans (and pull them out) to skip them with splitting by "space"
					table.insert(skipList.spans, m)
					el = el:gsub(m, "#span" .. spanCounter)
					spanCounter = spanCounter + 1
				end
			end
			table.insert(tmp, string.split(tostring(el))) --if parameter is table, split all strings inside
		end
		dynTable = table.flatten(tmp) --format from table.insert is {{"splitted", "string"}, {"splitted", "another", "string"}}, so it's needed to be flattened
		--mw.log(mw.dumpObject(dynTable))
	end
	
	if templateString then
		for m in templateString:gmatch("#(%d+)#") do --replace all #1# occurences
			local currentRegexString = "#" .. tonumber(m) .. "#"
			if tonumber(m) <= #dynTable then
				templateString = templateString:gsub(currentRegexString, dynTable[tonumber(m)])
			else
				templateString = templateString:gsub(currentRegexString, cstr.empty) --remove all remaining marks
			end
		end
	else
		templateString = table.join(dynTable, space)
	end
	
	templateString = string.trim(templateString)
	
	--lg(templateString)
	for m in templateString:gmatch("#skip(%d)") do --now we put spans back into string
		local currentRegexString = "#skip" .. tonumber(m)
		templateString = templateString:gsub(currentRegexString, skipList.skip[tonumber(m)])
	end
	
	for m in templateString:gmatch("#span(%d)") do --now we put spans back into string
		local currentRegexString = "#span" .. tonumber(m)
		templateString = templateString:gsub(currentRegexString, skipList.spans[tonumber(m)])
	end
	
	--mw.log(templateString)
	return templateString
end

function p.getCount(subject)
	local list
	subject = p.resolveParameter(subject)
	--if you have another list just add it into a list then call appropriate function
	if		subject == "map"		then list = maps
	elseif	subject == "realm"		then list = realms
	elseif	subject == "killer"		then list = killers
	elseif	subject == "survivor"	then list = survivors
	elseif	subject == "dlc"		then list = dlcs
	elseif	subject == "chapter"	then return getCountDlcType(1)
	elseif	subject == "paragraph"	then return getCountDlcType(2)
	elseif	subject == "clothing"	then return getCountDlcType(3)
	elseif	subject == "ost"		then return getCountDlcType(4)
	elseif	subject == "character"	then return getCountDlcType(5)
	elseif	subject == "ccy"		then return getCountCCY(true)
	elseif	subject == "ccy-gc"		then return getCountCCY(true) --redundant option to keep convention
	elseif	subject == "ccy-rc"		then return getCountCCY(false)
	elseif	subject == "killerPerk"	then return getPerksCount('K')
	elseif	subject == "survPerk"	then return getPerksCount('S')
	else return 0
	end
	
	local x = 0
	for _, item in ipairs(list) do if item.skip or item.retired then x = x + 1 end end
	
	return #list - x
end

function getCountDlcType(type)
	local count = 0
	
	for _, dlc in ipairs(dlcs) do
		if dlc.category == type and not dlc.skip then count = count + 1 end
	end
	
	return count
end

function getPerksCount(charType)
	local perks = mw.loadData("Módulo:Datatable/Perks").perks
	local count = 0
	
	for _, perk in ipairs(perks) do
		if perk.charType == charType and not perk.unused then count = count + 1 end
	end

	return count
end

function getCountCCY(gc)
	local list = ccy

	local i = 0
	while list[i + 1] and list[i + 1].gc == gc do i = i + 1 end

	return i
end

function isRealCcy(ccyId)
	require("Módulo:Datatable")
	for _, currency in ipairs(ccy) do
		if currency.id == ccyId then return not currency.gc end
	end
	return false
end

function p.getCcyById(id)
	for _, currCcy in ipairs(ccy) do
		if currCcy.id == id then return currCcy end
	end
	return nil
end

function p.getMaxId(tab)
	result = 0
	for _, item in ipairs(tab) do
		result = (item.id > result and item.id) or result
	end
	return result
end

-- Function allowing for consistent treatment of boolean-like wikitext input.
-- It works similarly to the template {{yesno}}.

function p.bool(val, default)
	-- If your wiki uses non-ascii characters for any of "yes", "no", etc., you
	-- should replace "val:lower()" with "mw.ustring.lower(val)" in the
	-- following line.
	val = type(val) == 'string' and val:lower() or val
	if val == nil then
		return nil
	elseif val == true 
		or val == 'yes'
		or val == 'y'
		or val == 'true'
		or val == 't'
		or val == 'on'
		or tonumber(val) == 1
	then
		return true
	elseif val == false
		or val == 'no'
		or val == 'n'
		or val == 'false'
		or val == 'f'
		or val == 'off'
		or tonumber(val) == 0
	then
		return false
	else
		return default
	end
end

function p.split(inputstr, sep)
        if sep == nil then
            sep = "%s"
        end
        local t = {}
        for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
            table.insert(t, str)
        end
        return t
end

--Example usage:
--amount = 1333444.1
--print(format_num(amount,2))
--print(format_num(amount,-2,"US$"))
--amount = -22333444.5634
--print(format_num(amount,2,"$"))
--print(format_num(amount,2,"$","()"))
--print(format_num(amount,3,"$","NEG "))

--Output:
--1,333,444.10
--US$1,333,400
---$22,333,444.56
--($22,333,444.56)
--NEG $22,333,444.563

function p.formatNum(amount, decimal, prefix, neg_prefix)
	return p.format_num(amount, decimal, prefix, neg_prefix)
end
function p.format_num(amount, decimal, prefix, neg_prefix)
  local str_amount,  formatted, famount, remain

  decimal = decimal or 2  -- default 2 decimal places
  neg_prefix = neg_prefix or "-" -- default negative sign

  famount = math.abs(mathOps.round(amount,decimal))
  famount = math.floor(famount)

  remain = mathOps.round(math.abs(amount) - famount, decimal)

        -- comma to separate the thousands
  formatted = p.commaFormat(famount)

        -- attach the decimal portion
  if (decimal > 0) then
    remain = string.sub(tostring(remain),3)
    formatted = formatted .. "." .. remain ..
                string.rep("0", decimal - string.len(remain))
  end

        -- attach prefix string e.g '$' 
  formatted = (prefix or "") .. formatted 

        -- if value is negative then format accordingly
  if (amount<0) then
    if (neg_prefix=="()") then
      formatted = "("..formatted ..")"
    else
      formatted = neg_prefix .. formatted 
    end
  end

  return formatted
end

function p.commaFormat(amount)
  local formatted = amount
  while true do  
    formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", '%1,%2')
    if (k==0) then
      break
    end
  end
  return formatted
end

--Converting Arabic numbers to Roman
function p.toRomanNumerals(s)
    local numbers = { 1, 5, 10, 50, 100, 500, 1000 }
	local chars = { "I", "V", "X", "L", "C", "D", "M" }

    s = tonumber(s)
    if not s or s ~= s then error"Unable to convert to number" end
    if s == math.huge then error"Unable to convert infinity" end
    s = math.floor(s)
    if s <= 0 then return s end
	local ret = ""
        for i = #numbers, 1, -1 do
        local num = numbers[i]
        while s - num >= 0 and s > 0 do
            ret = ret .. chars[i]
            s = s - num
        end
        for j = 1, i - 1 do
            local n2 = numbers[j]
            if s - (num - n2) >= 0 and s < num and s > 0 and num - n2 ~= n2 then
                ret = ret .. chars[j] .. chars[i]
                s = s - (num - n2)
                break
            end
        end
    end
    return ret
end

--Converting Roman numbers to Arabic
function ToNumeral(roman)
    local Num = { ["M"] = 1000, ["D"] = 500, ["C"] = 100, ["L"] = 50, ["X"] = 10, ["V"] = 5, ["I"] = 1 }
    local numeral = 0    
 
    local i = 1
    local strlen = string.len(roman)
    while i < strlen do
        local z1, z2 = Num[ string.sub(roman,i,i) ], Num[ string.sub(roman,i+1,i+1) ]
        if z1 < z2 then
            numeral = numeral + ( z2 - z1 )
            i = i + 2
        else
            numeral = numeral + z1
            i = i + 1    
        end        
    end
 
    if i <= strlen then numeral = numeral + Num[ string.sub(roman,i,i) ] end
 
    return numeral    
end

function resolveNameWithRomanNumbers(str)
	local index = string.find(str, " [^ ]*$") + 1
	local romanNumber = (string.sub(str, index))
	local result
	if string.match(romanNumber, "[MDCLXVI]+$") then --finding ONLY Roman letters in last "word"
		--Cowshed
		--mw.log(romanNumber)
		result = str
		--result = string.sub(str, 1, index - 1) .. ToNumeral(romanNumber)
	else
		--mw.log("Non-Roman form")
		result = str
	end
	
	return result
end

function p.CapitalizeName(str)
	--mw.log(string.gsub(" "..str, "[%s%-]%a", string.upper):sub(2))
	return string.gsub(" "..str, "[%s%-]%a", string.upper):sub(2)
end

function p.FirstLetterLower(str)
	return str:sub(1, 1):lower() .. str:sub(2)
end

function p.FirstLetterUpper(str)
    return str:sub(1,1):upper() .. str:sub(2)
end

--[[function p.fixDiacritics(str)
	local letterSequences = {
		["195"] = {fix = "â", sequence = {"162"}}
	}
	local fixedSeqsOffset = 0

	local currentLen = #str + fixedSeqsOffset
	local i = 1
	while i <= currentLen do
		local ascii = tostring(string.byte(str:sub(i,i)) or 0)
		for startSequence, seqData in pairs(letterSequences) do
			if ascii == startSequence then
				local foundSequence = true
				for j, seqCode in ipairs(seqData.sequence) do
					local seqAscii = tostring(string.byte(str:sub(i+j,i+j)))
					if seqAscii ~= seqCode then
						foundSequence = false
						break
					end
				end
				
				if foundSequence then
					fixedSeqsOffset = i - (i + #seqData.sequence)
					currentLen = #str + fixedSeqsOffset
					-- #seqData.sequence + 1 = number of ascii codes that needs to be replaced, the sequence list + the initial ascii code, i.e. the key value from letterSequences
					str = str:sub(1, i - 1) .. seqData.fix .. str:sub(i + #seqData.sequence + 1)
				end
			end
		end
		i = i + 1
	end
	return str
end]]

function p.RemoveSpecialCharacters(str, full, replaceDiacritics)
	str = string.gsub(str, "'", "")
	str = string.gsub(str, "®", "")
	str = string.gsub(str, "™", "")
	str = string.gsub(str, ":", "")
	str = string.gsub(str, "!", "") --perks
	str = string.gsub(str, "%&", "And") --probably can be changed to lower: "and"

	if replaceDiacritics then
		str = p.replaceDiacritics(str, full)
	end
		
	return str
end

function p.replaceDiacritics(str, full)
	--Diacritics
	specialLettersLight = {
		["A"] = {"À", "Á", "Â", "Ã", "Ä"},
		["a"] = {"à", "á", "â", "ã", "ä"},
		["e"] = {"è", "é", "ê", "ë", "ě"},
		["O"] = {"Ò", "Ó", "Ô", "Õ", "Ö"},
		["o"] = {"ò", "ó", "ô", "õ", "ö"}
	}
	specialLetters = {
		["A"] = {"À", "Á", "Â", "Ã", "Ä"},
		["a"] = {"à", "á", "â", "ã", "ä"},
		["E"] = {"È", "É", "Ê", "Ë", "Ě"},
		["e"] = {"è", "é", "ê", "ë", "ě"},
		["I"] = {"Ì", "Í", "Î", "Ñ", "Ï"},
		["i"] = {"ì", "í", "î", "ñ", "ï"},
		["O"] = {"Ò", "Ó", "Ô", "Õ", "Ö"},
		["o"] = {"ò", "ó", "ô", "õ", "ö"},
		["U"] = {"Ù", "Ú", "Û", "Ů", "Ü"},
		["u"] = {"ù", "ú", "û", "ů", "ü"},
		["Y"] = {	  "Ý",			 "Ÿ"},
		["y"] = {	  "ý",			 "ÿ"}
	}
	--Originally it was grouped by set [ÀÁ], however there is some sort of bug there that makes it not working
	--str = string.gsub(str, "[ÁÂ]", "A")
	--if debugRun then mw.log(mw.dumpObject(specialLetters)) end
	for letter, row in pairs(((full and specialLetters) or specialLettersLight)) do
		for _, special in ipairs(row) do
			str = str:gsub(special, letter)
		end
	end
	return str
end

function p.replaceSpecialCharacters(name)
	local charList = { 
		["'"] = '&#39;',
		["&"] = '&#38;'
	}
	if name == nil then error("Name parameter is empty, but it shouldnt?") end
	for repl, sChar in pairs(charList) do
		name = string.gsub(name, sChar, repl)
	end
	return name
end

function p.pageExists(name)
	return (not name == cstr.empty) or mw.title.new(name).exists
end

function p.isValidFileName(name, extension)
	extension = extension or "png"
	name = p.RemoveSpecialCharacters(name)
	_isValidFileNameCounter = _isValidFileNameCounter + 1
	mw.log("Counter:" .. tostring(_isValidFileNameCounter) .. ' - ' .. cstr.media .. name .. dot .. extension)
	return not (name == cstr.empty or not mw.title.new(cstr.media .. name .. dot .. extension).exists)
end

function p.resolveFileName(str, keepSpaces, removeDiacritics)
	keepSpaces = keepSpaces or false
	local result = ""
	
	result = string.lower(str)
	--mw.log(result)
	result = p.RemoveSpecialCharacters(result, keepSpaces, removeDiacritics)
	--mw.log(result)
	result = p.CapitalizeName(result)
	if not keepSpaces then
		result = string.gsub(result, "[ ]", "")
	end
	--In future if there will be needed replace charactere such as "é" just add another substitution
	
	--mw.log(result)
	return result
end
	
function p.resolveImageName(name)
	if mw.title.new(cstr.media .. name ..  dot .. cstr.png).exists then return name .. dot .. cstr.png end
	if mw.title.new(cstr.media .. name .. dot .. cstr.jpg).exists then return name .. dot .. cstr.jpg end
	return name .. dot .. cstr.png
end

function p.GetDisplayName(item)
	return (item.tName or item.name)
end

function p.IconLink(icon, pageLink, displayText)
	
	local result = cstr.empty
	displayText = displayText or p.resolveParameter(icon, 3, true)
	pageLink = pageLink or p.resolveParameter(icon, 2, true)
	local linkless = p.resolveParameter(icon, "linkless", true) or pageLink == "linkless" --linkless as a second parameter should be deprecated
	icon = p.resolveParameter(icon)
	local iconObject = p.getIconObject(icon)
	local iconFile = iconObject.iconFile
	local filename = cstr.file .. iconFile .. tl .. "link=" .. (pageLink or icon)
	local text = (displayText and pageLink .. tl .. displayText) or pageLink or icon --cstr.empty
	--local boxDesc = cstr.empty
	
	if pageLink == "img" then
		text = cstr.empty
		filename = cstr.file .. iconFile .. tl .. "link=" .. icon
	elseif pageLink == "Teachable" then
		local prkz = require("Módulo:Perks")
		text = ((displayText and displayText .. tl .. icon) or icon) .. space
		filename = cstr.file .. prkz.getTeachablePerkIconFilename({args={icon, 'png'}})
	end
	if not linkless and text ~= cstr.empty then
		text = link(text)
		--boxDesc = getBoxDescription(pageLink or icon) //hover box, currently disabled
	elseif linkless or pageLink == "linkless" then
		text = (displayText or icon)
	end	
	
	local imageFile = link(filename .. tl .. "32px")
	
	--result = '<span class = "wrap-span pcView" style = "display:none;"><span class="box-span">' .. boxDesc .. '</span>' .. text .. file .. '</span>'
	local padding = p.getPaddingsFromIcon(iconObject)
	result = text ..
		'<span style = "padding: ' .. padding.top .. space .. padding.right .. space .. padding.bottom .. space .. padding.left .. '">' ..
			'<span style = "display:none" class = "pcView">' .. p.tooltip(imageFile, link(filename, "96px", "link="), true, true) .. '</span>' ..
			'<span class = "mobileView">' .. imageFile .. '</span>' ..
		'</span>'

	mw.log(result)
	return result
	--return frame:expandTemplate{title = "IconLink", args = {icon, pageLink, displayText} }	
end

function p.getPaddingsFromIcon(icon)
	return {
		top = icon.paddingTop or 0,
		right = icon.paddingRight or 0,
		bottom = icon.paddingBottom or 0,
		left = icon.paddingLeft or 0
	}
end

function getBoxDescription(icon) --curently disabled
	local ic = p.getIconObject(icon)
	local result = cstr.empty
	if ic.category == "Perks" then
		--local prkz = require("Módulo:Perks")
		--local perk = prkz.getPerkByName(icon)
		
		result = '<h3>' .. icon .. '</h3><hr>'-- .. prkz.getPerkDescriptionByName(perk.name)
	end
	return result
end

function p.resolveTextColorByBackground(hexBgColor)
	if(type(hexBgColor) == "table") then
		hexBgColor = hexBgColor.args[1]
	end
	local red = tonumber(string.sub(hexBgColor, 1, 2), 16)
	local green = tonumber(string.sub(hexBgColor, 3, 4), 16)
	local blue = tonumber(string.sub(hexBgColor, 3, 4), 16)
	local darkness = (0.299 * red + 0.587 * green + 0.114 * blue) / 255
	
	--mw.log("Red: " .. red .." \t\tor\t Blue: " .. blue .. " | Green: " .. green)
	--mw.log(darkness)
	if(darkness < _brigtnessTreshold) then
		return "white"
	else
		return "black"
	end
end

function p.getPageTitle()
	return mw.title.getCurrentTitle().text
end
---------------------------------------------------------------------------------
function p.getSumOfASTiles(row)
	local result = 0
	local i = 1
	while row[i] do
		result = result + row[i][1] -- hardcoded first variable in table
		i = i + 1
	end
	return result
end

function compASTiles(row1, row2)
	local sum1 = row1.ASTiles
	local sum2 = row2.ASTiles
	if(type(sum1) == "table") then
		sum1 = p.getSumOfASTiles(sum1) --converting back from table to number
	end
	if(type(sum2) == "table") then
		sum2 = p.getSumOfASTiles(sum2)
	end
	
	if sum1 > sum2 then return true
	elseif sum1 < sum2 then return false
	else 
		if row1.realm < row2.realm then return true
		elseif row1.realm > row2.realm then return false
		else
			return resolveNameWithRomanNumbers(row1.name) < resolveNameWithRomanNumbers(row2.name)
		end
	end
end

function compMapRateSize(row1, row2)
	return row1.size > row2.size --comparison specificly made for mapRates table
end

function compName(row1, row2)
	if row1.diacritics then
		if row2.diacritics then
			return p.RemoveSpecialCharacters(row1.name, true) < p.RemoveSpecialCharacters(row2.name, true)
		else
			return p.RemoveSpecialCharacters(row1.name, true) < row2.name
		end
	elseif row2.diacritics then
		return row1.name < p.RemoveSpecialCharacters(row2.name, true)
	end
	return row1.name < row2.name
end

function compInt(row1, row2) -- 1, 2, 3
	return row1 < row2
end

function compIntDesc(row1, row2) -- 3, 2, 1
	return row1 > row2
end

function compLevel(row1, row2)
	return row1.level < row2.level
end

function compDlcCategory(row1, row2)
	if row1.category == row2.category then
		return row1.id < row2.id
	end
	return row1.category < row2.category
end

function compId(row1, row2)
	return row1.id < row2.id
end

function compRealCcyFirst(row1, row2)
	ccy1 = isRealCcy(row1.ccy)
	ccy2 = isRealCcy(row2.ccy)
	if (ccy1 and ccy2) or (not ccy1 and not ccy2) then --TRUE and TRUE or FALSE and FALSE
		return row1.id < row2.id
	elseif ccy1 and not ccy2 then --TRUE and FALSE
		return true
	else
		return false
	end
end

function compCharsKillersFirst(row1, row2)
	char1 = row1.power ~= nil
	char2 = row2.power ~= nil
	if (char1 and char2) or (not char1 and not char2) then --TRUE and TRUE or FALSE and FALSE
		return row1.id < row2.id
	elseif char1 and not char2 then --TRUE and FALSE
		return true
	else
		return false
	end
end

function rarityAndName(row1, row2)
	if row1.rarity < row2.rarity then
		return true
	elseif row1.rarity > row2.rarity then
		return false
	elseif row1.pieces and row2.pieces then
		local row1Name = cstr.empty
		local row2Name = cstr.empty
		for _, piece in pairs(row1.pieces) do row1Name = piece.name break end
		for _, piece in pairs(row2.pieces) do row2Name = piece.name break end
		return row1Name < row2Name
	else
		return compName(row1, row2)
	end
end

function p.sortMapsByASTiles()
	local m = require("Módulo:Datatable")
	--mw.log(mw.dumpObject(maps))
	table.sort(maps,compASTiles)
	--mw.log(mw.dumpObject(maps))
end

function p.sortMapRates(rankTable)
	--mw.log(mw.dumpObject(rankTable))
	table.sort(rankTable, compMapRateSize)
	--mw.log(mw.dumpObject(rankTable))
end

function p.sortItemsByName(tableList)
	table.sort(tableList, compName)
end

function p.sortTable(tableList)
	table.sort(tableList, compInt)
end

function p.sortTableDesc(tableList)
	table.sort(tableList, compIntDesc)
end

function p.sortDlcByCategory(tableList)
	table.sort(tableList, compDlcCategory)
end

function p.sortPerksByLevel(tableList)
	table.sort(tableList, compLevel)	
end

function p.sortTableById(tableList)
	table.sort(tableList, compId)
end

function p.sortRealCcyFirst(tableList)
	table.sort(tableList, compRealCcyFirst)
end

function p.sortCharsKillersFirst(tableList)
	table.sort(tableList, compCharsKillersFirst)
end

function p.sortCosmeticsByRarityAndName(tableList)
	table.sort(tableList, rarityAndName)
end
---------------------------------------------------------------------------------

function p.getCountOfSoundtracks()
	return utils.getCount("ost")
end

function p.today()
	return os.time(os.date("!*t"))
end

function p.getMonth(stamp)
	if type(stamp) ~= 'number' then
		stamp = p.toTimestamp(stamp)
	end
	return os.date("%m", stamp)
end

function p.getDay(stamp)
	if type(stamp) ~= 'number' then
		stamp = p.toTimestamp(stamp)
	end
	return os.date("%d", stamp)
end

function p.getTimeDiff(seconds, unit)
	return seconds / timeContants[unit]
end

function p.toTimestamp(datestamp)
	if type(datestamp) == 'string' then
		datestamp = p.GetDatetime(datestamp)
	end
	return os.time(datestamp)
end

function p.toDate(timestamp, timeFormat)
	return os.date(timeFormat or '%d %B %Y', timestamp) --21 October 2021
end

function p.resolveDateTime(datetime, skipDay) --with weekday
	local matchYear = "^(..)%.(..)%.(%d%d%d%d)$"
	local matchMonth = "^(..)%.(%d%d)%.(....)$"
	local matchDay = "^(%d%d)%.(..)%.(....)$"
	local year, month, day, dayName = false
	local rDate = os.time(p.GetDatetime(datetime))
	if string.find(datetime, matchYear) then
		year = os.date("%Y", rDate)
	end
	if string.find(datetime, matchMonth) then
		month = os.date("%B", rDate)
	end
	if string.find(datetime, matchDay) then
		day = os.date("%d", rDate)
		dayName = os.date("%A", rDate)
	end
	
	--(day and month) is to avoid showing a day as the function sets the month to January (first month) by default, if the month is not provided
	--day part: ((day and month and day .. space) or cstr.empty)
	--month part: ((month and months[month] .. space) or cstr.empty)
	--year part: year
	--dayName part: ((day and month and not skipDay and space .. brackets(days[dayName])) or cstr.empty)
	return ((day and month and day .. space) or cstr.empty) .. ((month and months[month] .. space) or cstr.empty) .. year .. ((day and month and not skipDay and space .. brackets(days[dayName])) or cstr.empty)
end

function p.IsFullDateTime(datestamp)
	local day, month, year = datestamp:match("^(..)%.(..)%.(....)$")
	if month == "##" or day == "##" or year == "####" then return false end
	return true
end

function p.GetDatetime(datestamp)
	local result = {}
	if type(datestamp) == types.string then
		local day, month, year = datestamp:match("^(..)%.(..)%.(%d%d%d%d)$")
		if month == "##" then
			month = 1
			result.fakeMonth = true
		end
		if day == "##" then
			day = 1
			result.fakeDay = true
		end
		
		result.year = year
		result.month = month
		result.day = day
		result.timestamp = p.toTimestamp(result)

		local tempDateObj = os.date("*t", os.time(result))
		result.yday = tempDateObj.yday
		result.wday = tempDateObj.wday
		result.dst = tempDateObj.isdst
	elseif type(datestamp) == types.number then
		result = os.date("*t", datestamp)
		result.timestamp = datestamp
	end
	
	return result
end

function p.getDatePart(datestamp, part)
	local day, month, year = datestamp:match("^(..)%.(..)%.(....)$")
	if year		== "####" then month = false end
	if month	== "##" then month = false end
	if day		== "##" then day = false end
	if		part == "day"	then return day
	elseif	part == "month"	then return month
	elseif	part == "year"	then return year
	end
end

function p.regularReplace(reggedString, regexTable)
	local result = ""
	local regexString = regex --"#pl%((%d)%)" -- looking and extracting number from "#pl(x)"
	for key, value in pairs(regexTable) do
		reggedString = reggedString:gsub(key, value)
	end
	--for m in reggedString:gmatch(regex) do
	--	mw.log(mw.dumpObject(m))
	--	result = reggedString:gsub(regexString, "string")
	--	mw.log(result)
	--end
	return reggedString
end

function p.getIcon(icon, searchParam)
	return p.getIconObject(icon, searchParam).iconFile
end

function p.getIconObject(icon, searchParam)
	searchParam = searchParam or p.resolveParameter(searchParam, 2, true)
	icon = p.resolveParameter(icon)
	local icons = mw.loadData("Módulo:Datatable/Icons").icons
	
	for _, element in ipairs(icons) do
		if (searchParam and element[searchParam] == icon) or element.techName == icon or icon == element.icon then
			return element
		end
	end
	return icons[1] --Icon not found, then pick the first one which is supposed to be the Unknown one
end

--************************* survivors & killers ******************************--

function p.getCharacterById(id, charTable)
	for _, character in ipairs(charTable) do
		if character.id == id then return character end
	end
	return strings.charNotFound
end

function p.getCharacter(object)
	local data = require("Módulo:Datatable")
	
	if object.killer or object.charType == 'K' then
		return p.getCharacterById(object.killer or object.id, killers)
	else
		return p.getCharacterById(object.survivor or object.id, survivors)	
	end
end

function p.resolveCharacterPortraitFileName(character, maxId)
	local fileConst = "_charPreview_portrait"
	local isKiller = character.power
	local unknownChar = (isKiller and "UnknownKiller") or "UnknownSurvivor"
	local fileName
	
	fileName = p.getFileNameFromTableById(character.id, (isKiller and killerImages) or survivorImages, "preview") --get custom name from table
	if fileName == cstr.empty or not p.isValidFileName(fileName) then --K/S{ID}_charPreview_portrait
		fileName = ((character.power and 'K') or 'S') .. string.format("%02d", character.id) .. fileConst
	end
	if (maxId and (character.id >= maxId - 1 and not p.isValidFileName(fileName)) or (not maxId and not p.isValidFileName(fileName))) then --File not Found; maxId - 1 to consider last two characters, not only the last one
		fileName = unknownChar .. fileConst
	end

	--mw.log(fileName)
	return fileName
end

function p.getFileNameFromTableById(id, fileTable, field)
	--mw.log(id)
	for j, sImage in ipairs(fileTable) do
		if sImage.id == id and sImage[field] ~= nil then
			return sImage[field]
		end
	end
	return cstr.empty
end

function p.resolveGender(abbr)
	if		abbr == 'M'		then return strings.male
	elseif	abbr == 'F'		then return strings.female
	elseif  abbr == 'N'		then return strings.nonhuman
	elseif	abbr == 'M/F'	then return strings.male .. comma .. strings.female
	elseif	abbr == 'F/M'	then return strings.female .. comma .. strings.male
	else						 return strings.undefined
	end
end

--classes in form: "sortable, class2, class3 ,..."
function p.wrapBasicTable(content, classes, styles, skipNtl)
	return '{| class = ' .. quotes('wikitable' .. ((classes and space .. p.getTableClasses(classes)) or cstr.empty)) .. ((styles and space .. 'style = ' .. quotes(styles)) or cstr.empty) .. nl ..
	(not skipNtl and ntl .. nl or cstr.empty) .. content ..'|}'
end

function p.wrapTable(content, classes, styles, skipNtl)
	return (classes and '{| class = ' .. quotes((p.getTableClasses(classes))) or cstr.empty) .. ((styles and space .. 'style = ' .. quotes(styles)) or cstr.empty) .. nl ..
	(not skipNtl and ntl .. nl or cstr.empty) .. content ..'|}'
end

function p.getTableClasses(classes)
	classes = p.resolveParameter(classes, 1)
	local unknownClass = "unknownClass"
	if not classes then return unknownClass end
	
	if type(classes) == "string" then
		classes = classes:gsub(", ", space):gsub(",", space)
	else --todo type(classes) == "table"
		return unknownClass
	end
	return classes
end

function p.getCharacterByName(name)
	local str = require("Módulo:Datatable")
	for _, surv in ipairs(survivors) do
		if surv.shortName == name or surv.name == name then
			return surv
		end
	end
	for _, killer in ipairs(killers) do
		if killer.shortName == name or killer.realName == name or the(killer) .. killer.name == name or killer.name == name then
			return killer
		end
	end
	return nil
end

function p.getCharsByDlc(dlc, charType)
	local str = require("Módulo:Datatable")
	local result = {}
	local listTable = (charType == 'S' and survivors) or (charType == nil and survivors) or killers --if the charType is not set then set the table by default to survivors
	
	for _, character in ipairs(listTable) do
		if character.dlc == dlc.id then
			result[#result + 1] =  character
		end
	end
	
	if charType ~= nil then --if the charType is set, we need loop through only one table. Otherwise charType wasn't set in order to retrieve both types of characters from DLC
		return result
	end
	
	for _, character in ipairs(killers) do --since the default table is survivor, we can hardcode killer table as a second table
		if character.dlc == dlc.id then
			result[#result + 1] =  character
		end
	end
	return result
end

function p.getCharacterFirstName(character)
	character = p.resolveParameter(character)
	if not character.name then
		character = p.getCharacterByName(character)
	end
	local regex = '([^ ]*) ?' --%a doesn't consider diacritics such as É (for instance: Élodie)
	local isKiller = p.isKiller(character)
	local text = (isKiller and character.name) or character.shortName or character.name --if the char is killer then use their name, otherwise it's survivor, thus use shortName or name
	if isKiller then return text end --if it's killer, then their nickname is already what it's supposed to return
	
	if regex and text:gmatch(regex) then
		for m in text:gmatch(regex) do
			return m --return first occurence before potential space, otherwise return whole string
		end
	end
	return "Name not found"
end

function p.replaceLastSpaceByNBSP(text) -- Non Breakable SPace
	local lastCharSpaceRegex = "^(.+) $"
	local regex = "^(.+) (.+)$" --should pick last space(?)
	text = text:gsub(lastCharSpaceRegex, "%1" .. nbspC):gsub(regex, "%1" .. nbspC .. "%2")
	return text
end

function p.isCharacterKiller(character) return p.isKiller(character) end
function p.isKiller(character) 
	local character = p.resolveParameter(character)
	return character.power ~= nil
end

function p.getPossessiveName(character, params)
	params = params or {link = p.bool(p.resolveParameter(character, "link", true)) or false}
	local langs = require("Módulo:Languages")
	if not character.name then
		character = p.getCharacterByName(p.resolveParameter(character))
	end
	local firstName = p.getCharacterFirstName(character)

	if _language ~= "en" then
		return langs["possessive_" .. _language](character, params) .. firstName
	end
	
	if character.possessive == true then
		firstName = firstName .. "'" --if it's true then just add the apostrof to the end
	elseif character.possessive then --it's not true nor false nor empty, hence it should be string (not checked)
		firstName = character.possessive
	else
		firstName = firstName .. "'s" --if nothing then simply add 's after first name
	end
	
	return firstName
end

--[[
function p.getDativeName(character, params)
	params = params or {link = p.bool(p.resolveParameter(character, "link", true)) or false}
	if not character.name then
		character = p.getCharacterByName(p.resolveParameter(character))
	end
	if _language ~= "en" then
		return langs["dative_" .. _language](character, params) .. firstName
	end
	
	return firstName
end
]]

function p.clr(color, text)
	text = text or p.resolveParameter(color, 2, true)
	color = p.resolveParameter(color)
	
	return clr(color, text)
	--return frame:expandTemplate{title = "clr", args = {color, text}}	
end

function clr(color, text)
	local result = 'inherit' --CSS value, don't change it
	--mw.log(tonumber(color, 10))
	color = tonumber(color, 10) or color:lower() --if the string is int then convert it to integer
	
	if	   color == 1		or color ==  "brown"			then result = "ab713c"
	elseif color == "1bg"									then result = "5d4533"
	elseif color == 2		or color ==  "yellow"			then result = "e8c252"
	elseif color == "2bg"									then result = "d7ad2f"
	elseif color == 3		or color ==  "green"			then result = "199b1e"
	elseif color == "3bg"									then result = "0f791f"
	elseif color == 4		or color ==  "purple"			then result = "ac3ee3"
	elseif color == "4bg"									then result = "672d7f"
	elseif color == 5		or color ==  "pink"				then result = "ff0955"
	elseif color == "5bg"									then result = "cf0b45"
	elseif color == 6		or color ==  "orange"			then result = "ff8800"
	elseif color == "6bg"									then result = "ff5300"
	elseif color == 7		or color ==  "grey"				then result = "808080"
	elseif color == 8		or color ==  "red"				then result = "d41c1c"
	elseif color == 9		or color ==  "beige"			then result = "e7cda2"
	elseif color == 10		or color ==  "blue"				then result = "0e98ff"
	elseif color == 11		or color ==  "violet"			then result = "b91a9b"
	elseif color == "11bg"									then result = "800080"
	elseif color == 12		or color ==  "light blue"		then result = "9bb0bf"
	elseif color == "12bg"									then result = "9bb0bf"
	elseif color == 13		or color ==  "faded jade"		then result = "418284"
	elseif color == 14		or color ==  "gold"				then result = "ffa800"
	elseif color == "14bg"									then result = "ffa800"
	elseif color == 15		or color ==  "fuchsia"			then result = "ec0dea"
	elseif color == "15bg"									then result = "b30ad2"
	elseif color == 16		or color ==  "white"			then result = "ffffff"
	elseif color == 17		or color ==  "vomit green"		then result = "8ad672"
	elseif color == 18		or color ==  "blood red"		then result = "900a0a"
	elseif color == 19		or color ==  "bright red"		then result = "ff0000"
	elseif color == 20		or color ==  "silver"			then result = "b5afb0"
	elseif color == 21		or color ==  "turquoise"		then result = "26eaea"
	elseif color == 22		or color ==  "cinnamon"			then result = "b74004"
	elseif color == 23		or color ==  "black"			then result = "000000"
	elseif color == 24		or color ==  "wiki gold"		then result = "b7a269"
	elseif color == 25		or color ==  "bronze"			then result = "c2593a"
	elseif color == 26		or color ==  "chartreuse"		then result = "b6fa36"
	elseif color == 27		or color ==  "screamin' green"	then result = "63ef98"
	elseif color == 28		or color ==  "lavender"			then result = "b57edc"
	elseif color == 29		or color ==  "aquamarine"		then result = "7fffd4"
	elseif color == 30		or color ==  "ultramarine"		then result = "120a8f"
	elseif color == 31		or color ==  "olive"			then result = "808000"
	end
	
	if text ~= nil and text ~= cstr.empty then
		result = '<span class="luaClr clr clr' .. color .. '" style="color: ' .. ((result ~= 'inherit' and '#') or cstr.empty) .. result .. ';">' .. text .. '</span>'
	end
	
	return result
end

--text = text containing a tooltip
--tooltip = tooltip content
--iconless = true/false whether the (i) icon should appear after the text containing tooltip (default false)
--borderless = true/false whether underline should be shown (default false)
function p.tooltip(text, tooltip, iconless, borderless)
	iconless = p.bool(iconless or p.resolveParameter(text, 3, true) or (p.resolveParameter(text, "tooltip") and p.resolveParameter(text, 2, true)) or p.resolveParameter(text, "iconless"))
	tooltip = tooltip or p.resolveParameter(text, 2, true) or p.resolveParameter(text, "tooltip")
	text = p.resolveParameter(text)
	local containsLink = p.containsLink(tooltip)
	
	return
		'<span class = "tooltip' .. ((containsLink and space .. 'linkIncluded') or cstr.empty) .. ((iconless and space .. 'iconless') or cstr.empty) .. ((borderless and space .. 'borderless') or cstr.empty) .. '">' .. 
			'<span class="tooltipBaseText">' .. text .. '</span>'.. 
			'<span class = "tooltiptext">' ..
				'<span class = "mobileView"> (</span>' .. --if it's mobile view then apply add brackets...
					'<span class="tooltipTextWrapper">' .. tooltip .. '</span>' .. 
				'<span class = "mobileView">) </span>' .. 
			'</span>' ..
		'</span>'
end

function p.containsLink(text)
	local regex = '%[%[.-%]%]'
	local rawRegex = '?%&%#91;%&%#91;.-%&%#93;%&%#93;'
	if string.match(text, regex) or string.match(text, rawRegex) then --we want to return boolean, not the index
		return true
	else
		return false
	end
end

function p.ptb(content, title, patch)
	return
		ptb(
			p.resolveParameter(content),
			title or p.resolveParameter(content, 2, true),
			patch or p.resolveParameter(content, 3, true)
		)
end

function p.isPatchLive(patchParam)
	local patch = p.getPatch(patchParam)
	
	--dLog(((patchParam and "[" .. patchParam .. ((not patch and " - Not Found") or cstr.empty) .. "]") or cstr.empty) .. " isPatchLive: " .. (patch and (p.IsFullDateTime(patch.rDate) and tostring(p.dateHasPassed(p.GetDatetime(patch.rDate)))) or tostring(false)))
	return patch and (p.IsFullDateTime(patch.rDate) and p.dateHasPassed(p.GetDatetime(patch.rDate))) or false
end

function p.isCharacterLive(character)
	if character.dlc then
		local dlcM = require("Módulo:DLCs")
		local dlc = dlcM.getDlcById(character.dlc).rDate
		return p.IsFullDateTime(dlc) and p.dateHasPassed(dlc)
	end
	return false
end

function p.getPatch(patch)
	local data = require("Módulo:Datatable")
	
	for _, patchObj in ipairs(patches) do
		if patchObj.patch == patch then return patchObj end
	end
end

function p.getLatestPatch()
	return require("Módulo:Datatable").latestPatch.patch
end

--timestamp = {year = 2022, month = 7, day = 19} --example format
function p.dateHasPassed(timestamp)
	local currentDate = os.date("*t") --gives current timestamp: {year = 1998, month = 9, day = 16, yday = 259, wday = 4, hour = 23, min = 48, sec = 10, isdst = false}
	local releaseOffset = 60 * 60 * 15 --release hour 15:00 (server time is -2 hours from my current time)
	timestamp = p.standardiseDateObject(timestamp)

	--mw.log(os.difftime(os.time(timestamp) + releaseOffset, os.time(currentDate))) 
	return os.difftime(os.time(timestamp) + releaseOffset, os.time(currentDate)) < 0 -- subtracting releaseOffset to adjust timing according to actual release hour
end

function p.standardiseDateObject(dateObj)
	if type(dateObj) == types.table then --if we pass table, we need to check time values and populate them to 0 by default
		if not dateObj.hour then dateObj.hour = 0 end
		if not dateObj.min then dateObj.min = 0 end
		if not dateObj.sec then dateObj.sec = 0 end
		
		dateObj = os.time(dateObj) --we convert it to seconds constant (resp. variable)
	end
	
	dateObj = p.GetDatetime(dateObj)
	--deprecated, GetDatetime function now accpets seconds variable
	--if type(dateObj) == types.string then --handling other formats, such as "19.07.2022"
	--	dateObj = p.GetDatetime(dateObj) -- os.date("*t", os.time())
	--elseif type(dateObj) == types.number then --we convert constant back to table that is going to have all time/date fields (or if the constant is passed directly to the function)
	--	dateObj = p.GetDatetime(dateObj) --p.GetDatetime(os.date("*t", dateObj))
	--end
	
	return dateObj
end

function p.resolvePatchList()
	local data = require("Módulo:Datatable")
	local result = cstr.empty
	local yearCounter = 0
	local tempResult = cstr.empty
	
	for _, patch in ipairs(patches) do
		local patchDate = p.standardiseDateObject(patch.rDate)
		dmp(patchDate)
		local hotfix = resolveHotfix(patch)
		
		if yearCounter ~= patchDate.year then
			if yearCounter ~= 0 then
				result = result .. frame:callParserFunction{ name = '#tag', args = {"tabber", tempResult} } .. nl
			end
			tempResult = cstr.empty
			yearCounter = patchDate.year
			result = result .. 
				"|-|" .. patchDate.year .. "=" .. nl
		end
		
		local patchNotes, result = pcall(function () return frame:expandTemplate{title = strings.patch .. space .. patch.patch} end)
		if patchNotes and --[[not patch.ptb and]] patch ~= patches[#patches] then
			local patchVersions = p.split(patch.patch, '.')
			if patchVersions[3] ~= "0" then patchVersions[3] = "0" end
			for i = #patchVersions, 4 , -1 do --in case that patch contains anything else after last patch number let's remove it (example: "6.1.2 / 6.1.3")
				table.remove(patchVersions)
			end
			local patchLink = strings.patch .. space .. patch.patch
			local patchLinkModified = strings.patch .. space .. table.concat(patchVersions, ".")
			
			tempResult = tempResult .. 
				frame:expandTemplate{title = "!"}.. "-" .. frame:expandTemplate{title = "!"} .. patch.patch .. "=" .. nl .. --creating individual patch tabs
				
				frame:expandTemplate{title = "(!"} .. space .. 'class = "wikitable"' .. nl ..
				"!" .. nl .. "==== " .. link(patchLinkModified .. ((hotfix and hotfix .. tl .. patchLink) or cstr.empty)) .. " - " .. p.toDate(os.time(patchDate)) .. " ====" .. nl .. --28 April 2022
				frame:expandTemplate{title = "!-"} .. nl ..
				frame:expandTemplate{title = "!"} .. result .. nl ..
				frame:expandTemplate{title = "!)"} .. nl
		end
		
	end
	result = result .. frame:callParserFunction{ name = "#tag", args = {"tabber", tempResult} } .. nl --post loop aiditon for last year
	
	result = frame:extensionTag{name = 'tabber',content = result}
	
	mw.log(result)
	return result
end

function resolveHotfix(patch)
	local versions = p.split(patch.patch, '.')
	if versions[3] ~= "0" then
		return "#Hotfix " .. patch.patch
	end
	return nil
end

--wt = wordTable
function p.getWord(wt, index)
	local keys = table.keys(wt)
	local maxValue = table.max(keys)
	local minValue = table.min(keys)
	if index > maxValue then return wt[maxValue] end --if the index is higher than highest amount, return word of highest amount available
	if wt[index] then return wt[index] end -- if the index is exactly of plural count then return exact word on index
	if index < minValue then return wt[minValue] end --if index not found, it means it's lower than lowest amount in a word table
	
	for i, keyValue in ipairs(keys) do 
		if index < keyValue then
			return wt[keys[i - 1]] --if the index is between two amounts(indexes) use the lower one
		end
	end
end

--{{header = 2022, content = "..."}, {}, ...}
function p.getTabberFromTable(tabberTable)
	local result = cstr.empty
	for _, section in ipairs(tabberTable) do
		result = result .. nl .. '|-|' .. section.header .. '=' .. nl .. section.content .. nl
	end
	
	result = frame:callParserFunction{ name = '#tag', args = {"tabber", result} } 
	
	return result
end

return p
Advertisement