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

WFDiagnosticLog.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.

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

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

	-- WF.core.lua
local KSData = require 'KSData'

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

	-- WF.core.reactive
local KSPromises = require 'KSPromises'
local KSReactive = require 'KSReactive'

	-- WF.core.table
local WFTableArchiving = require 'WFTableArchiving'

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


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

local diagnosticLogImp_lazy
local installedEnhancedMessages = {}

local function diagnosticLogImp()

	if not diagnosticLogImp_lazy then
		diagnosticLogImp_lazy = ( require 'WFWorkerThread' )( 'WFDiagnosticLogImp.lua' )
	end

	return diagnosticLogImp_lazy

end


--============================================================================--
--[[!
	\class WFDiagnosticLog
	\brief This namespace provides a way to capture a
		log file that can be used to diagnose user-reported problems.
]]--

-- @mark WFDiagnosticLog namespace

local WFDiagnosticLog = {

	Sections = {
		DOC_STORE = 'Doc Store',
		INDEX_STORE = 'Index Store',
		SYNC = 'Sync',
		HTTP = 'HTTP',
		PERFORMANCE = 'Performance',
		DB_PERFORMANCE = 'Database Performance',
		LOCAL_STORAGE = 'Local Storage',
		LOG_MESSAGES = 'Log Messages',
		APP_VERSION = 'App Version'
	}
	
}

--------------------------------------------------------------------------------
--[[!
	\brief Generates an HTML diagnostic log file designed to be sent to
		Revel developers.

	\param args (table) A table of arguments to pass into the report, with these keys:
		<ul>
			<li> \c appName: (optional, string) Name of the application.</li>
			<li> \c emailTarget: (optional, string) Address of e-mail recipient.</li>
		</ul>

	\return <code>P< string ></code> An HTML log file for diagnostic reports.
	\return <code>B< number ></code> Percentage of progress toward generation of the report.
]]--

function WFDiagnosticLog.generateHtmlReportP( args )

	WFArguments.check( 'table' )

	-- Load these namespaces late so we don't pay for them if user doesn't request
	-- a diagnostic log.

	local WFTimer = require 'WFTimer'
	local KSDate = require 'KSDate'
	local WFCloseSupport = require 'WFCloseSupport'

	-- Get ready to show progress.

	local progressB = KSReactive.variableB( 0 )
	local startTime = KSDate.timeNowForAnimation()
	local reportingInterval = 1 -- wrong but it will do until we know real value

	local function updateProgressB()

		if WFCloseSupport.hasBeenOrWillBeClosed( progressB ) then return end

		local elapsedTime = math.min( KSDate.timeNowForAnimation() - startTime, reportingInterval )
		progressB:set( elapsedTime / reportingInterval )

		WFTimer.afterDelayDo( .5, updateProgressB )

	end

	-- How long is this going to take? (Chill out, King Julian!)

	local reportingIntervalP = diagnosticLogImp():requestP( 'reportingInterval' )
	reportingIntervalP:whenResolvedDo( function( status, resultReportingInterval )

		if status == 'data' then
			reportingInterval = resultReportingInterval
			updateProgressB()
		end

	end ):attachTo( progressB )

	-- Now generate the actual log.

	local innerHTMLReportP = diagnosticLogImp():requestP( 'generateHtmlReport', args )

	innerHTMLReportP:whenResolvedDo( function()
		progressB:set( 1 )
		progressB:close()
	end ):attachTo( progressB )

	return innerHTMLReportP, progressB

end

--------------------------------------------------------------------------------
--[[!
	\brief Generates HTML diagnostic log file that can be embedded in another 
	 html document.

	\param appName (string) string describing the app

	\return <code>P< string ></code> An HTML log file for diagnostic reports.
	\return <code>B< number ></code> Percentage of progress toward generation of the report.
]]--

function WFDiagnosticLog.generateInnerHtmlReportP( appName )

	WFArguments.check( 'string' )

	return WFDiagnosticLog.generateHtmlReportP {
				appName = appName,
				noHTMLWrapperTag = true
			}

end
--------------------------------------------------------------------------------

local HACK_temporaryNotificationStream = KSReactive.sourceE()

--------------------------------------------------------------------------------
--[[!
	\brief TEMPORARY API for Lightroom: Send diagnostic log sections for
		main thread.
]]--

function WFDiagnosticLog.TEMP_sendDiagnosticLogSectionsForMainThread()

	HACK_temporaryNotificationStream:post( true )

end

--------------------------------------------------------------------------------
--[[!
	\brief Creates an event stream that fires when a diagnostic log is
		requested.

	The \c :onDataE() method for this event stream typically calls
	\c WFDiagnosticLog.addSectionToLog.

	\return The new event-stream object.
]]--

function WFDiagnosticLog.logDataRequestE()

	-- FIXME [scouten 2014-01-20]: Very hacky solution to the problem that
	-- Lightroom doesn't distribute system notifications to main-thread
	-- Lua code.

	if require 'WFCurrentThread'.DEBUG_getThreadName() ~= nil then

		return ( require 'KSSystemNotifications' )
			.notificationStreamE 'WFDiagnosticLog_sendLogDataNow'

	else

		return HACK_temporaryNotificationStream:relayE()

	end

end

--------------------------------------------------------------------------------
--[[!
	\brief Creates an Observable that fires when a diagnostic log is requested.

	A subscriber to this method would typically call \c WFDiagnosticLog.addSectionToLog.

	\return an Rx.Observable
]]--

function WFDiagnosticLog.logDataRequest()

	-- TODO [scouten 2015-08-11]: I'm doing that thing I said I wouldn't do:
	-- I'm bridging *from* classic reactive to Rx.Observable. Let's not make a habit
	-- of it, mmmkay?
	
	-- TODO [scouten 2015-08-11]: Rewrite KSSystemNotifications in terms of
	-- Observable. But not today.
	
	return Observable.create( function( observer )

		local dataRequestAction = WFDiagnosticLog.logDataRequestE()
			:onDataE( function( ... )
				observer:onNext( ... )
			end )
		
		return function()
			dataRequestAction:close()
		end
	
	end )

end	

--------------------------------------------------------------------------------
-- FIXME [scouten 2012-06-04]: This should probably be in a utils file somewhere.
-- Maybe part of WFArguments?

local function assertType( v, wantType, msg )

	local tv = type( v )

	if tv == wantType then
		return v
	end

	local emsg

	if type( msg ) == 'string' then
		emsg = string.format( "Expected %s got %s (%s)", wantType, tv, msg )
	else
		emsg = string.format( "Expected %s got %s", wantType, tv )
	end

	error( emsg, 2 )

end

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

local kSimpleTypes = { [ 'string' ] = true, [ 'boolean' ] = true, [ 'number' ] = true }

local function isSimple( v )
	return kSimpleTypes[ type( v ) ]
end

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

local function stripNonTransitableValues( t, breakcycle )

	breakcycle = breakcycle or {}
	local dest = {}

	if breakcycle[ t ] then
		return dest
	end

	breakcycle[ t ] = true

	for k, v in pairs( t ) do

		if not isSimple( k ) then
			k = "< " .. tostring( k ) .. " >"
		end

		if type( v ) == 'table' then
			v = stripNonTransitableValues( v, breakcycle )
		elseif not isSimple( v ) then
			k = "< " .. tostring( v ) .. " >"
		end

		dest[ k ] = v

	end

	return dest

end

--------------------------------------------------------------------------------
--[[!
	\brief Adds a section to the diagnostic log, in response
		to a \c logDataRequestE event having been fired.

	\param args (table) A table of parameters for this section, with these keys:
		<ul>
			<li> \c name: (string) User-visible name of the section.
				Must be HTML escaped.</li>
			<li> \c id: (string) HTML anchor name of the section. Must be a
				valid HTML ID.</li>
			<li> \c weight: (number) Sort order of the section; larger numbers
				sink to the bottom of the list.</li>
			<li> \c body: (string) HTML body of the section. Must be valid HTML
				or a Lua table containing only scalar values, which are converted
				to HTML.</li>
			<li> \c outputForAutomation: (optional, boolean) When \c true,
				this section is included in the abridged JSON flavor of the
				diagnostic log that is used for automated tests. Enabling this
				property requires that the body be a Lua table rather than HTML.</li>
			<li> \c parentSection: (optional, string) If set, this section will
				be nested within the supplied parent section within the diagnostic
				log. Example: parentSection = WFDiagnosticLog.Sections.DOC_STORE</li>
			<li> \c description: (optional, string) If provided, this text will be 
				included below the title and before the body
		</ul>

	\return <code>P< true ></code> A promise object that resolves when section
		has been added to log. Can be ignored.
]]--

function WFDiagnosticLog.addSectionToLog( args )

	WFArguments.check( '?table' )

	if not args or not next( args ) then return end

	assertType( args.name, 'string', 'name' )
	assertType( args.id, 'string', 'id' )
	assertType( args.weight, 'number', 'weight' )

	-- thread name cleanup, grabs the id and tosses the rest
	local tid = args.name:match('id = *"(%x+)"')
	if tid == nil then
		tid = args.name:match('"id":"(%x+)"')
	end
	if tid then
		args.name = args.name:match('([%a. :]+)')..tid
	end

	return WFDiagnosticLog.processLogSectionP( args )
		:mapDataP( function( _, transitableData )
			return diagnosticLogImp():requestP( 'addSectionToLog', transitableData )
		end )
		:runUntilDoneP()

end

--------------------------------------------------------------------------------
--[[!
	\brief Processes a diagnostic log section description and resolves all
		asynchronous behaviors.

	Does not actually contribute this section to a diagnostic log. Useful for
	unit testing diagnostic log section generation code.

	\param args (table) A table of parameters for this section, with these keys:
		<ul>
			<li> \c name: (string) User-visible name of the section.
				Must be HTML escaped.</li>
			<li> \c id: (string) HTML anchor name of the section. Must be a
				valid HTML ID.</li>
			<li> \c weight: (number) Sort order of the section; larger numbers
				sink to the bottom of the list.</li>
			<li> \c body: (string) HTML body of the section. Must be valid HTML
				or a Lua table containing only scalar values, which are converted
				to HTML.</li>
			<li> \c outputForAutomation: (optional, boolean) When \c true,
				this section is included in the abridged JSON flavor of the
				diagnostic log that is used for automated tests. Enabling this
				property requires that the body be a Lua table rather than HTML.</li>
		</ul>

	\return <code>P< true ></code> A promise object that resolves with fully
		populated log section data.
]]--

function WFDiagnosticLog.processLogSectionP( args )

	WFArguments.check( 'table' )

	assertType( args.name, 'string', 'name' )
	assertType( args.id, 'string', 'id' )
	assertType( args.weight, 'number', 'weight' )

	local bodyType = type( args.body )
	if bodyType ~= 'string' and bodyType ~= 'table' then
		error( string.format( "Expected string or table got %s (body)", bodyType ), 2 )
	end

	-- If body is a table, convert behaviors and promises at root level of table
	-- to actual values. Wait 1 second to give time for that to occur.

	local promiseOrBehaviorPopulationDelayP = nil
	local promiseOrBehaviorPopulationDelayAction = nil

	local body = args.body

	local WFTimer = require 'WFTimer'

	local function getPromiseOrBehaviorPopulationDelayAction()

		if not promiseOrBehaviorPopulationDelayP then
			promiseOrBehaviorPopulationDelayP = WFTimer.afterDelayP( 8 )
			promiseOrBehaviorPopulationDelayAction
				= promiseOrBehaviorPopulationDelayP:runUntilDone()
		end

		return promiseOrBehaviorPopulationDelayAction

	end

	local disposables = {}

	if bodyType == 'table' then

		for k, v in pairs( body ) do

			local tv = type( v )

			if tv == 'table' then

				local isEventNode, isBehavior = KSReactive.isEventNode( v )
				if isBehavior then

					local myK = k

					v:ownedOnDataB(
						getPromiseOrBehaviorPopulationDelayAction(),
						function( newValue )
							if newValue == nil then
								body[ myK ] = "(nil)"
							else
								body[ myK ] = newValue
							end
						end )

				elseif isEventNode then

					body[ k ] = "(event nodes not permitted in diagnostic log)"

				elseif Observable:isClassOf( v ) then

					local myK = k
					local didRespond = false

					body[ myK ] = "(observable did not respond in time)"

					local d = v:subscribe(
						function( newValue )
							didRespond = true
							if newValue == nil then
								body[ myK ] = "(nil)"
							else
								body[ myK ] = newValue
							end
						end,
						function( newError )
							didRespond = true
							if newError == nil then
								body[ myK ] = "ERROR: nil"
							elseif type( newError ) == 'table' then
								body[ myK ] = "ERROR: " .. WFTableArchiving.tableToString( newError )
							else
								body[ myK ] = "ERROR: " .. tostring( newError )
							end
						end,
						function()
							if not didRespond then
								body[ myK ] = "(empty response)"
							end
						end )

					if not promiseOrBehaviorPopulationDelayP then
						promiseOrBehaviorPopulationDelayP = WFTimer.afterDelayP( 8 )
						promiseOrBehaviorPopulationDelayAction
							= promiseOrBehaviorPopulationDelayP:runUntilDone()
					end

					disposables[ #disposables + 1 ] = d

				elseif KSPromises.isPromise( v ) then

					local myK = k
					local myPromise = v

					body[ myK ] = "(promise did not resolve in time)"

					if not promiseOrBehaviorPopulationDelayP then
						promiseOrBehaviorPopulationDelayP = WFTimer.afterDelayP( 8 )
						promiseOrBehaviorPopulationDelayAction
							= promiseOrBehaviorPopulationDelayP:runUntilDone()
					end

					getPromiseOrBehaviorPopulationDelayAction()

					myPromise:whenResolvedDo( function( _, newValue )
						if newValue == nil then
							body[ myK ] = "(nil)"
						else
							body[ myK ] = newValue
						end
					end ):runUntilDone() -- lame
						-- :attachTo( getPromiseOrBehaviorPopulationDelayAction() )
						-- Why doesn't this work???

				end

			end

		end

	end

	if promiseOrBehaviorPopulationDelayP == nil then
		promiseOrBehaviorPopulationDelayP = KSPromises.resolvedPromiseP( 'data', true )
	end

	return promiseOrBehaviorPopulationDelayP

		:mapP( function()

			pcall( function()

				for _, disp in ipairs( disposables ) do
					disp:dispose()
				end

			end )

			return 'data', stripNonTransitableValues( args )

		end )

end

--------------------------------------------------------------------------------
--[[!
	\brief Adds a function to generate a log section. Shortcut version for
		combined calls to \c logDataRequestE() and \c addSectionToLog().

	\param logSectionFunc (function) A function that is called when a diagnostic
		log is requested and returns a table in the format required by
		\c addSectionToLog()

	\sa WFDiagnosticLog.addSectionToLog
]]--

function WFDiagnosticLog.addLogSectionFunc( logSectionFunc )

	WFArguments.check( 'function' )

	WFDiagnosticLog.logDataRequestE()
		:onDataE( function()

			WFDiagnosticLog.addSectionToLog( logSectionFunc() )

		end )

end

--------------------------------------------------------------------------------
--[[!
	\brief Adds an object that generates a log section if it is still open
		when a diagnostic log is requested.

	\param logProvider (object) This object must have a \c sendDiagnosticLogData
		method and must also be a \c WFCloseTarget.
]]--

function WFDiagnosticLog.addLogProvider( logProvider )

	WFArguments.check( 'table' )

	if logProvider == nil then return end

	local logDataRequestE = WFDiagnosticLog.logDataRequestE()

	logDataRequestE:closingOwnedOnDataE( logProvider, function()

		WFDiagnosticLog.addSectionToLog( logProvider:sendDiagnosticLogData() )

	end )

end

--------------------------------------------------------------------------------
--[[!
	\brief Enhance the built-in diagnostic facilities with additional logic.

	\param namespace (string) This namespace will be required on the diagnostic
		log's dedicated message processor.

	\return A promise (boolean) for success.  The promise will runUntilDone.
]]--

function WFDiagnosticLog.registerDiagnosticUtilityP( namespace )

	WFArguments.check( 'string' )

	return diagnosticLogImp():requestP( 'registerDiagnosticUtility', namespace )
		:mapDataP( function( status, installedMessages )
			installedEnhancedMessages = KSTableUtils.mapArrayToSet( installedMessages )
			return status, true
		end )
		:runUntilDoneP()

end

--------------------------------------------------------------------------------
--[[!
	\brief Query an enhanced diagnostic utility that was installed in the diagnostic
		log's dedicated message processor via registerDiagnosticUtility.

	\param request name (string) The request name, as specified by WFWorkerThread:requestP.

	\param ... additional parameters to send

	\return a promise with the values returned by the enhanced utility. It will not
		automatically run until done.
]]--

function WFDiagnosticLog.makeEnhancedRequestP( message, ... )

	WFArguments.check( 'string' )

	if installedEnhancedMessages[ message ] then
		return diagnosticLogImp():requestP( message, ... )
	else
		return KSPromises.resolvedPromiseP( 'data', false )
	end

end

--------------------------------------------------------------------------------
--[[!
	\brief Is this a simple value: a number, a boolean or a string.

	\return True if it is simple. False otherwise. Note that \c nil is NOT
		considered simple for the purposes of generating diagnostic logs.
]]--

function WFDiagnosticLog.isSimpleValue( v )

	return isSimple( v )

end

--------------------------------------------------------------------------------
--[[!
	\brief Copy the simple values key/value pairs. If either the key or the value is
		a complex value (a table, userdata, function, or coroutine) then the pair is
		omitted.

	Useful for dumping a namespace into a diagnostic log, when the namespace might
	potentially grow over time to gain values that can't be transited simply.

	\return The copy.
]]--

function WFDiagnosticLog.shallowCopyOfSimpleValues( v )

	if isSimple( v ) then
		return v
	end

	if type( v ) == 'table' then

		local values = {}

		for key,value in pairs( v ) do
			if isSimple( key ) and isSimple( value ) then
				values[ key ] = value
			end
		end

		return values

	end

	return nil

end

--------------------------------------------------------------------------------
--[[!
	\brief Build a diagnostic log entry that contains already formatted data
		(as HTML and JSON).

	\param html (string) HTML representation
	\param data (table) lua type represenation (for use when diagnostic log
		is generated as alternate representaion (e.g. JSON)

	\return A special table containing keys that inform the diagnostic log
		to build this entry as the HTML representation (when possible).
]]--

function WFDiagnosticLog.buildPreformattedHtmlEntry( htmlString, optLuaAlternateForm )

	return {
		[ "< preformatted html >" ] = htmlString,
		[ "< raw data >" ] = optLuaAlternateForm or "no data form available",
	}

end

--------------------------------------------------------------------------------
--[[!
	\brief Insert a sparkline into the HTML output (or just the array of numbers
		into the JSON format)

	\param title (string) title for the sparkline
	\param arrayOfNumbers (table) an array of numbers

	\return A special table containing keys that inform the diagnostic log
		to build this entry as the HTML canvas sparkline.
]]--

function WFDiagnosticLog.sparkline( title, arrayOfNumbers )

	return {
		title = title,
		[ "< sparkline >" ] = arrayOfNumbers,
	}

end

--------------------------------------------------------------------------------
--[[!
	\brief Insert a box-and-whisker graph into the HTML output (or just the
		array of numbers into the JSON format)

	\param title (string) title for the box-and-whisker
	\param arrayOfNumbers (table) an array of numbers

	\return A special table containing keys that inform the diagnostic log
		to build this entry as the HTML canvas box-and-whisker.
]]--

function WFDiagnosticLog.boxAndWhisker( title, arrayOfNumbers )

	return {
		title = title,
		[ "< box-and-whisker >" ] = arrayOfNumbers,
	}

end

--------------------------------------------------------------------------------
--[[!
	\brief Create a ring buffer for storing a series of values. Suitable
		for feeding into a sparkline, for example.

	\param options (table)
		size : size of the ring buffer (default 1000)
		accumulate : add new values to previous ones
		mapFn : a function to run values thru
	\param title (string) title for the data
	\param ... titles for other fields

	\return WFDiagnosticRingBuffer object which contains add( num1, num2, num3 ) fn and
		visualize( 'sparkline', 'boxAndWhisker') fn.
]]--

function WFDiagnosticLog.makeRingBuffer( options )

	local WFDiagnosticLogRingBuffer = require 'WFDiagnosticLogRingBuffer'
	return WFDiagnosticLogRingBuffer( options )

end

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

function WFDiagnosticLog.makeSimpleModel( appName, logFileName )

	appName = appName or "WF"
	logFileName = logFileName or "sync_log.html"

	local directory = KSPathUtils.getStandardFilePath( 'temp' )

	local result = ReplaySubject( 1 )

	local innerHTMLReportP, progressB = WFDiagnosticLog.generateHtmlReportP { appName = appName, }

	innerHTMLReportP = innerHTMLReportP:mapDataP( function( _, reportHtml )

		local logData = KSData.make( reportHtml )

		local logFilePath = KSData.saveToUniquelyNamedFile( logData,
			KSPathUtils.child( directory, logFileName ) )

		result:onNext( { logFilePath = logFilePath, progress = 1 } )

		progressB = nil

	end )
	:runUntilDone()

	progressB:ownedOnDataB( innerHTMLReportP, function( progress )

		result:onNext( { logFilePath = nil, progress = progress } )

	end )

	return {
		value = result,
	}

end

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

function WFDiagnosticLog.makeInnerHTMLReport( appName )

	WFArguments.check( 'string' )

	return Rx.Observable.create(
			function( observer )

				local innerHTMLReportP, progressB = 
						WFDiagnosticLog.generateInnerHtmlReportP( appName )

				local isDone

				innerHTMLReportP = 
					innerHTMLReportP
						:mapDataP( 
							function( _, reportHtml, _, css, javascript )

								progressB = nil

								isDone = true

								observer:onNext( 1, reportHtml, css, javascript )

								observer:onCompleted()

							end )
						:runUntilDone()

				progressB
					:ownedOnDataB( 
						innerHTMLReportP, 
						function( progress )

							if isDone then

								return

							end

							observer:onNext( progress )

						end )

			return Rx.Disposable.create(
					function()

						innerHTMLReportP:close()

						innerHTMLReportP = nil

					end )

		end )

end

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

function WFDiagnosticLog.makeInnerHTMLReportAsFile( appName, logFileName )

	WFArguments.check( 'string', 'string' )

	return WFDiagnosticLog.makeInnerHTMLReport( appName )
				:map(
					function( progress, maybeHTML )

						if type( maybeHTML ) ~= "string" then 

							return progress

						end

						local reportHTML = maybeHTML

						local diagnosticLogParentDir = 
								KSPathUtils.getStandardFilePath( 'temp' )

						local logData = KSData.make( reportHTML )

						local logFilePath = 
								KSData.saveToUniquelyNamedFile( 
									logData,
									KSPathUtils.child( diagnosticLogParentDir, 
														logFileName ) )

						return 1, logFilePath

					end )

end

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

return WFDiagnosticLog
