--[[----------------------------------------------------------------------------

WFLogger.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2007 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

------------------------------------------------------------------------------]]

	-- RxLua
local Rx = require 'Rx'

	-- WF.core.logging
local WFFileLogger = require 'WFFileLogger'
local WFLoggerImp = require 'WFLoggerImp'

	-- WF.core.table
local WFTableArchiving = require 'WFTableArchiving'

	-- WF.core.utils
local KSStringUtils = require "KSStringUtils"
local WFArguments = require 'WFArguments'
local WFConfiguration = require 'WFConfiguration'
local WFFunctionUtils = require "WFFunctionUtils"
local WFRingBuffer = require "WFRingBuffer"
local WFCapabilities = require 'WFCapabilities'

--------------------------------------------------------------------------------

local WFFunctionUtils_reduce = assert( WFFunctionUtils.reduce,
			"WFFunctionUtils.map does not exist" )

--------------------------------------------------------------------------------

local check = assert( WFArguments.check )

local string_format = string.format

local WFRingBuffer_make = assert( WFRingBuffer.make )

local WFRingBuffer_addItem = assert( WFRingBuffer.addItem )

local WFRingBuffer_map = assert( WFRingBuffer.map )

local WFRingBuffer_full = assert( WFRingBuffer.full )

local WFLogger_externalLogModule

local externalLogModule = WFCapabilities.getCapability( "WFLogger.externalLogModule" )

if externalLogModule then
	
	WFLogger_externalLogModule = require( externalLogModule )
	
	assert( WFLogger_externalLogModule.logMessage, 
			"WFLogger.externalLogModule needs to implement the logMessage method" )
end

--============================================================================--

-- @mark WFLogger class

--------------------------------------------------------------------------------
--[[!
	\class WFLogger
	
	\brief This class provides a mechanism for writing debug output that can be
		viewed with an external log viewer application.

	Use the imported namespace as a constructor; access the functions through
	the created objects. The constructor creates a new logger with a given name,
	or finds and returns an existing one.  If no name is provided, it returns
	an application-wide "default" <code>WFLogger</code> object.
	
	Loggers are silent until configured with <code>WFLogger:enable()</code>.
	
	Common applications for viewing debug output are:
	
	<ul>
		<li>WinDBG (available for download from <a href="http://www.microsoft.com/whdc/DevTools/Debugging/default.mspx">Microsoft</a>)</li>
		<li>Microsoft Developer Studio</li>
		<li>Console (built-in application on Mac OS: look in /Applications/Utilities)</li>
		<li>Xcode</li>
	</ul>

	Configure loggers using the config.lua file as follows:

	<ul>
		<li>Create an entry for each logger of interest by specifying loggers.<i>loggerName</i> = {...}</li>
		<li>Arguments in the table include <code>logLevel</code> and a string "fatal", "error",
		"warn", "info", "debug", or "trace".</li>
		<li>For each valid log level, you can add an entry to modify the behavior of log messages for that level.
		For example, to log to a file, add the entry <code>trace = 'logfile',</code></li>
		<li>To specify an action for all log levels, add the entry <code>action = 'logfile',</code></li>
	</ul>

	Sample code:

	<code>
	log = require 'WFLogger' 'myPlugin'
	
	log:enable 'print'		-- this can also be configured in config.lua with:  logger.myPlugin = "print"
	
	log:warn 'foo - warning'
	</code>
]]--

--------------------------------------------------------------------------------

local loggerMethods = {

	_name = false,
		-- (string) name of logger (or nil for default)
	
	_actions = false,
		-- (table) what to do on each action (nil or function)
			-- key: (string) name of log type (trace, warn, info, error, or fatal)
			-- value: (string) what to do for this action (console or nil)
	
	_priority = false,
		-- (number) indicating what log level we are at, as defined in
			-- kLogLevelToPriority below
	
	_logFilePath = false,
		-- (string) If specified, it's the location of where log files will be
			-- written to disk.
	
	_savedConfigurations = false,
		-- saved actions table and priority values for making temporary changes
		-- to the logger behavior

	_userConfigured = false,
		-- If true, the logger was configured via the config.lua file or using
		-- WFConfiguration

}

--------------------------------------------------------------------------------

function loggerMethods:__init( name, optLogFilePath )

	check( 'WFLogger', '+string', '?+string' )
	
	self._name = name

	self._logFilePath = optLogFilePath

	self._savedConfigurations = {}

	self:disable()

end

--------------------------------------------------------------------------------
--[[!
	\brief Disables all log output from this logger.

 	Equivalent to <code>logger:enable( false )</code>.
]]--

function loggerMethods:disable()

	check( 'WFLogger' )

	self._actions = {}
	return self

end

--------------------------------------------------------------------------------

local threadName

--------------------------------------------------------------------------------

local function paramStrings( ... )

	local params = { ... }

	for i = 1, select( '#', ... ) do

		local p = params[ i ]

		if p == ':traceback:' then
		
			p = debug.traceback( '', 4 )
		
		elseif p == ':thread:' then
		
			if not threadName then
				threadName = require 'WFCurrentThread'.DEBUG_getThreadName()
			end
			
			p = threadName
		
		elseif type( p ) == 'table' then

			local success, pp = pcall( WFTableArchiving.debugTableToString, p )

			if success then

				p = pp

			else
			
				success, pp = pcall( tostring, p )
				if success then
					p = pp
				else
					p = 'ERROR: ' .. pp
						-- If tostring fails, the error message might be interesting.
				end
				
			end

		else
		
			local success, pp = pcall( tostring, p )
			if success then
				p = pp
			else
				p = 'ERROR: ' .. pp
					-- If tostring fails, the error message might be interesting.
			end

		end

		params[ i ] = tostring( p )

	end
	
	params.n = select( '#', ... )
	
	return params

end

--------------------------------------------------------------------------------

local function paramMessageString( ... )

	local params = { ... }
	for i = 1, select( '#', ... ) do
		local p = params[ i ]
		p = tostring( p )
		params[ i ] = p
	end

	return table.concat( params, ' \t ' )

end

--------------------------------------------------------------------------------

local function paramListToString( actionType, ... )

	local message = paramMessageString( ... )
	return string_format( '%s\t%s', string.upper( actionType ), message )

end

--------------------------------------------------------------------------------

local function loggerParamListToString( self, actionType, ... )

	local message = paramMessageString( ... )
	return string_format( '%s\t%s\t%s', self._name, string.upper( actionType ), message )

end

--------------------------------------------------------------------------------

local function makeSingleStringOutputFunc( outputFunc )

	return function( self, actionType, ... )
	
		outputFunc( loggerParamListToString( self, actionType, ... ) )
	
	end

end

--------------------------------------------------------------------------------

local function alwaysTracebackUnformatted( str )

	WFLoggerImp.alwaysLogTraceback( '%s', str )

end

--------------------------------------------------------------------------------
-- Log function lookup table.

local tracebacksDisplayedOnce = {}

local function tracebackOnce( self, actionType, ... )
	
	local tb = debug.traceback()
	
	if not tracebacksDisplayedOnce[ tb ] then
		alwaysTracebackUnformatted( loggerParamListToString( self, actionType, ... ) )
		tracebacksDisplayedOnce[ tb ] = true
	end

end

--------------------------------------------------------------------------------

local logFunctions = {

	print = makeSingleStringOutputFunc( WFLoggerImp.alwaysPrint ),
	
	traceback = makeSingleStringOutputFunc( alwaysTracebackUnformatted ),
	
	traceback_once = tracebackOnce,
	
	logfile = function( self, actionType, ... )
		WFFileLogger.writeLine( self._name, paramListToString( actionType, ... ), true, self._logFilePath or "" )
	end,
	
	testRunner = function( self, actionType, ... )
		WFLoggerImp.testRunnerLog( paramMessageString( ... ) )
	end,

}

--------------------------------------------------------------------------------

local function checkOptionalLogAction( action )

	if type( action ) == 'function' then
		return makeSingleStringOutputFunc( action )
	else
		return logFunctions[ action ]
	end

end

--------------------------------------------------------------------------------

local function checkLogAction( action )

	if not action then return end

	return assert( checkOptionalLogAction( action ),
			string_format( "WFLogger:enable: unknown log behavior %s", tostring( action ) ) )

end

--------------------------------------------------------------------------------
--[[!
	\brief Enables specific log output from this logger.
	
	This is usually accomplished through the config.lua file instead of this function.

	\param actions (string, function, or table) What action to take for each
		log action type. For each of the action types, this must be either a function
		that takes a string (the log message), or one of the following strings:

		<ul>
			<li><code>print</code>: Print to console log</li>
			<li><code>traceback</code>: Print to console log with stack traceback</li>
			<li><code>logfile</code>: Print to a log file named for the logger.
				This log file is written to your <code>~/Documents</code> folder in Mac OS,
				or to your <code>My Documents</code> folder in Windows</li>.
		</ul>

	As a shortcut, you can pass a single string or function, which is applied to all
	of the log output from this logger.
]]--

function loggerMethods:enable( --[[string/function/table]] actions )

	check( 'WFLogger', 'string|function|table' )

	if self._userConfigured then

		-- if self._userConfigured is set to true, then the logger should
		-- not be overridden by logger:enable( ... ).

		return self

	end

	local actionsType = type( actions )

	if actionsType == 'string' or actionsType == 'function' then

		actions = {
			trace = actions,
			debug = actions,
			info = actions,
			warn = actions,
			error = actions,
			fatal = actions,
		}

	end
	
	actions.trace = checkLogAction( actions.trace )
	actions.debug = checkLogAction( actions.debug )
	actions.info = checkLogAction( actions.info )
	actions.warn = checkLogAction( actions.warn )
	actions.error = checkLogAction( actions.error )
	actions.fatal = checkLogAction( actions.fatal )
	
	-- testrun log is only used in test infra logging
	actions.testrun = checkLogAction( 'testRunner' )
	
	self._actions = actions

	return self

end

--------------------------------------------------------------------------------

local kLogLevelToPriority = {
	testrun = 7,
	trace = 6,
	debug = 5,
	info = 4,
	warn = 3,
	error = 2,
	fatal = 1
}

local kPriorityToLogLevel = {
	'fatal',
	'error',
	'warn',
	'info',
	'debug',
	'trace',
	'testrun'
}

local kDefaultAction = 'print'
local kDefaultLogLevel = 'error'

--------------------------------------------------------------------------------

function loggerMethods:configure( logConfiguration )
	
	check( 'WFLogger', 'table' )
	
	local name = self._name
	local logLevel = logConfiguration.logLevel or kDefaultLogLevel
	local priority = kLogLevelToPriority[ logLevel ]
	local defaultAction = logConfiguration.action or kDefaultAction
	local optFilePath = logConfiguration.filePath 
	
	if not priority then
		debug.print( "Invalid log configuration for", name )
		return
	end
	
	local actionTable = {}
		
	for i = 1, priority do

		local levelName = kPriorityToLogLevel[ i ]
		local action = logConfiguration[ levelName ] or defaultAction
		
		if not checkOptionalLogAction( action ) then
			debug.print( "Invalid log action", action, "defined for", name )
			return
		end
		
		actionTable[ levelName ] = action

	end
	
	self._logFilePath = optFilePath or false
	
	self._priority = priority
	self:enable( actionTable )

end

--------------------------------------------------------------------------------

function loggerMethods:saveConfiguration()

	check( 'WFLogger' )

	table.insert( self._savedConfigurations,
		{
			priority = self._priority,
			actions = self._actions
		} )
	
end

--------------------------------------------------------------------------------

function loggerMethods:restoreConfiguration()

	check( 'WFLogger' )

	local config = table.remove( self._savedConfigurations )
	
	if not config then
		assert( false, "unbalanced call to restoreConfiguration" )
	end
	
	self._priority = config.priority
	self._actions = config.actions
	
end

--------------------------------------------------------------------------------


function loggerMethods:setLevel( level )

	WFArguments.check( 'WFLogger', '+string' )

	local wfLevel = string.lower( level )
	
	self:configure { logLevel = wfLevel, action = 'print' }
	
	return self

end

--------------------------------------------------------------------------------

function loggerMethods:isEnabled( level )

	WFArguments.check( 'WFLogger', 'string' )

	local wfLevel = string.lower( level )

	local priority = kLogLevelToPriority[ wfLevel ]

	if not priority then
		return false
	end

	return self._priority >= priority

end

--------------------------------------------------------------------------------

local function pformat( fmt, ... )

	-- A protected version of string.format
	
	local pstrs = paramStrings( ... )
	
	local success, text = pcall( string_format, fmt, unpack( pstrs, 1, pstrs.n ) )
	
	if success then
		return text
	else
		debug.print( "Text formatting failed:", text, ... )
	end
	
end

--------------------------------------------------------------------------------
--  begin diagnostic log support
--------------------------------------------------------------------------------

local _maxDiagnosticLogMessageCount = 50

--------------------------------------------------------------------------------

local _diagnosticLogMessages = {}

--------------------------------------------------------------------------------

local _diagnosticLogMessageCounts = {}

--------------------------------------------------------------------------------

local function wrapInTag( tag, str )

	return string.format( "<%s>%s</%s>", tag, str, tag )

end

--------------------------------------------------------------------------------

local function compose( f, g )

	return function( ... )

		return f( g( ... ) )

	end

end

--------------------------------------------------------------------------------

local function swap( a, b )

	return b, a

end

--------------------------------------------------------------------------------

local function makeDiagnosticLogErrorsHtmlTextForErrorLevel( allErrorText, 
																errorLevel )

	local maybeRingBuffer = _diagnosticLogMessages[ errorLevel ]

	if not maybeRingBuffer then

		return allErrorText
		
	end

	local errorLevelEscapedMessages =
			WFRingBuffer_map( KSStringUtils.escapeHtml, maybeRingBuffer )

	local errorLevelEscapedMessagesLi =
			WFFunctionUtils.map( 
				compose( wrapInTag, swap ), errorLevelEscapedMessages, "li" )

	local messageCounts = _diagnosticLogMessageCounts[ errorLevel ]

	local errorLevelDesc = not messageCounts and 
							errorLevel or
							string.format( "last %s %s (%s total)",
											math.min( 
												_maxDiagnosticLogMessageCount,
												messageCounts ),
											errorLevel, 
											messageCounts )

	return allErrorText ..
			wrapInTag( "h4", errorLevelDesc ) ..
				wrapInTag( "ul", table.concat( 
									errorLevelEscapedMessagesLi, "" ) )

end

--------------------------------------------------------------------------------

local function getFirstNNameForLogLevel( logLevel )

	return string.format( "first %s %s", 
							_maxDiagnosticLogMessageCount, logLevel )

end

--------------------------------------------------------------------------------

local function addFirstNNameForLogLevel( logLevelNames, logLevel )

	logLevelNames[ #logLevelNames + 1 ] = getFirstNNameForLogLevel( logLevel )

	logLevelNames[ #logLevelNames + 1 ] = logLevel

	return logLevelNames

end

--------------------------------------------------------------------------------

local _logLevelsForDiagnosticLog = { "error", "warn" }

--------------------------------------------------------------------------------

local _logLevelsToReport =
		WFFunctionUtils_reduce( {}, 
			addFirstNNameForLogLevel, _logLevelsForDiagnosticLog )

--------------------------------------------------------------------------------

local function maybeMakeDiagnosticLogSection()

	local body = 
		WFFunctionUtils_reduce( "",
			makeDiagnosticLogErrorsHtmlTextForErrorLevel,
				_logLevelsToReport )

	if body == "" then

		return

	end

	local ok, maybeThreadName = 
			pcall( 
				function()
					return require 'WFCurrentThread'.DEBUG_getThreadName()
				end )

	local thisThreadName = 
			ok and 
				maybeThreadName or 
				string.format( "Anonymous thread %s",
									require 'KSIdGenerator'.uuidInLowercaseHex() )

	require "WFDiagnosticLog".addSectionToLog {

		id = 'WFLoggerMessages' .. require "KSIdGenerator".uuidInLowercaseHex(),
		name = string.format( "Log Messages for %s", thisThreadName ),
		weight = -100,
		parentSection = 'Log Messages',
		body = body,

	}

end

--------------------------------------------------------------------------------

local function addToSet( set, k )

	set[ k ] = true

	return set

end

--------------------------------------------------------------------------------

local _shouldRecordWFLoggerMessageInDiagnosticLog =
		WFFunctionUtils_reduce( {}, addToSet, _logLevelsForDiagnosticLog )

--------------------------------------------------------------------------------

local KSDate_timeInIso8601Format

--------------------------------------------------------------------------------
-- WFLogger is loaded very early and can introduce circular dependencies if it
-- imports WFDiagnosticLog to early. We wait bootstrap diagnostic log support
-- until we actually log something we want to report in the diagnostic log.
--------------------------------------------------------------------------------

local _didBootstrapDiagnosticLog = false

--------------------------------------------------------------------------------

local function maybeRecordMessageInDiagnosticLog( logLevel, name, stringMakerFunc, ... )

	if not _shouldRecordWFLoggerMessageInDiagnosticLog[ logLevel ] then

		return

	end

	if type( stringMakerFunc ) ~= "function" then

		return

	end

	if not _didBootstrapDiagnosticLog then

		local ok, WFDiagnosticLog = pcall( require, "WFDiagnosticLog" )

		if ok then

			_didBootstrapDiagnosticLog = true

			WFDiagnosticLog.addLogSectionFunc( 
								maybeMakeDiagnosticLogSection )

		end

	end
	
	_diagnosticLogMessages[ logLevel ] = 
		_diagnosticLogMessages[ logLevel ] or 
		WFRingBuffer_make( _maxDiagnosticLogMessageCount )

	_diagnosticLogMessageCounts[ logLevel ] = 
		( _diagnosticLogMessageCounts[ logLevel ] or 0 ) + 1

	-- If we have a lot of errors, save off the first n errors we run into as the
	-- last n may be a repeated error that hides the initial problem.

	if WFRingBuffer_full( _diagnosticLogMessages[ logLevel ] ) then

		local logLevelFirst = getFirstNNameForLogLevel( logLevel )

		if _diagnosticLogMessages[ logLevelFirst ] == nil then

			_diagnosticLogMessages[ logLevelFirst ] = _diagnosticLogMessages[ logLevel ]

			_diagnosticLogMessages[ logLevel ] = WFRingBuffer_make( _maxDiagnosticLogMessageCount )

		end

	end

	local msg

	if select( "#", ... ) > 0 then

		local ok, msgOrErr = pcall( stringMakerFunc, ... )

		if ok then

			msg = msgOrErr

		end

	end

	msg = msg or string_format( "Bad logger.%s invocation on %s", logLevel, name )

	if WFLogger_externalLogModule then
		local msgFormat = select( 1, ... )
		WFLogger_externalLogModule.logMessage( logLevel, name, msg, msgFormat )
	end
	
	KSDate_timeInIso8601Format = KSDate_timeInIso8601Format or 
									require "KSDate".timeInIso8601Format

	local diagnosticLogMsg = string.format( "%s %s: %s", 
											KSDate_timeInIso8601Format(), name,
											msg )

	_diagnosticLogMessages[ logLevel ] = 
		WFRingBuffer_addItem( _diagnosticLogMessages[ logLevel ],
								diagnosticLogMsg )


end

--------------------------------------------------------------------------------
--  end of diagnostic log support
--------------------------------------------------------------------------------

local function addLoggerMethods( selector )

	loggerMethods[ selector ] = function( self, ... )

		check( 'WFLogger' )

		maybeRecordMessageInDiagnosticLog( selector, self._name, paramMessageString, ... )

		local actionFunc = self._actions[ selector ]
		if actionFunc then
			pcall( actionFunc, self, selector,
				   loggerParamListToString( self, selector, ... ) )
		end

	end
	
	loggerMethods[ selector .. 'f' ] = function( self, ... )

		check( 'WFLogger' )

		maybeRecordMessageInDiagnosticLog( selector, self._name, pformat, ... )

		local actionFunc = self._actions[ selector ]
		if actionFunc then
			local text = pformat( ... )
			if text then
				pcall( actionFunc, self, selector, text )
			end
		end

	end
	
	loggerMethods[ 'will_' .. selector ] = function( self )
	
		-- WARNING [scouten 2014-04-09]: This used to be an accessor when it was
		-- in Lightroom code. WF doesn't have any such concept, so this became
		-- a function instead.
	
		check( 'WFLogger' )
		
		return self._actions[ selector ] ~= nil

	end

end

addLoggerMethods( 'testrun' )

--------------------------------------------------------------------------------
--- Feeds log output through the action function defined for 'trace' messages.
	-- @name logger:trace
	-- @class function
	-- @param ... Data to be printed, of any type; <code>tostring()</code> is called on all values.

--- Feeds log output through the action function defined for 'trace' messages,
 -- using string.format to prepare the output.
	-- @name logger:tracef
	-- @class function
	-- @param format (string) formatting instructions (see Lua's string.format for details)
	-- @param ... Arguments to string.format required by format string

addLoggerMethods( 'trace' )

--------------------------------------------------------------------------------
--- Feeds log output through the action function defined for 'debug' messages.
	-- @name logger:debug
	-- @class function
	-- @param ... Data to be printed, of any type; <code>tostring()</code> is called on all values.

--- Feeds log output through the action function defined for 'debug' messages,
 -- using string.format to prepare the output.
	-- @name logger:debugf
	-- @class function
	-- @param format (string) formatting instructions (see Lua's string.format for details)
	-- @param ... Arguments to string.format required by format string

addLoggerMethods( 'debug' )

--------------------------------------------------------------------------------
--- Feeds log output through the action function defined for 'info' messages.
	-- @name logger:info
	-- @class function
	-- @param ... Data to be printed, of any type; <code>tostring()</code> is called on all values.

--- Feeds log output through the action function defined for 'info' messages,
 -- using string.format to prepare the output.
	-- @name logger:infof
	-- @class function
	-- @param format (string) formatting instructions (see Lua's string.format for details)
	-- @param ... Arguments to string.format required by format string

addLoggerMethods( 'info' )

--------------------------------------------------------------------------------
--- Feeds log output through the action function defined for 'warn' messages.
	-- @name logger:warn
	-- @class function
	-- @param ... Data to be printed, of any type; <code>tostring()</code> is called on all values.

--- Feeds log output through the action function defined for 'warn' messages,
 -- using string.format to prepare the output.
	-- @name logger:warnf
	-- @class function
	-- @param format (string) formatting instructions (see Lua's string.format for details)
	-- @param ... Arguments to string.format required by format string

addLoggerMethods( 'warn' )

--------------------------------------------------------------------------------
--- Feeds log output through the action function defined for 'error' messages.
	-- @name logger:error
	-- @class function
	-- @param ... Data to be printed, of any type; <code>tostring()</code> is called on all values.

--- Feeds log output through the action function defined for 'error' messages,
 -- using string.format to prepare the output.
	-- @name logger:errorf
	-- @class function
	-- @param format (string) formatting instructions (see Lua's string.format for details)
	-- @param ... Arguments to string.format required by format string

addLoggerMethods( 'error' )

--------------------------------------------------------------------------------
--- Feeds log output through the action function defined for 'fatal' messages.
	-- @name logger:fatal
	-- @class function
	-- @param ... Data to be printed, of any type; <code>tostring()</code> is called on all values.

--- Feeds log output through the action function defined for 'fatal' messages,
 -- using string.format to prepare the output.
	-- @name logger:fatalf
	-- @class function
	-- @param format (string) formatting instructions (see Lua's string.format for details)
	-- @param ... Arguments to string.format required by format string

addLoggerMethods( 'fatal' )

--------------------------------------------------------------------------------

local function nop() end

--------------------------------------------------------------------------------
--[[!
	\brief Creates optimized versions of specified log functions for use in
		tight loops.
	
	This version avoids the overhead of the method lookup, and is reduced
	to a no-op function when logging is disabled.

	For example:

	<pre>
		local warn, info = logger:quick( 'warn', 'info' )
		warn( 'something bad happened' )
	</pre>

	\param ... (one or more strings) The names of log functions to optimize.

	\return (one or more functions) The optimized functions.
]]--

function loggerMethods:quick( ... )

	check( 'WFLogger', '+string' )

	--[[
		INTERNAL USAGE NOTE:
		The functions returned from quick bypass the check for logger configuration, so they
		should not be retained indefinitely since changes to log level and action made via the
		configure method will be ignored.
	]]

	local results = {}
	local params = { ... }

	for i, name in ipairs( params ) do
	
		local action = self._actions[ name ]
		
		if action then
			results[ i ] = function( ... )
				pcall( action, self, name, ... )
			end
		else
			results[ i ] = nop
		end
		
	end
	
	return unpack( results )

end

--------------------------------------------------------------------------------
--[[!
	\brief Creates optimized versions of specified log functions for use in
		tight loops.

	This version avoids the overhead of the method lookup, and is reduced
	to a no-op function when logging is disabled. Unlike the functions returned
	by <code>logger:quick</code>, these functions take <code>string.format</code>
	instructions.

	For example:

	<pre>
		local warnf, infof = logger:quickf( 'warn', 'info' )
		warnf( 'something %s happened', 'bad' )
	</pre>

	\param ... (one or more strings) The names of log functions to optimize.

	\return (one or more functions) The optimized functions.
]]--

function loggerMethods:quickf( ... )

	check( 'WFLogger', '+string' )

	local results = {}
	local params = { ... }

	for i, name in ipairs( params ) do
	
		local action = self._actions[ name ]
		
		if action then
			results[ i ] = function( ... )
				local text = pformat( ... )
				if text then
					pcall( action, self, name, text )
				end
			end
		else
			results[ i ] = nop
		end
		
	end
	
	return unpack( results )

end

--------------------------------------------------------------------------------

function loggerMethods:__tostring()
	
	check( 'WFLogger' )
	
	return string_format( "WFLogger( %q )", self._name )
	
end

--------------------------------------------------------------------------------

loggerMethods.DEBUG_makeDiagnosticLogErrorsHtmlText = function()

	return WFFunctionUtils_reduce( "",
		makeDiagnosticLogErrorsHtmlTextForErrorLevel,
		_logLevelsToReport )

end

--------------------------------------------------------------------------------

loggerMethods.DEBUG_resetDiagnosticLogErrors = function()

	_diagnosticLogMessages = {}

end

--------------------------------------------------------------------------------

loggerMethods.DEBUG_UNIT_TEST_diagLogMessagesForLevel = function( _, logLevel )

	local diagLogMsgsRb = _diagnosticLogMessages[ logLevel ] 

	if not diagLogMsgsRb then

		return {}

	end

	return WFRingBuffer_map( function( msg ) return msg end, diagLogMsgsRb )

end

--------------------------------------------------------------------------------

local WFLoggerClass = Rx.Class 'WFLogger'( loggerMethods )

--------------------------------------------------------------------------------

local function globMatch( name, pattern )

	pattern = string.replace( pattern, "*", ".+" )
	pattern = string.replace( pattern, "?", ".?" )

	return string.find( name, pattern )

end

--------------------------------------------------------------------------------

local weakLoggers = setmetatable( {}, { __mode = 'kv' } )

--------------------------------------------------------------------------------

local function loggersByName( name, optLogFilePath )
	
	check( '+string' )
	
	if weakLoggers[ name ] then
		return weakLoggers[ name ]
	end
	
	local logger = WFLoggerClass( name, optLogFilePath )
	
	if name then

		local sortedLoggers = {}
		
		for loggerName in pairs( weakLoggers ) do
			table.insert( sortedLoggers, loggerName )
		end
		
		table.sort( sortedLoggers, function( name1, name2 )
			return #name1 > #name2
		end )

		-- Check for a wildcard configuration and copy its settings.

		for _, loggerName in ipairs( sortedLoggers ) do

			local matchedLogger = weakLoggers[ loggerName ]

			if matchedLogger
				and loggerName ~= name
				and globMatch( name, loggerName )
			then
				logger._priority = matchedLogger._priority
				logger._actions = matchedLogger._actions
				break
			end

		end

	end
	
	weakLoggers[ name ] = logger
	
	return logger
	
end

--------------------------------------------------------------------------------

local configuredLoggers = {}
rawset( _G, 'WFLogger.configuredLoggers', configuredLoggers )
	-- Ensure that this table doesn't get GC'd.

local function configureLogger( name, logConfiguration, isDefaultLogger )
	
	local logger = loggersByName( name )
	configuredLoggers[ name ] = logger
	logger:configure( logConfiguration )

	if not isDefaultLogger then
		logger._userConfigured = true
	end

end

--------------------------------------------------------------------------------

local function setupDefaultLogger()

	if weakLoggers[ '*' ] then
		return
	end
	
	configureLogger( '*', {
		logLevel = _G.DEBUG and 'warn' or 'error'
			-- [scouten 2014-04-09]: Was 'info' but there's too much traffic
			-- at that level in WF, so bumping up to 'warn'.
	}, true )

end

--------------------------------------------------------------------------------

for name, desc in pairs( WFConfiguration.loggers ) do

	if type( desc ) == 'string' then
		desc = { logLevel = desc }
	end

	configureLogger( name, desc )

end

setupDefaultLogger()

--------------------------------------------------------------------------------
 
local function WFLogger( name, optLogFilePath )

	check( '?string' )

	if name == 'reportMemoryUsage' then

		local count = 0
		for _ in pairs( weakLoggers ) do
			count = count + 1
		end

		WFLoggerImp.alwaysPrint( string.format( "WFLogger: %d active loggers", count ))

		return

	end
	
	local logger = weakLoggers[ name or "(default)" ]

	if not logger then
		logger = loggersByName( name or "(default)", optLogFilePath )
	end

	return logger

end

--============================================================================--

return WFLogger
