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

WFLuaModelsImp.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'
local Observable = Rx.Observable

	-- WF.core.logging
local WFLogger = require 'WFLogger'

	-- WF.core.lua
local WFIdle = require 'WFIdle'

	-- WF.core.reactive
local KSPromises = require 'KSPromises'
local KSReactive = require 'KSReactive'

	-- WF.core.utils
local KSTableUtils = require 'KSTableUtils'
local WFUnknown = require 'WFUnknown'

	-- WF.core.performance
local WFPerformanceReport = require 'WFPerformanceReport'

	-- WF.core.os
local KSDate = require 'KSDate'

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

local logger = WFLogger( 'WFModels' )
--	:setLevel 'debug'

local assert = assert
local type = type
local tostring = tostring
local pairs = pairs
local select = select

local KSPromises_isPromise = assert( KSPromises.isPromise )

local KSReactive_isBehavior = assert( KSReactive.isBehavior )
local KSReactive_isEventNode = assert( KSReactive.isEventNode )

local WFUnknown_isUnknown = assert( WFUnknown.isUnknown )

local WFIdle_doASAP = assert( WFIdle.doASAP )
local WFIdle_doLater = assert( WFIdle.doLater )

local KSDate_timeNowForAnimation = KSDate.timeNowForAnimation

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

local WFLuaModelsImp = {}

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

local function isObservableBasedModel( value )

	if not value then
		return false
	end

	if type( value ) == 'table' then

		if Observable:isClassOf( value ) then
			return true
		end

		local isEventNode, isBehavior = KSReactive_isEventNode( value )

		if isEventNode or isBehavior then
			return false
		end

		for k, vv in pairs( value ) do

			if type( vv ) == 'table' and not Observable:isClassOf( vv ) then
				return false
			end
			
		end
		
	end

	return true

end

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

local function makePromiseBasedModel( promise, name )

	local action
		-- Note that this can get set after we've closed if the promise
		-- is already resolved, but that doesn't matter because at that
		-- point the action is minimal in what it references and this
		-- variable won't stay alive much longer.

	return {

		_bindToModelStub = function( self, stub )

			action = promise:whenResolvedDo( function( status, ... )

				if status == 'data' then
					stub:_postData( ... )
				elseif status == 'error' then
					stub:_postError( tostring( ( ... ) ) )
				end

				stub:_postDone()
				stub:_closeModelStub()

			end )

			action:attachTo( nil, self )
				-- nothing with close support to close the action but
				-- we manage its lifetime from the model

		end,

		_closeLuaModel = function( self )

			if action then
				action:close()
				action = nil
			end

		end,

		_receiveEvent = function( self, selector, ... )

			if name then
				logger:warnf( "No event support in promise-based model %s. Received %s.",
					name, tostring( selector ) )
			else
				logger:warnf( "No event support in promise-based models. Received %s.",
					tostring( selector ) )
			end

		end,

		_openModel = function( self, selector, ... )

			if name then
				error( string.format(
					"No submodel support for promise-based model %s: %s",
					name, tostring( selector ) ) )
			else
				error( "No submodel support for promise-based models: " .. tostring( selector ) )
			end

		end,

	}

end

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

local kIgnoreMe = { }

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

-- Check to see whether the data contains unknown data anywhere.

local function isOrHasUnknown( t )

	if type( t ) ~= 'table' then
		return false
	end

	if WFUnknown_isUnknown( t ) then
		return true
	end

	-- If we have a metatable, then don't look inside.

	if getmetatable( t ) then
		return false
	end

	-- Check to see if any of the table elements are or have unknowns.

	for k, v in pairs( t ) do
		if isOrHasUnknown( v ) then
			return true
		end
	end

	return false

end

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

-- Copy a table recursively replacing unknowns with nils. Returns true, mapped_value
-- if the table contained something other than unknowns. Returns false, mapped_value
-- if everything was unknown. Empty tables return true, {} because we didn't
-- map away any unknowns.

local function copyClearingUnknowns( t )

	if type( t ) ~= 'table' then
		return true, t
	end

	if WFUnknown_isUnknown( t ) then
		return false, nil
	end

	if getmetatable( t ) then
		return true, t
	end

	local r = {}

	local hasKnown = false
	local hasEntries = false

	for k, v in pairs( t ) do

		hasEntries = true

		local vHasKnown, mv = copyClearingUnknowns( v )

		r[ k ] = mv

		if vHasKnown then
			hasKnown = true
		end

	end

	-- Empty tables are treated as being intrinsically interesting.

	if not hasEntries then
		hasKnown = true
	end

	return hasKnown, r

end

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

-- Replace unknowns with nil. Return true, mapped_value if there was any known
-- data. Return false, mapped_value if everything was unknown.

local function mapUnknown( v )

	if type( v ) ~= 'table' then
		return true, v
	end

	if not isOrHasUnknown( v ) then
		return true, v
	end

	return copyClearingUnknowns( v )

end

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

local function hasPostableData( v )

	if type( v ) ~= 'table' then
		return true
	end

	local nonEmpty = false

	for k, vv in pairs( v ) do
		nonEmpty = true
		if hasPostableData( vv ) then
			return true
		end
	end

	-- Empty tables are treated as postable. We might not want to do this...

	return not nonEmpty

end

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

local function stub_postData( stub, ... )

	local n = select( '#', ... )

	-- The empty tuple can go through.

	if n == 0 then
		stub:_postData( )
		return true
	end

	-- We special-case the single value.

	if n == 1 then

		local v = ( ... )

		if v == kIgnoreMe then
			return false
		end

		local haveData, data = mapUnknown( v )

		if haveData then
			stub:_postData( data )
			return true
		end

		return false

	end

	-- We need to process the whole tuple.

	local args = { ... }

	local haveData = false

	for i = 1, n do
		local haveArg
		haveArg, args[ i ] = mapUnknown( args[ i ] )
		if haveArg then
			haveData = true
		end
	end

	if haveData then
		stub:_postData( unpack( args, 1, n ) )
		return true
	end

	return false

end

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

local function mayHavePostableValue_imp( ... )

	local n = select( '#', ... )

	-- The empty tuple can go through.

	if n == 0 then
		return true
	end

	-- We special-case the single value.

	if n == 1 then

		local v = ( ... )

		if v == kIgnoreMe then
			return false
		end

		return hasPostableData( v )

	end

	-- Assume that with multiple values something must be postable.

	return true

end

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

local function mayHavePostableValue( b )

	if b:isErrorNow() then
		return true
	end

	return mayHavePostableValue_imp( b:valueNow() )

end

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

local function postBehaviorValueToStub( b, stub )

	local isError, message = b:isErrorNow()

	if isError then
		stub:_postError( tostring( message ) )
		return true
	else
		return stub_postData( stub, b:valueNow() )
	end

end

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

local function callCloseMethod( x )

	local method = x.close

	if method then
		method( x )
	end

end

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

local function makeQueuePending( queueFn )

	local pendingPosts = false

	local function drainPendingPosts()

		if not pendingPosts then
			return
		end

		local pending = pendingPosts

		pendingPosts = false

		for postFn in pairs( pending ) do
			pcall( postFn )
		end

	end

	return function( postFn )

		if not pendingPosts then
			queueFn( drainPendingPosts )
			pendingPosts = { }
		end

		pendingPosts[ postFn ] = true

	end

end

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

local queueASAP = makeQueuePending( WFIdle_doASAP )

local queueNormal = makeQueuePending( WFIdle_doLater )

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

local function queuePendingPost( postFn, asap )

	( asap and queueASAP or queueNormal )( postFn )

end

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

local function makeModelFromStandardizedParams( params, name )

	local valueB = params.valueB

	assert( valueB and KSReactive_isBehavior( valueB ),
		"Expected a behavior or something that could be used to construct a behavior for the model value" )

	local events = params.events
	assert( not events or type( events ) == 'table', "events should be a table" )

	local models = params.models
	assert( not models or type( models ) == 'table', "models should be a table" )

	local onValues
	local onDone

	local canPost = true

	local doPostValue = false
	local doPostDone = false

	local didPost = false

	return {

		_bindToModelStub = function( self, stub )

			local startTime = KSDate_timeNowForAnimation()
			
			local function postToStub()

				if not canPost then
					return
				end

				if doPostValue then
					doPostValue = false
					if postBehaviorValueToStub( valueB, stub ) then
						
						WFPerformanceReport.recordSingleValue( string.format( 'WFModel %s', name ), 
							{ timestamp = KSDate_timeNowForAnimation() - startTime } )
							
						didPost = true
					end
				end

				if doPostDone then
					doPostDone = false
					stub:_postDone()
				end

			end

			local function queueValueToStub()
				doPostValue = true
				queuePendingPost( postToStub, not didPost )
			end

			local function postValueToStub()
				doPostValue = true
				postToStub()
			end

			-- A fast done will also drain anything else so that we update together.

			local function postDoneToStub()
				doPostDone = true
				postToStub()
			end

			-- What we do when done depends on whether we can receive requests
			-- for events and models.

			local onDoneFn

			if events or models then

				onDoneFn = postDoneToStub

			else

				onDoneFn = function()
					postDoneToStub()
					stub:_closeModelStub()
				end

			end

			-- Send the value to the stub.

			if mayHavePostableValue( valueB ) then
				postValueToStub()
			end

			-- Wire up for any future activity.

			if valueB:isDone() then

				onDoneFn()

			else

				onValues = valueB:rawMapE( queueValueToStub ):protectFromAutoCloseE()

				onDone = valueB:weakOnDoneE( onDoneFn ):protectFromAutoCloseE()

			end

		end,

		_closeLuaModel = function( self )

			canPost = false -- avoid any further posting

			-- It would be good to close any event sources spawned from here.

			if onValues then onValues:close(); onValues = nil end
			if onDone then onDone:close(); onDone = nil end
			valueB:close()
			valueB = nil
			events = nil
			models = nil

			pcall( callCloseMethod, params )

		end,

		_receiveEvent = function( self, selector, ... )

			local handler = events and events[ selector ]

			if handler then
				handler( params, ... )
			else
				if name then
					logger:warnf(
						"No event handler for model %s for event: %s",
						name, tostring( selector ) )
				else
					logger:warnf(
						"No event handler for event: %s",
						tostring( selector ) )
				end
			end

		end,

		_openModel = function( self, selector, ... )

			local handler = models and models[ selector ]

			if not handler then
				error( "No submodel support for selector " .. tostring( selector ) )
			end

			return handler( params, ... )

		end,

	}

end

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

local function standardizeModelParams( name, params )

	if KSPromises_isPromise( params ) then
		return {
			valueB = params:untilResolvedB( 'data', kIgnoreMe )
		}
	end

	if KSReactive_isBehavior( params ) then
		return {
			valueB = params
		}
	end

	assert( type( params ) == 'table', "Model '" .. name .. "' parameters should be a table." )

	if params.valueB then
		return params
	end

	local value = params.value

	assert( value, "We must have a value for the model '" .. name .. "'." )

	local isEventNode, isBehavior = KSReactive.isEventNode( value )

	if isBehavior then
		params.valueB = value
		return params
	end

	if isEventNode then
		params.valueB = value:closingHoldB( kIgnoreMe )
		return params
	end

	if KSPromises_isPromise( value ) then
		params.valueB = value:untilResolvedB( 'data', kIgnoreMe )
		return params
	end

	local tvalue = type( value )

	if tvalue == 'table' and not getmetatable( value ) then
		params.valueB = KSReactive.tableB( value )
		return params
	end

	if tvalue == 'function' then
		params.valueB = value( params )
		return params
	end

	assert( false, "Could not construct valueB for model '" .. name .. "'." )

end

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

local function makeObservableBasedModel( params, name )

	local events = params.events
	assert( not events or type( events ) == 'table', "events should be a table" )

	local models = params.models
	assert( not models or type( models ) == 'table', "models should be a table" )

	local deliverAllValues = params.deliverAllValues

	local observable

	local value = params.value

	if Observable:isClassOf( value ) then
		observable = value
	elseif type( value ) ~= 'table' then
		observable = Observable.just( value )
	else

		local obsGroup = {}
		local keyArray = {}
		local tableTemplate = {}

		for k, o in pairs ( value ) do

			if type( o ) ~= 'table' and not Observable:isClassOf( o ) then
				o = Observable.just( o )
			end

			obsGroup[ #obsGroup + 1 ] = o
			keyArray[ #keyArray + 1 ] = k

		end

		local nArgs = #keyArray

		observable = Observable.combineLatest( function( ... )

			local values = { ... }
			local k

			for i = 1, nArgs do
				k = keyArray[ i ]
				tableTemplate[ k ] = values[ i ]
			end

			return tableTemplate

		end, unpack( obsGroup ) )

	end

	local kError = {}
	observable = observable:catch( function( ... )
				return Observable.just( kError, ... )
			end )

	local diagnosticLogParams = params.diagnosticLog
	local diagnosticLogSubscription
	local diagnosticLogValue

	if diagnosticLogParams then

		if diagnosticLogParams.name ~= nil then
			assert( type( diagnosticLogParams.name ) == 'string' and #diagnosticLogParams.name > 0,
					"diagnosticLog.name should be a non-empty string" )
		end

		if diagnosticLogParams.id ~= nil then
			assert( type( diagnosticLogParams.id ) == 'string' and #diagnosticLogParams.id > 0,
					"diagnosticLog.id should be a non-empty string" )
		end

		assert( type( diagnosticLogParams.weight ) == 'number',
				"diagnosticLog.weight should be a nubmer" )

		if diagnosticLogParams.outputForAutomation ~= nil then
			assert( type( diagnosticLogParams.outputForAutomation ) == 'boolean',
					"diagnosticLog.outputForAutomation should be a boolean" )
		end

		local WFDiagnosticLog = require 'WFDiagnosticLog'
			-- Don't require this at file scope unless requested.

		diagnosticLogSubscription = WFDiagnosticLog.logDataRequest()

			:subscribe( function()

				WFDiagnosticLog.addSectionToLog {
					name = diagnosticLogParams.name or name,
					id = diagnosticLogParams.id or name,
					weight = diagnosticLogParams.weight,
					body = diagnosticLogValue or {},
				}

			end )

	end

	local subscription, deliverAllValuesSubscription

	return {

		_bindToModelStub = function( self, stub )

			logger:infof( "Observable model %s subscribing to underlying Observable" )

			if deliverAllValues then

				if Observable:isClassOf( deliverAllValues ) then

					deliverAllValuesSubscription = deliverAllValues
						:take( 1 )
						:subscribe( function()
							stub:_setDeliverAllValues()
						end )

				else

					stub:_setDeliverAllValues()

				end

			end

			observable = WFPerformanceReport.wrapObservable( string.format( 'WFModel %s', name ), observable )
			
			subscription = observable

				:subscribe( function( ... )

					if( ... ) == kError then

						stub:_postError( tostring( select( 2, ... ) ) )
						logger:errorf( "Observable model %s posting transient error %s, %s, %s", name, ... )

						if diagnosticLogSubscription then
							diagnosticLogValue = { error = { ... } }
						end

					else

						logger:debugf( "Observable model %s posting data %s, %s, %s", name, ... )
						stub:_postData( ... )

						if diagnosticLogSubscription then
							diagnosticLogValue = KSTableUtils.shallowcopy( ... )
						end

					end

				end,

				function( ... )

					logger:errorf( "Observable model %s closed because error %s, %s, %s", name, ... )
					stub:_postError( tostring( ( ... ) ) )

					if diagnosticLogSubscription then
						diagnosticLogValue = { error = { ... } }
					end

					stub:_closeModelStub()

				end,

				function()

					logger:infof( "Observable model %s posting DONE", name )

					stub:_postDone()
					-- stub:_closeModelStub()
						-- No, we can't close the stub when the sender sends onCompleted
						-- because we may still need the stub to create submodels.
						-- It's still up to whomever created this model to close
						-- it from the client!

					if diagnosticLogSubscription then
						diagnosticLogSubscription:dispose()
						diagnosticLogSubscription = nil
						diagnosticLogValue = nil
					end

				end )

		end,

		_closeLuaModel = function( self )

			logger:infof( "Observable model %s closing", name )

			if subscription then
				subscription:dispose()
				subscription = nil
			end

			if deliverAllValuesSubscription then
				deliverAllValuesSubscription:dispose()
				deliverAllValuesSubscription = nil
			end

			if diagnosticLogSubscription then
				diagnosticLogSubscription:dispose()
				diagnosticLogSubscription = nil
				diagnosticLogValue = nil
			end

			events = nil
			models = nil
			pcall( callCloseMethod, params )

		end,

		_receiveEvent = function( self, selector, ... )

			logger:infof( "Observable model %s received event %s with params %s %s %s",
						  name, selector, ... )

			local handler = events and events[ selector ]

			if handler then
				handler( params, ... )
			else
				if name then
					logger:warnf(
						"No event handler for model %s for event: %s",
						name, tostring( selector ) )
				else
					logger:warnf(
						"No event handler for event: %s",
						tostring( selector ) )
				end
			end

		end,

		_openModel = function( self, selector, ... )

			logger:infof( "Observable model %s received submodel request %s with params %s %s %s",
						  name, selector, ... )

			local handler = models and models[ selector ]

			if not handler then
				error( "No submodel support for selector " .. tostring( selector ) )
			end

			return handler( params, ... )

		end,
	}

end

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

function WFLuaModelsImp.asModel( params, name, adapter, ... )

	if name then
		name = tostring( name )
	end

	if KSPromises_isPromise( params ) and not adapter then
		return makePromiseBasedModel( params, name )
	end

	if params.value and isObservableBasedModel( params.value ) and not adapter then
		return makeObservableBasedModel( params, name )
	end

	assert( not params.deliverAllValues,
			"deliverAllValues not implemented for non-Observable models" )

	local modelParams = standardizeModelParams( name, params )

	if adapter then

		local preAdaptationValueB = modelParams.valueB

		local adaptedParams = adapter( modelParams, ... ) or modelParams
		assert( type( adaptedParams ) == 'table',
				"Adapted model parameters must be a table." )

		-- If we didn't generate a new valueB but we have a value from which we
		-- presumably generated valueB to begin with, then we want to re-run the
		-- generation logic.

		if adaptedParams.valueB == preAdaptationValueB and adaptedParams.value then
			adaptedParams.valueB = nil
		end

		modelParams = standardizeModelParams( name, adaptedParams )

		-- We also want to make the pre-adaptation valueB have a lifetime
		-- dependent on the new valueB. This means that the adapted model
		-- can't result in us doing less overall work but so it goes. A revised
		-- reactive API could improve this.

		if preAdaptationValueB ~= modelParams.valueB then
			modelParams.valueB:addCloseTarget( preAdaptationValueB )
			rawset( modelParams.valueB,
					'private:preAdaptationValueB',
					preAdaptationValueB )
		end

	end

	return makeModelFromStandardizedParams( modelParams, name )

end

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

return WFLuaModelsImp
