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

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

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


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

-- @mark WFLimbo namespace

--[[!

    \class WFLimbo
    
    \brief This namespace provides a standard mechanism for tracking objects that are expected
to be claimed in some way at a later point.

A limbo pool is a set of objects that have been created but not yet claimed.
An object in a limbo pool is associated with a handler to process
it if it is still in the pool when that pool is terminated.
Limbo pools can be nested. This namespace provides functions
to manage the pools and their contents, and process the contained objects.

]]--

--[[

Provides the following functions:

function WFLimbo.addToLimbo( obj, handler )
	Adds obj to the current pool of limbo'd objects. If obj is not removed
	from limbo before the pool is terminated, the pool logic will invoke
	handler( obj, forSuccess, traceback ). forSuccess indicates whether
	the pool is going away in response to success or an error. The traceback
	is an optionally captured traceback from the point at which the object
	was added to limbo. It is an error to call this if obj is already in
	a limbo pool, but doing so will just result in a console log warning and
	the object being left in the original limbo pool. If there is no current
	pool, one will be created and queued to be terminated by a task run at
	a later point on the message processor.
	
function WFLimbo.removeFromLimbo( obj )
	If obj is currently in a limbo pool, remove it.
	
function WFLimbo.withPoolDo( fn, ... )
	Evaluate fn( ... ) in a new limbo pool. The pool is terminated when this
	function exits. This returns the value of fn( ... ). Any objects in
	the current limbo pool that are amongst the function results will be
	moved up the stack to the next limbo pool.
	
function WFLimbo.processCurrentLimboPool( forSuccess )
	Process the items in the current limbo pool now and replace the limbo
	pool with a new limbo pool.
	
function WFLimbo.processLimboPoolIfRoot()
	Process the current limbo pool if it is the root pool -- i.e., we aren't
	nested within other pools. Returns true if it did process (or there was
	no root pool).

]]--
--============================================================================--

local WFLimbo = { }

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

local setmetatable = setmetatable
local assert = assert
local pairs = pairs
local pcall = pcall
local next = next
local debug_print = debug.print
local debug_traceback = debug.traceback
local string_format = string.format

local WFArguments = require 'WFArguments'
local WFArguments_check = assert( WFArguments.check )

local WFIdle_lazy = nil
	-- We need this to be lazy because we may need to import recursively (ugh)

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

local LOG_LIMBO_TRACEBACKS = _G.DEBUG
	-- Do we want to log limbo tracebacks

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

local fullyWeakMT = { __mode = 'kv' }

local obj2pool = setmetatable( { }, fullyWeakMT )
	-- A mapping from objects to the pools containing them
	
local obj2traceback = setmetatable( { }, fullyWeakMT )
	-- A mapping from objects to the tracebacks for when they were added to limbo

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

local gCurrentPool = nil
	-- The current limbo pool
	
local gRootPool = nil
	-- The current root pool
	
--------------------------------------------------------------------------------

local queueProcessCallback
	-- forward declaration

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

local function processCallback()
	if not WFLimbo.processLimboPoolIfRoot() then
		debug_print(
			"Called at later time to process the limbo pool but it isn't the root pool." )
		queueProcessCallback()
	end
end

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

queueProcessCallback = function()
	
	if not WFIdle_lazy then
		WFIdle_lazy = require 'WFIdle'
	end
	
	WFIdle_lazy.laterP():whenResolvedDo( processCallback ):runUntilDone()
	
end

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

local function addToCurrentPool( obj, handler )

	if gCurrentPool == nil then
		gCurrentPool = { [ obj ] = handler }
		gRootPool = gCurrentPool
		queueProcessCallback()
	else
		gCurrentPool[ obj ] = handler
	end
	
	obj2pool[ obj ] = gCurrentPool

end

--------------------------------------------------------------------------------
--[[!
	When called in the context of a \c withPoolDo() function, adds an object
	to the current limbo pool. If the object is already
	in the pool, this call reports a console log warning and
       	the object remains in the pool.
       	
       	If called in another context, there is no current pool, so
       	the call creates a root pool and adds this object to it. The
       	new pool is queued to be terminated by a task run at a later point
       	in the thread. If a root pool exists that has not yet been terminated,
	it is re-used.

	\param obj The object to add.
	\param handler The function for handling this object if it is still
		in the pool when the pool is terminated.

		<code>handler( obj, forSuccess, traceback )</code>.
		<ul><li>\c forSuccess is \c false or nil if
       			the pool is being terminated in response to an error.
			Otherwise, it is being terminated on successful completion.
		<li>\c traceback is an optionally captured traceback from
			the point at which the object was added to the pool.</li> </ul>
]]--

function WFLimbo.addToLimbo( --[[object]] obj, --[[function]] handler )

	if not obj then
		return
	end

	WFArguments_check( 'table | userdata', 'callable' )
	
	if obj2pool[ obj ] then
		debug_print( string_format(
			"Tried to add an object (%s) to limbo that was already added at %s",
			tostring( obj ),
			( obj2traceback[ obj ] or "(no traceback recorded)" ) ) )
		return
	end

	if LOG_LIMBO_TRACEBACKS then
		obj2traceback[ obj ] = debug_traceback()
	end
	
	addToCurrentPool( obj, handler )

end

--------------------------------------------------------------------------------
--[[!
 	Removes an object from any limbo pool it is in.

	\param obj The object.
]]--

function WFLimbo.removeFromLimbo( --[[object]] obj )

	if not obj then
		return
	end

	local pool = obj2pool[ obj ]
	
	obj2traceback[ obj ] = nil
	
	if not pool then
		return
	end
	
	pool[ obj ] = nil
	obj2pool[ obj ] = nil

end

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

local function doProcessPool( pool, forSuccess )

	for obj, handler in pairs( pool ) do
		pool[ obj ] = nil
		local traceback = obj2traceback[ obj ]
		obj2traceback[ obj ] = nil
		obj2pool[ obj ] = nil
		handler( obj, forSuccess, traceback )
	end

end

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

local function processPool( pool, forSuccess )

	if not next( pool ) then
		return
	end
	
	while not pcall( doProcessPool, pool, forSuccess ) do end

end

--------------------------------------------------------------------------------
--[[!
	Terminates the current limbo pool, processing all remaining objects
	by calling their associated termination handlers.
	Removes the reference to the old pool. If this is also the root pool,
	clears everything; otherwise installs a new pool.

	\param forSuccess  (boolean) \c true if the pool is being terminated
		in response to success, \c false or nil if because of error.

]]--

function WFLimbo.processCurrentLimboPool( --[[boolean]] forSuccess )

	local oldPool = gCurrentPool
	
	if not oldPool then
		return
	end
	
	-- Remove the reference to the old pool. If this is also the root pool,
	-- then clear everything. If this is not the root pool, then install a
	-- new pool.
	
	if gRootPool == oldPool then
		gCurrentPool = nil
		gRootPool = nil
	else
		gCurrentPool = { }
	end
	
	forSuccess = ( forSuccess and true ) or false
	
	processPool( oldPool, forSuccess )

end

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

--[[!
	Terminates this limbo pool if it is the root pool, processing all remaining objects
	by calling their associated termination handlers.

	\return (boolean) \c true if this was the root pool and was processed successfully,
		or if there was no root pool.
]]--

function WFLimbo.processLimboPoolIfRoot()

	if not gRootPool then
		if gCurrentPool then
			debug_print( "Current limbo pool exists but root pool is nil" )
		end
		return true
	end

	local currentPool = gCurrentPool
	
	if currentPool ~= gRootPool then
		return false
	end
	
	if not currentPool then
		debug_print( "Root limbo pool exists but current pool is nil" )
		return false
	end
	
	gCurrentPool = nil
	gRootPool = nil
	
	processPool( currentPool, true )
	
	return true

end

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

local function pcall2call( success, ... )
	if success then
		return ...
	end
	error( ( ... ) ) -- FIXME: Figure out how many levels up to go here
end

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

local function finish_withPoolDo( oldPool, success, ... )

	-- Pop back up the pool stack

	local terminatingPool = gCurrentPool
	
	gCurrentPool = oldPool
	
	-- On successes, we need to move any results up out of the terminating pool and
	-- on to the next level.
	
	if success and ( next( terminatingPool ) ) then
		local n = select( '#', ... )
		for i = 1, n do
			local obj = select( i, ... )
			local handler = terminatingPool[ obj ]
			if handler then
				terminatingPool[ obj ] = nil
				addToCurrentPool( obj, handler )
			end
		end
	end
	
	-- Process the terminating pool
	
	processPool( terminatingPool, success )
	
	-- Return the final result
	
	return pcall2call( success, ... )

end

--------------------------------------------------------------------------------
--[[!
	Evaluates a function in a new limbo pool, which is terminated when the
	function exits. If the function calls \c addToLimbo(), the object is
	added to this pool.
	
	Any objects that are returned from the function are moved up
	the stack to the next limbo pool.
	
	<em>Is this right? what is the "next" pool?</em>

		\param fn The function to execute.
		\param ... The function parameters.

		\return The result of the function call.
]]--

function WFLimbo.withPoolDo( --[[function]] fn, ... )

	local oldPool = gCurrentPool
	
	gCurrentPool = { }
	
	return finish_withPoolDo( oldPool, pcall( fn, ... ) )

end

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

return WFLimbo
