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

WFCatModelForwarder.lua

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

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

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

local Rx = require 'Rx'

	-- WF.core.models
local WFLuaModelsImp = require 'WFLuaModelsImp'

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local WFArguments = require 'WFArguments'
local _ = require 'WFArguments_uuid'
local WFLogger = require 'WFLogger'

local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local WFRxScheduler = require 'WFRxScheduler'

local ReplaySubject = Rx.ReplaySubject
local Observable = Rx.Observable

local runUntilDone = WFRxRunUntilDone
local check = assert( WFArguments.check )

local logger = WFLogger 'WFCatModelForwarder'

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

local sParentModels = {}

logger:debugf( "Inited parent models" )

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

local _deliverAllValuesCookie = ':deliverAllValues:79923eb153da15e6bc1056ab15bf02ed:'
	-- Hacky way to communicate the deliverAllValues flag across processes
	-- and/or threads. Yes, it's evil, but it was deemed more expedient than
	-- introducing a new communications channel.


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

local kNamespace = 'n'
local kFunction = 'f'
local kCommand = 'c'
local kParentModel = 'p'
local kSubModel = 's'
local kParams = 'a'
local kNArgs = '#'
local kModelGuid = 'g'

local function makeModelParams( namespace, func, ... )
	return {
		[kNamespace] = namespace,
		[kFunction] = func,
		[kNArgs] = select( '#', ... ),
		[kParams] = { ... }
	}
end

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

local function makeModelParamsWithParent( parentModel, subModel, ... )
	return {
		[kParentModel] = parentModel,
		[kSubModel] = subModel,
		[kNArgs] = select( '#', ... ),
		[kParams] = { ... }
	}
end

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

local function validateModelParams( modelParams )
	local nextModelParams = modelParams
	while nextModelParams do
		local params = nextModelParams
		nextModelParams = params[kParentModel]
		assert( params[kModelGuid], 'All models have guid' )
		if nextModelParams then
			-- validate parentModel based subModels have a func field
			assert( params[kSubModel], 'subModel must include field "subModel"' )
			assert( params[kNamespace] == nil, 'subModels do not include namespace' )
		else
			assert( params[kNamespace], 'Root models must include namespace' )
			assert( params[kFunction], 'Root models must include func' )
		end
	end
end

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

local function buildAndStoreStub( modelParams, maybeParentStub )

	local asModelParams
	local name
	if maybeParentStub then
		asModelParams = maybeParentStub.model:_openModel( modelParams[kSubModel], unpack( modelParams[ kParams ], 1, modelParams[kNArgs] ) )
		name = maybeParentStub.name .. "/" .. modelParams[kSubModel]
	else
		asModelParams = require( modelParams[kNamespace] )[ modelParams[kFunction] ]( unpack( modelParams[kParams], 1, modelParams[kNArgs] ) )
		name = modelParams[kNamespace] .. "." .. modelParams[kFunction]
	end
	
	local priority = asModelParams.priority

	local deliverAllValues = asModelParams.deliverAllValues
	
	local model = WFLuaModelsImp.asModel( asModelParams, name )

	local deliverAllValuesCookieStream = ReplaySubject( 1 )

	local mostRecentModelValueStream = ReplaySubject( 1 )

	local modelIsClosedStream = ReplaySubject( 1 )

	-- set up a stream that is guaranteed to send _deliverAllValuesCookie at
	-- most once interleaved with the most recent values from model:value()
	local modelValueOrDeliverAllValuesCookieStream=
			Rx.Observable.merge(
				deliverAllValuesCookieStream:take( 1 ),
				mostRecentModelValueStream )

	local stub = {
		name = name,
		model = model,
		priority = priority,
		deliverAllValues = deliverAllValues,
		result = modelValueOrDeliverAllValuesCookieStream,
		modelIsClosedStream = modelIsClosedStream,

		_postData = function( _, data )
			mostRecentModelValueStream:onNext( data )
		end,
		_closeModelStub = function( self )
			modelIsClosedStream:onNext( true )
			self.modelIsClosedStream = nil
			deliverAllValuesCookieStream:dispose()
			mostRecentModelValueStream:dispose()
			local myModel = self.model
			if myModel then
				myModel:_closeLuaModel()
				self.model = nil
			end
			deliverAllValuesCookieStream = nil
			mostRecentModelValueStream = nil
			self.result = nil
		end,
		_postDone = function()
			mostRecentModelValueStream:onCompleted()
			deliverAllValuesCookieStream:onCompleted()
		end,
		_postError = function( _, err )
			-- TODO [arahn 2015-09-17]: Or do we send data with a status??
			-- e.g. are models expected to recover from errors?
			mostRecentModelValueStream:onError( err )
		end,
		_setDeliverAllValues = function()
			if deliverAllValuesCookieStream  then
				deliverAllValuesCookieStream:onNext( _deliverAllValuesCookie )
			end
		end,

	}

	model:_bindToModelStub( stub )

	local modelGuid = modelParams[kModelGuid]
	logger:debugf("Server opened model (%s) : %s", modelGuid, name )
	sParentModels[ modelGuid ] = stub

	return stub

end

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

local function buildModelStubAfterFindingOrBuildingParents( modelParams )

	local lastStub = sParentModels[ modelParams[kModelGuid] ]
	if lastStub then
		return lastStub
	end
	local modelsNeedingConstruction = { modelParams }
		-- while iterating it is the parentStub, but at the end it is our result
	while #modelsNeedingConstruction > 0 do
		local nextModel = table.remove( modelsNeedingConstruction, #modelsNeedingConstruction )
		if nextModel[kParentModel] then
			if not lastStub then
				lastStub = sParentModels[ nextModel[kParentModel][kModelGuid] ]
			end
			if not lastStub then
				-- add this and parent model to queue and repeat.
				table.insert( modelsNeedingConstruction, nextModel )
				table.insert( modelsNeedingConstruction, nextModel[kParentModel] )
			else
				lastStub = buildAndStoreStub( nextModel, lastStub )
			end
		else
			lastStub = buildAndStoreStub( nextModel )
		end
	end

	return lastStub
end

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

-- @mark WFCatModelForwarder namespace

--[[!
	\class WFCatModelForwarder
	\brief	 This namespace provides a way to forward a WFModel from one thread
	to another.
--]]


local WFCatModelForwarder = {}

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

--[[
	This code runs in the daemon.  This is an internal method; do not call.
	Clear the references to running models, which is enough of a simulation
	of a new daemon process starting to test the reconnect logic.
--]]
function WFCatModelForwarder.daemon_SimulateReboot( )

	sParentModels = {}
	logger:warnf("Server simulated reboot" )

	return Observable.timer( .2, WFRxScheduler.sharedScheduler() )

end

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

--[[
	This code runs in the daemon.  This is an internal method; do not call.
	Create the requested model and relay its value back to the calling process.

	\param modelParams (table) The table containing the modelGuid, namespace and
		func for this root model, or a field subModel(String) and a the
		parentModel as modelParams.  Arguments are passed in the array portion.

	\return result (Rx.Observable) the value of the model as an event stream.
			priority (optional, string) the priority of this model's responses
			name (string) model name for debugging
--]]
function WFCatModelForwarder.daemon_forwardModel( modelParams )

	validateModelParams( modelParams )

	local stub = buildModelStubAfterFindingOrBuildingParents( modelParams )
	
	local modelGuid = modelParams[ kModelGuid ]

	return stub.result, stub.priority, stub.name, modelGuid,
			stub.deliverAllValues, stub.modelIsClosedStream

end


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

local function closeModelStub( closeReason, modelGuid )

	local stub = sParentModels[ modelGuid ]

	sParentModels[ modelGuid ] = nil

	if stub then
		stub:_closeModelStub()
	else
		logger:warnf("%s will not close model: no stub for model guid (%s)", closeReason, modelGuid )
	end

	return stub and stub.name

end

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

--[[
	This code runs in the daemon.  This is an internal method; do not call.
	Closes the model identified by modelGuid.

	\param modelGuid (string) The for this model.

	\return model name or model guid (string) used for debugging.
--]]

function WFCatModelForwarder.daemon_closeModel( modelGuid )

	check( "uuid" )

	local closedModelName = closeModelStub( "daemon_closeModel", modelGuid )

	if closedModelName then
		logger:debugf("Server closed model %s (%s)", closedModelName, modelGuid )
	end

	return closedModelName or modelGuid

end

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

--[[
	daemon_forceCloseAllModels runs in the daemon.  The main app typically closes
	models when it quits. Models can be left open when the main app crashes or is
	force quit.

	The main reason for doing this is to closee pixelProviderProcessor so it will
	not continue trying to use ACR during daemon termination.

--]]

function WFCatModelForwarder.daemon_forceCloseAllModels()

	repeat

		local modelGuid = next( sParentModels )

		if modelGuid then

			local closedModelName = 
				closeModelStub( "daemon_forceCloseAllModels", modelGuid )

			if closedModelName then
				logger:warnf("Force close model %s (%s)", closedModelName, modelGuid )
			end

		end

	until not modelGuid

end

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

--[[
	This code runs in the daemon.  It is an internal method; do not call.
	Call an event on a model.

	\param func (string) the selector for the event on the parent model
	\param parentModeParams (table) the modelParams (see above) of the parent
		model
	\param ... arguments to the event call

	\return model name and event (string) model name and event for debugging

--]]
function WFCatModelForwarder.daemon_callModelEvent( eventName, modelParams, ... )

	local stub = buildModelStubAfterFindingOrBuildingParents( modelParams )

	stub.model:_receiveEvent( eventName, ... )

	return string.format( "%s.%s", stub.name, eventName )

end

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

--[[
	Shared code used to close a model
--]]
local function _closeModel( runCommand, modelGuid )
	local params = {
		[kNArgs] = 1,
		[kNamespace] = 'WFCatModelForwarder',
		[kCommand] = 'daemon_closeModel',
		[kParams] = { modelGuid },
	}

	logger:debugf("Client closed model (%s)", modelGuid )

	return runCommand( params )
		:call( runUntilDone )
end

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

--[[
	Shared code used for building a model.
--]]
local function _startModel( runCommand, modelParams, ... )

	local modelGuid = KSIdGenerator.uuidInLowercaseHex( )
	logger:debugf("Opening model (%s) : %s", modelGuid, ... )

	modelParams[kModelGuid] = modelGuid

	local params = {
		[kNArgs] = select( '#', ... ) + 1,
		[kNamespace] = 'WFCatModelForwarder',
		[kCommand] = 'daemon_forwardModel',
		[kParams] = { modelParams, ... },
	}

	local result = runCommand( params )

	return result, modelGuid

end

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

--[[
	internal implementation, used for new root models and subModels.
--]]
local function _makeForwardedModel( runCommand, modelParams )

	local rawValue, modelGuid = _startModel( runCommand, modelParams )
	
	local deliverAllValues = Rx.Subject()

	local value = rawValue
		:filter( function( value )

			-- Hacky way for forwarded model (typically Nimbus daemon)
			-- to signal that we need to be in "deliver all values" mode.

			if value == _deliverAllValuesCookie then
				deliverAllValues:onNext( true )
				deliverAllValues:onCompleted()
				return false
			else
				return true
			end

		end )

	local createSubModel = function( model, ... )
		return _makeForwardedModel(
				runCommand,
				makeModelParamsWithParent( modelParams, model, ... ) )
	end

	local model = {

		deliverAllValues = deliverAllValues,

		value = value,

		models = setmetatable( {
		}, { __index = function( _, k )
				return function( _, ... )
					return ( createSubModel( k, ... ) )
				end
			end
		} ),
		events = setmetatable( {
		}, { __index = function( _, k )
				return function( _, ... )
					local eventParams = {
						[kNArgs] = select( '#', ... ) + 2,
						[kNamespace] = 'WFCatModelForwarder',
						[kCommand] = 'daemon_callModelEvent',
						[kParams] = { k, modelParams, ... }
					}
					runCommand( eventParams )
						:take( 1 )
						:call( runUntilDone )
				end
			end
		} ),
		close = function()
			_closeModel( runCommand, modelGuid )
		end,

	}
	require 'WFCloseSupport'.addCloseSupportMethods( model )
	return model, createSubModel
end

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

function WFCatModelForwarder.debug_simulateReboot( runCommand )
	local params = {
		[kNamespace] = 'WFCatModelForwarder',
		[kCommand] = 'daemon_SimulateReboot'
	}
	return runCommand( params )
		:filter( function( done )
			if done then
				return true
			end
			return false
		end )
		:take( 1 )
		:takeLast( 0 )
		:call( runUntilDone )
end

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

--[[!
	\brief Create a model which forwards via an event-based runCommand function.

	\param runCommand (function) A function which takes two arguments:
		1) A table of parameters that includes a field n (namespace) and a field
		c (command) defining what function to call.  And an field a containing
		an array of arguments to that function.  It also has a field n useful
		for argument portions containing embedded nil.  The function is invoked
		on the remote system.  The remote function will return a single Observable.
		runCommand() must return an Observable that delivers a copy of the stream
		of values that the remote invocation did.  It must complete when the
		remote invocation's value completes.
	\param namespace (string) the name of the namespace for the model to run on
		the daemon
	\param func (string) the selector to call in that namespace to create the
		model on the daemon
	\param ... arguments to send to the daemon

	\return a table suitable for returning as a model.  It will proxy all events
		and subModel calls on to the daemon.
	\return createSubModel (function) a function you may use to create subModels
		of this model directly in this universe. FIXME: Close support / lifetime?
--]]
function WFCatModelForwarder.makeForwardedModel( runCommand, namespace, func, ... )
	return _makeForwardedModel( runCommand, makeModelParams( namespace, func, ... ) )
end

function WFCatModelForwarder.isDeliverAllValuesCookie( val )

	return val == _deliverAllValuesCookie

end
--==============================================================================

return WFCatModelForwarder
