local I=true local X={}local Z={}local function d(I,X)local Z=("\t"):rep(X)return Z..I:gsub("\n","\n"..Z)end local function v(I,X)local Z={}for I,v in ipairs(I)do table.insert(Z,d(v,X))end return table.concat(Z,"\n")end local l={}function l.__tostring(I)local X={"\076\111\103\073\110\102\111 \123"}local Z=#I.errors local d=#I.warnings local l=#I.infos if(Z+d)+l==0 then table.insert(X,"\t\040\110\111 \109\101\115\115\097\103\101\115\041")end if Z>0 then table.insert(X,("\t\069\114\114\111\114\115 \040\037\100\041 \123"):format(Z))table.insert(X,v(I.errors,2))table.insert(X,"\t\125")end if d>0 then table.insert(X,("\t\087\097\114\110\105\110\103\115 \040\037\100\041 \123"):format(d))table.insert(X,v(I.warnings,2))table.insert(X,"\t\125")end if l>0 then table.insert(X,("\t\073\110\102\111\115 \040\037\100\041 \123"):format(l))table.insert(X,v(I.infos,2))table.insert(X,"\t\125")end table.insert(X,"\125")return table.concat(X,"\n")end local function j()local I={errors={};warnings={},infos={}}setmetatable(I,l)return I end local z={}function z.capture(Z)local d=j()local v=I I=false X[d]=true local l,z=pcall(Z)X[d]=nil I=v assert(l,z)return d end function z.warn(Z,...)local v=Z:format(...)for I in pairs(X)do table.insert(I.warnings,v)end local l=(debug.traceback("",2)):sub(2)local j=("\037\115\n\037\115"):format(v,d(l,1))if I then warn(j)end end function z.warnOnce(I,...)local X=debug.traceback()if Z[X]then return end Z[X]=true z.warn(I,...)end return z
--[[
	Centralized place to handle logging. Lets us:
	- Unit test log output via `Logging.capture`
	- Disable verbose log messages when not debugging Roact

	This should be broken out into a separate library with the addition of
	scoping and logging configuration.
]]

-- Determines whether log messages will go to stdout/stderr
local outputEnabled = true

-- A set of LogInfo objects that should have messages inserted into them.
-- This is a set so that nested calls to Logging.capture will behave.
local collectors = {}

-- A set of all stack traces that have called warnOnce.
local onceUsedLocations = {}

--[[
	Indent a potentially multi-line string with the given number of tabs, in
	addition to any indentation the string already has.
]]
local function indent(source, indentLevel)
	local indentString = ("\t"):rep(indentLevel)

	return indentString .. source:gsub("\n", "\n" .. indentString)
end

--[[
	Indents a list of strings and then concatenates them together with newlines
	into a single string.
]]
local function indentLines(lines, indentLevel)
	local outputBuffer = {}

	for _, line in ipairs(lines) do
		table.insert(outputBuffer, indent(line, indentLevel))
	end

	return table.concat(outputBuffer, "\n")
end

local logInfoMetatable = {}

--[[
	Automatic coercion to strings for LogInfo objects to enable debugging them
	more easily.
]]
function logInfoMetatable:__tostring()
	local outputBuffer = { "LogInfo {" }

	local errorCount = #self.errors
	local warningCount = #self.warnings
	local infosCount = #self.infos

	if errorCount + warningCount + infosCount == 0 then
		table.insert(outputBuffer, "\t(no messages)")
	end

	if errorCount > 0 then
		table.insert(outputBuffer, ("\tErrors (%d) {"):format(errorCount))
		table.insert(outputBuffer, indentLines(self.errors, 2))
		table.insert(outputBuffer, "\t}")
	end

	if warningCount > 0 then
		table.insert(outputBuffer, ("\tWarnings (%d) {"):format(warningCount))
		table.insert(outputBuffer, indentLines(self.warnings, 2))
		table.insert(outputBuffer, "\t}")
	end

	if infosCount > 0 then
		table.insert(outputBuffer, ("\tInfos (%d) {"):format(infosCount))
		table.insert(outputBuffer, indentLines(self.infos, 2))
		table.insert(outputBuffer, "\t}")
	end

	table.insert(outputBuffer, "}")

	return table.concat(outputBuffer, "\n")
end

local function createLogInfo()
	local logInfo = {
		errors = {},
		warnings = {},
		infos = {},
	}

	setmetatable(logInfo, logInfoMetatable)

	return logInfo
end

local Logging = {}

--[[
	Invokes `callback`, capturing all output that happens during its execution.

	Output will not go to stdout or stderr and will instead be put into a
	LogInfo object that is returned. If `callback` throws, the error will be
	bubbled up to the caller of `Logging.capture`.
]]
function Logging.capture(callback)
	local collector = createLogInfo()

	local wasOutputEnabled = outputEnabled
	outputEnabled = false
	collectors[collector] = true

	local success, result = pcall(callback)

	collectors[collector] = nil
	outputEnabled = wasOutputEnabled

	assert(success, result)

	return collector
end

--[[
	Issues a warning with an automatically attached stack trace.
]]
function Logging.warn(messageTemplate, ...)
	local message = messageTemplate:format(...)

	for collector in pairs(collectors) do
		table.insert(collector.warnings, message)
	end

	-- debug.traceback inserts a leading newline, so we trim it here
	local trace = debug.traceback("", 2):sub(2)
	local fullMessage = ("%s\n%s"):format(message, indent(trace, 1))

	if outputEnabled then
		warn(fullMessage)
	end
end

--[[
	Issues a warning like `Logging.warn`, but only outputs once per call site.

	This is useful for marking deprecated functions that might be called a lot;
	using `warnOnce` instead of `warn` will reduce output noise while still
	correctly marking all call sites.
]]
function Logging.warnOnce(messageTemplate, ...)
	local trace = debug.traceback()

	if onceUsedLocations[trace] then
		return
	end

	onceUsedLocations[trace] = true
	Logging.warn(messageTemplate, ...)
end

return Logging
