--[=[ TemplatePar 2015-02-14
Template parameter utility
* assert
* check
* count
* countNotEmpty
* downcase()
* match
* valid
* verify()
* TemplatePar()
]=]



-- Module globals
local TemplatePar = { }
local MessagePrefix = "lua-module-TemplatePar-"
local L10nDef = {}
L10nDef.en = {
	badPattern  = "#invoke:TemplatePar pattern syntax error",
	dupOpt	  = "#invoke:TemplatePar repeated optional parameter",
	dupRule	 = "#invoke:TemplatePar conflict key/pattern",
	empty	   = "Error in template * undefined value for mandatory",
	invalid	 = "Error in template * invalid parameter",
	invalidPar  = "#invoke:TemplatePar invalid parameter",
	minmax	  = "#invoke:TemplatePar min > max",
	missing	 = "#invoke:TemplatePar missing library",
	multiSpell  = "Error in template * multiple spelling of parameter",
	noMSGnoCAT  = "#invoke:TemplatePar neither message nor category",
	noname	  = "#invoke:TemplatePar missing parameter name",
	notFound	= "Error in template * missing page",
	tooLong	 = "Error in template * parameter too long",
	tooShort	= "Error in template * parameter too short",
	undefined   = "Error in template * mandatory parameter missing",
	unknown	 = "Error in template * unknown parameter name",
	unknownRule = "#invoke:TemplatePar unknown rule"
}
L10nDef.de  = {
	badPattern  = "#invoke:TemplatePar Syntaxfehler des pattern",
	dupOpt	  = "#invoke:TemplatePar Optionsparameter wiederholt",
	dupRule	 = "#invoke:TemplatePar Konflikt key/pattern",
	empty	   = "Fehler bei Vorlage * Pflichtparameter ohne Wert",
	invalid	 = "Fehler bei Vorlage * Parameter ungültig",
	invalidPar  = "#invoke:TemplatePar Ungültiger Parameter",
	minmax	  = "#invoke:TemplatePar min > max",
	multiSpell  = "Fehler bei Vorlage * Mehrere Parameter-Schreibweisen",
	noMSGnoCAT  = "#invoke:TemplatePar weder Meldung noch Kategorie",
	noname	  = "#invoke:TemplatePar Parameter nicht angegeben",
	notFound	= "Fehler bei Vorlage * Seite fehlt",
	tooLong	 = "Fehler bei Vorlage * Parameter zu lang",
	tooShort	= "Fehler bei Vorlage * Parameter zu kurz",
	undefined   = "Fehler bei Vorlage * Pflichtparameter fehlt",
	unknown	 = "Fehler bei Vorlage * Parametername unbekannt",
	unknownRule = "#invoke:TemplatePar Unbekannte Regel"
}
local Patterns = {
	[ "ASCII" ]	= "^[ -~]*$",
	[ "ASCII+" ]   = "^[ -~]+$",
	[ "ASCII+1" ]  = "^[!-~]+$",
	[ "n" ]		= "^[%-]?[0-9]*$",
	[ "n>0" ]	  = "^[0-9]*[1-9][0-9]*$",
	[ "N+" ]	   = "^[%-]?[1-9][0-9]*$",
	[ "N>0" ]	  = "^[1-9][0-9]*$",
	[ "x" ]		= "^[0-9A-Fa-f]*$",
	[ "x+" ]	   = "^[0-9A-Fa-f]+$",
	[ "X" ]		= "^[0-9A-F]*$",
	[ "X+" ]	   = "^[0-9A-F]+$",
	[ "0,0" ]	  = "^[%-]?[0-9]*,?[0-9]*$",
	[ "0,0+" ]	 = "^[%-]?[0-9]+,[0-9]+$",
	[ "0,0+?" ]	= "^[%-]?[0-9]+,?[0-9]*$",
	[ "0.0" ]	  = "^[%-]?[0-9]*[%.]?[0-9]*$",
	[ "0.0+" ]	 = "^[%-]?[0-9]+%.[0-9]+$",
	[ "0.0+?" ]	= "^[%-]?[0-9]+[%.]?[0-9]*$",
	[ ".0+" ]	  = "^[%-]?[0-9]*[%.]?[0-9]+$",
	[ "ID" ]	   = "^[A-Za-z]?[A-Za-z_0-9]*$",
	[ "ID+" ]	  = "^[A-Za-z][A-Za-z_0-9]*$",
	[ "ABC" ]	  = "^[A-Z]*$",
	[ "ABC+" ]	 = "^[A-Z]+$",
	[ "Abc" ]	  = "^[A-Z]*[a-z]*$",
	[ "Abc+" ]	 = "^[A-Z][a-z]+$",
	[ "abc" ]	  = "^[a-z]*$",
	[ "abc+" ]	 = "^[a-z]+$",
	[ "aBc+" ]	 = "^[a-z]+[A-Z][A-Za-z]*$",
	[ "w" ]		= "^%S*$",
	[ "w+" ]	   = "^%S+$",
	[ "base64" ]   = "^[A-Za-z0-9%+/]*$",
	[ "base64+" ]  = "^[A-Za-z0-9%+/]+$",
	[ "aa" ]	   = "[%a%a].*[%a%a]",
	[ "pagename" ] = string.format( "^[^#<>%%[%%]|{}%c-%c%c]+$",
									1, 31, 127 ),
	[ "+" ]		= "%S"
}
local patternCJK = false



local function containsCJK( s )
	-- Is any CJK character present?
	-- Precondition:
	--	 s  -- string
	-- Postcondition:
	--	 Return false iff no CJK present
	-- Uses:
	--	 >< patternCJK
	--	 mw.ustring.char()
	--	 mw.ustring.match()
	local r = false
	patternCJK = patternCJK or mw.ustring.char(91,
									   13312, 45,  40959,
									  131072, 45, 178207,
									  93 )
	if mw.ustring.match( s, patternCJK ) then
		r = true
	end
	return r
end -- containsCJK()



local function facility( accept, attempt )
	-- Check string as possible file name or other source page
	-- Precondition:
	--	 accept   -- string; requirement
	--						 file
	--						 file+
	--						 file:
	--						 file:+
	--						 image
	--						 image+
	--						 image:
	--						 image:+
	--	 attempt  -- string; to be tested
	-- Postcondition:
	--	 Return error keyword, or false
	-- Uses:
	--	 Module:FileMedia
	--	 FileMedia.isType()
	local r
	if attempt and attempt ~= "" then
		local lucky, FileMedia = pcall( require, "Module:FileMedia" )
		if type( FileMedia ) == "table" then
			FileMedia = FileMedia.FileMedia()
			local s, live = accept:match( "^([a-z]+)(:?)%+?$" )
			if live then
				if FileMedia.isType( attempt, s ) then
					if FileMedia.isFile( attempt ) then
						r = false
					else
						r = "notFound"
					end
				else
					r = "invalid"
				end
			elseif FileMedia.isType( attempt, s ) then
				r = false
			else
				r = "invalid"
			end
		else
			r = "missing"
		end
	elseif accept:match( "%+$" ) then
		r = "empty"
	else
		r = false
	end
	return r
end -- facility()



local function factory( say )
	-- Retrieve localized message string in content language
	-- Precondition:
	--	 say  -- string; message ID
	-- Postcondition:
	--	 Return some message string
	-- Uses:
	--	 >  MessagePrefix
	--	 >  L10nDef
	--	 mw.language.getContentLanguage()
	--	 mw.message.new()
	local c = mw.language.getContentLanguage():getCode()
	local m = mw.message.new( MessagePrefix .. say )
	local r = false
	if m:isBlank() then
		local l10n = L10nDef[ c ] or L10nDef[ "en" ]
		r = l10n[ say ]
	else
		m:inLanguage( c )
		r = m:plain()
	end
	r = r or string.format( "(((%s)))", say )
	return r
end -- factory()



local function failsafe( story, scan )
	-- Test for match (possibly user-defined with syntax error)
	-- Precondition:
	--	 story  -- string; parameter value
	--	 scan   -- string; pattern
	-- Postcondition:
	--	 Return nil, if not matching, else non-nil
	-- Uses:
	--	 mw.ustring.match()
	return  mw.ustring.match( story, scan )
end -- failsafe()



local function failure( spec, suspect, options )
	-- Submit localized error message
	-- Precondition:
	--	 spec	 -- string; message ID
	--	 suspect  -- string or nil; additional information
	--	 options  -- table or nil; optional details
	--				 options.template
	-- Postcondition:
	--	 Return string
	-- Uses:
	--	 factory()
	local r = factory( spec )
	if type( options ) == "table" then
		if type( options.template ) == "string" then
			if #options.template > 0 then
				r = string.format( "%s (%s)", r, options.template )
			end
		end
	end
	if suspect then
		r = string.format( "%s: %s", r, suspect )
	end
	return r
end -- failure()



local function fault( store, key )
	-- Add key to collection string and insert separator
	-- Precondition:
	--	 store  -- string or nil or false; collection string
	--	 key	-- string or number; to be appended
	-- Postcondition:
	--	 Return string; extended
	local r
	local s
	if type( key ) == "number" then
		s = tostring( key )
	else
		s = key
	end
	if store then
		r = string.format( "%s; %s", store, s )
	else
		r = s
	end
	return r
end -- fault()



local function feasible( analyze, options, abbr )
	-- Check content of a value
	-- Precondition:
	--	 analyze  -- string to be analyzed
	--	 options  -- table or nil; optional details
	--				 options.pattern
	--				 options.key
	--				 options.say
	--	 abbr	 -- true: abbreviated error message
	-- Postcondition:
	--	 Return string with error message as configured;
	--			false if valid or no answer permitted
	-- Uses:
	--	 >  Patterns
	--	 failure()
	--	 mw.text.trim()
	--	 facility()
	--	 failsafe()
	--	 containsCJK()
	local r	= false
	local s	= false
	local show = nil
	local scan = false
	if type( options.pattern ) == "string" then
		if options.key then
			r = failure( "dupRule", false, options )
		else
			scan = options.pattern
		end
	else
		if type( options.key ) == "string" then
			s = mw.text.trim( options.key )
		else
			s = "+"
		end
		if s ~= "*" then
			scan = Patterns[ s ]
		end
		if type( scan ) == "string" then
			if s == "n" or s == "0,0" or s == "0.0" then
				if not analyze:match( "[0-9]" )  and
				   not analyze:match( "^%s*$" ) then
					scan = false
					if options.say then
						show = string.format( "'%s'", options.say )
					end
					if abbr then
						r = show
					else
						r = failure( "invalid", show, options )
					end
				end
			end
		elseif s ~= "*" then
			local op, n, plus = s:match( "([<!=>]=?)([-0-9][%S]*)(+?)" )
			if op then
				n = tonumber( n )
				if n then
					local i = tonumber( analyze )
					if i then
						if op == "<" then
							i = ( i < n )
						elseif op == "<=" then
							i = ( i <= n )
						elseif op == ">" then
							i = ( i > n )
						elseif op == ">=" then
							i = ( i >= n )
						elseif op == "==" then
							i = ( i == n )
						elseif op == "!=" then
							i = ( i ~= n )
						else
							n = false
						end
					end
					if not i then
						r = "invalid"
					end
				elseif plus then
					r = "undefined"
				end
			elseif s:match( "^image%+?:?$" )  or
				   s:match( "^file%+?:?$" ) then
				r = facility( s, analyze )
				n = true
			elseif s:match( "langW?%+?" ) then
				n = "lang"
-- lang lang+
-- langW langW+
			end
			if not n and not r then
				r = "unknownRule"
			end
			if r then
				if options.say then
					show = string.format( "'%s' %s", options.say, s )
				else
					show = s
				end
				if abbr then
					r = show
				else
					r = failure( r, show, options )
				end
			end
		end
	end
	if scan then
		local legal, got = pcall( failsafe, analyze, scan )
		if legal then
			if not got then
				if s == "aa" then
					got = containsCJK( analyze )
				end
				if not got then
					if options.say then
						show = string.format( "'%s'", options.say )
					end
					if abbr then
						r = show
					else
						r = failure( "invalid", show, options )
					end
				end
			end
		else
			r = failure( "badPattern",
						 string.format( "%s *** %s", scan, got ),
						 options )
		end
	end
	return r
end -- feasible()



local function fed( haystack, needle )
	-- Find needle in haystack map
	-- Precondition:
	--	 haystack  -- table; map of key values
	--	 needle	-- any; identifier
	-- Postcondition:
	--	 Return true iff found
	local k, v
	for k, v in pairs( haystack ) do
		if k == needle then
			return true
		end
	end -- for k, v
	return false
end -- fed()



local function fetch( light, options )
	-- Return regular table with all parameters
	-- Precondition:
	--	 light	-- true: template transclusion;  false: #invoke
	--	 options  -- table; optional details
	--				 options.low
	-- Postcondition:
	--	 Return table; whitespace-only values as false
	-- Uses:
	--	 TemplatePar.downcase()
	--	 mw.getCurrentFrame()
	--	 frame:getParent()
	local g, k, v
	local r = { }
	if options.low then
		g = TemplatePar.downcase( options )
	else
		g = mw.getCurrentFrame()
		if light then
			g = g:getParent()
		end
		g = g.args
	end
	if type( g ) == "table"  then
		r = { }
		for k, v in pairs( g ) do
			if type( v ) == "string" then
				if v:match( "^%s*$" ) then
					v = false
				end
			else
				v = false
			end
			if type( k ) == "number" then
				k = tostring( k )
			end
			r[ k ] = v
		end -- for k, v
	else
		r = g
	end
	return r
end -- fetch()



local function figure( append, options )
	-- Extend options by rule from #invoke strings
	-- Precondition:
	--	 append   -- string or nil; requested rule
	--	 options  --  table; details
	--				  ++ .key
	--				  ++ .pattern
	-- Postcondition:
	--	 Return sequence table
	local r = options
	if type( append ) == "string" then
		local story = mw.text.trim( append )
		local sub   = story:match( "^/(.*%S)/$" )
		if type( sub ) == "string" then
			sub			 = sub:gsub( "%%!", "|" )
			sub			 = sub:gsub( "%%%(%(", "{{" )
			sub			 = sub:gsub( "%%%)%)", "}}" )
			options.pattern = sub
			options.key	 = nil
		else
			options.key	 = story
			options.pattern = nil
		end
	end
	return r
end -- figure()



local function fill( specified )
	-- Split requirement string separated by '='
	-- Precondition:
	--	 specified  -- string or nil; requested parameter set
	-- Postcondition:
	--	 Return sequence table
	-- Uses:
	--	 mw.text.split()
	local r
	if specified then
		local i, s
		r = mw.text.split( specified, "%s*=%s*" )
		for i = #r, 1, -1 do
			s = r[ i ]
			if #s == 0 then
				table.remove( r, i )
			end
		end -- for i, -1
	else
		r = { }
	end
	return r
end -- fill()



local function finalize( submit, options, frame )
	-- Finalize message
	-- Precondition:
	--	 submit   -- string or false or nil; non-empty error message
	--	 options  -- table or nil; optional details
	--				 options.format
	--				 options.preview
	--				 options.cat
	--				 options.template
	--	 frame	-- object, or false
	-- Postcondition:
	--	 Return string or false
	-- Uses:
	--	 factory()
	local r = false
	if submit then
		local opt, s
		local lazy = false
		local show = false
		if type( options ) == "table" then
			opt  = options
			show = opt.format
			lazy = ( show == ""  or  show == "0"  or  show == "-" )
			s	= opt.preview
			if type( s ) == "string"  and
				s ~= ""  and  s ~= "0"  and  s ~= "-" then
				if lazy then
					show = ""
					lazy = false
				end
				frame = frame or mw.getCurrentFrame()
				if frame:preprocess( "{{REVISIONID}}" ) == "" then
					if s == "1" then
						show = "*"
					else
						show = s
					end
				end
			end
		else
			opt = { }
		end
		if lazy then
			if not opt.cat then
				r = string.format( "%s %s",
								   submit,  factory( "noMSGnoCAT" ) )
			end
		else
			r = submit
		end
		if r  and  not lazy then
			local i
			if not show  or  show == "*" then
				show = "<span class=\"error\">@@@</span>"
			end
			i = show:find( "@@@", 1, true )
			if i then
				-- No gsub() since r might contain "%3" (e.g. URL)
				r = string.format( "%s%s%s",
								   show:sub( 1,  i - 1 ),
								   r,
								   show:sub( i + 3 ) )
			else
				r = show
			end
		end
		s = opt.cat
		if type( s ) == "string" then
			if opt.errNS then
				local ns = mw.title.getCurrentTitle().namespace
				local st = type( opt.errNS )
				if st == "string" then
					local space  = string.format( ".*%%s%d%%s.*", ns )
					local spaces = string.format( " %s ", opt.errNS )
					if spaces:match( space ) then
						opt.errNS = false
					end
				elseif st == "table" then
					for i = 1, #opt.errNS do
						if opt.errNS[ i ] == ns then
							opt.errNS = false
							break	-- for i
						end
					end -- for i
				end
			end
			if opt.errNS then
				r = ""
			else
				r = r or ""
				if s:find( "@@@" ) then
					if type( opt.template ) == "string" then
						s = s:gsub( "@@@", opt.template )
					end
				end
				local i
				local cats = mw.text.split( s, "%s*#%s*" )
				for i = 1, #cats do
					s = mw.text.trim( cats[ i ] )
					if #s > 0 then
						r = string.format( "%s[[Category:%s]]", r, s )
					end
				end -- for i
			end
		end
	end
	return r
end -- finalize()



local function finder( haystack, needle )
	-- Find needle in haystack sequence
	-- Precondition:
	--	 haystack  -- table; sequence of key names, downcased if low
	--	 needle	-- any; key name
	-- Postcondition:
	--	 Return true iff found
	local i
	for i = 1, #haystack do
		if haystack[ i ] == needle then
			return true
		end
	end -- for i
	return false
end -- finder()



local function fix( valid, duty, got, options )
	-- Perform parameter analysis
	-- Precondition:
	--	 valid	-- table; unique sequence of known parameters
	--	 duty	 -- table; sequence of mandatory parameters
	--	 got	  -- table; sequence of current parameters
	--	 options  -- table or nil; optional details
	-- Postcondition:
	--	 Return string as configured; empty if valid
	-- Uses:
	--	 finder()
	--	 fault()
	--	 failure()
	--	 fed()
	local k, v
	local r = false
	for k, v in pairs( got ) do
		if not finder( valid, k ) then
			r = fault( r, k )
		end
	end -- for k, v
	if r then
		r = failure( "unknown",
					 string.format( "'%s'", r ),
					 options )
	else -- all names valid
		local i, s
		for i = 1, #duty do
			s = duty[ i ]
			if not fed( got, s ) then
				r = fault( r, s )
			end
		end -- for i
		if r then
			r = failure( "undefined", r, options )
		else -- all mandatory present
			for i = 1, #duty do
				s = duty[ i ]
				if not got[ s ] then
					r = fault( r, s )
				end
			end -- for i
			if r then
				r = failure( "empty", r, options )
			end
		end
	end
	return r
end -- fix()



local function flat( collection, options )
	-- Return all table elements with downcased string
	-- Precondition:
	--	 collection  -- table; k=v pairs
	--	 options	 -- table or nil; optional messaging details
	-- Postcondition:
	--	 Return table, may be empty; or string with error message.
	-- Uses:
	--	 mw.ustring.lower()
	--	 fault()
	--	 failure()
	local k, v
	local r = { }
	local e = false
	for k, v in pairs( collection ) do
		if type ( k ) == "string" then
			k = mw.ustring.lower( k )
			if r[ k ] then
				e = fault( e, k )
			end
		end
		r[ k ] = v
	end -- for k, v
	if e then
		r = failure( "multiSpell", e, options )
	end
	return r
end -- flat()



local function fold( options )
	-- Merge two tables, create new sequence if both not empty
	-- Precondition:
	--	 options  -- table; details
	--				 options.mandatory   sequence to keep unchanged
	--				 options.optional	sequence to be appended
	--				 options.low		 downcased expected
	-- Postcondition:
	--	 Return merged table, or message string if error
	-- Uses:
	--	 finder()
	--	 fault()
	--	 failure()
	--	 flat()
	local i, e, r, s
	local base   = options.mandatory
	local extend = options.optional
	if #base == 0 then
		if #extend == 0 then
			r = { }
		else
			r = extend
		end
	else
		if #extend == 0 then
			r = base
		else
			e = false
			for i = 1, #extend do
				s = extend[ i ]
				if finder( base, s ) then
					e = fault( e, s )
				end
			end -- for i
			if e then
				r = failure( "dupOpt", e, options )
			else
				r = { }
				for i = 1, #base do
					table.insert( r, base[ i ] )
				end -- for i
				for i = 1, #extend do
					table.insert( r, extend[ i ] )
				end -- for i
			end
		end
	end
	if options.low  and  type( r ) == "table" then
		r = flat( r, options )
	end
	return r
end -- fold()



local function form( light, options, frame )
	-- Run parameter analysis on current environment
	-- Precondition:
	--	 light	-- true: template transclusion;  false: #invoke
	--	 options  -- table or nil; optional details
	--				 options.mandatory
	--				 options.optional
	--	 frame	-- object, or false
	-- Postcondition:
	--	 Return string with error message as configured;
	--			false if valid
	-- Uses:
	--	 fold()
	--	 fetch()
	--	 fix()
	--	 finalize()
	local duty, r
	if type( options ) == "table" then
		if type( options.mandatory ) ~= "table" then
			options.mandatory = { }
		end
		duty = options.mandatory
		if type( options.optional ) ~= "table" then
			options.optional = { }
		end
		r = fold( options )
	else
		options = { }
		duty	= { }
		r	   = { }
	end
	if type( r ) == "table" then
		local got = fetch( light, options )
		if type( got ) == "table" then
			r = fix( r, duty, got, options )
		else
			r = got
		end
	end
	return finalize( r, options, frame )
end -- form()



local function format( analyze, options )
	-- Check validity of a value
	-- Precondition:
	--	 analyze  -- string to be analyzed
	--	 options  -- table or nil; optional details
	--				 options.say
	--				 options.min
	--				 options.max
	-- Postcondition:
	--	 Return string with error message as configured;
	--			false if valid or no answer permitted
	-- Uses:
	--	 feasible()
	--	 failure()
	local r = feasible( analyze, options, false )
	local show
	if options.min  and  not r then
		if type( options.min ) == "number" then
			if type( options.max ) == "number" then
				if options.max < options.min then
					r = failure( "minmax",
								 string.format( "%d > %d",
												options.min,
												options.max ),
								 options )
				end
			end
			if #analyze < options.min  and  not r then
				show = " <" .. options.min
				if options.say then
					show = string.format( "%s '%s'", show, options.say )
				end
				r = failure( "tooShort", show, options )
			end
		else
			r = failure( "invalidPar", "min", options )
		end
	end
	if options.max  and  not r then
		if type( options.max ) == "number" then
			if #analyze > options.max then
				show = " >" .. options.max
				if options.say then
					show = string.format( "%s '%s'", show, options.say )
				end
				r = failure( "tooLong", show, options )
			end
		else
			r = failure( "invalidPar", "max", options )
		end
	end
	return r
end -- format()



local function formatted( assignment, access, options )
	-- Check validity of one particular parameter in a collection
	-- Precondition:
	--	 assignment  -- collection
	--	 access	  -- id of parameter in collection
	--	 options	 -- table or nil; optional details
	-- Postcondition:
	--	 Return string with error message as configured;
	--			false if valid or no answer permitted
	-- Uses:
	--	 mw.text.trim() 
	--	 format()
	--	 failure()
	local r = false
	if type( assignment ) == "table" then
		local story = assignment.args[ access ] or ""
		if type( access ) == "number" then
			story = mw.text.trim( story ) 
		end
		if type( options ) ~= "table" then
			options = { }
		end
		options.say = access
		r = format( story, options )
	end
	return r
end -- formatted()



local function furnish( frame, action )
	-- Prepare #invoke evaluation of .assert() or .valid()
	-- Precondition:
	--	 frame	-- object; #invoke environment
	--	 action   -- "assert" or "valid"
	-- Postcondition:
	--	 Return string with error message or ""
	-- Uses:
	--	 form()
	--	 failure()
	--	 finalize()
	--	 TemplatePar.valid()
	--	 TemplatePar.assert()
	local options = { mandatory = { "1" },
					  optional  = { "2",
									"cat",
									"errNS",
									"low",
									"max",
									"min",
									"format",
									"preview",
									"template" },
					  template  = string.format( "&#35;invoke:%s|%s|",
												 "TemplatePar",
												 action )
					}
	local r	   = form( false, options, frame )
	if not r then
		local s
		options = { cat	  = frame.args.cat,
					errNS	= frame.args.errNS,
					low	  = frame.args.low,
					format   = frame.args.format,
					preview  = frame.args.preview,
					template = frame.args.template
				  }
		options = figure( frame.args[ 2 ], options )
		if type( frame.args.min ) == "string" then
			s = frame.args.min:match( "^%s*([0-9]+)%s*$" )
			if s then
				options.min = tonumber( s )
			else
				r = failure( "invalidPar",
							 "min=" .. frame.args.min,
							 options )
			end
		end
		if type( frame.args.max ) == "string" then
			s = frame.args.max:match( "^%s*([1-9][0-9]*)%s*$" )
			if s then
				options.max = tonumber( s )
			else
				r = failure( "invalidPar",
							 "max=" .. frame.args.max,
							 options )
			end
		end
		if r then
			r = finalize( r, options, frame )
		else
			s = frame.args[ 1 ] or ""
			r = tonumber( s )
			if ( r ) then
				s = r
			end
			if action == "valid" then
				r = TemplatePar.valid( s, options, frame )
			elseif action == "assert" then
				r = TemplatePar.assert( s, "", options )
			end
		end
	end
	return r or ""
end -- furnish()



TemplatePar.assert = function ( analyze, append, options )
	-- Perform parameter analysis on a single string
	-- Precondition:
	--	 analyze  -- string to be analyzed
	--	 append   -- string: append error message, prepending <br />
	--				 false or nil: throw error with message
	--	 options  -- table; optional details
	-- Postcondition:
	--	 Return string with error message as configured;
	--			false if valid
	-- Uses:
	--	 format()
	local r = format( analyze, options )
	if ( r ) then
		if ( type( append ) == "string" ) then
			if ( append ~= "" ) then
				r = string.format( "%s<br />%s", append, r )
			end
		else
			error( r, 0 )
		end
	end
	return r
end -- TemplatePar.assert()



TemplatePar.check = function ( options )
	-- Run parameter analysis on current template environment
	-- Precondition:
	--	 options  -- table or nil; optional details
	--				 options.mandatory
	--				 options.optional
	-- Postcondition:
	--	 Return string with error message as configured;
	--			false if valid
	-- Uses:
	--	 form()
	return form( true, options, false )
end -- TemplatePar.check()



TemplatePar.count = function ()
	-- Return number of template parameters
	-- Postcondition:
	--	 Return number, starting at 0
	-- Uses:
	--	 mw.getCurrentFrame()
	--	 frame:getParent()
	local k, v
	local r = 0
	local t = mw.getCurrentFrame():getParent()
	local o = t.args
	for k, v in pairs( o ) do
		r = r + 1
	end -- for k, v
	return r
end -- TemplatePar.count()



TemplatePar.countNotEmpty = function ()
	-- Return number of template parameters with more than whitespace
	-- Postcondition:
	--	 Return number, starting at 0
	-- Uses:
	--	 mw.getCurrentFrame()
	--	 frame:getParent()
	local k, v
	local r = 0
	local t = mw.getCurrentFrame():getParent()
	local o = t.args
	for k, v in pairs( o ) do
		if not v:match( "^%s*$" ) then
			r = r + 1
		end
	end -- for k, v
	return r
end -- TemplatePar.countNotEmpty()



TemplatePar.downcase = function ( options )
	-- Return all template parameters with downcased name
	-- Precondition:
	--	 options  -- table or nil; optional messaging details
	-- Postcondition:
	--	 Return table, may be empty; or string with error message.
	-- Uses:
	--	 mw.getCurrentFrame()
	--	 frame:getParent()
	--	 flat()
	local t = mw.getCurrentFrame():getParent()
	return flat( t.args, options )
end -- TemplatePar.downcase()



TemplatePar.valid = function ( access, options, frame )
	-- Check validity of one particular template parameter
	-- Precondition:
	--	 access   -- id of parameter in template transclusion
	--				 string or number
	--	 options  -- table or nil; optional details
	--	 frame	-- object; #invoke environment
	-- Postcondition:
	--	 Return string with error message as configured;
	--			false if valid or no answer permitted
	-- Uses:
	--	 mw.text.trim()
	--	 TemplatePar.downcase()
	--	 frame:getParent()
	--	 formatted()
	--	 failure()
	--	 finalize()
	local r = type( access )
	if r == "string" then
		r = mw.text.trim( access )
		if #r == 0 then
			r = false
		end
	elseif r == "number" then
		r = access
	else
		r = false
	end
	if r then
		local params
		if type( options ) ~= "table" then
			options = { }
		end
		if options.low then
			params = TemplatePar.downcase( options )
		else
			params = frame:getParent()
		end
		r = formatted( params, access, options )
	else
		r = failure( "noname", false, options )
	end
	return finalize( r, options, frame )
end -- TemplatePar.valid()



TemplatePar.verify = function ( options )
	-- Perform #invoke parameter analysis
	-- Precondition:
	--	 options  -- table or nil; optional details
	-- Postcondition:
	--	 Return string with error message as configured;
	--			false if valid
	-- Uses:
	--	 form()
	return form( false, options, false )
end -- TemplatePar.verify()



-- Provide external access
local p = {}



function p.assert( frame )
	-- Perform parameter analysis on some single string
	-- Precondition:
	--	 frame  -- object; #invoke environment
	-- Postcondition:
	--	 Return string with error message or ""
	-- Uses:
	--	 furnish()
	return furnish( frame, "assert" )
end -- .assert()



function p.check( frame )
	-- Check validity of template parameters
	-- Precondition:
	--	 frame  -- object; #invoke environment
	-- Postcondition:
	--	 Return string with error message or ""
	-- Uses:
	--	 form()
	--	 fill()
	local options = { optional  = { "all",
									"opt",
									"cat",
									"errNS",
									"low",
									"format",
									"preview",
									"template" },
					  template  = "&#35;invoke:TemplatePar|check|"
					}
	local r = form( false, options, frame )
	if not r then
		options = { mandatory = fill( frame.args.all ),
					optional  = fill( frame.args.opt ),
					cat	   = frame.args.cat,
					errNS	 = frame.args.errNS,
					low	   = frame.args.low,
					format	= frame.args.format,
					preview   = frame.args.preview,
					template  = frame.args.template
				  }
		r	   = form( true, options, frame )
	end
	return r or ""
end -- .check()



function p.count( frame )
	-- Count number of template parameters
	-- Postcondition:
	--	 Return string with digits including "0"
	-- Uses:
	--	 TemplatePar.count()
	return tostring( TemplatePar.count() )
end -- .count()



function p.countNotEmpty( frame )
	-- Count number of template parameters which are not empty
	-- Postcondition:
	--	 Return string with digits including "0"
	-- Uses:
	--	 TemplatePar.countNotEmpty()
	return tostring( TemplatePar.countNotEmpty() )
end -- .countNotEmpty()



function p.match( frame )
	-- Combined analysis of parameters and their values
	-- Postcondition:
	--	 Return string with error message or ""
	-- Uses:
	--	 mw.text.trim()
	--	 mw.ustring.lower()
	--	 failure()
	--	 form()
	--	 TemplatePar.downcase()
	--	 figure()
	--	 feasible()
	--	 fault()
	--	 finalize()
	local r = false
	local options = { cat	  = frame.args.cat,
					  errNS	= frame.args.errNS,
					  low	  = frame.args.low,
					  format   = frame.args.format,
					  preview  = frame.args.preview,
					  template = frame.args.template
					}
	local k, v, s
	local params = { }
	for k, v in pairs( frame.args ) do
		if type( k ) == "number" then
			s, v = v:match( "^ *([^=]+) *= *(%S.*%S*) *$" )
			if s then
				s = mw.text.trim( s )
				if s == "" then
					s = false
				end
			end
			if s then
				if options.low then
					s = mw.ustring.lower( s )
				end
				if params[ s ] then
					s = params[ s ]
					s[ #s + 1 ] = v
				else
					params[ s ] = { v }
				end
			else
				r = failure( "invalidPar",  tostring( k ),  options )
				break -- for k, v
			end
		end
	end -- for k, v
	if not r then
		s = { }
		for k, v in pairs( params ) do
			s[ #s + 1 ] = k
		end -- for k, v
		options.optional = s
		r = form( true, options, frame )
	end
	if not r then
		local errMiss, errValues, lack, rule
		local targs = frame:getParent().args
		options.optional = nil
		if options.low then
			targs = TemplatePar.downcase()
		else
			targs = frame:getParent().args
		end
		errMiss   = false
		errValues = false
		for k, v in pairs( params ) do
			options.say = k
			errValue	= false
			s = targs[ k ]
			if s then
				if s == "" then
					lack = true
				else
					lack = false
				end
			else
				s	= ""
				lack = true
			end
			for r, rule in pairs( v ) do
				options = figure( rule, options )
				r	   = feasible( s, options, true )
				if r then
					if lack then
						if errMiss then
							errMiss = string.format( "%s, '%s'",
													 errMiss, k )
						else
							errMiss = string.format( "'%s'", k )
						end
					elseif not errMiss then
						errValues = fault( errValues, r )
					end
					break -- for r, rule
				end
			end -- for s, rule
		end -- for k, v
		r = ( errMiss or errValues )
		if r then
			if errMiss then
				r = failure( "undefined", errMiss, options )
			else
				r = failure( "invalid", errValues, options )
			end
			r = finalize( r, options, frame )
		end
	end
	return r or ""
end -- .match()



function p.valid( frame )
	-- Check validity of one particular template parameter
	-- Precondition:
	--	 frame  -- object; #invoke environment
	-- Postcondition:
	--	 Return string with error message or ""
	-- Uses:
	--	 furnish()
	return furnish( frame, "valid" )
end -- .valid()



function p.TemplatePar()
	-- Retrieve function access for modules
	-- Postcondition:
	--	 Return table with functions
	return TemplatePar
end -- .TemplatePar()



return p