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

WFRateLimiters.lua

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

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

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

local KSPromises = require 'KSPromises'
local WFCloseSupport = require 'WFCloseSupport'
local WFTimer = require 'WFTimer'

local pcall = pcall
local next = next

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

local kMaxTaskTime = 15
	-- Don't let tasks block work for more than 15 seconds

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

local weakMT = { __mode = 'kv' }

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

local function makeTaskPool()

	return {
	
		n = 0,
		
		tasks = setmetatable( { }, weakMT )
	
	}

end

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

-- Add a task to a pool. Returns true if this was new task.

local function taskPool_addTask( pool, taskKey )

	if pool.tasks[ taskKey ] ~= nil then
		return false
	end
	
	pool.tasks[ taskKey ] = taskKey
	
	pool.n = pool.n + 1
	
	return true

end

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

-- Remove a task from a pool.

local function taskPool_removeTask( pool, taskKey, checkEmpty )

	if pool.tasks[ taskKey ] ~= nil then
	
		pool.tasks[ taskKey ] = nil
		pool.n = pool.n - 1
		
	elseif checkEmpty and 0 < pool.n then
	
		-- Check to see whether there are any items outstanding or whether
		-- it has all been GC'd
	
		if next( pool.tasks ) == nil then
			pool. n = 0
		end

	end

end

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

-- Standard methods for all rate limiters built on the unique methods for
-- enqueue, start, and finish.

local kStandardMethods = { }

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

function kStandardMethods:enqueueP( fnP )

	return KSPromises.suspendLimbo( function()
		local p, r = KSPromises.promiseP( true )
				
		local function trigger()
			if not WFCloseSupport.hasBeenOrWillBeClosed( r )
				and r:canResolvePromise()
			then
				self:startedTask( r )
				local rp = KSPromises.argTupleP( fnP() )
				rp = rp:observeP( function() self:finishedTask( r ) end )
				r( rp )
			end
		end
		
		self:enqueueTask( trigger )
		
		return p
	end )

end

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

local function makeRateLimiter( makeQueue, maxSimultaneous, otherMethods )

	local enqueue, dequeue = makeQueue()
	
	local outstandingTasks = makeTaskPool()

	local startingActions = false
	
	local function startActions( self )
	
		if startingActions then
			return
		end
		
		startingActions = true
		
		while outstandingTasks.n < maxSimultaneous do

			local fn = dequeue()

			if not fn then
				break
			end
			
			pcall( fn )
			
		end
		
		startingActions = false
		
	end
	
	local result = { }
	
	for k, v in pairs( kStandardMethods ) do
		result[ k ] = v
	end
	
	if otherMethods then
		for k, v in pairs( otherMethods ) do
			result[ k ] = v
		end
	end
	
	result.enqueueTask = function( self, fn )

		if not fn then
			return
		end
		
		enqueue( fn )
		
		startActions( self )
	
	end
	
	local function didFinishTask( self, key, fromTimer )
	
		-- Remove the task from the pool
	
		taskPool_removeTask( outstandingTasks, key, fromTimer )
		
		-- Try to start actions
		
		startActions( self )
	
	end
	
	result.startedTask = function( self, key )
	
		if taskPool_addTask( outstandingTasks, key ) then
		
			-- This is a new task, set up timer to kill it's ability to rate limit
		
			WFTimer.afterDelayDo( kMaxTaskTime, function()
				didFinishTask( self, key, true )
			end )

		end

	end

	result.finishedTask = function( self, key, fromTimer )
	
		didFinishTask( self, key, false )

	end
	
	return result

end

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

-- TODO [mhamburg 2013-06-14 via scouten]: This could use some API documentation.
-- It's not readily apparent how to use this function.

local function makeDualRateLimiters( makeQueue, maxSimultaneous, otherMethods )

	local enqueueHigh, dequeueHigh = makeQueue()
	local enqueueLow, dequeueLow = makeQueue()

	local resultHigh = { }
	local resultLow = { }
	
	local outstandingHighTasks = makeTaskPool()
	local outstandingLowTasks = makeTaskPool()
		
	local startingActions = false
	
	local function startActions()
	
		if startingActions then
			return
		end
		
		startingActions = true
		
		-- We will start high priority tasks if we haven't hit the cap.
		
		while outstandingHighTasks.n < maxSimultaneous do

			local highFn = dequeueHigh()
			
			-- We only start low priority tasks if we haven't got a high priority
			-- task and we have no outstanding high priority tasks and the number
			-- of low priority tasks is under the threshold.
			
			local lowFn =
				not highFn
				and ( outstandingHighTasks.n == 0 )
				and ( outstandingLowTasks.n < maxSimultaneous )
				and dequeueLow()
			
			local taskFn = highFn or lowFn

			if not taskFn then
				break
			end
			
			pcall( taskFn )
			
		end
		
		startingActions = false
		
	end
	
	local function makeEnqueueTask( enq )
	
		return function( self, fn )
		
			if not fn then
				return
			end
			
			enq( fn )
			
			startActions()

		end

	end
	
	local function makeStartFinish( taskPool )
	
		local function didFinish( key, fromTimer )
		
			taskPool_removeTask( taskPool, key, fromTimer )
		
			startActions()
		
		end
		
		local function startedTask( self, key )
		
			if taskPool_addTask( taskPool, key ) then
				WFTimer.afterDelayDo( kMaxTaskTime,
					function() didFinish( key, true ) end )
			end
		
		end
		
		local function finishedTask( self, key )
		
			didFinish( key, false )
		
		end
		
		return startedTask, finishedTask
	
	end
	
	for k, v in pairs( kStandardMethods ) do
		resultHigh[ k ] = v
		resultLow[ k ] = v
	end
	
	if otherMethods then
		for k, v in pairs( otherMethods ) do
			resultHigh[ k ] = v
			resultLow[ k ] = v
		end
	end
	
	resultHigh.enqueueTask = makeEnqueueTask( enqueueHigh )
	resultHigh.startedTask, resultHigh.finishedTask
					= makeStartFinish( outstandingHighTasks )
	
	resultLow.enqueueTask = makeEnqueueTask( enqueueLow )
	resultLow.startedTask, resultLow.finishedTask
					= makeStartFinish( outstandingLowTasks )
	
	return resultHigh, resultLow

end

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

return {
	makeRateLimiter = makeRateLimiter,
	makeDualRateLimiters = makeDualRateLimiters,
}

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