local p = {}
local namedColours = mw.loadData( 'Module:Box-header/colours' )
local modes = {
	lightest = { sat=0.10, val=1.00 },
	light = { sat=0.15, val=0.95 },
	normal = { sat=0.40, val=0.85 },
	dark = { sat=0.90, val=0.70 },
	darkest = { sat=1.00, val=0.45 },
	content = { sat=0.04, val=1.00 },
	grey = { sat=0.00 }
local min_contrast_ratio_normal_text = 7 -- i.e 7:1
local min_contrast_ratio_large_text = 4.5 -- i.e. 4.5:1

-- Template parameter aliases
-- Specify each as either a single value, or a table of values
-- Aliases are checked left-to-right, i.e. `['one'] = { 'two', 'three' }` is equivalent to using `} }}} }}}` in a template
local parameterAliases = {
	['1'] = 1,
	['2'] = 2,
	['colour'] = 'color'

local colourContrastModule = require('Module:Color contrast')
local hex = require( 'luabit.hex' )

local function getParam
	if args[parameter] then
		return args[parameter]
	end
	local aliases = parameterAliases[parameter]
	if not aliases then
		return nil
	end
	if type(aliases) ~= 'table' then
		return args[aliases]
	end
	for _, alias in ipairs(aliases) do
		if args[alias] then
			return args[alias]
		end
	end
	return nil
end

local function setCleanArgs(argsTable)
	local cleanArgs = {}
	for key, val in pairs(argsTable) do
		if type(val) == 'string' then
			val = val:match('^%s*(.-)%s*$')
			if val ~= '' then
				cleanArgs[key] = val
			end
		else
			cleanArgs[key] = val
		end
	end
	return cleanArgs
end

-- Merge two tables into a new table. If the are any duplicate keys, the values from the second overwrite the values from the first.
local function mergeTables
	local merged = {}
	for key, val in pairs(first) do
		merged[key] = val
	end
	for key, val in pairs(second) do
		merged[key] = val
	end
	return merged
end

local function toOpenTagString(selfClosedHtmlObject)
	local closedTagString = tostring(selfClosedHtmlObject)
	local openTagString = mw.ustring.gsub
	return openTagString
end

local function normaliseHexTriplet(hexString)
	if not hexString then return nil end
	local hexComponent = mw.ustring.match(hexString, '^#(%x%x%x)$') or mw.ustring.match(hexString, '^#(%x%x%x%x%x%x)$')
	if hexComponent and #hexComponent == 6 then
		return mw.ustring.upper(hexString)
	end
	if hexComponent and #hexComponent == 3 then
		local r = mw.ustring.rep(mw.ustring.sub, 2)
		local g = mw.ustring.rep(mw.ustring.sub, 2)
		local b = mw.ustring.rep(mw.ustring.sub, 2)
		return '#' .. mw.ustring.upper(r .. g .. b)
	end
	return nil
end

local function decimalToPaddedHex(number)
	local prefixedHex = hex.to_hex(tonumber(number)) -- prefixed with '0x'
	local padding = #prefixedHex == 3 and '0' or '' 
	return mw.ustring.gsub
end
local function hexToDecimal(hexNumber)
	return tonumber
end
local function RGBtoHexTriplet
	return '#' .. decimalToPaddedHex(R) .. decimalToPaddedHex(G) .. decimalToPaddedHex(B)
end
local function hexTripletToRGB(hexTriplet)
	local R_hex, G_hex, B_hex = string.match(hexTriplet, '(%x%x)(%x%x)(%x%x)')
	return hexToDecimal(R_hex), hexToDecimal(G_hex), hexToDecimal(B_hex)
end
local function HSVtoRGB -- per HSL and HSV#Converting_to_RGB
	local C = V * S
	local H_prime = H / 60
	local X = C * ( 1 - math.abs(math.fmod - 1) )
	local R1, G1, B1
	if H_prime <= 1 then
		R1 = C
		G1 = X
		B1 = 0
	elseif H_prime <= 2 then
		R1 = X
		G1 = C
		B1 = 0
	elseif H_prime <= 3 then
		R1 = 0
		G1 = C
		B1 = X
	elseif H_prime <= 4 then
		R1 = 0
		G1 = X
		B1 = C
	elseif H_prime <= 5 then
		R1 = X
		G1 = 0
		B1 = C
	elseif H_prime <= 6 then
		R1 = C
		G1 = 0
		B1 = X
	end	
	local m = V - C
	local R = R1 + m
	local G = G1 + m
	local B = B1 + m

	local R_255 = math.floor(R*255)
	local G_255 = math.floor(G*255)
	local B_255 = math.floor(B*255)
	return R_255, G_255, B_255
end
local function RGBtoHue -- per HSL and HSV#Hue and chroma
	local R = R_255/255
	local G = G_255/255
	local B = B_255/255

	local M = math.max
	local m = math.min
	local C = M - m
	local H_prime
	if C == 0 then
		return null
	elseif M == R then
		H_prime = math.fmod(((G - B)/C + 6), 6) -- adding six before taking mod ensures positive value
	elseif M == G then
		H_prime = (B - R)/C + 2
	elseif M == B then
		H_prime = (R - G)/C + 4
	end
	local H = 60 * H_prime
	return H
end
local function nameToHexTriplet(name)
	if not name then return nil end
	local codename = mw.ustring.gsub(mw.ustring.lower(name), ' ', '')
	return namedColours[codename]
end

local function calculateColours
	local bgColour = RGBtoHexTriplet(HSVtoRGB)
	local textColour = colourContrastModule._greatercontrast({bgColour})
	local contrast = colourContrastModule._ratio
	if contrast >= minContrast then
		return bgColour, textColour
	elseif textColour == '#FFFFFF' then
		-- make the background darker and slightly increase the saturation
		return calculateColours(H, math.min, math.max, minContrast)
	else
		-- make the background lighter and slightly decrease the saturation
		return calculateColours(H, math.max, math.min, minContrast)
	end
end

local function makeColours
	local mode = modes[modeName]
	local isGrey = not(hue)
	if isGrey then hue = 0 end

	local borderSat = isGrey and modes.grey.sat or 0.15
	local border = RGBtoHexTriplet(HSVtoRGB)

	local titleSat = isGrey and modes.grey.sat or mode.sat
	local titleBackground, titleForeground = calculateColours

	local contentSat = isGrey and modes.grey.sat or modes.content.sat
	local contentBackground, contentForeground = calculateColours

	return border, titleForeground, titleBackground, contentForeground, contentBackground
end

local function findHue(colour)
	local colourAsNumber = tonumber(colour)
	if colourAsNumber and ( -1 < colourAsNumber ) and ( colourAsNumber < 360) then
		return colourAsNumber
	end

	local colourAsHexTriplet = normaliseHexTriplet(colour) or nameToHexTriplet(colour)
	if colourAsHexTriplet then
		return RGBtoHue(hexTripletToRGB(colourAsHexTriplet))
	end

	return null
end

local function normaliseMode(mode)
	if not mode or not modes[mw.ustring.lower(mode)] or mw.ustring.lower(mode) == 'grey' then
		return 'normal'
	end
	return mw.ustring.lower(mode)
end
local function boxHeaderOuter(args)
	local baseStyle = {
		clear = 'both',
		['box-sizing'] = 'border-box',
		border = ( getParam or 'solid' ) .. ' ' .. ( getParam or getParam or '#ababab' ),
		background = getParam or '#bcbcbc',
		color = getParam or '#000',
		padding = getParam or '.1em',
		['text-align'] = getParam or 'center',
		['font-family'] = getParam or 'sans-serif',
		['font-size'] = getParam or '100%',
		['margin-bottom'] = '0px',

	local tag = mw.html.create
	return toOpenTagString(tag)
end

local function boxHeaderTopLinks(args)
	local style = {
		float = 'right',
		['margin-bottom'] = '.1em',
		['font-size'] = getParam or '80%',
		color = getParam or '#000'
	local tag = mw.html.create
	return toOpenTagString(tag)
end

local function boxHeaderEditLink(args)
	local page = getParam
	if not page or page == '}'
	then
		return ''
	end
	local style = {
		color = getParam or '#000'
	local tag = mw.html.create('span')
	local linktext = tostring(tag)
	local linktarget = tostring(mw.uri.fullUrl(page, {action='edit', section=getParam}))
	return '[' .. linktarget .. ' ' .. linktext .. ']&nbsp;'
end

local function boxHeaderViewLink(args)
	local style = {
		color = getParam or '#000'
	local tag = mw.html.create('span')
	local linktext = tostring(tag)
	local linktarget = ':' .. getParam
	return "')
	end
	table.insert(output, boxHeaderTitle(args))
	table.insert
	table.insert(output, boxBody(args))
	if not getParam then
		table.insert
	end
	if not getParam then
		table.insert
	end
	table.insert(output, contrastCategories(args))

	return table.concat(output)
end

-- Entry point for templates (automatically calculated colours)
function p.autoColour(frame)
	local args = getArgs(frame)
	local colourParam = getParam
	local generatedColour = nil
	if not colourParam or colourParam == '' then
		-- convert the root page name into a number and use that
		local root = mw.title.getCurrentTitle.rootPageTitle.prefixedText
		local rootStart = mw.ustring.sub
		local digitsFromRootStart = mw.ustring.gsub(rootStart, ".", function(s) return math.fmod(string.byte or string.byte, 10) end)
		local numberFromRoot = tonumber
		generatedColour = math.fmod
	end
	local output = p._autoColour
	if mw.ustring.find then
		return frame:preprocess(output)
	end
	return output
end

-- Entry point for modules (automatically calculated colours)
function p._autoColour
	local args = setCleanArgs(_args)
	local hue = generatedColour or findHue(getParam)
	local mode = normaliseMode(getParam)
	local border, titleForeground, titleBackground, contentForeground, contentBackground = makeColours
	local boxTemplateArgs = mergeTables(args, {
		title = getParam or '}',
		editpage = getParam or '',
		noedit = getParam and '' or 'yes',
		border = border,
		titleforeground = titleForeground,
		titlebackground = titleBackground,
		foreground = contentForeground,
		background = contentBackground
	return p._boxHeader(boxTemplateArgs)
end