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

WFProgressReporter.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2015 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remains 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 are protected by all applicable
intellectual property laws, including trade secret and copyright laws.
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 Subject = Rx.Subject
local ReplaySubject = Rx.ReplaySubject

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

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

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

local check = assert( WFArguments.check )

local log = WFLogger 'WFProgressReporter'
--	:setLevel( 'DEBUG' )


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

-- @mark WFProgressReporter class

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

local WFProgressReporter = {

	_description = "",
		-- (string) A description of the progress

	_totalCount = 0,
		-- (number) Total number of things to process

	_processedCount = 0,
		-- (number) Number of things that have been processed

	_errorCount = 0,
		-- (number) Number of things that were processed with errors

	_status = false,
		-- (Rx.Subject< table >) Reports on the progress

	_nonElidedStatus = false,
		-- (Rx.ReplaySubject< table >) Reports on the progress with updates
		-- that are required to be delivered.

	_isDone = false,
		-- (boolean) Indicates if the progress reporter has finished

}

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

--[[!

	\brief Initiate a new progress reporter.

	\param description (string) A string that can be used to identify the use of
	the progress reporter.

	\return (void)
	
--]]

function WFProgressReporter:__init( description )

	check( 'WFProgressReporter', 'string' )

	self._description = description

	self._status = Subject()

	self._nonElidedStatus = ReplaySubject( math.huge )
		-- Purposefully not providing a buffer size so that all updates sent
		-- across this stream are delivered to future subscribers.

end

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

--[[!

	\brief Set the description in case it needs to be changed.

	\return (void)
	
--]]

function WFProgressReporter:setDescription( description )

	self._description = description

end

--[[!

	\brief Reset the progress reporter.

	\return (void)
	
--]]

function WFProgressReporter:reset()

	check( 'WFProgressReporter' )

	log:infof( "Reseting the progress reporter: %s", self._description )

	self._totalCount = 0
	self._processedCount = 0

	if self._isDone then

		self._errorCount = 0

		if self._status then
			self._status:dispose()
		end

		self._status = Subject()

		if self._nonElidedStatus then
			self._nonElidedStatus:dispose()
		end

		self._nonElidedStatus = ReplaySubject( math.huge )

		self._isDone = false

	end
	
end

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

local function reportProgress( self, extraInfo, mustDeliver )

	if self._totalCount ~= -1 and self._totalCount < self._processedCount then
		log:warnf( "Total for %s updated as a fallback: %d", self._description, self._processedCount ) 
		self._totalCount = self._processedCount
	end

	local percentComplete = 0
	
	if self._totalCount > 0 then
		percentComplete = self._processedCount / self._totalCount
	end

	local statusReport = {
		description = self._description,
		totalCount = self._totalCount,
		errorCount = self._errorCount,
		processedCount = self._processedCount,
		percentComplete = percentComplete,
	}
	
	if extraInfo ~= nil then
		statusReport.extraInfo = extraInfo
	end

	local logPercent = statusReport.percentComplete * 100

	log:infof( "Progress Status: %s\n%d of %d, %d%% with %d error(s).", 
			statusReport.description,
			statusReport.processedCount,
			statusReport.totalCount,
			logPercent,
			statusReport.errorCount )

	if mustDeliver then
		self._nonElidedStatus:onNext( statusReport )
	else
		self._status:onNext( statusReport )
	end
	
end

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

--[[!

	\brief Set the total number of items expected to be processed by the progress 
	reporter.

	\param totalCount (number) The total items to be reported.
	
	\param mustDeliver (optional, boolean) Set to true if the message generated
	from setting the total must not be elided.
	
	\param extraInfo (optional, table) Optional info about the total.
	
	\return (void)
	
--]]


function WFProgressReporter:setTotal( totalCount, mustDeliver, extraInfo )

	check( 'WFProgressReporter', 'number', '?boolean', '?table' )

	if totalCount ~= self._totalCount then

		log:infof( "Total updated: %d", totalCount )

		self._totalCount = totalCount
		reportProgress( self, extraInfo, mustDeliver )

	end

end

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

--[[!

	\brief Set the total number of items expected to be processed by the progress 
	reporter. The message generated from this event is guarenteed to be delivered.

	\param totalCount (number) The total items to be reported.

	\return (void)
	
--]]

function WFProgressReporter:setTotalMustDeliver( totalCount )

	check( 'WFProgressReporter', 'number' )

	WFProgressReporter.setTotal( self, totalCount, true )
	
end

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

--[[!

	\brief Set the number of items processed by the progress reporter.

	\param mustDeliver (optional, boolean) Set to true if the message generated
	from setting the processed count must not be elided.
	
	\param extraInfo (optional, table) Optional info about the processed count.
	
	\return (void)
	
--]]

function WFProgressReporter:setProcessedCount( processedCount, mustDeliver, extraInfo )

	check( 'WFProgressReporter', 'number', '?boolean', '?table' )

	if processedCount ~= self._processedCount then

		log:infof( "ProcessCount updated: %d", processedCount )

		self._processedCount = processedCount

		reportProgress( self, extraInfo, mustDeliver )

	end

end

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

--[[!

	\brief Set the number of errors reported to the progress reporter.

	\return (void)
	
--]]

function WFProgressReporter:setErrorCount( errorCount )

	check( 'WFProgressReporter', 'number' )

	log:infof( "ErrorCount updated: %d", errorCount )

	self._errorCount = errorCount

	reportProgress( self )

end

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

--[[!

	\brief Report a success to the progress reporter. Success notifs reported with this
	API may be elided for late subscribers.

	\param extraInfo (optional, table) Optional info about the success.
	\param mustDeliver (optional, boolean) If set to true, this success will not be elided.
	
	\return (void)
	
--]]

function WFProgressReporter:reportSuccess( extraInfo, mustDeliver )

	check( 'WFProgressReporter', '?table|string', '?boolean' )
	
	log:infof( "Success reported." ) 
	
	mustDeliver = mustDeliver or false
	
	self._processedCount = self._processedCount + 1
	
	reportProgress( self, extraInfo, mustDeliver )
	
end

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

--[[!

	\brief Report multiple successes to the progress reporter. Success notifs reported with this
	API may be elided for late subscribers.

	\param count (number) The number of successes to report.
	\param extraInfo (optional, table) Optional info about the successes.
	\param mustDeliver (optional, boolean) If set to true, this successes will not be elided.
	
	\return (void)
	
--]]

function WFProgressReporter:reportBulkSuccess( count, extraInfo, mustDeliver )

	check( 'WFProgressReporter', 'number', '?table|string', '?boolean' )
	
	log:infof( "Bulk success reported." ) 
	
	mustDeliver = mustDeliver or false
	self._processedCount = self._processedCount + count
	
	reportProgress( self, extraInfo, mustDeliver )
	
end

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

--[[!

	\brief Report a success to the progress reporter that MUST be delivered. Any
	success reported through this API is guarenteed to not be elided.

	\param extraInfo (optional, table) Optional info about the success.

	\return (void)
	
--]]

function WFProgressReporter:reportSuccessMustDeliver( extraInfo )

	check( 'WFProgressReporter', '?table|string' )

	WFProgressReporter.reportSuccess( self, extraInfo, true )
	
end

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

--[[!

	\brief Report a success to the progress reporter that MUST be delivered. Any
	success reported through this API is guarenteed to not be elided.

	\param extraInfo (optional, table) Optional info about the success.

	\return (void)
	
--]]

function WFProgressReporter:reportInfo( extraInfo, mustDeliver )

	check( 'WFProgressReporter', 'table|string' )
	reportProgress( self, extraInfo, mustDeliver )
	
end

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

--[[!

	\brief Report an error to the progress reporter. Errors reported with this
	API may be elided for late subscribers.

	\param extraInfo (optional, table) Optional info about the error.
	\param mustDeliver (optional, boolean) If set to true, the error will not be elided.
	
	\return (void)
	
--]]

function WFProgressReporter:reportError( extraInfo, mustDeliver )

	check( 'WFProgressReporter', '?table|string', '?boolean' )
	
	log:infof( "Failure reported." ) 
	
	mustDeliver = mustDeliver or false
	self._processedCount = self._processedCount + 1
	self._errorCount = self._errorCount + 1
	reportProgress( self, extraInfo, mustDeliver )
	
end

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

--[[!

	\brief Report multiple errors to the progress reporter. Errors reported with this
	API may be elided for late subscribers.

	\param count (number) The number of errors to report.
	\param extraInfo (optional, table) Optional info about the error.
	\param mustDeliver (optional, boolean) If set to true, this error will not be elided.
	
	\return (void)
	
--]]

function WFProgressReporter:reportBulkError( count, extraInfo, mustDeliver )

	check( 'WFProgressReporter', 'number', '?table|string', '?boolean' )
	
	log:infof( "Bulk failure reported." ) 
	
	mustDeliver = mustDeliver or false
	self._processedCount = self._processedCount + count
	self._errorCount = self._errorCount + count
	
	reportProgress( self, extraInfo, mustDeliver )
	
end

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

--[[!

	\brief Report an error to the progress reporter that MUST be delivered. Any
	errors reported through this API are guarenteed to not be elided.

	\param extraInfo (optional, table) Optional info about the error.

	\return (void)
	
--]]

function WFProgressReporter:reportErrorMustDeliver( extraInfo )

	check( 'WFProgressReporter', '?table|string' )

	WFProgressReporter.reportError( self, extraInfo, true )
	
end

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

--[[!

	\brief Mark the progress reporter as complete.

	\return (void)
	
--]]

function WFProgressReporter:done( extraInfo )

	check( 'WFProgressReporter', '?table|string' )
	
	log:infof( "Done" ) 
	
	if extraInfo then
		reportProgress( self, extraInfo, true )
	end
	
	self._isDone = true
	self._status:onCompleted()
	self._nonElidedStatus:onCompleted()
	
end

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

--[[!

	\brief Check to see if the progress reporter has completed.

	\return (bool) True if the progress reporter has completed, false otherwise.
	
--]]


function WFProgressReporter:isDone()

	check( 'WFProgressReporter' )
	
	return self._isDone
	
end

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

function WFProgressReporter:updateProgressWithInfo( progressInfo, mustDeliver )

	check( 'WFProgressReporter', 'table', '?boolean' )

	progressInfo.description = self._description

	if mustDeliver then
		self._nonElidedStatus:onNext( progressInfo )
	else
		self._status:onNext( progressInfo )
	end

end

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

--[[!

	\brief Retrieve the progress stream that fires with progress updates.

	\return (Rx.ReplaySubject< table >) Reports on the progress.
	
--]]

function WFProgressReporter:status()

	check( 'WFProgressReporter' )
	
	return self._nonElidedStatus
	
		:merge( self._status )
	
end

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

return Rx.Class 'WFProgressReporter'( WFProgressReporter )
