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

WFPerformanceUtils.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2016 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.

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

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local WFArguments = require 'WFArguments'
local WFRingBuffer = require 'WFRingBuffer'
local WFDiagnosticLog = require 'WFDiagnosticLog'

	-- WF.core.os
local KSDate = require 'KSDate'

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

local logger = require 'WFLogger'( 'WFPerformanceUtils' )

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

local KSDate_timeNowForAnimation = KSDate.timeNowForAnimation

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

local WFArguments_check = assert( WFArguments.check, "No WFArguments.check" )

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

local function pack( ... )

	return { ... }, select( '#', ... )

end

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

local function wrapInLongExecutionHandler( threshold, longExecHandlerFunc, 
											desc, makeDescFunc, f )

	WFArguments_check( "number", "function", "string", "function", "function" )

	return function( ... )

		local start = KSDate_timeNowForAnimation()

		local ret, nRet = pack( f( ... ) )

		local duration = KSDate_timeNowForAnimation() - start
		
		if duration > threshold then

			local invocationDesc = makeDescFunc( desc, ... )

			longExecHandlerFunc( invocationDesc, start, duration, threshold )

		end

		return unpack( ret, 1, nRet )

	end

end

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

local function justReturn( v )
	
	return v

end

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

local _longExecutionRingBuffersPerCategory = {}

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

local function makeLongExecHTMLTableRow( perfArray )

	return string.format( "<tr><td>%s</td></tr>", 
								table.concat( perfArray, "</td><td>" ) )

end

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

local function makeLongExecHTMLTableRows( ringBuffer )

	return table.concat( 
			WFRingBuffer.map( makeLongExecHTMLTableRow, ringBuffer) )

end

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

local function transformLongExecutionDataToHTML()

	local html = ""

	for category, ringBuffer in pairs( _longExecutionRingBuffersPerCategory ) do

		html = string.format( "%s<h3>%s</h3><table><tr><th>desc</th><th>start</th><th>duration</th><th>threshold</th></tr>%s</table>",
								html, category, 
								makeLongExecHTMLTableRows( ringBuffer ) )

	end

	return html

end

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

local function maybeMakeLongExecutionDiagnosticLogSection()

	if not next( _longExecutionRingBuffersPerCategory ) 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",
											KSIdGenerator.uuidInLowercaseHex() )

	WFDiagnosticLog.addSectionToLog {

		id = 'Long_execution_warnings ' .. KSIdGenerator.uuidInLowercaseHex(),

		name = string.format( 'Long execution warnings for: %s', thisThreadName ),

		weight = -100,

		body = transformLongExecutionDataToHTML(),

	}

end

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

WFDiagnosticLog.addLogSectionFunc( maybeMakeLongExecutionDiagnosticLogSection )

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

local function roundToN( num, n )

	return math.floor( num * 10 ^ n + 0.5 ) / 10 ^ n 

end

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

local function addLongExecToCategoryRingBuffer( category, desc, start,
													duration, threshold )

	_longExecutionRingBuffersPerCategory[ category ] =
		_longExecutionRingBuffersPerCategory[ category ] or
		WFRingBuffer.make( 50 )

	_longExecutionRingBuffersPerCategory[ category ] =
		WFRingBuffer.addItem( 
			_longExecutionRingBuffersPerCategory[ category ],
			{ desc, roundToN( start, 5 ), roundToN( duration, 5 ), threshold } )

	local appStartTime = KSDate_timeNowForAnimation() - 
							KSDate.timeIntervalSinceAppLaunch()

	logger:warnf( "Took %.5f for %s:%s, threshold is %.2f. This occurred %.2f after app launch",
					duration, category, desc, threshold, start - appStartTime )

end

--------------------------------------------------------------------------------
--[[!
	\brief Wraps a function. When wrapped function runs, it Adds a category
	entry to diagnostic log if underlying function execution time exceeds
	threshold.

	\param category (string) A categeory name for this warning. Use this to 
	group long execution warnings. Only the 50 most recent warnings will be
	sent to the diagnostic log per category.

	\param threshold (number) Time in seconds. If the wrapped function exceeds
	this value, an entry is added to the diagnostic log ring buffer for the
	category.  

	\param desc (string) A string describing the wrapped function.

	\param makeDesc (function) A function that is invoked as 
	
		makeDescFunc( desc, ... ) 
		
	where ... are the arguments passed to the wrapped function.  makeDescFunc
	is invoked if the wrapped function takes longer than threshold to
	exeuction. makeDescFunc should return a string describing the wrapped
	function. The canonical use case is to describe how many items were passed
	to a function that takes an array as an argument

	\param f (function) The function to wrap

	\return a wrapped function that will be monitored for long execution
]]--

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

local function wrapInLogLongExecutionInDiagnosticLogWithDynamicDescription( 
					category, threshold, desc, makeDescFunc, f )

	WFArguments_check( "string", "number", "string", "function", "function" )

	local function addLongExecToRingBuffer( ... )

		addLongExecToCategoryRingBuffer( category, ... )

	end

	return wrapInLongExecutionHandler( threshold, addLongExecToRingBuffer, 
											desc, makeDescFunc, f )

end

--------------------------------------------------------------------------------
--[[!
	\brief Wraps a function. When wrapped function runs, it Adds a category
	entry to diagnostic log if underlying function execution time exceeds
	threshold.

	\param category (string) A categeory name for this warning. Use this to 
	group long execution warnings. Only the 50 most recent warnings will be
	sent to the diagnostic log per category.

	\param threshold (number) Time in seconds. If the wrapped function exceeds
	this value, an entry is added to the diagnostic log ring buffer for the
	category.  

	\param desc (string) A string describing the wrapped function.

	\param f (function) The function to wrap

	\return a wrapped function that will be monitored for long execution
]]--

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

local function wrapInLogLongExecutionInDiagnosticLogWithStaticDescription( 
					category, threshold, desc, f )

	WFArguments_check( "string", "number", "string", "function" )

	return wrapInLogLongExecutionInDiagnosticLogWithDynamicDescription(
					category, threshold, desc, justReturn, f )

end

--------------------------------------------------------------------------------
--[[!
	\brief Adds a category entry to diagnostic log if function execution time
	exceeds threshold.

	\param category (string) A categeory name for this warning. Use this to 
	group long execution warnings. Only the 50 most recent warnings will be
	sent to the diagnostic log per category.

	\param desc (string) A string describing the wrapped function.

	\param start (number) The KSDate.timeNowForAnimation() value that
	represents the start time this desc.

	\param threshold (number) Time in seconds. If the wrapped function exceeds
	this value, an entry is added to the diagnostic log ring buffer for the
	category.  

]]--

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

local function maybeLogLongExecutionInDiagnosticLogWithStaticDescription(
					category, desc, start, threshold )

	WFArguments_check( "string", "string", "number", "number" )

	local duration = KSDate_timeNowForAnimation() - start
	
	if duration <= threshold then

		return

	end

	addLongExecToCategoryRingBuffer( category, desc, start, duration,
										threshold )

end

--------------------------------------------------------------------------------
--[[!
	\brief Adds a category entry to diagnostic log if function execution time
	exceeds threshold.

	\param category (string) A categeory name for this warning. Use this to 
	group long execution warnings. Only the 50 most recent warnings will be
	sent to the diagnostic log per category.

	\param start (number) The KSDate.timeNowForAnimation() value that
	represents the start time this desc.

	\param threshold (number) Time in seconds. If the wrapped function exceeds
	this value, an entry is added to the diagnostic log ring buffer for the
	category.  

	\param makeDesc (function) a function of signature function( desc, ... )
	end that returns a string that will be added to the long exec warning buffer
				

]]--

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

local function maybeLogLongExecutionInDiagnosticLogWithDynamicDescription(
					category, start, threshold, makeDescFunc, ... )

	WFArguments_check( "string", "number", "number", "function" )

	local duration = KSDate_timeNowForAnimation() - start
	
	if duration <= threshold then

		return

	end

	local dynamicDesc = makeDescFunc( ... )

	addLongExecToCategoryRingBuffer( category, dynamicDesc, start, duration,
										threshold )

end

--------------------------------------------------------------------------------
--[[!
	\brief Adds a category entry to long exeuction diagnostic log 
	exceeds threshold.

	\param category (string) A categeory name for this warning. Use this to 
	group long execution warnings. Only the 50 most recent warnings will be
	sent to the diagnostic log per category.

	\param start (number) The KSDate.timeNowForAnimation() value that
	represents the start time this desc.

	\param duration (number) The duration of the operation in seconds

	\param threshold (number) Time in seconds. If the wrapped function exceeds
	this value, an entry is added to the diagnostic log ring buffer for the
	category.  
				

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

local function logLongExecutionInDiagnosticLogWithStaticDescription(
					category, desc, start, duration, threshold )

	WFArguments_check( "string", "string", "number", "number", "number" )

	addLongExecToCategoryRingBuffer( category, desc, start, duration,
										threshold )

end

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

return {

	-- use this function wrapping API if you want to describe the function
	-- based on what was passed to it. That said, most of the time
	-- wrapInLogLongExecutionInDiagnosticLogWithStaticDescription will work
	-- well enough.
	wrapInLogLongExecutionInDiagnosticLogWithDynamicDescription = 
		wrapInLogLongExecutionInDiagnosticLogWithDynamicDescription,

	-- use this function wrapping API if you don't need to describe the values
	-- passed to the function 
	wrapInLogLongExecutionInDiagnosticLogWithStaticDescription = 
		wrapInLogLongExecutionInDiagnosticLogWithStaticDescription,

	-- use this API when you can't or do not wish to wrap a function
	maybeLogLongExecutionInDiagnosticLogWithStaticDescription =
		maybeLogLongExecutionInDiagnosticLogWithStaticDescription,

	-- use this API when you can't or do not wish to wrap a function
	maybeLogLongExecutionInDiagnosticLogWithDynamicDescription =
		maybeLogLongExecutionInDiagnosticLogWithDynamicDescription,

	-- use this API to add an entry to the diagnostic log
	logLongExecutionInDiagnosticLogWithStaticDescription =
		logLongExecutionInDiagnosticLogWithStaticDescription,
}
