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

PixelPathProviderSchedulerDiagnosticLog.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
local KSTableUtils = require "KSTableUtils"
local WFCloseSupport = require "WFCloseSupport"
local WFDiagnosticLog = require "WFDiagnosticLog"
local WFFiniteStateMachine = require "WFFiniteStateMachine"
local WFFunctionUtils = require "WFFunctionUtils"

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

local PixelPathProviderRequestTypes = require 'PixelPathProviderRequestTypes'
local PixelPathProviderSchedulerDebugUtils = 
		require "PixelPathProviderSchedulerDebugUtils"

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

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

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

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

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

local function recordDiagnosticLogTiming( diagnosticLog, desc, duration )

	duration = tonumber( duration ) or 0

	local descTimings = diagnosticLog._timings[ desc ] or {}

	diagnosticLog._timings[ desc ] = descTimings

	descTimings.count = ( descTimings.count or 0 ) + 1

	descTimings.total = ( descTimings.total or 0 ) + duration

	descTimings.min = descTimings.min and 
			math.min( descTimings.min, duration ) or duration

	descTimings.max = descTimings.max and 
			math.max( descTimings.max, duration ) or duration
	
end

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

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 reduceT( init, accumf, t, ... )

	local ret = init

	for k, v in pairs( t ) do

		ret = accumf( ret, k, v, ... )

	end

	return ret
	
end

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

local function derefWeakRef( weakRef )

	return next( weakRef )

end

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

local function makeDiagnosticLogPathsSentText( diagnosticLog )

	local scheduler =
			derefWeakRef( diagnosticLog._weakPixelPathProviderScheduler )

	if not scheduler then

		return ""

	end

	return wrapInTag( "h4", "Paths sent via bufferedPixelPathStream" ) ..
				wrapInTag( "h3", rawget( scheduler, "_DEBUG_daemonBufferedPathsSentWorker" ) or 
											0 )

end

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

local function emitTimingStatStr( statName, timingData, funcName )

	if statName == "func" then

		return funcName

	elseif statName == "count" then

		return tostring( timingData.count )

	elseif statName == "min" then

		return string.format( "%.3f", tonumber( timingData.min ) or 0 )

	elseif statName == "ave" then

		if tonumber( timingData.count ) > 0 and
				tonumber( timingData.total ) then

			return string.format( "%.3f", tonumber( timingData.total ) /
											tonumber( timingData.count ) ) 

		end

		return ""

	elseif statName == "max" then

		return string.format( "%.3f", tonumber( timingData.max ) or 0 )

	elseif statName == "total" then

		return string.format( "%.3f", tonumber( timingData.total ) or 0 )

	end

	return ""

end

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

local function bind1st( f, v )

	return function( ... )

		return f( v, ... )

	end

end

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

local function concatTimingRow( rows, funcName, timingData, columns )
	
	rows[ #rows + 1 ] = wrapInTag( "tr", table.concat(
								WFFunctionUtils_map(
									compose( bind1st( wrapInTag, "td" ), 
												emitTimingStatStr ),
										columns, timingData, funcName ) ) )

	return rows

end

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

local function makeRequestStatusRow( request )

	local pixStreamId, localAssetId, _, _, currState = unpack( request )

	local visibleRequestRow = { pixStreamId, localAssetId, currState }

	return wrapInTag( "tr", table.concat(
								WFFunctionUtils_map(
									compose( wrapInTag, swap ),
										visibleRequestRow, "td" ) ) )

end

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

local function makeDiagnosticLogRequestStatuses( diagnosticLog, title, 
													priority )

	local scheduler =
			derefWeakRef( diagnosticLog._weakPixelPathProviderScheduler )

	if not scheduler then

		return ""

	end

	local requestsWithPriority = 
			scheduler._requestPriorityQueue
					:_DEBUG_getRequestsByPriority( priority )

	local columns = { "pixStreamId", "localAssetId", "state" }

	return wrapInTag( "h4", title ) ..
			wrapInTag( "table",
				wrapInTag( "tr", table.concat(
								WFFunctionUtils_map(
									compose( wrapInTag, swap ),
										columns, "th" ) ) ) ..
				table.concat( WFFunctionUtils_map(
									makeRequestStatusRow,
									requestsWithPriority ) ) )


end

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

local function makeDiagnosticLogTimingsText( timings )

	if not next( timings ) then 
	
		return "" 
		
	end

	local columns = { "func", "count", "min", "ave", "max", "total" }

	return wrapInTag( "h4", "Timings" ) ..
			wrapInTag( "table",
				wrapInTag( "tr", table.concat(
								WFFunctionUtils_map(
									compose( wrapInTag, swap ),
										columns, "th" ) ) ) ..
				table.concat( reduceT( {}, concatTimingRow, 
											timings, columns ) ) )

end

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

local function makeRequestRow( requestStat )

	local requestTypeName, requestTypeCount = unpack( requestStat )

	return wrapInTag( "tr", wrapInTag( "td", requestTypeName ) ..
							wrapInTag( "td", requestTypeCount )  )

end

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

local function appendRequestStatsTable( tables, pixStreamId, requestCounts, 
											columns )

	tables[ #tables + 1 ] =
			wrapInTag( "h5", pixStreamId ) ..
			wrapInTag( "table",
				wrapInTag( "tr", table.concat(
								WFFunctionUtils_map(
									compose( wrapInTag, swap ),
										columns, "th" ) ) ) ..
				table.concat( WFFunctionUtils_map(
									makeRequestRow, requestCounts ) ) )

	return tables

end

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

local function makeDiagnosticLogRequestStatsText( diagnosticLog )

	local _pixelPathProviderScheduler =
			derefWeakRef( diagnosticLog._weakPixelPathProviderScheduler )

	if not _pixelPathProviderScheduler then

		return ""

	end

	local stats = 
			_pixelPathProviderScheduler._requestPriorityQueue
										:_DEBUG_getRequestCountsByState()

	local columns = { "type", "count" }

	return wrapInTag( "h4", "Requests" ) ..
			table.concat( reduceT( {}, appendRequestStatsTable, 
											stats, columns ) )

end

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

local PixelPathProviderSchedulerDebugUtils_getStateCategory =
		PixelPathProviderSchedulerDebugUtils.getStateCategory

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

local PixelPathProviderSchedulerDebugUtils_getCategoryStateNames =
		PixelPathProviderSchedulerDebugUtils.getCategoryStateNames

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

local function addRequestStateBenchmarkToCategory(
				requestStateBenchmarksByCategory, rawRequestStateBenchmark )

	local stateName, elapsed, occurrences = unpack( rawRequestStateBenchmark )

	local category = PixelPathProviderSchedulerDebugUtils_getStateCategory( 
						stateName )

	if not category then

		return requestStateBenchmarksByCategory

	end

	if not requestStateBenchmarksByCategory[ category ] then

		requestStateBenchmarksByCategory[ category ] = { elapsed, occurrences }
		
		return requestStateBenchmarksByCategory

	end

	local currStats = requestStateBenchmarksByCategory[ category ]

	currStats[ 1 ] = currStats[ 1 ] + elapsed

	currStats[ 2 ] = currStats[ 2 ] + occurrences

	return requestStateBenchmarksByCategory

end

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

local function makeCategoryStateBenchmarkRecord( 
					category, requestStateBenchmarksByCategory )

	return { category, unpack( requestStateBenchmarksByCategory[ category ] ) }

end

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

local function extractKeys( t )

	local keys = {}

	for k in pairs( t ) do

		keys[ #keys + 1 ] = k

	end

	return keys

end

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

local function processRawRequestStateBenchmarks( rawRequestStateBenchmarks )

	rawRequestStateBenchmarks = rawRequestStateBenchmarks or {}
	
	local requestStateBenchmarksByCategory = 
			WFFunctionUtils_reduce( {},
				addRequestStateBenchmarkToCategory,
					rawRequestStateBenchmarks )

	local categorizedStateBenchmarks =
			WFFunctionUtils_map(
				makeCategoryStateBenchmarkRecord,
					extractKeys( requestStateBenchmarksByCategory ),
						requestStateBenchmarksByCategory )

	table.sort( categorizedStateBenchmarks,
					function( benchmarkRecord1, benchmarkRecord2 )

						return benchmarkRecord1[ 2 ] > benchmarkRecord2[ 2 ]

					end )

	return categorizedStateBenchmarks

end

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

local function makeRequestStateStatDisplayRecord( requestStateBenchmark )

	local stateName, elapsed, occurrences = unpack( requestStateBenchmark )

	return { stateName, 
				string.format( "%.3f", elapsed ),
				tostring( occurrences ),
				string.format( "%.3f", 
								occurrences == 0 and 
									0 or elapsed / occurrences ) }
					
end

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

local function appendRequestStateBenchmarkRow( rows, displayRequestStateBenchmark )

	return rows ..
			wrapInTag( "tr", table.concat(
					WFFunctionUtils_map(
						compose( wrapInTag, swap ),
							displayRequestStateBenchmark, "td" ) ) )


end

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

local function appendStateTransitionsHTMLRow( rows, categoryStateTransitionCountRow )

	rows[ #rows + 1 ] = 
		wrapInTag( "tr", 
			table.concat( 
				WFFunctionUtils_map(
					compose( wrapInTag, swap ),
						categoryStateTransitionCountRow, "td" ) ) )

	return rows

end

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

local function appendStateTransitionsRows( rows, stateName, scheduler )

	return WFFunctionUtils_reduce( rows,
		appendStateTransitionsHTMLRow,
			scheduler._requestPriorityQueue:_DEBUG_getRequestStateTransitionCounts( 
										stateName ) )

end

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

local function addTransitionsPerCategory( requestStateBenchmark, scheduler )

	local category = unpack( requestStateBenchmark )

	local columns = { "initial state", "next state", "occurrences" }

	local categoryStateNames =
			PixelPathProviderSchedulerDebugUtils_getCategoryStateNames( 
				category, 
				WFFiniteStateMachine.getStateMap( scheduler._stateMachine ) )

	return 
		wrapInTag( "h5", string.format( "%s transition occurrences", 
												category ) ) ..
			wrapInTag( "table",
				wrapInTag( "tr", table.concat(
								WFFunctionUtils_map(
									compose( wrapInTag, swap ),
										columns, "th" ) ) ) ..
				table.concat( WFFunctionUtils_reduce( {},
									appendStateTransitionsRows,
										categoryStateNames, scheduler ) ) )

end

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

local function makeDiagnosticLogRequestStatesText( diagnosticLog )

	local scheduler =
			derefWeakRef( diagnosticLog._weakPixelPathProviderScheduler )

	if not scheduler then

		return ""

	end

	local requestStateBenchmarks = 
			processRawRequestStateBenchmarks(
				scheduler._requestPriorityQueue:_DEBUG_getRequestStateBenchmarks() )

	if #requestStateBenchmarks == 0 then

		return ""

	end

	local displayRequestStateBenchmarks =
			WFFunctionUtils_map(
				makeRequestStateStatDisplayRecord,
					requestStateBenchmarks )

	local columns = { "state", "time", "occurrences", "ave" }

	return wrapInTag( "h4", "Request State Costs" ) ..
			wrapInTag( "table",
				wrapInTag( "tr", table.concat(
								WFFunctionUtils_map(
									compose( wrapInTag, swap ),
										columns, "th" ) ) ) ..
				WFFunctionUtils_reduce( "",
					appendRequestStateBenchmarkRow,
						displayRequestStateBenchmarks ) ) ..
		wrapInTag( "h4", "Transition occurrences" ) ..
			table.concat(
				WFFunctionUtils_map(
					addTransitionsPerCategory,
						requestStateBenchmarks, scheduler ) )

end

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

local function appendLongSideRenderCountRow( rowStr, longSide, renderCount )

	return string.format( "%s<tr><td>%s</td><td>%s</td></tr>", 
							rowStr, longSide, renderCount )

end

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

local function rendersPerLongSideTable( rendersPerLongSide )

	rendersPerLongSide = 
		type( rendersPerLongSide ) == "table" and rendersPerLongSide or {}

	local columns = { "long side", "render count" }

	return wrapInTag( "table",
				wrapInTag( "tr", table.concat(
								WFFunctionUtils_map(
									compose( wrapInTag, swap ),
										columns, "th" ) ) ) ..
				reduceT( "",
					appendLongSideRenderCountRow,
						rendersPerLongSide ) )

end

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

local function appendLongSideRenderCountRowForPixStreamId( 
				rowStr, longSide, renderCount, pixStreamId )

	return string.format( "%s<tr><td>%s</td><td>%s</td><td>%s</td></tr>", 
							rowStr, pixStreamId, longSide, renderCount )

end

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

local function appendLongSideRenderCountRowsForPixStreamId( 
				rowStr, pixStreamId, rendersPerLongSide )

	return reduceT( rowStr, appendLongSideRenderCountRowForPixStreamId,
							rendersPerLongSide, pixStreamId )

end

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

local function makeOrphanedRenderTable( orphanedRenderData )


	local columns = { "pix stream", "long side", "render count" }

	return wrapInTag( "table",
				wrapInTag( "tr", table.concat(
								WFFunctionUtils_map(
									compose( wrapInTag, swap ),
										columns, "th" ) ) ) ..
				reduceT( "",
					appendLongSideRenderCountRowsForPixStreamId,
						orphanedRenderData ) )

end

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

local function maybeMakeOrphanedRendersTable( tableDesc, orphanedRenderData ) 

	if not next( orphanedRenderData ) then

		return ""

	end

	return wrapInTag( "h4", tableDesc ) ..
			makeOrphanedRenderTable( orphanedRenderData )

end

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

local function makeDiagnosticLogRenderStatsText()

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

	if not ok then

		return ""

	end

	if type( PixelProviderRenderTask.getRenderStats ) ~= "function" then

		return ""

	end

	local rendersPerLongSide, preemptedRenderCount,
			canceledRenderCount, 
			orphanedRendersWithPendingCancel,
			unexpectedOrphanedRenders =
				PixelProviderRenderTask.getRenderStats()

	return 
		wrapInTag( "h4", "Renders per long side" ) ..
			rendersPerLongSideTable( rendersPerLongSide ) ..
		wrapInTag( "h4", string.format( "Preempted render count = %s",
											preemptedRenderCount ) ) ..
		wrapInTag( "h4", string.format( "Canceled render count = %s",
											canceledRenderCount ) ) ..
		maybeMakeOrphanedRendersTable( 
			"Completed renders with pending cancel",
			orphanedRendersWithPendingCancel ) ..
		maybeMakeOrphanedRendersTable( 
			"Unexpected completed renders", 
			unexpectedOrphanedRenders )

end

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

local function makeDiagnosticLogHtmlText( diagnosticLog )

	return makeDiagnosticLogPathsSentText( diagnosticLog ) ..
			makeDiagnosticLogRequestStatuses( diagnosticLog,
												"visible",
												PixelPathProviderRequestTypes.names.grid_visible ) ..
			makeDiagnosticLogRequestStatuses( diagnosticLog,
												"idle",
												PixelPathProviderRequestTypes.names.idle ) ..
			makeDiagnosticLogTimingsText( diagnosticLog._timings ) ..
			makeDiagnosticLogRequestStatsText( diagnosticLog ) ..
			makeDiagnosticLogRequestStatesText( diagnosticLog ) ..
			makeDiagnosticLogRenderStatsText() 

end

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

local function sendDiagnosticLogData( diagnosticLog )

	return {

		name = "Pixel Provider",

		id = "PixelPathProviderScheduler",

		weight = 410,

		body = makeDiagnosticLogHtmlText( diagnosticLog ),
	
	}

end

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

local function makeWeakRef( v )

	local weakRef = KSTableUtils.makeWithMode( "kv" )

	weakRef[ v ] = v

	return weakRef

end

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

local function makeDiagnosticLog( scheduler )

	return WFCloseSupport.addCloseSupportMethods( {

		_weakPixelPathProviderScheduler = makeWeakRef( scheduler ),

		_timings = {},

		sendDiagnosticLogData = sendDiagnosticLogData,

	} )

end

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

local _diagLogKey = {}

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

local function maybeBootstrapDiagnosticLog( scheduler )

	local diagnosticLog = rawget( scheduler, _diagLogKey )

	if not diagnosticLog then

		diagnosticLog = makeDiagnosticLog( scheduler )

		scheduler:addCloseTarget( diagnosticLog )

		WFDiagnosticLog.addLogProvider( diagnosticLog )

		rawset( scheduler, _diagLogKey, diagnosticLog )

	end

	return diagnosticLog

end

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

local function recordTimingForDiagnosticLog( scheduler, desc, duration )

	local diagnosticLog = maybeBootstrapDiagnosticLog( scheduler )

	recordDiagnosticLogTiming( diagnosticLog, desc, duration )

end

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

return {

	processRawRequestStateBenchmarks = processRawRequestStateBenchmarks,

	recordTimingForDiagnosticLog = recordTimingForDiagnosticLog,

}
