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

WFPerformanceReport.lua

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

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

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

	-- Rx
local Rx = require 'Rx'

	-- WF.core.logging
local WFLogger = require 'WFLogger'

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

	-- WF.core.rxlua
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local WFRxScheduler = require 'WFRxScheduler'

	-- WF.core.utils
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFCapabilities = require 'WFCapabilities'

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

local logger = WFLogger 'WFPerformanceReport'

local check = assert( WFArguments.check )

local runUntilDone = WFRxRunUntilDone

local staminaTest = WFConfiguration.Stamina.TestRun or 
						WFCapabilities.getCapability( 'WFPerformanceReportIsEnabled' ) or false

local wrappersShouldRun

local perfReportImp_lazy

local KSDate_timeNowForAnimation = KSDate.timeNowForAnimation

local KSDate_timeIntervalSinceAppLaunch = KSDate.timeIntervalSinceAppLaunch

local ENABLE_SLEUTH = WFConfiguration.Shared.enableSleuth or false

if ENABLE_SLEUTH then
	-- redirect output for sleuth
	WFCapabilities.setCapability( 'WFPerformanceReportImpModule' , 'WFPerformanceLocalFileReportImp' )
	wrappersShouldRun = true  -- allow code wrappers to run without stamina client setup
end

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

local function pack( ... )

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

end

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

local function perfReportImp()

	if not perfReportImp_lazy then
		perfReportImp_lazy = ( require 'WFWorkerThread' )( 'WFPerformanceReportMessageProcessor.lua' )
	end

	return perfReportImp_lazy

end

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

-- @mark WFPerformanceReport class

--------------------------------------------------------------------------------
--[[!
	\class WFPerformanceReport

	TO DO: Write  documentation for this
]]--

local WFPerformanceReport = {}

--------------------------------------------------------------------------------
--[[!
	\brief Record a single performance measurement

	\param key : The name of the performance measurement
	\param value: a table containing the key 'timestamp' whose value is 
				 length the operation took (in seconds)
				plus additional information caller wants to log in form of 
				key-value pairs. 

	\return Observable: Will send onCompleted when the measurement has been recorded.
		(It is safe to ignore this value.)
]]--

function WFPerformanceReport.recordSingleValue( key, value )

	check( 'string', 'table' )

	if not staminaTest and not wrappersShouldRun then
		return
	end

	return perfReportImp():request( 'recordSingleValue', key, value )
		:call( runUntilDone )

end

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

local function addTimestamp( logData, start )

	logData.timestamp = KSDate_timeNowForAnimation() - start

	return logData

end

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

local function addTimeInterval( logData )

	logData.timeIntervalSinceAppLaunch = KSDate_timeIntervalSinceAppLaunch()

	return logData

end

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

function WFPerformanceReport.startTimedValue()

	local logData = {}
	if staminaTest or wrappersShouldRun then
		addTimeInterval( logData )
		logData.startTime = KSDate_timeNowForAnimation()
	end
	return logData

end

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

function WFPerformanceReport.recordTimedValue( key, value )
	check( 'string', 'table' )

	if staminaTest or wrappersShouldRun then
		addTimestamp( value, value.startTime or 0 )
		WFPerformanceReport.recordSingleValue( key, value )
	end
end

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

local function makeRecordEventFunctions( key, logs, optSelectorFn )

	local startTime = KSDate_timeNowForAnimation()
	
	local onNextEventCounter =  0
	local function recordEvent( eventName, primaryValue, ... )
		
		logs = logs or {}
		local tempLogs = KSTableUtils.filter( 
							function( _, value ) 
								return type( value ) ~= 'function' 
							end,
							logs )
		
		local observableResultLogs = {}
		if type( logs[ eventName ] ) == 'function' then
			observableResultLogs = logs[ eventName ]( primaryValue, ... ) or {}
		end	
		
		for k, v in pairs( observableResultLogs ) do
			tempLogs[ k ] = v -- this will overwrite anything in the original tempLogs, that is intentional
		end
		
		tempLogs = addTimestamp( tempLogs, startTime )

		if tempLogs[ 'observableValue' ] then 
			tempLogs[ 'observableValue' ] = primaryValue
		end 
		
		tempLogs = addTimeInterval( tempLogs )
		
		if eventName == 'onNext' then 
			onNextEventCounter = onNextEventCounter + 1
			tempLogs[ 'onNextEventCounter' ] = onNextEventCounter
		end
		
		if eventName == 'onError' then 
			tempLogs[ 'errorMessage' ] = tostring( primaryValue )
		end
		
		if optSelectorFn then
			tempLogs = optSelectorFn( eventName, tempLogs, primaryValue, ... )
		end
		
		WFPerformanceReport.recordSingleValue( key .. " " .. tostring( eventName ), tempLogs )
		
		return tempLogs.timestamp
		
	end

	return recordEvent

end

--------------------------------------------------------------------------------
--[[!
	\brief Instrument the Observable to record the time from origination to
		 completion of the Observable. Also the frequency of any onNext notifications
		 that are sent between start and completion. This recorded information is sent
		to Lightroom's performance monitoring app Stamina. Instrumentation happens only
		 when Stamina config variables are set in the config file.
	
	\param key : The name of the API/Observable
	\param observable: Observable in question we are recording time for
	\param logs(optional): a table containing any additional information
						 caller wants to log 
	\For example: 
	\local logs = {} 
	\logs[ 'nPhotos in array' ]  = 3
	
	\Some predefined keys for logs table are as follows
	\1.logs[ 'isUnitTest' ] = true , to be used only by WF unit test app.
	\2.logs[ 'observableValue' ] = true , for recording observable value for onNext events
	\3.logs[ 'onCompleted' ] = true, for tapping and recording onCompleted event 
	
	\return Observable: same observable, it doesnt effect the observable in question.
	
]]--

function WFPerformanceReport.wrapObservable( key, observable, logs, optSelectorFn )

	check( '+string', '_', '?table', '?function' )
		-- Check for Rx.Observable doesn't work. :-(

	local tapEventFn = makeRecordEventFunctions( key, logs, optSelectorFn )
	
	-- checking if wrapObservable was called from unit test app.
	-- wrapObservable required stamina vars to be set in config file in order to record
	-- anything. In WF test app , we are using different setup which is combination of 
	-- recordObservable operator and WFPerformanceReportTestUtils class. It was tricky 
	-- to setup Stamina config vars for WF test app. So we are using special key 'isUnitTest'
	-- in logs table which is passed from  above mentioned combo.
	local isUnitTest = logs and logs.isUnitTest or false
	
	if not staminaTest and not isUnitTest and not wrappersShouldRun then
		return observable
	end

	return observable
		:tap(
			function( primaryValue, ... )
				-- record onNext event
				local elapsedTime = tapEventFn( "onNext", primaryValue, ... )
				logger:debugf( "tap onNext for key %s with value %s with elasped time %.2f", key, tostring( primaryValue ), elapsedTime )
			end,
			function( err )
				-- record onError event
				tapEventFn( "onError", err )
				logger:debugf( "tap onError for key %s with error %s", key, err )
			end,
			function()
				-- record onCompleted event only if caller has onCompleted flag
				if logs and logs.onCompleted then 
					local elapsedTime = tapEventFn( "onCompleted" )
					logger:infof( "wrapObservable completed for key %s after %.2f seconds",
							  key, elapsedTime )
				else 
					logger:infof( "wrapObservable completed for key %s", key )
				end 

			end )

end

--------------------------------------------------------------------------------
--[[!
	\brief Specialized API that transforms then records the onNext values of an
	underlying Rx.Observable. This is a simpler API that forwards the
	implementation to the more generic WFPerformanceReport.wrapObservable.
	
	\param key : Same as WFPerformanceReport.wrapObservable param key
	\param observable: Same as WFPerformanceReport.wrapObservable observable key
	\param tapFunction: This is a function that will receive onNext values
	from the observable param. The function should emit a Lua table that will be
	consumed by the stamina post processor.
	
	\return Observable: same observable, it doesnt effect the observable in question.
	
]]--

function WFPerformanceReport.tapObservableOnNext( key, observable, tapFunction )

	check( '+string', '_', 'function' )
		-- Check for Rx.Observable doesn't work. :-(

	return WFPerformanceReport.wrapObservable( 
								key, observable, { onNext = tapFunction } )

end

--------------------------------------------------------------------------------
--[[!
	\brief Specialized API that instruments the onNext values of an underlying 
	Rx.Observable. This is a simpler API that forwards the implementation to
	the more generic WFPerformanceReport.wrapObservable.
	
	\param key : Same as WFPerformanceReport.wrapObservable param key
	\param observable: Same as WFPerformanceReport.wrapObservable observable key
	
	\return Observable: same observable, it doesnt effect the observable in question.
	
]]--

function WFPerformanceReport.wrapObservableValue( key, observable )

	check( '+string', '_' )
		-- Check for Rx.Observable doesn't work. :-(


	return WFPerformanceReport.wrapObservable( 
								key, observable, { observableValue = true } )

end

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

local staminaLogDataStreamWeakFuncArgs = KSTableUtils.makeWithMode( "k" )

--------------------------------------------------------------------------------
--[[!
	\brief API that calls a function that generates stamina log data at 
	regular specified intervals. Comes with companion API
	WFPerformanceReport.deregisterPollingMetric.
	
	\param interval: how frequently to collect the metric. This is in seconds,
	but only down to 1/16 of a second. In addition, it's not guaranteed to be
	any more precise than the WFRxScheduler.sharedScheduler.

	\param metricName: A string identifying the polling metric

	\param staminaLogDataFunc: A function that is invoked with ... (i.e.
	staminaLogDataFunc( ... ) ) every interval seconds. This function must
	return a Lua table with data that will be analyzed by the Stamina post
	processor

	\param ...: an optional variable list of arguments, These arguments will be
	retained and passed to staminaLogDataFunc as long as the subscription is
	kept alive
	
	\return opaque value: This value must be retained by client to keep the
	polling alive.  If the value is not retained, the polling will stop.
	
]]--

function WFPerformanceReport.registerPollingMetric( interval, metricName, 
													staminaLogDataFunc, ... )

	if not staminaTest and not wrappersShouldRun then

		return

	end

	local weakFuncArgs

	local staminaLogDataStream =
		Rx.Observable.timer( interval, interval, WFRxScheduler.sharedScheduler() )
			:map(
				function()

					local _, maybeArgs = next( weakFuncArgs )
			
					if not maybeArgs then

						-- client abandoned polling subscription
						
						return

					end

					local startTime = KSDate_timeNowForAnimation()

					-- staminaLogDataFunc will return staminaLogData
					local logData = staminaLogDataFunc( unpack( maybeArgs ) )

					if type( logData ) ~= "table" then

						logger:warnf( "Wrong type of data returned from staminaLogDataFunc. Type is '%s'", 
											type( logData ) )

						return

					end

					logData = addTimestamp( logData, startTime )
		
					logData = addTimeInterval( logData )

					return logData
					
				end )
			:filter(
				function( staminaLogData )
					
					return staminaLogData ~= nil

				end )

	local staminaLogDataStreamSubscription = 
			staminaLogDataStream:subscribe(
				function( staminaLogData )

					WFPerformanceReport.recordSingleValue( 
											metricName, staminaLogData )

				end )


	-- Store the function args in a weak table with the lifetime managed
	-- by the subscription instance returned by this function.
	--
	-- This prevents memory leaks because it will allow { ... } to be
	-- garbage collected if we stop managing the subscription.

	weakFuncArgs = KSTableUtils.makeWithMode( "k" )

	weakFuncArgs[ staminaLogDataStreamSubscription ] = { ... }

	-- weakFuncArgs is a local variable, so we must keep it alive by weakly
	-- storing it in the staminaLogDataStreamWeakFuncArgs global. This will
	-- ensure it's kept alive until the subscription is released.
	staminaLogDataStreamWeakFuncArgs[ staminaLogDataStreamSubscription ] = 
									weakFuncArgs

	return staminaLogDataStreamSubscription
	
end

--------------------------------------------------------------------------------
--[[!
	\brief companion API to registerPollicMetric. This stops the polling when
	called with opaque value returned by registerPollicMetric
	
	\param opaque value: this is value returned by registerPollicMetric
	
]]--

function WFPerformanceReport.deregisterPollingMetric( subscription )

	if subscription == nil then

		return

	end

	local weakFuncArgs = staminaLogDataStreamWeakFuncArgs[ subscription ]

	staminaLogDataStreamWeakFuncArgs[ subscription ] = nil

	if type( weakFuncArgs ) == "table" then

		weakFuncArgs[ subscription ] = nil

	end

	if type( subscription ) ~= "table" or
			not Rx.Disposable:isClassOf( subscription ) then

		return

	end

	subscription:dispose()

end

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

local function timeFunc( logData, f, ... )

	local startTime = KSDate_timeNowForAnimation()

	local packed, nPacked = pack( f( ... ) )

	logData = addTimestamp( logData, startTime )

	logData = addTimeInterval( logData )

	return logData, packed, nPacked

end

--------------------------------------------------------------------------------
--[[!
	\brief API that creates a wrapped function. Every time the wrapped function
	is called, it is forwarded to the original function and a stamina log entry
	is added when it completes. This function probably won't give accurate
	results in the underlying function calls coroutine.yield.
	
	\param metricName: A string identifying the metric

	\param f: A function that is wrapped.
	
	\return a wrapped function: clients can call the wrapped function and
	it ultimately call the underlying function. If stamina is not enabled,
	the same function will be returned.
	
]]--

function WFPerformanceReport.wrapFunction( metricName, f )

	check( 'string', 'function' )

	if not staminaTest and not wrappersShouldRun then

		return f

	end

	return function( ... )

		local logData, packed, nPacked = timeFunc( {}, f, ... )

		WFPerformanceReport.recordSingleValue( metricName, logData )

		return unpack( packed, 1, nPacked )

	end
	
end

--------------------------------------------------------------------------------
--[[!
	\brief API that creates a wrapped function that returns an observable.
	
	\param key: A string identifying the function

	\param f: A function to wrap that returns an observable.
	
	\param logs(optional): a table containing any additional information
	caller wants to log. See WFPerformanceReport.wrapObservable for more info.
	
	\return a wrapped function: clients can call the wrapped function and
	it ultimately call the underlying function. If stamina is not enabled,
	the same function will be returned.
]]--

function WFPerformanceReport.wrapObservableFunction( key, f, logs )

	check( 'string', 'function', '?table' )

	if not staminaTest and not wrappersShouldRun then

		return f

	end

	return function( ... )

		local observable = f( ... )
		return WFPerformanceReport.wrapObservable( key, observable, logs )

	end
	
end

--------------------------------------------------------------------------------
--[[!
	\brief API that creates a wrapped function. Every time the wrapped function
	is called, it is forwarded to the original function and a stamina log entry
	is added when it completes. The stamina log data is generated by
	tapInputFunc.
	
	\param metricName: A string identifying the metric

	\param f: A function that is wrapped.

	\param tapInputFunc: A function that receives the ... arg list that is passed
	to f. tapInputFunc must not change any of the ... values. tapInputFunc must
	return a lua table to be passed to the log processor.
	
	\return a wrapped function: clients can call the wrapped function and
	it ultimately call the underlying function. If stamina is not enabled,
	the same function will be returned.
	
]]--

function WFPerformanceReport.tapFunctionInput( metricName, f, tapInputFunc )

	check( 'string', 'function', 'function' )

	if not staminaTest and not wrappersShouldRun then

		return f

	end

	return function( ... )

		local tapLogData = tapInputFunc( ... )

		if type( tapLogData ) ~= "table" then

			logger:warnf( "tapInputFunc returned a '%s' instead of a table", 
							type( tapLogData ) )

			tapLogData = {}

		end

		local logData, packed, nPacked = timeFunc( tapLogData, f, ... )

		WFPerformanceReport.recordSingleValue( metricName, logData )

		return unpack( packed, 1, nPacked )

	end
	
end

--------------------------------------------------------------------------------
--[[!
	\brief API that creates a wrapped function. Every time the wrapped function
	is called, it is forwarded to the original function and a stamina log entry
	is added when it completes. The stamina log data is generated by
	tapOutputFunc.
	
	\param metricName: A string identifying the metric

	\param f: A function that is wrapped.

	\param tapOutputFunc: A function that receives the return values from f( ... ) 
	tapOutputFunc must not change any of the f( ... ) values. tapOutputFunc must
	return a lua table to be passed to the log processor.
	
	\return a wrapped function: clients can call the wrapped function and
	it ultimately call the underlying function. If stamina is not enabled,
	the same function will be returned.
	
]]--

function WFPerformanceReport.tapFunctionOutput( metricName, f, tapOutputFunc )

	check( 'string', 'function', 'function' )

	if not staminaTest and not wrappersShouldRun then

		return f

	end

	return function( ... )

		local logData, packed, nPacked = timeFunc( {}, f, ... )

		local tapLogData = tapOutputFunc( unpack( packed, 1, nPacked ) )

		if type( tapLogData ) ~= "table" then

			logger:warnf( "tapOutputFunc returned a '%s' instead of a table", 
							type( tapLogData ) )

			tapLogData = {}

		end

		for k, v in pairs( logData ) do

			if tapLogData[ k ] == nil then

				tapLogData[ k ] = v

			end

		end

		WFPerformanceReport.recordSingleValue( metricName, tapLogData )

		return unpack( packed, 1, nPacked )

	end
	
end
													
--------------------------------------------------------------------------------
--[[!
	\brief Retrieve all recorded performance data.

	\param optClear (boolean) If true, all data is removed from storage in
		\c WFPerformanceReportImp after delivery.

	\return Observable< key, duration > one tuple for each performance measurement
]]--

function WFPerformanceReport.retrieve( optClear )

	check( '?boolean' )

	return perfReportImp():request( 'retrieve', optClear )

end

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

--[[!
	\brief sends collected performance metrics to EMP every few secs
	and it clears the performance report once it has been sent to EMP.
	As a prerequisite WFConfiguration.Stamina.EMPServerAddress and 
	WFConfiguration.Stamina.EMPPort config vars needs to be set in 
	the config file.
	\param interval ( number ) send metrics after every 'interval' secs
]]--

function WFPerformanceReport.sendPerfData( interval )
	
	check( 'number' )
	return perfReportImp():request( 'sendPerfData', interval )

end

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

--[[!
	\brief clear performance report from memory
	it will be useful for unit tests where we need to clear the report
	recorded in tear down of test.
]]--

function WFPerformanceReport.clearPerfReport()

	return perfReportImp():request( 'clearPerfReport' )
		:call( runUntilDone )

end

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

--[[!
	\brief Creates a new Performance report
	
	For local file based performance reports, this call creates a new file, so
	multiple performance reports end up in their own files
]]--

function WFPerformanceReport.newPerformanceReport( reportName )

	if staminaTest then
		return perfReportImp():request( 'newPerformanceReport', reportName )
	else
		return Rx.Observable.empty()
	end
	
end

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

--[[!
	\brief runs f( ... ) with performance report gathering enabled. Use this to enable
	WFPerformanceReport without requiring stamina setup. (mainly in tests)
]]--

function WFPerformanceReport.alwaysEnableWFPerformanceReport( f )

	return function( ... )

		wrappersShouldRun = true

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

		wrappersShouldRun = false

		return unpack( ret, 1, retN )

	end

end

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

return WFPerformanceReport
