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

KSReactiveTimer.lua

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

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

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

--[[

	KSReactiveTimer: Timed event support
	
	function KSReactiveTimer.laterB()
		Returns a behavior that transitions from false to true when the event processor
		gets a chance to process later.
		
	function KSReactiveTimer.timerE( delayTime ) -> timer event stream
		Returns an event stream that will fire the value true after the given delay time.
		Delay times are rounded and grouped so that we can avoid creating too many
		separate timers. If one passes 'later' for delayTime, the event stream fires
		essentially at the same time laterB would fire.
		
	function KSReactiveTimer.repeatingE( delayTime ) -> timer event stream
		Returns an event stream that will fire the value true repeatedly after the
		given delay time. Delay times are rounded and grouped so that we can avoid
		creating too many separate timers. Unlike timerE, 'later' is not an acceptable
		value for the delay time. Note that because of delay time rounding, this should
		not be viewed as a source of reliaby spaced ticks. Rather this is a way to get
		a series of roughly spaced tick events.
		
	function KSReactiveTimer.timedSeqB( value0, delay, value1 )
		Returns a behavior that starts at value0 and transitions to value1 after the indicated
		delay.
	
	function KSReactiveTimer.falseThenTrueB( delay )
		Returns a behavior that starts at false and transitions to true after the indicated
		delay.
	
	function KSReactiveTimer.trueThenFalseB( delay )
		Returns a behavior that starts at true and transitions to false after the indicated
		delay.
		
	function KSReactiveTimer.calmE( e, interval, startTimer )
	
	function KSReactiveTimer.calmB( b, interval, startTimer )
		
]]

--@mark KSReactiveTimer namespace

--[[!
      \class KSReactiveTimer

      \brief This namespace provides support for timed events.
--]]

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

local KSReactive = require 'KSReactive'

local WFArguments = require 'WFArguments'

local KSReactive_variableB = assert( KSReactive.variableB )
local KSReactive_sourceE = assert( KSReactive.sourceE )
local KSReactive_constantB = assert( KSReactive.constantB )

local WFIdle = require 'WFIdle'

local WFTimer = require 'WFTimer'

local WFTimer_afterDelayDo = assert( WFTimer.afterDelayDo )

local WFCloseSupport = require 'WFCloseSupport'

local WFCloseSupport_hasBeenOrWillBeClosed = assert( WFCloseSupport.hasBeenOrWillBeClosed )

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

local function makeCachedIdleTimerFactory( scheduleFn )

	WFArguments.check( 'function' )

	local cachedB = nil
	
	return function()
	
		if not cachedB then
	
			local varB = KSReactive_variableB( false )
			
			local trigger = function()
				cachedB = nil -- We will also need a new behavior
				varB:set( true )
				varB:close()
			end
			
			scheduleFn( trigger )
			
			cachedB = varB:noCloseRelayB()
	
		end
		
		return cachedB
	
	end

end


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

--[[!
	Creates a behavior that transitions from false to true when the worker thread
	gets a chance to process it.

	\return The new behavior object.
]]--

local laterB = makeCachedIdleTimerFactory( WFIdle.doLater )
local asapB = makeCachedIdleTimerFactory( WFIdle.doASAP )
local idleB = makeCachedIdleTimerFactory( WFIdle.onIdleDo )


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

--[[!
       Creates an event stream that fires the value true after a given delay time.
       	Delay times are rounded and grouped so that we can avoid creating too many
       	separate timers.

	\param delay The number of seconds to delay, or 'later' to fire true when
		the worker thread gets a chance to process it.

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

local function timerE( delay )

	if delay == 'later' then
		return laterB()
	end

	if delay == 'asap' then
		return asapB()
	end

	if delay == 'idle' then
		return idleB()
	end

	if delay < 0 then
		error( "KSReactive.timerE: delay must be a positive number", 2 )
	end

	-- FIXME [scouten 2012-03-28]: Not sure about the 0 => 0.1 mapping.
	
	-- Force the delay up to a minimum value
	
	delay = math.max( delay, 0.01 )

	-- NOTE: The quantization that used to occur in this function is now
	-- handled in WFTimer.

	local resultTimerE = KSReactive_sourceE()
	
	WFTimer_afterDelayDo( delay, function()
		if not WFCloseSupport_hasBeenOrWillBeClosed( resultTimerE ) then
			resultTimerE:post( true )
			resultTimerE:close()
		end
	end )
	
	return resultTimerE:autoCloseRelayE()

end


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

--[[!
       Creates an event stream that fires the value true after a given delay time.
       	Delay times are rounded and grouped so that we can avoid creating too many
       	separate timers. The timer will keep firing until the event stream is closed.
		Since delay times are rounded, this should be treated as a way to get
		repeating semi-regular tick events rather than as a way to get evenly spaced
		tick events.

			NOTE: The timer is rescheduled once it has fired so it will not fire
			on exact interval boundaries, as it will accumulate this rescheduling
			delay.

	\param delay The number of seconds to delay.

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

-- We need to hold repeating timers weakly. We do so via one big index to timer
-- map.

local repeatingTimers = setmetatable( { }, { __mode = 'kv' } )
	
local repeatingTimersIndex = 0

local function repeatingE( delay )

	WFArguments.check( 'number' )

	if delay < 0 then
		error( "KSReactive.repeatingE: delay must be a positive number", 2 )
	end

	-- FIXME [scouten 2012-03-28]: Not sure about the 0 => 0.1 mapping.
	
	-- Force the delay up to a minimum value
	
	delay = math.max( delay, 0.01 )

	-- NOTE: The quantization that used to occur in this function is now
	-- handled in WFTimer.
	
	-- We want to weakly hold the timer.
	
	local resultTimerE = KSReactive_sourceE()
	
	local index = repeatingTimersIndex + 1
	
	repeatingTimersIndex = index
	
	repeatingTimers[ index ] = resultTimerE

	local fireTimerE
	
	local fireTimerE_action = function()
		local timerE = repeatingTimers[ index ]
		if timerE and not WFCloseSupport_hasBeenOrWillBeClosed( timerE ) then
			fireTimerE()
			timerE:post( true )
		end
	end
	
	fireTimerE = function()
		WFTimer_afterDelayDo( delay, fireTimerE_action )
	end
	
	fireTimerE()
	
	return resultTimerE:autoCloseRelayE()

end


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

-- @mark timedSeqB, falseThenTrueB

--------------------------------------------------------------------------------
--[[!
       Creates a behavior whose initial value transitions to another value
	after a delay.

	\param value0 The initial value.
	\param delay The number of seconds to delay.
	\param value1 The post-delay value.

	\return The new behavior object.
]]--

local function timedSeqB( value0, delay, value1 )

	return timerE( delay ):constantE( value1 ):holdB( value0 )

end

--------------------------------------------------------------------------------
--[[!
       Creates a behavior whose initial value is false, which transitions to true
	after a delay.

 	\param delay The number of seconds to delay.
 
	\return The new behavior object.
]]--

local function falseThenTrueB( delay )

	return timedSeqB( false, delay, true )

end

--------------------------------------------------------------------------------
--[[!
       Creates a behavior whose initial value is true, which transitions to false
	after a delay.

 	\param delay The number of seconds to delay.
 
	\return The new behavior object.

]]--

local function trueThenFalseB( delay )

	return timedSeqB( true, delay, false )

end


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

-- @mark calmE, calmB

--------------------------------------------------------------------------------
--[[!
       (what does this do?)(how is it different from calmB?)

       \param e	 A timer event-stream object (??)
       \param interval	A number of seconds.
       \param startTimer (boolean) (??)

	\return An event-stream object.
]]--

local function calmE( e, interval, startTimer )

	-- We have a current timer if it hasn't actually fired.
	
	local currentTimerE = nil
	
	local function closeCurrentTimer()
		
		if currentTimerE then
			currentTimerE:close()
			currentTimerE = nil
		end
		
	end
	
	local function startCurrentTimer()

		if not currentTimerE then

			-- Create a timer if necessary. Appended with logic to clear the timer
			-- reference once it fires. Note that since we reveal the map function,
			-- then the reference will get cleared before anything else can see the
			-- event.

			local thisTimerE

			thisTimerE = timerE( interval ):closingMapE( function( ... )
				closeCurrentTimer()
					-- It should be closing anyway after firing, but we want to clean
					-- up our link and we standardize how we do that.
				return ...
			end )

			currentTimerE = thisTimerE

		end

	end
	
	-- Errors clear out the current timer so that a new value won't get
	-- delayed.
	
	local clearForErrorsE = e:rawMapE( function( status, ... )
		if status ~= 'data' then
			closeCurrentTimer()
				-- We don't want to use it anymore. This is a little
				-- bit messy since the next stage is still waiting for
				-- it to fire and its rude to close something in the
				-- middle of the chain. On the other hand, we know that
				-- any subsequent open nodes is a constantE that then
				-- plugs into the mapSwitchE below and will be closed by
				-- the mapSwitchE.
		end
		return 'same'
	end )
	
	-- If we have a timer, then we wait to fire. If not, we pass it through
	-- immediately. Note that errors will automatically pass through this
	-- and will have cleared the timer.
	
	local timerFiredE = clearForErrorsE:closingMapSwitchE( function( ... )
		if currentTimerE then
			return currentTimerE:constantE( ... )
				-- Non-closing on the timer since we may generate multiple events
				-- off of the timer as we receive a series of values.
		else
			return KSReactive_constantB( ... )
		end
	end )
	
	-- Whenever we fire a data value, we start the timer if it wasn't already
	-- started.
		
	timerFiredE = timerFiredE:closingMapE( function( ... )
		startCurrentTimer()
		return ...
	end )
	
	if startTimer then
		startCurrentTimer()
	end
	
	return timerFiredE

end

--------------------------------------------------------------------------------
--[[!
       (what does this do?)(how is it different from calmB?)

       \param b	 A timer behavior object (??)
       \param interval	A number of seconds.
       \param startTimer (boolean) (??)

	\return (??)

]]--

local function calmB( b, interval, startTimer )

	return calmE( b, interval, startTimer ):closingHoldB( b:valueNow() )

end


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

return {
	timerE = timerE,
	repeatingE = repeatingE,
	timedSeqB = timedSeqB,
	falseThenTrueB = falseThenTrueB,
	trueThenFalseB = trueThenFalseB,
	laterB = laterB,
	asapB = asapB,
	idleB = idleB,
	calmE = calmE,
	calmB = calmB
}
