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

WFObjectProxy.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2014 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remains 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 are protected by all applicable
intellectual property laws, including trade secret and copyright laws.
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.lua.threading
local WFCurrentThread = require 'WFCurrentThread'

	 -- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFCloseSupport = require 'WFCloseSupport'


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

local WFCloseSupport_fetchCached = assert( WFCloseSupport.fetchCached )
local check = WFArguments.check
local hasBeenOrWillBeClosed = WFCloseSupport.hasBeenOrWillBeClosed

local kClosed = "< closed >"

local DEBUG_INSTANCE_LOGGING --= 'WFWorkerThread'
	-- Normally this should be set to nil. If you are having trouble tracking down
	-- which proxy instance is being left open, set this to the class name of the
	-- object you're working on.


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

local WFObjectProxyMethods = {

	_proxyClass = false,

	_sharedInstance = false,

	WFCloseSupport.addCloseSupportMethods,

}

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

function WFObjectProxyMethods:__init( proxyClass, sharedInstance )

	self._proxyClass = proxyClass
	self._sharedInstance = sharedInstance

	rawget( sharedInstance, 'private:refsToProxies' )[ self ] = true

end

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

function WFObjectProxyMethods:__tostring()

	if self._sharedInstance then
		return tostring( self._sharedInstance )
	else
		return "(closed proxy instance)"
	end

end

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

function WFObjectProxyMethods:__eq( other )

	if type( other ) == 'table' then

		local otherSharedInstance = rawget( other, '_sharedInstance' )
		return otherSharedInstance == self._sharedInstance

	end

	return false

end

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

function WFObjectProxyMethods:close()

	local logWhenClosed = DEBUG_INSTANCE_LOGGING and rawget( self, '__logWhenClosed' )

	if logWhenClosed then
		print( "Closing instance:" .. rawget( self, '__instanceNumber' ) .." "
					 ..rawget( self, '__realKey' )..":: created via " .. self.__traceback )
	end

	if self._sharedInstance then

		rawget( self._sharedInstance, 'private:refsToProxies' )[ self ] = nil

		local remainingInstance = next( rawget( self._sharedInstance, 'private:refsToProxies' ) )

		if not remainingInstance then

			self._sharedInstance:close()
			rawset( self._sharedInstance, 'private:refsToProxies', kClosed )

		elseif logWhenClosed then

			print( "Remaining proxy (instance "
						 .. rawget( remainingInstance, '__instanceNumber' )
						 .. ") for this object created via "
						 .. remainingInstance.__traceback )

		end

	end

end

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

function WFObjectProxyMethods:relayObject()

	return self._proxyClass( self._proxyClass, self._sharedInstance )

end

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

function WFObjectProxyMethods:isSharedInstanceClosed()

	local target = self._sharedInstance
	
	local isClosed = hasBeenOrWillBeClosed( target )
	
	return isClosed
	
end

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

local function mapTargetToSelf( target, self, ... )

	-- Only handle the simple chaining case. WFVarArgs could handle the more general case.

	if select( '#', ... ) == 1 and target == ( ... ) then
		return self
	end

	return ...

end

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

local function makeForwardingMethod( selector )

	return function( self, ... )

		assert( self, "'self' parameter sent to proxy object is nil" )

		if hasBeenOrWillBeClosed( self ) then

			error( string.format( "Method %s called on closed proxy: %s", 
									selector, self ) )

		end

		local target = self._sharedInstance

		assert( not hasBeenOrWillBeClosed( target ), "Method called on proxy after base object closed" )

		return mapTargetToSelf( target, self, target[ selector ]( target, ... ) )

	end

end

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

function WFObjectProxyMethods:__index( k )

	local result = self._sharedInstance[ k ]

	if result and type( result ) == 'function' then

		result = makeForwardingMethod( k )

		rawset( self, k, result )
			-- Store proxy method in self to avoid running __index metamethod
			-- again for this instance.

	end

	return result

end

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

local function fetchFromCacheAndValidate( cachedObjects, validator, key, ... )

	local result, realKey

	if validator then
		realKey, result = validator( cachedObjects, key, ... )
	end

	result = result or WFCloseSupport_fetchCached( cachedObjects, realKey or key )

	return realKey, result

end


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

local ClassProxyMT = {}

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

function ClassProxyMT:__newindex( k )

	error( "Illegal attempt to write field " .. tostring( k ) .. " of class "
			.. self.__proxy.className )

end

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

function ClassProxyMT:__index( k )

	return self.__proxy.ProxyClass[ k ]

end

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

function ClassProxyMT:__call( key, ... )

	local cachedObjects = self.__proxy.cachedObjects
	local validator = self.__proxy.validator

	local realKey, result = fetchFromCacheAndValidate( cachedObjects, validator, key, ... )

	realKey = realKey or key

	if not result then

		result = self.__proxy.BaseClass( key, ... )
		rawset( result, 'private:refsToProxies', {} )

		cachedObjects[ realKey ] = result

	end

	local proxy = self.__proxy.ProxyClass( self.__proxy.ProxyClass, result )

	if self.__proxy.mustClose then
		WFCloseSupport.mustClose( proxy )
	end

	if self.__proxy.logWhenClosed then

		local instanceNumber = KSIdGenerator.localCounter()

		debug.print( "Creating " .. DEBUG_INSTANCE_LOGGING ..
					 " instance number " .. instanceNumber ..
					 " in thread " .. WFCurrentThread.DEBUG_getThreadName() ..
					 " via " .. debug.traceback() )

		rawset( proxy, '__logWhenClosed', true )
		rawset( proxy, '__traceback', debug.traceback() )
		rawset( proxy, '__instanceNumber', instanceNumber )
		rawset( proxy, '__realKey', realKey )

	end

	return proxy

end

--------------------------------------------------------------------------------
--[[!
	\brief WFObjectProxy is a mechanism for sharing an object with multiple consumers
		via lightweight copies. Each copy has close support. Once all the copies are
		closed, the original instance of the base class is also closed.

	\arg name the name of the class

	\arg BaseClass the Rx.Class definition for the base class implementation. It is
		assumed that this class is constructed with one argument, a value which can
		be used as the cache key for determining when to reuse an instance. You may
		provide additional arguments but doing so is risky, because your additional
		arguments may be ignored if a previous instance matching the first argument
		can be recycled.

	\arg optValidator this is an optional function which can be used to validate
		that an object can be recycled.	The function will be passed the cache of
		object, along with the arguments used to construct the proxy. Return a key
		to use for recycling the objects -- this allows you to combine multiple
		arguments in order to determine how to reuse objects. To avoid an object from
		being recycled anymore, you can remove it from the cache by assigning the
		object for a particular reuse-id to nil.

	\arg mustClose (optional, boolean) true if instances of the underlying base class
		must be closed via WFCloseSupport.mustClose
]]--

local function WFObjectProxy( name, BaseClass, optValidator, optMustClose )

	check( '+string', '_', '?function' )

	local WFObjectProxyMethodsCopy = KSTableUtils.shallowcopy( WFObjectProxyMethods )
		-- URG! Rx.Class, as a side effect, contaminates the prototype argument
		-- passed into it. So we have copied our prototype to allow for reuse.
		-- TODO [scouten 2015-09-15]: Is this still true for Rx.Class? (Was true for
		-- its predecessor.)
	
	local baseClassMT = KSTableUtils.shallowcopy( getmetatable( BaseClass ) )
	baseClassMT.__type = name

	local ProxyClass = Rx.Class( name )( WFObjectProxyMethodsCopy )
	local cachedObjects = KSTableUtils.makeFullyWeak()

	local WrapperClassData = {

		isClassOf = function( self, obj )
			return BaseClass:isClassOf( obj )
				or ProxyClass:isClassOf( obj )
		end,

		__proxy = {
			className = name,
			ProxyClass = ProxyClass,
			BaseClass = BaseClass,
			cachedObjects = cachedObjects,
			validator = optValidator,
			mustClose = optMustClose,
			logWhenClosed = name == DEBUG_INSTANCE_LOGGING,
		},
		
		__metatable = baseClassMT,
		
	}
	
	return setmetatable( WrapperClassData, ClassProxyMT )

end

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

return WFObjectProxy
