-- UserLinks --
-- This module creates a list of links about a given user. It can be used on --
-- its own or from a template. See the /doc page for more documentation. --

-- Require necessary modules
local yesno = require('Module:Yesno')

-- Lazily initialise modules that we might or might not need
local mExtra -- Module:UserLinks/extra
local mArguments -- Module:Arguments
local mToolbar -- Module:Toolbar
local mCategoryHandler -- Module:Category handler
local mTableTools -- Module:TableTools
local interwikiTable -- Module:InterwikiTable, loaded with mw.loadData

-- Load shared helper functions
local mShared = require('Module:UserLinks/shared')
local raiseError = mShared.raiseError
local maybeLoadModule = mShared.maybeLoadModule
local makeWikitextError = mShared.makeWikitextError
local makeWikilink = mShared.makeWikilink
local makeUrlLink = mShared.makeUrlLink
local makeFullUrlLink = mShared.makeFullUrlLink
local message = mShared.message

local p = {}

-- Link table

function p.getLinks(snippets)
	--[=[
	-- Get a table of links that can be indexed with link codes. The table
	-- returned is blank, but links are added to it on demand when it is
	-- indexed. This is made possible by the metatable and by the various link
	-- functions, some of which are defined here, and some of which are defined
	-- at Module:UserLinks/extra.
	--]=]
	local links, linkFunctions = {}, {}

	-- Link functions
	--
	-- The following functions make the links from the link codes and the user
	-- data snippets. New link functions should be added below the existing
	-- functions.

	function linkFunctions.u(snippets)
		-- User page
		return makeWikilink
	end

	function linkFunctions.t(snippets)
		-- User talk page
		return makeWikilink(
			snippets.interwiki,
			3,
			snippets.username,
			message('display-talk')
		)
	end

	function linkFunctions.c(snippets)
		-- Contributions
		return makeWikilink(
			snippets.interwiki,
			-1,
			'Contribs/' .. snippets.username,
			message('display-contributions')
		)
	end
	
	function linkFunctions.c64(snippets)
		-- Contributions
		local first64 = snippets.username:match('^%x+:%x+:%x+:%x+:')
			or snippets.username:match('^%x+:%x+:%x+:')
			or snippets.username:match('^%x+:%x+:')
			or snippets.username:match('^%x+:')
		return first64 and makeWikilink(
			snippets.interwiki,
			-1,
			'Contribs/' .. first64 .. ':/64',
			'(/64)'
		) or ''
	end

	function linkFunctions.ct(snippets)
		-- Edit count
		return makeUrlLink(
				host = 'xtools.wmflabs.org',
				path = '/ec/',
				query = {
					username = snippets.username,
					project = snippets.toolLang .. '.' .. snippets.projectLong .. '.org'
			message('display-count')
		)
	end

	function linkFunctions.m(snippets)
		-- Page moves
		return makeWikilink(
			snippets.interwiki,
			-1,
			'Log/move/' .. snippets.username,
			message('display-moves')
		)
	end

	function linkFunctions.l(snippets)
		-- Logs
		return makeWikilink(
			snippets.interwiki,
			-1,
			'Log/' .. snippets.username,
			message('display-logs')
		)
	end

	function linkFunctions.ae(snippets)
		-- Automated edits (and non-automated contributions).
		return makeUrlLink(
				host = 'xtools.wmflabs.org',
				path = '/autoedits/',
				query = {
					username = snippets.username,
					project = snippets.toolLang .. '.' .. snippets.projectLong .. '.org'
			message('display-autoedits')
		)
	end

	function linkFunctions.bl(snippets)
		-- Block log
		return makeFullUrlLink(
			snippets.interwiki,
			-1,
			'Log/block',
			message('display-blocklog')
		)
	end

	function linkFunctions.bls(snippets)
		-- Blocks
		return makeWikilink(
			snippets.interwiki,
			-1,
			'Log/block/' .. snippets.username,
			message('display-blocks')
		)
	end

	function linkFunctions.bu(snippets)
		-- Block user
		return makeWikilink(
			snippets.interwiki,
			-1,
			'Block/' .. snippets.username,
			message('display-blockuser')
		)
	end

	function linkFunctions.ca(snippets)
		-- Central auth
		return makeWikilink(
			snippets.interwiki,
			-1,
			'CentralAuth/' .. snippets.username,
			message('display-centralauth')
		)
	end

	function linkFunctions.dc(snippets)
		-- Deleted contribs
		return makeWikilink(
			snippets.interwiki,
			-1,
			'DeletedContributions/' .. snippets.username,
			message('display-deletedcontributions')
		)
	end

	function linkFunctions.e(snippets)
		-- Email
		return makeWikilink(
			snippets.interwiki,
			-1,
			'EmailUser/' .. snippets.username,
			message('display-email')
		)
	end

	function linkFunctions.es(snippets)
		-- Edit summaries
		return makeUrlLink(
				host = 'xtools.wmflabs.org',
				path = '/editsummary/',
				query = {
					username = snippets.username,
					project = snippets.toolLang .. '.' .. snippets.projectLong .. '.org'
			message('display-editsummaries')
		)
	end

	function linkFunctions.del(snippets)
		-- Deletions
		return makeWikilink(
			snippets.interwiki,
			-1,
			'Log/delete/' .. snippets.username,
			message('display-deletions')
		)
	end

	function linkFunctions.lu(snippets)
		-- List user
		return makeFullUrlLink(
			snippets.interwiki,
			-1,
			'ListUsers',
			message('display-listuser')
		)
	end

	function linkFunctions.sul(snippets)
		-- SUL
		return makeWikilink(
			nil,
			nil,
			'sulutil:' .. snippets.username,
			message('display-sul')
		)
	end

	function linkFunctions.tl(snippets)
		-- Target logs
		return makeFullUrlLink(
			snippets.interwiki,
			-1,
			'Log',
			message('display-targetlogs')
		)
	end

	function linkFunctions.efl(snippets)
		-- Edit filter log
		return makeFullUrlLink(
			snippets.interwiki,
			-1,
			'AbuseLog',
			message('display-abuselog')
		)
	end

	function linkFunctions.pr(snippets)
		-- Protections
		return makeWikilink(
			snippets.interwiki,
			-1,
			'Log/protect/' .. snippets.username,
			message('display-protections')
		)
	end

	function linkFunctions.rl(snippets)
		-- User rights
		return makeWikilink(
			snippets.interwiki,
			-1,
			'Log/rights/' .. snippets.username,
			message('display-rights')
		)
	end

	function linkFunctions.ren(snippets)
		-- Renames
		return makeWikilink(
			snippets.interwiki,
			-1,
			'Log/renameuser/' .. snippets.username,
			message('display-renames')
		)
	end

	function linkFunctions.rfa(snippets)
		-- Requests for adminship
		return makeWikilink(
			nil,
			-1,
			'PrefixIndex/' .. message('page-rfa') .. '/' .. snippets.username,
			message('display-rfa')
		)
	end

	function linkFunctions.api(snippets)
		-- API user data
		return makeUrlLink(
				host = snippets.fullDomain,
				path = '/w/api.php',
				query = {
					action = 'query',
					list = 'users',
					usprop = 'groups|editcount',
					ususers = snippets.username
			message('display-api')
		)
	end

	function linkFunctions.up(snippets)
		-- Uploads
		return makeWikilink(
			snippets.interwiki,
			-1,
			'ListFiles/' .. snippets.username,
			message('display-uploads')
		)
	end
	
	-- End of link functions

	-- Define the metatable that memoizes the link functions, and fetches link
	-- functions from Module:UserLinks/extra if necessary.

	-- Lazily initialise the extraLinkFunctions table. We only want to load
	-- Module:UserLinks/extra as necessary, so it has a low transclusion
	-- count.
	local extraLinkFunctions

	-- Define functions for shared code in the metatable.
	local function validateCode(code)
		-- Checks whether code is a valid link code - i.e. checks that it is a
		-- string and that it is not the blank string. Returns the code if
		-- the check passes, and nil if not.
		if type(code) == 'string' and code ~= '' then
			return code
		else
			return nil
		end
	end

	local function getExtraLinkFunctions
		-- Loads the table of extra link functions from the /extra module.
		-- If there is a problem with loading it, return false. We use the
		-- distinction between false and nil to record whether we have already
		-- tried to load it.
		if extraLinkFunctions ~= nil then
			return extraLinkFunctions
		end
		if mExtra == nil then
			-- If loading the module fails, maybeLoadModule returns false.
			-- Here we use the distinction between false and nil to record
			-- whether we have already tried to load the /extra module.
			mExtra = maybeLoadModule('Module:UserLinks/extra')
		end
		if type(mExtra) == 'table'
			and type(mExtra.linkFunctions) == 'table'
		then
			extraLinkFunctions = mExtra.linkFunctions
		else
			extraLinkFunctions = false
		end
		return extraLinkFunctions
	end

	local function memoizeExtraLink
		local success, link = pcall
		if success and type(link) == 'string' then
			links[code] = link
			return link
		end
		return nil
	end

	-- Define the metatable.
	setmetatable(links, {
		__index = function 
			local code = validateCode(key)
			if not code then
				raiseError(
					message('error-malformedlinkcode'),
					message('error-malformedlinkcode-section')
				)
			end
			local linkFunction = linkFunctions[code]
			local link
			if linkFunction then
				link = linkFunction(snippets)
				links[code] = link
			else
				extraLinkFunctions = getExtraLinkFunctions
				if extraLinkFunctions then
					local extraLinkFunction = extraLinkFunctions[code]
					if type(extraLinkFunction) == 'function' then
						link = memoizeExtraLink
					end
				end
			end
			if link then
				return link
			else
				raiseError(
					message,
					message('error-invalidlinkcode-section')
				)
			end
		end,
		__pairs = function 
			extraLinkFunctions = getExtraLinkFunctions
			if extraLinkFunctions then
				for code, func in pairs(extraLinkFunctions) do
					if validateCode(code) and type(func) == 'function' then
						memoizeExtraLink
					end
				end
			end
			-- Allow built-in functions to overwrite extra functions.
			for code, func in pairs(linkFunctions) do
				local link = func(snippets)
				links[code] = link
			end
			return function 
				return next
			end
		end
	return links
end

-- User data snippets

function p.getSnippets(args)
	--[=[
	-- This function gets user data snippets from the arguments, and from
	-- Module:InterwikiTable. The data is loaded as necessary and memoized
	-- in the snippets table for performance. 
	--
	-- Snippets default to the blank string, '', so they can be used in
	-- concatenation operations without coders having to worry about raising
	-- errors. Because of this, the local functions snippetExists and
	-- getSnippet have been written to aid people writing new snippets. These
	-- functions treat the blank string as false. It is not necessary to return
	-- the blank string from a snippet function, as nil and false values are
	-- automatically converted into the blank string by the metatable.
	--
	-- If you add a new snippet, please document it at
	-- Module:UserLinks#Adding new links.
	--]=]
	local snippets, snippetFunctions = {}, {}
	setmetatable(snippets, {
		__index = function 
			local snippetFunction = snippetFunctions[key]
			if snippetFunction then
				snippets[key] = snippetFunction or ''
				return snippets[key]
			else
				raiseError(
					message,
					message('error-nosnippet-section')
				)
			end
		end

	-- Define helper functions for writting the snippet functions.
	local function snippetExists(key)
		-- We have set the metatable up to make snippets default to '', so we
		-- don't have to test for false or nil.
		return snippets[key] ~= ''
	end

	local function getSnippet(key)
		local ret = snippets[key]
		if ret == '' then
			return nil
		else
			return ret
		end
	end

	-- Start snippet functions.

	function snippetFunctions.username
		-- The username.
		local username = args.user or args.User
		return username or raiseError(
			message('error-nousername'),
			message('error-nousername-section')
		)
	end

	function snippetFunctions.usernameHtml
		-- The username html-encoded. Spaces are encoded as pluses.
		return mw.uri.encode(snippets.username)
	end

	function snippetFunctions.project
		-- The project name.
		-- Also does the work for snippetFunctions.interwikiTableKey, and adds
		-- the project value to snippets.lang if it is a valid language code.
		local project = args.Project or args.project
		if not project then
			return nil
		end
		local projectValidated, interwikiTableKey = p.validateProjectCode(project)
		if not projectValidated then
			if mw.language.isKnownLanguageTag(project) then
				if not snippetExists('lang') then
					snippets.lang = project
				end
			else
				raiseError(
					message,
					message('error-invalidproject-section')
				)
			end
		end
		snippets.interwikiTableKey = interwikiTableKey
		return project
	end

	function snippetFunctions.interwikiTableKey
		-- The key for the project in Module:InterwikiTable.
		-- Relies on snippetFunctions.project to do the real work.
		local temp = snippets.project -- required; puts key in snippets table
		return rawget
	end

	function snippetFunctions.toolProject
		-- The short project code for use with toolserver or labs. It is always
		-- present, even if the "project" argument is absent. The default value
		-- is the "snippet-project-default" message.
		local project = getSnippet('project')
		if not project then
			return message('snippet-project-default')
		else
			return project
		end
	end

	function snippetFunctions.projectLong
		-- The long form of the project name, e.g. "wikipedia" or "wikibooks".
		local key = getSnippet('interwikiTableKey')
		if not key then
			return message('snippet-projectlong-default')
		end
		interwikiTable = interwikiTable or mw.loadData('Module:InterwikiTable')
		local prefixes = interwikiTable[key].iw_prefix
		-- Using prefixes[2] is a bit of a hack, but should find the long name
		-- most of the time.
		return prefixes[2] or prefixes[1] 
	end

	function snippetFunctions.lang
		-- The language code.
		local lang = args.lang or args.Lang
		if not lang then
			return nil
		end
		if mw.language.isKnownLanguageTag(lang) then
			return lang
		else
			raiseError(
				message,
				message('error-invalidlanguage-section')
			)
		end
	end

	function snippetFunctions.toolLang
		-- The language code for use with toolserver or labs tools. It is always
		-- present, even if the "lang" argument is absent. The default value is
		-- the "snippet-lang-default" message. 
		return getSnippet('lang') or message('snippet-lang-default')
	end

	function snippetFunctions.interwiki
		-- The interwiki prefix, consisting of the project and language values,
		-- separated by colons, e.g. ":wikt:es:".
		local project = getSnippet('project')
		local lang = getSnippet('lang')
		if not project and not lang then
			return nil
		end
		local ret = {}
		ret[#ret + 1] = project
		ret[#ret + 1] = lang
		return table.concat
	end

	function snippetFunctions.fullDomain
		-- The full domain name of the site, e.g. www.mediawiki.org,
		-- en.wikpedia.org, or ja.wikibooks.org.
		local fullDomain
		local lang = getSnippet('toolLang')
		local key = getSnippet('interwikiTableKey')
		if key then
			interwikiTable = interwikiTable or mw.loadData('Module:InterwikiTable')
			local domain = interwikiTable[key].domain
			local takesLangPrefix = interwikiTable[key].takes_lang_prefix
			if takesLangPrefix then
				fullDomain = lang .. '.' .. domain
			else
				fullDomain = domain
			end
		else
			fullDomain = lang .. '.wikipedia.org'
		end
		return fullDomain
	end

	-- End snippet functions. If you add a new snippet function, please
	-- document it at Module:UserLinks#Adding new links.

	return snippets
end 

function p.validateProjectCode(s)
	-- Validates a project code, by seeing whether it is present in
	-- Module:InterwikiTable. If it is present, returns the code and the
	-- InterwikiTable key for the corresponding site. If not present,
	-- returns nil for both.
	interwikiTable = interwikiTable or mw.loadData('Module:InterwikiTable')
	for key, t in pairs(interwikiTable) do
		for i, prefix in ipairs(t.iw_prefix) do
			if s == prefix then
				return s, key
			end
		end
	end
	return nil, nil
end

-- Main functions

local function makeInvokeFunction(funcName)
	-- Makes a function that can be accessed from #invoke. This is only required
	-- for functions that need to access arguments.
	return function (frame)
		mArguments = require('Module:Arguments')
		local args = mArguments.getArgs(frame)
		return p[funcName](args)
	end
end

p.main = makeInvokeFunction('_main')

function p._main(args)
	-- The main function. This is the one called from Template:User-multi,
	-- via p.main.
	local options = p.getOptions(args)
	local snippets = p.getSnippets(args)
	local codes = p.getCodes(args)
	local links = p.getLinks(snippets)
	-- Overload the built-in Lua error function to generate wikitext errors
	-- meant for end users to see. This makes things harder to debug when
	-- real errors occur, but it is the only realistic way to show wikitext
	-- errors and and still have sane code when using metatables, etc.
	local success, result = pcall
	if success then
		return result
	else
		return makeWikitextError
	end
end

function p.getOptions(args)
	-- Gets the options from the args table, so that we don't have to pass
	-- around the whole args table all the time.
	local options = {}
	options.isDemo = yesno(args.demo) or false
	options.toolbarStyle = yesno(args.small) and 'font-size: 90%;' or nil
	options.sup = yesno
	options.separator = args.separator
	options.span = args.span
	return options
end

function p.getCodes(args)
	-- Gets the link codes from the arguments. The codes aren't validated
	-- at this point.
	mTableTools = maybeLoadModule('Module:TableTools')
	local codes
	if mTableTools then
		codes = mTableTools.compressSparseArray(args)
	else
		codes = {}
		for i, code in ipairs(args) do
			codes[i] = code
		end
	end
	return codes
end

function p.export
	-- Make the user link.
	local userLink = links.u

	-- If we weren't passed any link codes, just return the user link.
	if #codes < 1 then
		return userLink
	end

	-- Make the toolbar.
	mToolbar = require('Module:Toolbar')
	local toolbarArgs = {}
	for i, code in ipairs(codes) do
		local link = links[code]
		toolbarArgs[#toolbarArgs + 1] = link
	end
	toolbarArgs.style = options.toolbarStyle
	toolbarArgs.separator = options.separator or 'dot'
	toolbarArgs.span = options.span
	local toolbar = mToolbar.main(toolbarArgs)

	-- Apply the sup option.
	if options.sup then
		toolbar = ''
	end
	
	-- If we are transcluding, add a non-breaking space, but if we are substing
	-- just use a normal space
	local space = mw.isSubsting and ' ' or '&nbsp;'
	
	return userLink .. space .. toolbar
end

-- Single link function

p.single = makeInvokeFunction('_single')

function p._single(args)
	-- Fetches a single link from the link table.
	local options = p.getOptions(args)
	local snippets = p.getSnippets(args)
	local links = p.getLinks(snippets)
	local code = args[1]
	local success, link = pcall
	if success then
		return link
	else
		return makeWikitextError
	end
end

function p.exportSingle
	-- If any errors occur, they will probably occur here. This function
	-- exists purely so that all the errors that will occur in p._single can
	-- be handled using a single pcall.
	if not code then
		raiseError(
			message('error-nolinkcode'),
			message('error-nolinkcode-section')
		)
	end
	return links[code]
end

-- Link table

function p.linktable
	-- Returns a wikitext table of link codes, with an example link for each
	-- one. This function doesn't take any arguments, so it can be accessed
	-- directly from wiki pages without using makeInvokeFunction.
	local args = {user = 'Example'}
	local snippets = p.getSnippets(args)
	local links = p.getLinks(snippets)

	-- Assemble the codes and links in order
	local firstCodes = {'u', 't', 'c'}
	local firstLinks, firstCodesKeys = {}, {}
	for i, code in ipairs(firstCodes) do
		firstCodesKeys[code] = true
		firstLinks[#firstLinks + 1] = {code, links[code]}
	end
	local secondLinks = {}
	for code, link in pairs(links) do
		if not firstCodesKeys[code] then
			secondLinks[#secondLinks + 1] = {code, link}
		end
	end
	table.sort(secondLinks, function
		return t1[1] < t2[1]
	end)
	local links = {}
	for i, t in ipairs(firstLinks) do
		links[#links + 1] = t
	end
	for i, t in ipairs(secondLinks) do
		links[#links + 1] = t
	end

	-- Output the code table in table format
	local ret = {}
	ret[#ret + 1] = '{| class="wikitable plainlinks sortable"'
	ret[#ret + 1] = '|-'
	ret[#ret + 1] = '! ' .. message('linktable-codeheader')
	ret[#ret + 1] = '! ' .. message('linktable-previewheader')
	for i, t in ipairs(links) do
		local code = t[1]
		local link = t[2]
		ret[#ret + 1] = '|-'
		ret[#ret + 1] = "| **" .. code .. "**" 
		ret[#ret + 1] = '| ' .. link
	end
	ret[#ret + 1] = '|}'
	return table.concat
end