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

WFCloseSupport.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2011 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'

	-- WF.core.threading
local WFCurrentThread = require 'WFCurrentThread'
local WFDebugTraceback = require 'WFDebugTraceback'
local WFIdle_lazy = nil

	-- WF.core.utils
local KSUserdataProxy = require 'KSUserdataProxy' -- for the must close logic
local WFArguments = require 'WFArguments'


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

local KSFunctionUtils_memoize = require 'KSFunctionUtils_memoize'

local WFArguments_check = assert( WFArguments.check )

local memoize = assert( KSFunctionUtils_memoize.memoizeFn )
local kForget = assert( KSFunctionUtils_memoize.kForget )

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

-- Do we want to spread the close network basically as soon as something has
-- been touched? This more aggressively gets objects into the willClose state.

local SPREAD_CLOSURE_FAST = true


--============================================================================--
--[[!
	\class WFCloseTarget

	\brief Represents a source or destination for data that can be explicitly closed.

	This is a mix-in class. Use the function \c WFCloseSupport.addCloseSupportMethods
	in the class declaration to add close support to that class.
	
	\todo Add an example of a class declaration.
	
	Any object that derives from \c WFCloseTarget can be explicitly closed by
	calling its <code>:close()</code> method.
	
	A \c WFCloseTarget can have zero or more <b>close targets</b>, which can form
	a directed acyclic graph of objects that depend on this object for their
	lifetime. When a close target object is closed, all objects marked as
	close targets of that object are also recursively closed.
	
	\sa WFCloseSupport
]]--

-- @mark WFCloseTarget class

local closeSupportMethods = {
	-- Note that this name and this method table are merely here for
	-- purposes of API documentation.
}

--------------------------------------------------------------------------------
--[[!
	\brief Closes this object and all of its close targets.
	
	This method can be overridden, in which case it is called once for each
	target object before any of the target objects are closed.
	If an object is already closed, this function does nothing.
]]--

function closeSupportMethods:close()
end

--------------------------------------------------------------------------------
--[[!
	\brief Converts this object so that it cannot be closed.
	
	Sets the <code>:close()</code> method to a no-op and causes
	<code>:hasBeenOrWillBeClosed()</code> to report false for this object.
]]--

function closeSupportMethods:bounceClosure()
end

--------------------------------------------------------------------------------
--[[!
	\brief Adds a target to a list of objects to close when this object closes.
		The target is weakly held.

	\param target The new target object. If not supplied, or a target that
		has already been closed, this call does nothing.
		
	\param noRemove An optional boolean indicating that we have no intention to
		remove this as a target. This will bypass the checks for double-adds.
	
	\return This object (useful for chaining).
]]--

function closeSupportMethods:addCloseTarget( --[[WFCloseTarget]] target,
											 --[[optional boolean]] noRemove )
end

--------------------------------------------------------------------------------
--[[!
	\brief Adds a function to be run when this object is closed.

	\param actionFn The function.
	
	\return This object (useful for chaining).
]]--
	
function closeSupportMethods:addCloseAction( --[[function]] actionFn )
end

--------------------------------------------------------------------------------
--[[!
	\brief Sets a field and assumes ownership of a closeable object.

	\param assignments (table) Mapping showing keys to set to owned objects.
]]--
	
function closeSupportMethods:setOwned( --[[table]] assignments )
end

--============================================================================--
-- @mark WFCloseSupport namespace

--[[!
	\class WFCloseSupport

	\brief This namespace helps you manage the lifetimes of objects by
	  creating classes whose objects can be closed, and which can be associated
	  with dependant objects  (targets) that are automatically closed with it, recursively.

	Typically, you use \c WFCloseSupport.addCloseSupportMethods() as a part of
	a class declaration, and then interact with the methods declared on \c WFCloseTarget.

	
	\sa WFCloseTarget
	
]]--

local WFCloseSupport = {}

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

local function declare() end
declare( closeSupportMethods )

local assert = assert
local type = type
local pairs = pairs
local setmetatable = setmetatable
local rawset = rawset
local rawget = rawget

local DEBUG = _G.DEBUG

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

-- Avoid depdenency on KSTableUtils since WFCloseSupport is supposed to be
-- very primitive.

local weakKeysMT = { __mode = 'k' }
local weakSetMT = { __mode = 'kv' }

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

-- We set the close method for objects bouncing closure to close_nop.

local function close_nop() end

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

-- We maintain a mapping from objects with close targets to the weak set of
-- those targets. This mapping is weak-keyed in the object itself.

-- We maintain a mapping from objects that have been set as close targets to
-- the objects for which they are close targets. This is a weak mapping based
-- on a weak-keyed mapping from close targets to fully-weak sets of close sources.
-- We are safe from cyclic issues because the semi-weak table only leads to weak
-- sets which cannot lead back to the keys.

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

local forwardCloseTargetMap = setmetatable( { }, weakKeysMT )

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

-- We maintain a set of closed objects so that we will only close an object
-- once.

local closedObjects = setmetatable( { }, weakSetMT )

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

-- We maintain a stack of objects to close (and functions to call) so that
-- we can avoid deep recursion when scanning the graph. This also allows us to
-- further reduce pcall overhead in most cases. We use the closedObjects set
-- to track objects that have been queued by setting their status to a magic
-- queued value.

local closeStack = { }
local closeStackTop = 0
local processingCloseStack = false

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

local kQueued = { }
local kClosed = { }

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

-- The following function returns true if the object is not already marked as
-- closed. It also marks the object as closed.

local function closeGuard( obj )

	if not obj then
		return false
	end

	local status = closedObjects[ obj ]
	
	if status == kClosed then
		return false
	end
	
	closedObjects[ obj ] = kClosed
	
	return true

end

--------------------------------------------------------------------------------
--[[!
	\brief Reports whether this object has been closed or is scheduled for closure.

	An object can be scheduled for closure if it is in a chain of close targets
	that has not yet been fully processed.

	\param obj The object to test. If nil, the call returns true.
	
	\return \c true if and only if this object has already been closed, directly
	or indirectly, or if the object is in the queue waiting to be closed.
	This can happen, for example, if an object that marked \c obj was closed, but
	the chain of close targets hasn't been fully processed yet.)

	FIXME [mhamburg 2011-10-03]: The <code>obj == nil</code> behavior is dubious.
]]--

function WFCloseSupport.hasBeenOrWillBeClosed( --[[object]] obj )

	if obj == nil or obj == false then
		return true
	end
	
	if closedObjects[ obj ] == nil then
		return false
	end
	
	if type( obj ) == 'table' and rawget( obj, 'close' ) == close_nop then
		return false
	end
	
	return true

end

local WFCloseSupport_hasBeenOrWillBeClosed = WFCloseSupport.hasBeenOrWillBeClosed

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves a value from a cache, unless it is an object that has been closed.
		If the value is an object that has been closed, removes if from the table
		and returns nil.
	
	\param cache The cache table.
	\param key The key for the value to retrieve.
	
	If <code>cache[ key ]</code> points to an object that has been closed, remove
	that entry from the table and return \c nil.
	
	\return The value of the given key, or nil if that value was a closed object.
]]--

function WFCloseSupport.fetchCached( --[[table]] cache,  --[[value]] key )

	local value = cache[ key ]
	
	if value and WFCloseSupport_hasBeenOrWillBeClosed( value ) then
		cache[ key ] = nil
		value = nil
	end
	
	return value

end

--------------------------------------------------------------------------------
--[[!
	\brief Returns a function that maintains a fully weak table mapping single
		parameter values to single result which must be an object that
		has close support.  If the result is closed then we do not return
		the cached result, but instead run fn( x ) again to generate another.
	
	\param fn the function accepting a single parameter that generates the result to be cached
	
	\see WFCloseSupport.fetchCached
	\return The value of the given key, or nil if that value was a closed object.
]]--

function WFCloseSupport.memoizeFn( --[[function]] fn )
	local memoizer = memoize( fn )
	return function( x )
		local value = memoizer( x )
		if value and WFCloseSupport_hasBeenOrWillBeClosed( value ) then
			memoizer( kForget, x )
			value = memoizer( x )
		end
		return value
	end
end

--------------------------------------------------------------------------------
--[[!
	\brief Construct a method that will construct a memoized subobject for the
	supplied reference object.
	
	\param Function taking the reference object as a parameter. The
	reference object should be valid as a close source.
]]

function WFCloseSupport.memoizeCloseTargetFn( --[[function]] fn )

	return WFCloseSupport.memoizeFn( function( argument )
	
		assert( type( argument ) == 'table',
			"Argument is not a table." )
	
		assert( not WFCloseSupport_hasBeenOrWillBeClosed( argument ),
			"Self has already been closed." )
	
		local result = fn( argument )
		
		argument:addCloseTarget( result )
		
		return result
	
	end )

end

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

local debugEnqueueHook = nil
	-- A hook for seeing when things get enqueued.
	
local enqueueCloseTargets
	-- forward declaration

--------------------------------------------------------------------------------
--[[!
	\brief Enqueues an object for closing, but does not run the close queue.
	Currently used only by \c KSPromises implementation. Please contact MarkH
	before using this API.

	\param target The object to enqueue.
	
	\sa WFCloseSupport.runCloseQueue
]]--

function WFCloseSupport.enqueueTargetForClose( --[[WFCloseTarget]] target )

	if target and not closedObjects[ target ] then
		if debugEnqueueHook then
			debugEnqueueHook( target )
		end
		closeStackTop = closeStackTop + 1
		closeStack[ closeStackTop ] = target
		closedObjects[ target ] = kQueued
		if SPREAD_CLOSURE_FAST then
			return enqueueCloseTargets( target )
				-- tail call since neither of these routines return anything
		end
	end

end

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

local enqueueTargetForClose = WFCloseSupport.enqueueTargetForClose

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

enqueueCloseTargets = function( self )

	-- Note that if enqueue fails due to lack of memory we bail on the whole close
	-- process, but at that point we were in pretty bad shape anyway or we are closing
	-- a ridiculously large network of objects.
	
	local closeTargets = forwardCloseTargetMap[ self ]
	
	if closeTargets then

		forwardCloseTargetMap[ self ] = nil
		
		for target in pairs( closeTargets ) do
			enqueueTargetForClose( target )
		end
		
	end
		
end

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

local closingObject = nil
	-- The object currently being closed. We mark it closed in the event of
	-- an error. This is basically a status value for processCloseStack to
	-- use in the event that doCloseTargetsInStack fails.

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

local WFCloseSupport_closeMethod -- forward declaration

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

-- Close the targets clearing them from the table as we go. This clears first
-- so that in the event of an error (not caught here) we can safely restart.

local function doProcessCloseStack()

	while 0 < closeStackTop do
	
		repeat -- continue support
	
			-- Pop it
		
			local entry = closeStack[ closeStackTop ]
			closeStack[ closeStackTop ] = nil
			closeStackTop = closeStackTop - 1
			
			-- Check to see whether we've already closed and if so don't come
			-- this way again.
			
			local status = closedObjects[ entry ]
			
			if status == kClosed then
				break -- continue
			end
			
			-- Invoke the operation
			
			closingObject = entry
		
			if type( entry ) == 'function' then
				entry()
			else
				local eclose = entry.close or WFCloseSupport_closeMethod
				eclose( entry )
			end
			
			-- We no longer need to remember that this object was in the midst of being closed.
			
			closingObject = nil
			
			-- Mark it as closed though generally this also gets handled in the
			-- close method.
			
			closedObjects[ entry ] = kClosed
			
		until true -- continue support

	end

end

--------------------------------------------------------------------------------
--[[!
	\brief Closes all of the objects that have been queued for closure.

	Currently used only by \c KSPromises implementation. Please contact MarkH
	before using this API.

	\sa WFCloseSupport.enqueueTargetForClose()
	
]]--

function WFCloseSupport.runCloseQueue()

	if processingCloseStack then
		return
	end
	
	processingCloseStack = true
	
	-- Repeatedly pcall into doCloseTargetsInStack. Errors result in closing the
	-- object that was in the process of closing and picking up where we left
	-- off.

	repeat
		local success = pcall( doProcessCloseStack )
		if closingObject then
			closedObjects[ closingObject ] = kClosed
			closingObject = nil
		end
	until success
	
	processingCloseStack = false

end

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

WFCloseSupport.isProcessingCloseNetwork = function()

	return processingCloseStack

end

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

local processCloseStack = WFCloseSupport.runCloseQueue

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

local function supportsClose( target )

	local targetType = type( target )
	
	-- We can't really introspect userdata objects, so we gamble and assume
	-- that a close method exists.
	
	if targetType == 'userdata' then
		return true
	end

	if targetType ~= 'table' then
		return false
	end

	-- We can, to some extent, introspect table objects, so we try to see
	-- if there is a close method. Some objects derive from Rx.Class; some
	-- do not, so this is more than a little tricky.

	if type( rawget( target, 'close' ) ) == 'function' then
		return true
	end

	-- Assumption: If target object has a metatable, it's probably an object.
	-- Ideally, we'd make WFCloseSupport a true mix-in in the sense of Rx.Class,
	-- but there are too many homegrown ways of building objects at the moment.
	-- (KSPromises and KSReactive, I'm looking at you!) If you get an error at
	-- this line of code, odds are you're trying to call :addCloseTarget with
	-- a target that isn't closable.

	if getmetatable( target ) and target:isClosable() then
		return true
	end

	return false

end

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

local function addForwardMapEntry( source, target, noRemove )

	local closeTargets = forwardCloseTargetMap[ source ]
	
	if not closeTargets then
		closeTargets = setmetatable( { }, weakSetMT )
		forwardCloseTargetMap[ source ] = closeTargets
	end
	
	-- Make sure we aren't adding the same object multiple times since that would
	-- make remove unreliable. (The other choice would be to disallow removal.)
	
	if ( not noRemove ) and closeTargets[ target ] then
		debug.print( string.format( "Can only add a close target once: %s -> %s",
			tostring( source ), tostring( target ) ) )
		debug.print( debug.traceback() )
		error( "Can only add a close target once.", 2 )
	end
	
	closeTargets[ target ] = target

end

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

local function removeForwardMapEntry( source, target )

	local closeTargets = forwardCloseTargetMap[ source ]
	
	if closeTargets then
		closeTargets[ target ] = nil
	end

end

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

local function hasForwardMapEntry( source, target )

	local closeTargets = forwardCloseTargetMap[ source ]
	
	return closeTargets and ( closeTargets[ target ] ~= nil )

end

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

local function WFCloseSupport_addCloseTargetMethod(
									--[[WFCloseTarget]] self,
									--[[WFCloseTarget]] target,
									--[[optional boolean]] noRemove )

	if target == nil then
		return self
	end
	
	if not self then
		debug.print( "addCloseTarget: self is not a viable close source:\n" .. debug.traceback() )
		error( "addCloseTarget: self is not a viable close source", 2 )
	end
	
	if closedObjects[ self ] then
		debug.print( "addCloseTarget: source for close has already been closed:\n" .. debug.traceback() )
		error( "addCloseTarget: source for close has already been closed", 2 )
	end
	
	if WFCloseSupport_hasBeenOrWillBeClosed( target ) then
		return self
	end
	
	if not supportsClose( target ) then
		debug.print( "addCloseTarget: target unsupported for close: " .. tostring( target ) .. "\n" .. debug.traceback() )
		error( "addCloseTarget: target unsupported for close", 2 )
	end

	addForwardMapEntry( self, target, noRemove )
	
	return self

end

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

local function hasCloseTarget( self, target )

	if target == nil then
		return false
	end
	
	return hasForwardMapEntry( self, target )

end

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

local function removeCloseTarget( self, target )

	if target == nil then
		return self
	end
	
	removeForwardMapEntry( self, target )
	
	return self

end

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

local function addCloseActionMethod( self, actionFn )

	WFArguments_check( 'table', 'callable' )

	if actionFn then
	
		if closedObjects[ self ] then
			debug.print( "addCloseAction: source for close has already been closed:\n" .. debug.traceback() )
			error( "addCloseAction: source for close has already been closed", 2 )
		end
		
		if closedObjects[ actionFn ] then
			debug.print( "addCloseAction: action for close has already been run or queued:\n" .. debug.traceback() )
			error( "addCloseAction: action for close has already been run or queued", 2 )
		end
		
		-- Wrap the action up inside of a function and hang it off of the root object.
		-- It clears the link when it closes so that anything retained by the
		-- action function can be collected.
		
		local actionObj
		
		actionObj = {
			close = function() actionFn(); rawset( self, actionObj, nil ) end
		}
		
		WFCloseSupport_addCloseTargetMethod( self, actionObj )
		
		rawset( self, actionObj, actionObj )

	end
	
	return self

end

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

local function bounceClosureMethod( self )

	rawset( self, 'close', close_nop )
	
	return self

end

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

local ownedObjects = setmetatable( { }, weakSetMT )

function WFCloseSupport.setOwnedMethod( self, assignments )

	WFArguments_check( 'table', 'table' )
	
	for key, obj in pairs( assignments ) do
	
		local oldObj = self[ key ]
		
		-- If we had the same value at this slot, do nothing
		
		if oldObj ~= obj then
		
			-- If we have an object, then it better not already be owned and we
			-- should add it as a close target.
		
			if obj then
				assert( not ownedObjects[ obj ], "Object has already been owned" )
				self:addCloseTarget( obj )
				ownedObjects[ obj ] = obj
			end
		
			-- Set the field
		
			self[ key ] = obj
		
			-- Close the old object if any
		
			-- FIXME: This test against oldObj isn't conclusive that it has close
			-- support.
		
			if oldObj and hasCloseTarget( self, oldObj ) then
				removeCloseTarget( self, oldObj )
				oldObj:close()
			end

		end

	end

end

local WFCloseSupport_setOwnedMethod = WFCloseSupport.setOwnedMethod

--------------------------------------------------------------------------------
--[[!
	\brief (deprecated) Closes this object. If it has already been closed removes
	it as a close target and marks it as closed to avoid subsequent passes.
	
	Called at the start of the standard close methods.
	The standard usage pattern looks like:
	
\pre
function MyObj:close()

	if not WFCloseSupport.willClose( self ) then
		return
	end

	-- Do the standard close logic because we haven't already
	-- been closed.

end
\pre
	
	\param obj The object to close.

	\return \c false if the object has already been closed,
		\c true if the object will be closed as a result of this call.

	\deprecated This is currently used by the Objective-C bridge code and
		that usage will be reviewed later. Calling the <code>:close()</code>
		method on the close target directly is preferred.
--]]

function WFCloseSupport.willClose( --[[WFCloseTarget]] obj )

	if not closeGuard( obj ) then
		return false
	end
	
	-- Enqueue the close targets for this object. This may have already happened
	-- when we started the close process, but if so we will simply have no close
	-- targets and exit rapidly.
	
	enqueueCloseTargets( obj )
	
	-- Process the close stack. This is known not to fail. It returns quickly if
	-- we're already processing.
	
	processCloseStack()
	
	return true

end

local WFCloseSupport_willClose = WFCloseSupport.willClose

--------------------------------------------------------------------------------
--[[!
	\brief (deprecated) Equivalent of <code>self:close()</code>, except that it can work on
	objects or values that are not \c WFCloseTarget instances. (In that case,
	it is a no-op.)
	
	\deprecated Bypassing the \c Rx.Class system for creating close targets is
		no longer supported. The only remaining use of this syntax is in
		\c KSReactiveBindingTest. Once that is gone, this function will become
		non-public. Use <code>WFCloseSupport.addCloseSupportMethods</code> to
		create a full \c WFCloseTarget instance instead.
]]--

function WFCloseSupport.closeMethod( --[[WFCloseTarget]] self )

	-- By default, when we close an object, we just need to spread that closure to
	-- its dependents (and remove it as a close target from anything where it
	-- currently is a close target). willClose handles all of this work for us.
	-- We wrap it in another function to hide the result of willClose.

	WFCloseSupport.willClose( self )
	
end

WFCloseSupport_closeMethod = WFCloseSupport.closeMethod

--------------------------------------------------------------------------------
--[[!
	\brief Introspection: Is this object closeable?

	This should be considered a hack. A better solution would be to make
	WFCloseSupport a true mix-in class in the sense of Rx.Class and convert
	all closable objects to use that. But that's a task for another day.
]]--

function WFCloseSupport.isClosableMethod()

	return true

end

local WFCloseSupport_isClosableMethod = WFCloseSupport.isClosableMethod

--------------------------------------------------------------------------------
--[[!
	\brief Defines the close-target methods for a new class.

	\param methodTable A table to which to add the function pointers.
		This table can contain an override implementation for the
		\c WFCloseTarget:close() method, but must not contain any
		other method implementations.
	
	\sa WFCloseTarget::close()
]]--

function WFCloseSupport.addCloseSupportMethods( --[[table]] methodTable )

	assert( methodTable.addCloseTarget == nil, "Already had addCloseTarget method" )
	
	assert( methodTable.addCloseAction == nil, "Already had addCloseAction method" )
	assert( methodTable.bounceClosure == nil, "Already had methodTable.bounceClosure method" )
	
	assert( methodTable.setOwned == nil, "Already had methodTable.setOwned method" )
	
	assert( methodTable.isCloseable == nil, "Already had methodTable.isClosable method" )

	methodTable.addCloseTarget = WFCloseSupport_addCloseTargetMethod

	methodTable.addCloseAction = addCloseActionMethod
	
	methodTable.bounceClosure = bounceClosureMethod
	
	methodTable.setOwned = WFCloseSupport_setOwnedMethod

	methodTable.isClosable = WFCloseSupport_isClosableMethod

	local oldClose = methodTable.close
	
	if oldClose then

		methodTable.close = function( self )
		
			-- We need to wrap oldClose in the willClose logic so that we
			-- only close once.
					
			if WFCloseSupport_willClose( self ) then
				oldClose( self )
			end
			
		end

	else

		methodTable.close = WFCloseSupport_closeMethod

	end
	
	return methodTable

end

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

local function close( target )

	if target then
	
		assert( supportsClose( target ), "WFCloseSupport.close: target does not support methods" )
		
		-- Queue up the object and start running the close logic.
		
		enqueueTargetForClose( target )
		
		processCloseStack()
	
	end

end

--------------------------------------------------------------------------------
--[[!
	\brief Intended for debugging use only.
]]--

function WFCloseSupport._debug_setEnqueueHook( --[[function]] hookFn )

	debugEnqueueHook = hookFn

end


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

local gcQueue = nil

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

local function processGCQueue()

	if not gcQueue then
		return false
	end
	
	local q = gcQueue
	
	gcQueue = nil
	
	for i = 1, #q do
		local obj = q[ i ]
		if not WFCloseSupport_hasBeenOrWillBeClosed( obj ) then
			close( obj )
		end
	end
	
	return true
	
end

WFCloseSupport.processGCQueue = processGCQueue

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

local function enqueueFromGC( obj )

	if not gcQueue then
		gcQueue = { }
		if not WFIdle_lazy then
			WFIdle_lazy = require 'WFIdle'
		end
		WFIdle_lazy.onIdleDo( processGCQueue )
	end
	
	gcQueue[ #gcQueue + 1 ] = obj

end

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

-- We create a userdata proxy for the purpose of closing the request when
-- it gets collected or when requested to close. It stores the reference to
-- the request within the environment table.

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

local objectCloserFactory, objectCloserTest, _, _, objectCloserMT =
	KSUserdataProxy.makeFactory( "WFCloseSupport.mustClose" )

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

local threadName

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

local function objectCloser_doClose( self, fromGC )

	local proxy, env = objectCloserTest( self )
	
	if proxy and env then

		local object = env._object

		if object then

			env._object = nil

			if not WFCloseSupport_hasBeenOrWillBeClosed( object ) then

				if fromGC and DEBUG then

					if not threadName then
						threadName = require 'WFCurrentThread'.DEBUG_getThreadName()
					end
					
					local label = env._label
					if label then
						debug.print( string.format( "Closing target in thread %s due to GC (%s): %s",
									 threadName, tostring( label ), tostring( object ) ) )
					else
						debug.print( string.format( "Closing target in thread %s due to GC: %s",
									 threadName, tostring( object ) ) )
					end

					local creationBacktrace = env._creationBacktrace
					if creationBacktrace then
						debug.print( "Object was created at: ", creationBacktrace )
					end

				end

				if fromGC then
					enqueueFromGC( object )
				else
					close( object )
				end

			end

		end

	else
		debug.print( "Object closer failed type test" )
	end

end

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

function objectCloserMT.__gc( self )
	objectCloser_doClose( self, true )
end

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

-- We also want to allow explicit closure.

objectCloserMT.__index = {
	close = function( self ) objectCloser_doClose( self, false ) end
}

--------------------------------------------------------------------------------
--[[!
	\brief Creates a user-data object that will close a given object when it gets
	garbage collected.
	
	The \c mustClose() logic is based on creating an object with a \c __gc metamethod
	that closes the target object. We don't actually have to just attach this to the
	target object and instead can create such a closer object "free-floating" such
	that we can either close it via the closer or via the closer getting collected.
	Again, be cautious about weak tables since those won't keep such closer objects
	from getting invoked. Note also that a closer represents a strong link to the
	object and hence hanging onto the closer hangs onto the object.

	\param object The object to be closed on GC.
	\param label A name for the closer object.
	\param creationBacktrack If supplied a backtrace. Otherwise one will be captured
			in debug builds.
]]--

function WFCloseSupport.makeCloser(
					--[[WFCloseTarget]] object,
					--[[string]] label,
					--[[string]] creationBacktrace )
									
	if not WFIdle_lazy then
		WFIdle_lazy = require 'WFIdle'
	end
	
	if DEBUG and not creationBacktrace then
		creationBacktrace = WFDebugTraceback.traceback { 'WFCloseSupport.lua', 'KSPromises.lua', 'Class.lua' }
	end
	
	return objectCloserFactory {
				_object = object,
				_label = label,
				_creationBacktrace = creationBacktrace
			}

end

local makeObjectCloser = WFCloseSupport.makeCloser

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

local rootCloseClass = Rx.Class 'WFRootCloseTarget' {
	WFCloseSupport.addCloseSupportMethods,
}

local rootCloseObject = rootCloseClass()

--------------------------------------------------------------------------------
--[[!
	\brief Forces an object to be closed rather than simply GC'd.
	
	Use sparingly, but it can be helpful. Note that it can be dangerous when used
	with weak tables since the finalizer will get invoked when the object is
	only weakly referenced so it essentially undermines the weak table usage.
	Furthermore, because the object will be referenced from the closer object
	(see \c makeCloser()), it may not actually get removed from the weak table and
	you need to check it with \c hasBeenOrWillBeClosed() or simply use
	\c fetchCached().

	\param obj The \c WFCloseTarget object to be closed at thread termination.
]]--

function WFCloseSupport.mustClose( --[[WFCloseTarget]] obj )

	assert( type( obj ) == 'table' )
	
	local closer = rawget( obj, 'private:mustClose' )
	
	if not closer then
		closer = makeObjectCloser( obj, 'mustClose' )
		rawset( obj, 'private:mustClose', closer )
	end
	
	WFCloseSupport.mustCloseAtThreadTermination( obj )
	
	return closer

end

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

WFCloseSupport.makeCloser = makeObjectCloser

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

local threadHasClosed = false

--------------------------------------------------------------------------------
--[[!
	\brief Forces an object to close when the thread is terminated, if it has
		not been closed or GC'd earlier.
	
	This has less overhead than \c WFCloseSupport.mustClose.
	Recommended when you need a deterministic close sequence for objects.
	
	\param obj The \c WFCloseTarget object to be closed at thread termination.
--]]

function WFCloseSupport.mustCloseAtThreadTermination( --[[WFCloseTarget]] obj )

	assert( type( obj ) == 'table' )

	-- FIXME [scouten 2015-11-20]: If this call occurs after the thread gets its
	-- termination notice, the attempt to add obj as a close target will fail.
	-- For now, we silently ignore this condition, but looking for a better
	-- way to handle this.

	if not threadHasClosed then
		rootCloseObject:addCloseTarget( obj )
	end

end

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

WFCurrentThread.registerTerminateHandler( function()

	threadHasClosed = true

	rootCloseObject:close()

end )

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

return WFCloseSupport
