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

WFDiagnosticLogImp.lua

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

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

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

This message processor is used in the implementation of the WFDiagnosticLog
namespace. Do not access directly.

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

	-- RxLua
local Rx = require 'Rx'
local Observable = Rx.Observable

	-- WF.core.diagnostic_log
local WFDiagnosticLogNative = require 'WFDiagnosticLogNative'

	-- WF.core.lua.threading
local WFRequestDispatcher = require 'WFRequestDispatcher'

	-- WF.core.notifications
local KSSystemNotifications = require 'KSSystemNotifications'

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

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

	-- WF.core.string
local KSStringUtils = require 'KSStringUtils'

	-- WF.core.prefs
local KSPrefs -- require 'KSPrefs' -- lazy loaded


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

local reportingInterval = 15 -- seconds (how long do we wait for DL sections to come in?)

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

local htmlEscape = KSStringUtils.escapeHtml

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

local kSparklineJS = [[<script>var color='#0000aa';var height=23;function doSparkline(container){var data=container.innerHTML;container.innerHTML="";data=data.split(",");var i;var x=0;var y=0;var width=50;if(width<data.length){width=data.length}var itemGap=width/data.length;var cap=0;for(i=0;i<data.length;i++){var val=parseInt(data[i]);if(val>cap){cap=val}}var canvas=document.createElement('canvas');canvas.width=width;canvas.height=height;container.appendChild(canvas);var ctx=canvas.getContext('2d');ctx.fillStyle='#ffffff';ctx.fillRect(0,0,canvas.width,canvas.height);ctx.fillStyle=color;ctx.strokeStyle=color;var first=1;ctx.beginPath();for(i=0;i<data.length;i++){var val=parseInt(data[i]);var barheight=height-Math.floor(val/cap*(height-2));x+=itemGap;if(first){ctx.moveTo(Math.floor(x),barheight);first=0}else{ctx.lineTo(Math.floor(x),barheight)}}ctx.stroke()} window.addEventListener("load", function(){ var containers=document.getElementsByClassName('sparkline');for(var i=0;i<containers.length;i++){var container=containers[i];doSparkline(container)}});</script>]]

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

local kBoxAndWhiskerJS = [[<script>function getIndex (node) { var parent=node.parentElement||node.parentNode, i=-1, child; while (parent && (child=parent.childNodes[++i])) if (child==node) return i; return -1; } function _getPath (node) { var parent, path=[], index=getIndex(node); (parent=node.parentElement||node.parentNode) && (path=_getPath(parent)); index > -1 && path.push(index); return path; } function getPath (container) { parent = container.parentNode.parentNode; return _getPath(parent).join(","); } var height=30; var boxMax = {}; function parseData( container ) { var data=container.innerHTML; var path=getPath(container); var max=boxMax[path]; if (typeof max === 'undefined') { max = 0; } data=data.split(","); if (data.length < 2) { return; } for(i=0; i<data.length; i++){ var val=parseFloat(data[i]); data[ i ] = val; if (val > max) { max = val; } } boxMax[path]= max; return data; } var kBoxWhiskerWidth=200; function getCoord( container, data, ratio ) {; var path=getPath(container); var max=boxMax[path]; var x = Math.floor( ( data.length - 1 ) * ratio ); x = data[ x ]; x = Math.floor((kBoxWhiskerWidth-2) * x / max)+1; return x; } function doBoxAndWhisker(container){ var data = parseData(container); if (!data || data.length<2) { return; }data.sort(function(a,b){return a-b}); var wMax = getCoord( container, data, 1 ); var wQHi = getCoord( container, data, 0.75 ); var wMedian = getCoord( container, data, 0.5 ); var wQLo = getCoord( container, data, 0.25 ); var wMin = getCoord( container, data, 0 ); container.innerHTML=""; var en = document.createElement('span'); en.innerHTML=data[ 0 ].toPrecision( 3 ); container.appendChild(en); var canvas=document.createElement('canvas'); canvas.width=kBoxWhiskerWidth; canvas.height=height; container.appendChild(canvas); var en = document.createElement('span'); en.innerHTML=data[ data.length - 1 ].toPrecision( 3 ); container.appendChild(en); var ctx=canvas.getContext('2d'); ctx.fillStyle='#eeeeee'; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.fillStyle=color; ctx.strokeStyle='#000000'; var first=1; ctx.beginPath(); ctx.moveTo(wMax,5); ctx.lineTo(wMax,25); ctx.moveTo(wMin,5); ctx.lineTo(wMin,25); ctx.moveTo(wMin,15); ctx.lineTo(wQLo,15); ctx.moveTo(wMax,15); ctx.lineTo(wQHi,15); ctx.moveTo(wMedian,8); ctx.lineTo(wMedian,22); ctx.moveTo(wQHi,8); ctx.lineTo(wQHi,22); ctx.lineTo(wQLo,22); ctx.lineTo(wQLo,8); ctx.closePath(); ctx.stroke(); } window.addEventListener( "load", function() { var boxAndWhiskerEls=document.getElementsByClassName('box-and-whisker'); for(var bwEl=0; bwEl<boxAndWhiskerEls.length; bwEl++){ var container=boxAndWhiskerEls[bwEl]; parseData(container); } for(var bwEl=0; bwEl<boxAndWhiskerEls.length; bwEl++){ var container=boxAndWhiskerEls[bwEl]; doBoxAndWhisker(container); }});</script>]]

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


local kAccordianJS = [[
<script>
var acc = document.getElementsByClassName("sectionHeader");
var i;

for (i = 0; i < acc.length; i++) {

	var section = acc[i];
	var panel = section.nextElementSibling;
	panel.style.display = "block";
	
    acc[i].addEventListener("click", function() {
	
        this.classList.toggle("active");
        var panel = this.nextElementSibling;
        if (panel.style.display === "block") {
            panel.style.display = "none";
        } else {
            panel.style.display = "block";
        }
		
    });
}

function expandAllSections() {

    for (i = 0; i < acc.length; i++) {

		var section = acc[i];

		section.classList.remove("active");
		
		var panel = section.nextElementSibling;
		panel.style.display = "none";

	}

}
</script>]]

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

local automationOutputSections

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

local function automationOutputEnabled()

	if not KSPrefs then
		KSPrefs = require 'KSPrefs'
	end

	local enabled = not _G.DISTRIBUTION and KSPrefs[ 'debug_automationOutputEnabled' ]

	if enabled and not automationOutputSections then
		automationOutputSections = {}
	end

	return enabled

end


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

-- @mark HTML report generation

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

local function generateVisualizedHtml( node, selector )

	local data = node[ "< " .. selector .. " >" ]
	local title = node.title

	return string.format( "%s <div class='%s'>%s</div>", title or "",
		selector, table.concat( data, "," ) )

end

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

local function tableToHtml( t )

	-- Support preformatted representation.

	if t[ "< preformatted html >" ] then
		return t[ "< preformatted html >" ]
	end

	-- Support preformatted representation.

	if t[ "< sparkline >" ] then
		return generateVisualizedHtml( t, 'sparkline' )
	end

	-- Support preformatted representation.

	if t[ "< box-and-whisker >" ] then
		return generateVisualizedHtml( t, 'box-and-whisker' )
	end

	-- Sort scalar keys. Ignore (with warning) non-scalar keys.

	local keysInTable = {}
	local nKeys = 0
	local ignoredNonScalarKeys = false

	for k in pairs( t ) do
		local tk = type( k )
		if tk == 'number' or tk == 'string' then
			nKeys = nKeys + 1
			keysInTable[ nKeys ] = k
		else
			ignoredNonScalarKeys = true
		end
	end

	table.sort( keysInTable, function( a, b )
		local ta = type( a )
		local tb = type( b )
		if ta ~= tb then
			return ta < tb
		else
			return a < b
		end
	end )

	if nKeys == 0 and not ignoredNonScalarKeys then
		return "<p><i>(empty table)</i></p>"
	end

	local htmlForKeyAtIndex = {}

	for i = 1, nKeys do

		local key = keysInTable[ i ]
		local htmlKey = htmlEscape( key )

		local value = t[ key ]
		local tv = type( value )

		local htmlValue

		if tv == 'table' then
			htmlValue = tableToHtml( value )
		else
			htmlValue = htmlEscape( value )
		end

		htmlForKeyAtIndex[ i ] = string.format( "<li><em class='fieldKey'>%s</em>: <em class='fieldValue'>%s</em></li>", htmlKey, htmlValue )
		
	end

	if ignoredNonScalarKeys then
		htmlForKeyAtIndex[ nKeys + 1 ] = "<li><i>(ignored non-scalar keys)</i></li>"
	end

	return string.format( "<ul>%s</ul>\n", table.concat( htmlForKeyAtIndex, "\n" ) )

end

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

local kStyleSheet = 
[[
<style>

/*----------------------------------------------------------------------------*/
/* Sparkline */
/*----------------------------------------------------------------------------*/

.sparkline {
display:inline-block;
}
.box-and-whisker * {
vertical-align:middle;
}
.box-and-whisker span {
width: 200px;
display: inline-block;
}
.box-and-whisker>span:first-of-type {
text-align:right;
}
.box-and-whisker canvas {
margin: 20px 10px;
padding: 2px;
}

/*----------------------------------------------------------------------------*/

body,
html {
	background:#fff;
	color:#000;
	text-align:left;
	font-family: "omnes-pro", "helvetica", sans-serif;
}

li {
	padding:5px 0;
}

hr {
	border: none 0; 
	border-top: 1px solid #000;
	width: 100%;
	height: 1px;
	margin:10px 0 10px 0;
	text-align: center;
	float:left;
}

/*----------------------------------------------------------------------------*/
/* CLASS STYLES */
/*----------------------------------------------------------------------------*/

.center {
	float:none;
	margin: auto;
	display:block;
	text-align:center;
}

.toc {
	background:#eee;
	color:#000;
	padding:10px;
	margin-top:20px;
	margin-bottom:10px;
}

.sectionHeader {
    cursor: pointer;
    padding: 5px;
    width: 100%;
    text-align: left;
    transition: 0.4s;
    background:#fff;
    border-top: 1px solid #000;
}

.active, .sectionHeader:hover {
    background-color: #eee;
    border-left:5px solid #000;
}

.sectionContent {
	padding:10px;
	margin:0;
    overflow: hidden;
    background:#fff;
}

.diagHeader {
	padding-top:10px;
}

.purpose {
	font-style:italic;
	font-size:0.9em;
}

.sectionContent ul {
	 list-style-type: circle;
}

/*----------------------------------------------------------------------------*/
/* FONT STYLES */
/*----------------------------------------------------------------------------*/

h1 {
	color:#000;
	font-weight:lighter;
} 

h2, h3 {
	color:#000;
	padding:10px;
	font-weight:lighter;
}

h4, h5, h6 {
	color: #000;
	font-weight:lighter;
	padding: 0;
}

.sectionHeader h2 {
	text-decoration: underline;
	color:#1473E6;
}

.fieldKey {
	font-weight:bolder;
	font-style:normal;
	color:#000;
}

.fieldValue {
	font-style:normal;
	padding-left: 10px;
	color:#000;
}

/*----------------------------------------------------------------------------*/

a:link, a:visited, a:active {
	text-decoration: underline;
	color: #1473E6;
	border: none 0;
} a:hover {
	text-decoration:underline;
	color: #252525;
	border: none 0;
}

</style>
]]

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

local function makeDocTypeOpeningTag( appName )

	local escapedAppName = htmlEscape( appName or "(unnamed Wichita Foundation app)" )

	local openingTag = string.format(
[[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<title>%s Diagnostic Log</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
%s
%s
</head>
<body>
]],
		escapedAppName,
		kStyleSheet )

	return openingTag

end

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

local function makeIntroduction( appName )

	local escapedAppName = htmlEscape( appName or "(unnamed Wichita Foundation app)" )

	local appUptime = KSDate.timeIntervalSinceAppLaunch()
	local appUptimeStr

	if appUptime >= 3600 then
		appUptimeStr = string.format( "%.0d seconds (%.0d hours, %.0d minutes, %.0d seconds)",
									  appUptime,
									  appUptime / 3600,
									  ( appUptime / 60 % 60 ),
									  appUptime % 60 )
	elseif appUptime >= 60 then
		appUptimeStr = string.format( "%.2d seconds (%.0d minutes, %.2d seconds)",
									  appUptime, appUptime / 60, appUptime % 60 )
	else
		appUptimeStr = string.format( "%.2d seconds", appUptime )
	end

	return string.format(
[[
<div class='diagHeader'>
<h1>%s Diagnostic Log</h1>
<p>Generated: %s%s</p>
<p>App has been running for %s.</p>
<p class='purpose'>This information will be used by Adobe engineers to help diagnose the problem you are reporting.</p>
</div>
]],
		escapedAppName,
		KSDate.timeInIso8601FormatNearestSecond( KSDate.timeNow(), true ),
		KSDate.timeZoneInIso8601Format(),
		appUptimeStr
	)

end

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

local parentSections = {}

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

local function sortSectionList()

	local sortedSubSectionsByParent = {}
	local sortedParentSections = {}

	if parentSections[ 'App Version' ] then
		sortedParentSections[ #sortedParentSections + 1 ] = 'App Version'
	end
	
	if parentSections[ 'Sync' ] then
		sortedParentSections[ #sortedParentSections + 1 ] = 'Sync'
	end
	
	if parentSections[ 'Log Messages' ] then
		sortedParentSections[ #sortedParentSections + 1 ] = 'Log Messages'
	end
	
	for parentSectionName, sections in pairs( parentSections ) do
	
		local sortedSubSections = {}
		
		if parentSectionName ~= 'App Version' and
			parentSectionName ~= 'Sync' and
			parentSectionName ~= 'Log Messages' and
			parentSectionName ~= 'Other' then
			sortedParentSections[ #sortedParentSections + 1 ] = parentSectionName
		end
		
		for _, section in pairs( sections ) do
	
			sortedSubSections[ #sortedSubSections + 1 ] = section
			
		end
		
		table.sort( sortedSubSections,
				function( a, b )

					if a.weight ~= b.weight then
						return a.weight < b.weight
					else
						return a.id < b.id
					end

				end )
		
		sortedSubSectionsByParent[ parentSectionName ] = sortedSubSections
		
	end

	if parentSections[ 'Other' ] then
		sortedParentSections[ #sortedParentSections + 1 ] = 'Other'
	end
	
	return sortedParentSections, sortedSubSectionsByParent

end

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

local function redactUserNameFromEmbeddedPaths( stringWithPath )
	stringWithPath = string.gsub( stringWithPath, "/Users/[^/]+", "/Users/USERNAME" )
	stringWithPath = string.gsub( stringWithPath, "\\\\Users\\\\[^\\]+", "\\\\Users\\\\USERNAME" )
	return string.gsub( stringWithPath, "\\Users\\[^\\]+", "\\Users\\USERNAME" )
end

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

local function anchorNameForSection( sectionProvider )

	
	local anchorName = string.format( '%d-%s', sectionProvider.weight, redactUserNameFromEmbeddedPaths( sectionProvider.id ) )
	
	-- remove whitespace
	anchorName = anchorName:gsub( "%s", "" )
	
	-- FIXME [matthewj 2016-11-30]: Assumes section provider IDs are mostly clean...this htmlEscape function is mostly garbage.
	anchorName = htmlEscape( anchorName )
	
	return anchorName

end

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

local function makeHtmlToc( sortedParentSections, sortedSubSectionsByParent )

	local tocForEach = {}

	for _, parentSectionName in ipairs( sortedParentSections ) do

		local subSections = {}

		local sections = sortedSubSectionsByParent[ parentSectionName ] or {}

		for _, section in ipairs( sections ) do

			subSections[ #subSections + 1 ] = string.format( '<li><a href="#%s">%s</a></li>',
					anchorNameForSection( section ),
                	htmlEscape( redactUserNameFromEmbeddedPaths( section.name ) ) )

		end

		tocForEach[ #tocForEach + 1 ] = string.format( 
			[[
			<li>
				<a href="#%s">%s</a>
				<ol type='a'>
					%s
				</ol>
			</li>
			]],
			parentSectionName,
			parentSectionName,
			table.concat( subSections, '\n' ) )

	end

	return string.format(
		[[
		<hr />
		<input class='expandBtn' type="button" value="Collapse All Sections" onclick="expandAllSections()" />
		<hr />
		<br />
		<div class='toc'>
			<h2>Table of Contents</h2>
			<ol>
			%s
			</ol>
		</div>]],
		table.concat( tocForEach, '\n' ) )

end

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

local function makeHtmlSections( sortedParentSections, sortedSubSectionsByParent )

	local htmlOutput = {}

	for _, parentSectionName in ipairs( sortedParentSections ) do

		htmlOutput[ #htmlOutput + 1 ] = string.format( 
				[[
					<a name='%s' ></a>
					<h1 class='parentSection'>%s</h1>
				]], 
				parentSectionName, 
				parentSectionName )

		local sections = sortedSubSectionsByParent[ parentSectionName ] or {}

		for _, section in ipairs( sections ) do
			
			local description = ''
			if section.description then
				description = string.format( "%s", htmlEscape( section.description ) )
			end
			
			htmlOutput[ #htmlOutput + 1 ] = string.format(
					[[
						<a name='%s' ></a>
						<div class='sectionHeader active'>
							<h2>%s</h2>
							%s
						</div>
						<div class='sectionContent'>
							%s
						</div>
					]],
					anchorNameForSection( section ),
					htmlEscape( redactUserNameFromEmbeddedPaths( section.name ) ),
					description,
					redactUserNameFromEmbeddedPaths( section.body ) )
							
		end

	end

	return table.concat( htmlOutput, '\n' )

end

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

local _threadStats = {}

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

local function stripPreformattedHtml( node )

	if type( node ) == 'table' then

		if node[ "< raw data >" ] then
			return node[ "< raw data >" ]
		elseif node[ "< sparkline >" ] then
			return {
				title = node.title,
				data = node[ "< sparkline >" ],
			}
		else
			local strippedCopy = {}
			for k,v in pairs( node ) do
				strippedCopy[ k ] = stripPreformattedHtml( v )
			end
			return strippedCopy
		end

	else

		return node

	end

end

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

local function gatherAutomationOutput()

	local resultTable = {}

	for k, v in pairs( automationOutputSections ) do
		if v.tableData then
			resultTable[ k ] = stripPreformattedHtml( v.tableData )
		end
	end

	return resultTable

end

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

WFRequestDispatcher.onMessage( 'registerDiagnosticUtility', function( namespace )

	local x = require( namespace )
	if x ~= nil then
		return WFRequestDispatcher.getInstalledMessages()
	else
		return false
	end

end )

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

WFRequestDispatcher.onMessage( 'reportingInterval', function()

	return reportingInterval

end )

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

local _1MbAsK = 1024

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

local _1GbAsK = 1024 * _1MbAsK

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

local function formatKilobytesAsReadableUnit( kb )

	local unit = kb < _1MbAsK and 1 or ( kb < _1GbAsK and _1MbAsK or _1GbAsK )

	local unitStr = kb < _1MbAsK and "kb" or ( kb < _1GbAsK and "Mb" or "Gb" )

	return string.format( "%.2f %s", kb / unit, unitStr )

end

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

local function maybeOmitString( shouldOmit, str )

	return shouldOmit and "" or str

end

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

local function wrapInTag( tag, str )

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

end

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

local function makeThreadStatsHTML()

	table.sort( _threadStats, function( a, b )

		if a.currentSize ~= b.currentSize then
			return a.currentSize > b.currentSize
		else
			return a.threadName > b.threadName
		end

	end )

	local threadStatTableRows = {}

	threadStatTableRows[ #threadStatTableRows + 1 ] =
			wrapInTag( "tr",
				wrapInTag( "th", "thread" ) ..
				wrapInTag( "th", "current mem size" ) ..
				wrapInTag( "th", "mem size after GC " ) ..
				wrapInTag( "th", "peak mem size" ) ..
				wrapInTag( "th", "wall clock time" ) )

	for _, stat in ipairs( _threadStats ) do

		local peakStr = 
				stat.peakSize and 
					htmlEscape( formatKilobytesAsReadableUnit( stat.peakSize ) ) or
					"n/a"

		local wallClockTimeStr = 
				type( stat.wallClockTime ) == 'number' and
					string.format( "%.2f seconds", stat.wallClockTime ) or
					"n/a"
			or ""

		-- thread name cleanup code cloned from WFDiagnosticLog
		-- grabs the id and throws away the rest
		local threadName = stat.threadName
		local tid = threadName:match('id = *"(%x+)"')
		if tid == nil then
			tid = threadName:match('"id":"(%x+)"')
		end
		if tid then
			threadName = threadName:match('([%a. :]+)')..tid
		end

		threadStatTableRows[ #threadStatTableRows + 1 ] =
			wrapInTag( "tr",
				wrapInTag( "td", wrapInTag( "b",  htmlEscape( redactUserNameFromEmbeddedPaths( threadName ) ) ) ) ..
				wrapInTag( "td", htmlEscape( formatKilobytesAsReadableUnit( stat.currentSize ) ) ) ..
				wrapInTag( "td", htmlEscape( formatKilobytesAsReadableUnit( stat.sizeAfterGC ) ) ) ..
				wrapInTag( "td", peakStr ) ..
				wrapInTag( "td", wallClockTimeStr ) )

	end

	return wrapInTag( "table", table.concat( threadStatTableRows, "" )  )

end

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

WFRequestDispatcher.onMessage( 'generateHtmlReport', function( args )

	local includeAutomationOutput = automationOutputEnabled()

	WFDiagnosticLogNative.sendDiagnosticLogRequest()

	args = args or {}

	_threadStats = {}

	parentSections = {}

	if includeAutomationOutput then

		automationOutputSections = {}

	end

	return Observable.timer( reportingInterval, WFRxScheduler.sharedScheduler() )
		:map( function()
	
			local automationOutputOrNil
	
			if includeAutomationOutput then
				automationOutputOrNil = gatherAutomationOutput()
			end
	
			-- Special case: Gather thread performance stats and build a section
			-- for them.
	
			if #_threadStats > 0 then
	
				local performanceSection = parentSections.Performance or {}
	
				performanceSection.WFLuaMessagingProcessor = {
	
					id = 'WFLuaMessagingProcessor',
					name = "Messaging Processors",
					weight = 1500,
					body = makeThreadStatsHTML(),
					
				}
				
				parentSections.Performance = performanceSection
				
			end
	
			-- Now roll up all the sections that we received.
	
			local sortedParentSections, sortedSubSectionsByParent = sortSectionList()
	
			local result = 
						maybeOmitString( args.noHTMLWrapperTag,  makeDocTypeOpeningTag( args.appName ) )
						.. makeIntroduction( args.appName )
						.. makeHtmlToc( sortedParentSections, sortedSubSectionsByParent )
						.. makeHtmlSections( sortedParentSections, sortedSubSectionsByParent )
						.. maybeOmitString( args.noHTMLWrapperTag, kSparklineJS )
						.. maybeOmitString( args.noHTMLWrapperTag, kBoxAndWhiskerJS )
						.. maybeOmitString( args.noHTMLWrapperTag, kAccordianJS )
						.. maybeOmitString( args.noHTMLWrapperTag, "</body></html>\n" )
	
			if args.noHTMLWrapperTag then

				return result, automationOutputOrNil, 
						kStyleSheet .. kSparklineJS .. kBoxAndWhiskerJS, kAccordianJS

			end

			return result, automationOutputOrNil
	
		end )

end )

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

WFRequestDispatcher.onMessage( 'addSectionToLog', function( args )

	if type( args.body ) == 'table' then

		if automationOutputEnabled() and args.outputForAutomation then
			args.tableData = args.body
			automationOutputSections[ args.id ] = args
		end

		pcall( function()
			args.body = tableToHtml( args.body )
		end )

	end
	
	local parentSection = args.parentSection or 'Other'
	local sections = parentSections[ parentSection ] or {}
	sections[ args.id ] = args
	
	parentSections[ parentSection ] = sections

end )

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

KSSystemNotifications.onNotificationDo( 'receiveThreadStats',
	function( stats )
		_threadStats[ #_threadStats + 1 ] = stats
	end )


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

-- @mark built-in section providers

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

local WFDiagnosticLog = require 'WFDiagnosticLog'

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

WFDiagnosticLog.addLogSectionFunc( function()

	local WFPlatformInfo = require 'WFPlatformInfo'
	local WFPlatformInfo_copy = {}

	for k, v in pairs( WFPlatformInfo ) do
		if type( v ) == 'function' then
			v = v()
		end
		if WFDiagnosticLog.isSimpleValue( v ) then
			WFPlatformInfo_copy[ k ] = v
		end
	end

	WFPlatformInfo_copy.folderSeparator = nil

	return {

		id = 'WFPlatformInfo',
		name = "Platform Description",
		weight = -1000,
		body = WFPlatformInfo_copy,

	}

end )
