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

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

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

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

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

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


--============================================================================--
--[[
	\brief Creates an Observable that can retry an operation a finite number of
		times with an optional delay in the event of an error.

	\param args (table)

		- \c maxAttempts (number) maximum # of times to attempt the operation
		- \c retryDelay (number) how many seconds to wait between attempts
			(non-integer values are allowed); if omitted, retry is immediate

		- \c factory (function) returns an Observable for a single attempt at
			this operation. It receives one argument, which is a counter of the
			number of attempts (starting from 1).

		- \c retryFilter (optional, function) this function gets called with the
			onError value from any Observable created by \c factory, above,
			unless the \c maxAttempts limit would disallow a retry. If the
			filter function returns a true value, another attempt is made
			at the operation (\c factory is called again, etc.). If the filter
			function returns \c false or \c nil, the original error is passed
			through (\c maxAttempts is thus ignored). If this function is not
			provided, all operations are tried up to the \c maxAttempts limit.
			This function must provide an immediate result. The function may
			also return a second value (number) to signify the retryDelay in
			seconds, if provided it will override the retryDelay parameter above
			if it has a larger value.
			
		- \c progressiveBackoffDelay (optional, number) The number of seconds
			wait in between retry attempts. This value scales up with the number
			of retries: actual delay = progressiveBackoffDelay * ( retry count - 1 )

		- \c maximumRetryDelay (optional, number) The maximum amount of time to
			wait in between retry attempts. Can be used in conjuction with the 
			progressiveBackoffDelay to define an upper boundary on the retry delay.

		- \c scheduler (optional, Rx.Scheduler) allows you to specify a scheduler
			on which to run this operation; in general, this is only useful for
			testing. If omitted, defaults to WFRxScheduler.sharedScheduler().

	\return Observable combines the results of all attempts, omitting errors,
		except for errors observed on the final attempt
]]--

return function( args )

	WFArguments.check( 'table' )

	local factory = args.factory
	if type( factory ) ~= 'function' then
		error( "WFRxRetryObservable: factory must be a function", 2 )
	end

	local maxAttempts = args.maxAttempts or 1
	if type( maxAttempts ) ~= 'number' or maxAttempts < 1 then
		error( "WFRxRetryObservable: maxAttempts must be a positive number", 2 )
	end

	local retryDelay = args.retryDelay
	if retryDelay ~= nil
		and ( type( retryDelay ) ~= 'number' or retryDelay <= 0 )
	then
		debug.print( "WFRxRetryObservable: retryDelay should be a positive number: ignoring" )
		retryDelay = nil
	end

	local retryFilter = args.retryFilter
	if retryFilter ~= nil and type( retryFilter ) ~= 'function' then
		error( "WFRxRetryObservable: retryFilter must be a function", 2 )
	end

	local progressiveBackoffDelay = args.progressiveBackoffDelay
	if progressiveBackoffDelay ~= nil
		and ( type( progressiveBackoffDelay ) ~= 'number' or progressiveBackoffDelay <= 0 )
	then
		debug.print( "WFRxRetryObservable: progressiveBackoffDelay should be a positive number: ignoring" )
		progressiveBackoffDelay = nil
	end

	local maximumRetryDelay = args.maximumRetryDelay
	if maximumRetryDelay ~= nil
		and ( type( maximumRetryDelay ) ~= 'number' or maximumRetryDelay <= 0 )
	then
		debug.print( "WFRxRetryObservable: maximumRetryDelay should be a positive number: ignoring" )
		maximumRetryDelay = nil
	end

	local count = 0

	return Observable.just( true )

		-- Use a flatMapLatest so we can re-issue a new request
		-- via the retryWhen call below. This is neccessary because
		-- some Observables can not be restarted by re-subscribing to them.

		:flatMapLatest( function()

			count = count + 1

			local result = factory( count )

			if result ~= nil and not Rx.Observable:isClassOf( result ) then
				error( string.format( "factory function %s returned %s, should have been Observable",
									  tostring( result ), tostring( factory ) ) )
			end

			if result == nil then
				return Observable.empty()
			end

			return result

		end )

		-- :retryWhen: If the observable returned from the factory method above
		-- completes without error, we just pass it through. If it raises an error,
		-- we'll restart it via the clause below.

		:retryWhen( function( errors )

			local retryCount = 0

			return errors

				:flatMapLatest( function( ... )

					retryCount = retryCount + 1

					if retryCount >= maxAttempts then

						-- We've tried the maximum number of times.
						-- Give up and pass the error through.

						return Observable.throw( ... )

					end

					-- Another attempt would be permitted. Let's see if filter
					-- wants to retry.

					local delayForThisRetry = retryDelay

					if retryFilter then

						local retry, delayFromFilter = retryFilter( ... )

						if not retry then
							return Observable.throw( ... )
						end

						if type( delayFromFilter ) == 'number' then
							delayForThisRetry = math.max( delayFromFilter, retryDelay or 0 )
						end

					end

					if not delayForThisRetry and progressiveBackoffDelay then
					
						delayForThisRetry = progressiveBackoffDelay * ( retryCount - 1 )
						
					end
					
					if maximumRetryDelay then
					
						delayForThisRetry = math.min( maximumRetryDelay, delayForThisRetry )
					
					end
					
					-- OK, filter signed off on it. Delay or immediate?

					if delayForThisRetry then

						-- A retry delay was specified. Wait the designated time,
						-- then fire a retry event.

						return Observable.timer( delayForThisRetry, args.scheduler or WFRxScheduler.sharedScheduler() )

					else

						-- No retry delay was specified. Try again immediately.

						return Observable.just( true )

					end

				end )

		end )

end
