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

NmbRootModel.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'
local Observable = Rx.Observable
local WFRxScheduler = require "WFRxScheduler"

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

	-- WF.core.utils
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local KSPrefs = require 'KSPrefs'
local KSDate = require 'KSDate'
local KSJsonDecoder = require 'KSJsonDecoder'
local KSTableUtils = require 'KSTableUtils'
local WFModelsUtils = require 'WFModelsUtils'
local WFPlatformInfo = require 'WFPlatformInfo'

	-- WF.oz_client.managed_catalog
local NmbCatalog = require 'NmbCatalog'
local NmbModelUtils = require 'NmbModelUtils'
local DebuggingStreams = require "DebuggingStreams"
local WFCatAssets = require 'WFCatAssets'
local WFOzDocUtils = require 'WFOzDocUtils'
local NmbDbCorruptionUtils = require "NmbDbCorruptionUtils"

	-- WF.lightroomcc.file_management.purge
local NmbPurgeControl = require "NmbPurgeControl"
local NmbCatWatermark = require "NmbCatWatermark"

	-- WF.oz_client.managed_catalog.pixel_provider
local PixelProviderDownloadNegativeModel = 
		require "PixelProviderDownloadNegativeModel"
local PixelProviderVideoLoupeModel =
		require "PixelProviderVideoLoupeModel"
local PixelProviderGridPixelPathsModel = require "PixelProviderGridPixelPathsModel"
local PixelProviderLoupePixelPathsModel = require "PixelProviderLoupePixelPathsModel"
local PixelProviderIdlePixelPathsModel = require "PixelProviderIdlePixelPathsModel"
local PixelProviderPixelPathsModel = require "PixelProviderPixelPathsModel"

	-- WF.oz_client.managed_catalog.develop
local WFCatDevelopSettings = require 'WFCatDevelopSettings'

	-- WF.oz_client.managed_catalog.export
local WFCatExport = require 'WFCatExport'

local WFFileUtils = require 'WFFileUtils'
local NmbConnectors = require 'NmbConnectors'
local NmbFileManagementSettings = require 'NmbFileManagementSettings'
local NmbTrackedDataBroadcaster = require 'NmbTrackedDataBroadcaster'

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

local logger = WFLogger 'NmbRootModel'

local derefOpt = KSTableUtils.derefOpt

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

local function logAndRaiseError( errStr, ... )

	if select( '#', ... ) > 0 then

		errStr = string.format( errStr, ... )

	end

	logger:error( errStr )

	error( errStr )

end

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

local function isNonNil( v )

	return v ~= nil

end

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

local function returnArg( n )

	return function( ... ) 

		return select( n, ... )

	end

end

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

local function getNonNilWFCatalogStream( wfCatalogStream )
	return wfCatalogStream
		:filter(
			function( wfCatalog )

				return wfCatalog ~= nil

			end )
		:take( 1 )
end

--------------------------------------------------------------------------------
-- makeSimpleSubmodelWithDefaultValue returns a function that will construct
-- a submodel that has the return value of fn as the submodel's value. The 
-- first parameter of fn will be a WFCatalog instance. If we fail to log in, 
-- the model's value will be ... instead
--------------------------------------------------------------------------------

local function makeSimpleSubmodelWithPriorityAndDefaultValueImp( 
				wfCatalogStreamOfStream, priority, fn, defaultReturnVals,
				defaultReturnValsN ) 

	return function( ... )

		local params = { ... }

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

		-- return a submodel specification with the value pointing to
		-- the stream generated by fn( wfCatalog, ... )

		return {

			priority = priority,

			value = wfCatalogStreamOfStream
					:flatMapLatest( 
						function( wfCatalog )

							if not wfCatalog then

								return Rx.Observable.just( 
										unpack( defaultReturnVals, 1, defaultReturnValsN ) )

							end
							
							return fn( wfCatalog, unpack( params, 2, nParams ) )

						end )
		}

	end

end

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

local function makeSimpleSubmodelWithPriorityAndDefaultValue( 
				wfCatalogStreamOfStream, priority, fn, ... )

	if type( fn ) ~= "function" then

		logAndRaiseError( "function required, but got %s", type( fn ) )

	end

	local defaultReturnVals = { ... }

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

	return makeSimpleSubmodelWithPriorityAndDefaultValueImp( 
				wfCatalogStreamOfStream, priority, fn, defaultReturnVals,
				defaultReturnValsN ) 

end

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

local function makeSimpleSubmodelWithDefaultValue( 
				wfCatalogStreamOfStream, fn, ... )

	return makeSimpleSubmodelWithPriorityAndDefaultValue( 
			wfCatalogStreamOfStream, nil, fn, ... )

end

--------------------------------------------------------------------------------
-- makeSimpleSubmodelForValidWFCatalog returns a function that will construct
-- a submodel that has the return value of fn as the submodel's value. The 
-- first parameter of fn will be a WFCatalog instance. 
--------------------------------------------------------------------------------

local function makeSimpleSubmodelForValidWFCatalog( fn, wfCatalogStream )

	if type( fn ) ~= "function" then

		logAndRaiseError( "function required, but got %s", type( fn ) )

	end

	return function( _, ... )

		local params = { ... }

		local nParams = select( '#', ... )
			
		return {
		
			value = wfCatalogStream
						:filter( isNonNil )
						:take( 1 )
						:flatMapLatest(  
							function( wfCatalog )

								return fn( wfCatalog, unpack( params, 1, nParams ) )

							end )
			
		}
			
	end

end

--------------------------------------------------------------------------------
-- Given a stream that emits a stream of single values
-- 
-- val1
-- val2
-- ...
--
-- makeRateLimitedBufferedStream will buffer the results in an array and return
-- the array buffer no more than buffersPerSecond
--
-- Note:
--
-- this is meant to be equivalent to 
--
-- stream:bufferWithTime( 1/buffersPerSecond ) but with as many
-- Rx.AnonymousObservable eliminated as possible so that it is faster and
-- lighter
--------------------------------------------------------------------------------

local _makeRateLimitedBufferedStreamWarnings = {}

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

local function makeRateLimitedBufferedStream( desc, stream, buffersPerSecond )

	if type( desc ) ~= "string" then

		assert( "makeRateLimitedBufferedStream requires a description" )

	end

	if type( buffersPerSecond ) ~= "number" or buffersPerSecond <= 0 then

		assert( "invalid buffersPerSecond" )

	end

	local buffersPerSecondAsSeconds = 
			Rx.Scheduler.normalize( 1 / buffersPerSecond )
	
	local scheduler = require 'WFRxScheduler'.sharedScheduler()

	return Rx.Observable.create(
		function( observer )

			local currentBufferedValues = {}

			local havePendingTimer

			-- holds a "most recent" timer that can be killed via :dispose()
			local currTimerSubscr = Rx.SerialDisposable()

			local valueCollectorSubscr =
				stream	
					:subscribe(
						function( v, ... )

							-- check to make sure we didn't get a multiret
							-- stream and warn once if we did
							if select( "#", ... ) > 0 and 
									not _makeRateLimitedBufferedStreamWarnings[ desc ] then

								_makeRateLimitedBufferedStreamWarnings[ desc ] = true

								logger:warnf( "makeRateLimitedBufferedStream Will not buffer extra values from %s", desc )

							end

							-- buffer the value
							currentBufferedValues[ #currentBufferedValues + 1 ] = v

							if not havePendingTimer then

								-- start a  new "most recent" timer to emit the
								-- buffered values
								havePendingTimer = true

								currTimerSubscr
									:setDisposable(
        								scheduler
											:scheduleFuture( 
												buffersPerSecondAsSeconds,
												function()

													-- it is time time to send the values.
													local thisIntervalBuffer = currentBufferedValues

													currentBufferedValues = {}

													-- (scheduleFuture fires once)
													havePendingTimer = false

													observer:onNext( thisIntervalBuffer )

												end ) )

							end

						end,
						function( errStr, ... )

							observer:onError( errStr, ... )  -- relay errors

						end,
						function()

							observer:onCompleted() -- relay completion

						end )

		return Rx.CompositeDisposable(
				currTimerSubscr, valueCollectorSubscr )

	end )

end

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

-- @mark NmbRootModel

local NmbRootModel = {}

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

function NmbRootModel.makeModel( wfCatalogStream, dbCorruptionEventStream )

	assert( Observable:isClassOf( wfCatalogStream ), 
				"wfCatalogStream is not an observable" )

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

	wfCatalogStream = 
		wfCatalogStream
			:tap( 
				function( wfCatalog )

					if wfCatalog and not NmbCatalog:isClassOf( wfCatalog ) and not WFPlatformInfo.isUnitTest() then
						logger:errorf( "Catalog model was passed something that's not a catalog: %s",
									   wfCatalog )
					end

					logger:infof( "New catalog: %s",
								  wfCatalog
										and tostring( wfCatalog )
										or ":unknown:" )

				end )
			
	-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

	local wfCatalogMetadataStream =
			wfCatalogStream
				:map( 
					function( maybeWFCatalog )

						return {

							hasCatalog = not not maybeWFCatalog,

							ozCatalogId = maybeWFCatalog and maybeWFCatalog:ozCatalogId(),

							ozUserId = maybeWFCatalog and maybeWFCatalog:ozUserId(),

							pixelsPath = maybeWFCatalog and 
											maybeWFCatalog:fileSpec() and
											maybeWFCatalog:fileSpec():pixelsPath(),

						}

					end )
	
	
	-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	
	local pixelProviderProcessorStream =
				getNonNilWFCatalogStream( wfCatalogStream )
					:map( returnArg( 2 ) )

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

	local function makeForwardEventToPixelProviderProccessor( methName )

		
		-- _ is catalogModel
		return function( _, ... )

			-- this has fire and forget semantics
			
			local params = { ... }

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

			WFRxRunUntilDone(
				pixelProviderProcessorStream
					:flatMapLatest(
						function( pixelProviderProcessor )

							return pixelProviderProcessor
									:runUntilDone( 
										methName, 
											unpack( params, 1, nParams ) )

						end ) )

		end

	end

	-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	
	local profileCatalogStream = 
				wfCatalogStream:filter( isNonNil ):map( returnArg( 3 ) )
	
	-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

	local wfCatSynchronizerStream =
				wfCatalogStream:filter( isNonNil ):map( returnArg( 4 ) )

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

	local cameraProfileIngestInProgress = Rx.ReplaySubject( 1 )

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

	local nmbModelDefinition = {

		value = wfCatalogMetadataStream,

		-- highFidelityModels are submodels that will deliver everything sent
		-- via the subModel's Rx.Observable value field. i.e. If you call
		-- highFidelitySubModel:value():subscribe( ... ), the subscription's
		-- onData handler will fire every time value:onNext( ... ) fires.
		highFidelityModels = {
		
			--------------------------------------------------
			-- addImagesToCatalog
			--
			-- High fidelity because need to see all status 
			-- values to collect up all errors.
			--------------------------------------------------
			addImagesToCatalog = function( _, optImportOptions )
				return require 'WFCatImport'.makeAddImagesModel( wfCatalogStream:filter( isNonNil ),
																profileCatalogStream,
																wfCatSynchronizerStream,
																optImportOptions )
			end,

			--------------------------------------------------
			-- addImageArray
			-- High fidelity because need to see all status 
			-- values to collect up all errors.
			--------------------------------------------------
			addImageArray = function( _, importInfo, optImportOptions )
				return {
						
					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ), 
													profileCatalogStream, 
													wfCatSynchronizerStream )
								:take( 1 )
								:flatMapLatest( function( wfCatalog, profileCatalog, wfCatSynchronizer )
									
									local WFCatImport = require 'WFCatImport'
									return WFCatImport.addImageArrayToCatalog( wfCatalog, profileCatalog, importInfo, wfCatSynchronizer, optImportOptions )
									
								end ),
				}
			end,

			-------------------------------------------------
			-- Create Text Watermark
			-------------------------------------------------
			
			createWatermark = function( _, name, settings, optFilePath, optFileName, optId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						NmbCatWatermark.createWatermark, name, settings, optFilePath, optFileName, optId )

			end,

			--------------------------------------------------
			-- Update watermark
			--------------------------------------------------
			
			updateWatermark = function( _, id, name, settings, optFilePath, optFileName )
				
				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						NmbCatWatermark.updateWatermark, id, name, settings, optFilePath, optFileName )
			
			end,
			
			
			--------------------------------------------------
			-- delete watermarks
			--------------------------------------------------
			deleteWatermarks = function( _, watermarkIds )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatWatermark'.deleteWatermarks, watermarkIds )

			end,
			
			--------------------------------------------------
			-- get watermark Info
			--------------------------------------------------
			getWatermarkInfo = function( _, id )
			
				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						NmbCatWatermark.getWatermarkInfo, id )

			end,

			--------------------------------------------------
			-- get All watermark Infos
			--------------------------------------------------
			getAllWatermarksInfo = function( )
			
				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						NmbCatWatermark.getAllWatermarksInfo )

			end,
			
			-----------------------------------------------------
			 -- download image watermark
			-----------------------------------------------------
			downloadImageWatermark = function( _, docId )
			
				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						NmbCatWatermark.downloadImageWatermark, docId ) 
						
			end,
			
			
			--------------------------------------------------
			-- purge orphan watermark assets
			--------------------------------------------------
			
			purgeOrphanWatermarkAssets = function()
			
				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						NmbCatWatermark.purgeOrphanWatermarkAssets ) 
			
			end,
			
			
			--------------------------------------------------------------------------------
			-- is Camera Profile Custom
			--------------------------------------------------
			isCameraProfileCustom = function( _, cameraProfileDigest, cameraModelName )
				return {
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
										return wfCatalog:getCatalogProfileCache():getHrefAndFileNameForProfileDigestModel( 
																					cameraProfileDigest, cameraModelName )
										:map( function( fileName, href )
											return fileName ~= nil or href ~= nil
										end )
									end ),
				}
			end,

			-----------------------------------------------
			-- listenForNewPresets
			--------------------------------------------------
			listenForNewPresets = function()
				return {
				
					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
													wfCatSynchronizerStream:filter( isNonNil ) )
								:flatMapLatest( function( _, wfCatSynchronizer )
								
									return wfCatSynchronizer
										:getStyleSynchronizer()
										:getStyleRefreshStream()
						
								end )
				
				}
			end,

			--------------------------------------------------
			-- addPresets
			--------------------------------------------------	
			addPresets = function( _, presetArray )
				return {
				
					-- Until ACR gives us explicit control of the file name for v2
					-- presets and profiles, so it doesn't have to match the preet/profile
					-- name, but rather allowing us to use a document id, we need
					-- to pass the styleSynchronizer instance in to allow
					-- a refresh of the ACR cache after the daemon renames the file
					-- under the hood. Alternately, we could just rename the file
					-- in the client and perform the ACR cache refresh before triggering
					-- the daemon to sync it.
				
					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
										wfCatSynchronizerStream:filter( isNonNil ) )
								:take( 1 )
								:flatMapLatest( function( wfCatalog, wfCatSynchronizer )
						
									local NmbCatPreset = require 'NmbCatPreset'
									return NmbCatPreset.addPresetArrayToCatalog( wfCatalog,
															presetArray, 
															wfCatSynchronizer:getStyleSynchronizer() )
						
						end )
				
				}
			end,

			--------------------------------------------------
			-- renamePreset
			--------------------------------------------------			
			renamePreset = function( _, params )
				return {
				
					-- Same (roughly) comment applies here as for 'addPreset' with regard to
					-- file name and ACR cache refreshing.
				
					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
									wfCatSynchronizerStream:filter( isNonNil ) )
								:take( 1 )
								:flatMapLatest( function( wfCatalog, wfCatSynchronizer )
						
									return require "NmbCatPreset".renamePreset( wfCatalog, params, 
										wfCatSynchronizer:getStyleSynchronizer() )	
						
						end )
						:call( WFRxRunUntilDone )
						
				}
			end,

			--------------------------------------------------
			-- deletePresets
			--------------------------------------------------			
			deletePresets = function( _, presetIds )
				return {
				
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
						
									return require "NmbCatPreset".deletePresets( 
										wfCatalog, presetIds )	
						
						end )
						:call( WFRxRunUntilDone )

				}
			end,
			
			--------------------------------------------------
			-- deleteProfile
			--------------------------------------------------			
			deleteProfile = function( _, profileId, favoriteFingerprint )
				return {
				
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
						
									return require "NmbCatPreset".deleteProfile( 
										wfCatalog, profileId, favoriteFingerprint )	
						
						end )
						:call( WFRxRunUntilDone )

				}
			end,

			--------------------------------------------------
			-- updatePresetSettings
			--------------------------------------------------			
			updatePresetSettings = function( _, presetId, path, digest )
				return {
				
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
						
									return require "NmbCatPreset".updatePresetSettings( 
										wfCatalog, presetId, path, digest )
									
						end )
						:call( WFRxRunUntilDone )
						
				}
			end,
			
			--------------------------------------------------
			-- syncDCPs
			--------------------------------------------------		
			syncDCPs = function( _, params )
				return {
				
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
						
									return require "WFCatDCPs".syncDCPs( 
										wfCatalog, params )
									
						end )
				
				}
			end,

			--------------------------------------------------
			-- copyAndSyncDCPs
			--------------------------------------------------		
			copyAndSyncDCPs = function( _, appDataFolderInfo )
				return {
				
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
								
									cameraProfileIngestInProgress:onNext( true )
								
									return require "WFCatDCPs".copyAndSyncDCPs( 
										wfCatalog, appDataFolderInfo )
										
									:finally( function()
									
										cameraProfileIngestInProgress:onNext( false )
									
									end )
										
						end )
						:call( WFRxRunUntilDone )
						
				}
			end,

			--------------------------------------------------
			-- importDCPs
			--------------------------------------------------		
			importDCPs = function( _, explicitProfilePaths )
				return {
				
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
									
									-- Not notifying client side about camera profile
									-- import in progress in this case, since the client
									-- application would have initiated it, and thus is
									-- aware of it already.
									
									return require "WFCatDCPs".importDCPs( 
										wfCatalog, explicitProfilePaths )
										
									:flatMap( function( result )
									
										return Observable.just( result )
										
									end )
									
						end )
						
				}
			end,
			
			--------------------------------------------------
			-- isCameraProfileIngestInProgress
			--
			-- Allows observation of camera profile (dcp)
			-- ingest (one-time auto-slurp) status.
			--------------------------------------------------			
			isCameraProfileIngestInProgress = function()
				return {
				
					value = cameraProfileIngestInProgress:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function(  )
									return cameraProfileIngestInProgress
								end )
				}
			end,

			--------------------------------------------------
			-- isPresetAndProfileIngestInProgress
			--
			-- Allows observation of xmp preset/profile ingest
			-- (one-time auto-slurp) status.
			--------------------------------------------------
			isPresetAndProfileIngestInProgress = function()
				return {
				
					value = wfCatSynchronizerStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatSynchronizer )
									return wfCatSynchronizer:getStyleSynchronizer():getPresetProfileIngestInProgressStream()
								end )
				}
			end,

			--------------------------------------------------
			-- changeStyleFavoriteStatus
			--------------------------------------------------			
			changeStyleFavoriteStatus = function( _, fingerprint, 
				isProfile, isFavorite, isDefaultFavorite )
				
				return {
				
					value = wfCatalogStream:filter( isNonNil )
						:take( 1 )
						:flatMapLatest( function( wfCatalog )
				
							return require "NmbStyleFavorites".changeFavoriteStatus( 
								wfCatalog, fingerprint, isProfile, 
								isFavorite, isDefaultFavorite )
									
						end )
						:call( WFRxRunUntilDone )
				
				}
			end,

			--------------------------------------------------
			-- listenForNewDCPs
			--------------------------------------------------			
			listenForNewDCPs = function()
				return {
				
					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
													wfCatSynchronizerStream:filter( isNonNil ),
													profileCatalogStream:filter( isNonNil ) )
								:flatMapLatest( function( wfCatalog, wfCatSynchronizer, profileCatalog )
									
									local styleSynchronizer = wfCatSynchronizer:getStyleSynchronizer()
									
									-- An NmbDCPDocumentProcessor instance is required for ongoing link fixing
									-- as Lightroom CC uploads already-referenced custom camera profiles.
									-- Note that NmbDCPDocumentProcessor relies on catalog:getAllVolumeAgents()
									-- returning a NON-empty table, so it must be initialized late enough for
									-- that to be the case. (At the time of WFCatStyleSynchronizer initialization,
									-- that is not the case.)
									
									styleSynchronizer:initCameraProfileLinkProcessor( wfCatalog, profileCatalog )
								
									return styleSynchronizer
										:getCameraProfileRefreshStream()
						
								end )
				
				}
			end,

			--------------------------------------------------
			-- doc store update progress
			--
			-- High fidelity because need update per bulk doc upgrade.
			--------------------------------------------------
			docStoreUpdateProgress = function()

				return {
						
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
								
									local docStoreUpdater = wfCatalog:getDocStoreUpdater()
								
									if docStoreUpdater then
										return docStoreUpdater:blockingUpdatesProgress()
									else
										return Observable.just( true )
									end
									
								end ),
								
				}
				
			end,

			--------------------------------------------------
			-- asset migration
			--
			-- High fidelity because Need update per asset
			--------------------------------------------------
			assetMigration = function( _, fileMoveRequired )

				local fileMover

				return {
						
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
								
									fileMover = wfCatalog:getFileMover()
								
									local performFileMove = false
								
									local postponeFileMoveUntilDate = KSPrefs.postponeFileMoveUntilDate or 0
								
									if postponeFileMoveUntilDate <= KSDate.timeNow() then
										performFileMove = true
										KSPrefs.postponeFileMoveUntilDate = nil
									end
								
									if fileMover and performFileMove then
										return fileMover:maybePerformMigration( fileMoveRequired )
									else
										return Observable.just( true )
									end
									
								end ),
					
					close = function()

						if fileMover then
							return fileMover:stop()
						end
						
					end
								
				}
				
			end,

			--------------------------------------------------
			-- importFromManifest
			--
			-- High fidelity because need to see all status 
			-- values to collect up all errors.
			--------------------------------------------------
			importFromManifest = function( _, path, fileName, contextInfo )
				return {
						
					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ), 
													profileCatalogStream, 
													wfCatSynchronizerStream )
								:take( 1 )
								:flatMapLatest( function( wfCatalog, profileCatalog, wfCatSynchronizer )
									
									local WFCatImportManifestProcessor = require 'WFCatImportManifestProcessor'
									return WFCatImportManifestProcessor.importManifest( path, fileName, wfCatalog, profileCatalog, wfCatSynchronizer, contextInfo )
									
								end ),
				}
			end,

			--------------------------------------------------
			-- exportAssets
			--
			-- High fidelity because need update per asset
			--------------------------------------------------
			exportAssets = function( _, assetIds, commonExportSettings, extraSettingsByAssetId, maybeKeepSyncRunning )

				local _exportTask
				local _cleanupFn
				
				return {

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
														wfCatSynchronizerStream )
						:take( 1 )
						:map( function( wfCatalog, maybeSynchronizer )

							-- Prevent the exported assets from being purged during the export.
							
							local fileSpec = wfCatalog:fileSpec()
							local originalsVolumeGuid = fileSpec:originalsPathInfo().volumeGuid

							local volumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( originalsVolumeGuid )

							volumeAgent:protectFromPurge( assetIds )
							
							-- `maybeKeepSyncRunning` indicates whether the export needs the sync
							-- mechanism. This is hopefully temporary and can be removed when we
							-- decouple exporting of shared-with-you images from the sync feed.

							-- Prevent idle downloader from taking up space.
							local wasSyncRunning 
							if maybeSynchronizer and not maybeKeepSyncRunning then
								wasSyncRunning = maybeSynchronizer:isSyncRunningNow()
								maybeSynchronizer:stopSyncing()
							end
								
							_cleanupFn = function()
								volumeAgent:releaseProtectionFromPurge( assetIds )
								if maybeSynchronizer and wasSyncRunning then
									maybeSynchronizer:startSyncing()
								end
							end
							
							return WFCatExport.exportAssetsFromCatalog(
								wfCatalog,
								pixelProviderProcessorStream,
								maybeSynchronizer:getStyleSynchronizer(),
								assetIds,
								commonExportSettings,
								extraSettingsByAssetId )

						end )
						:tap( function( exportTask ) 
							_exportTask = exportTask
						end )
						:flatMapLatest( function( exportTask ) 
						
							return exportTask:status()
						end )
						:filter( function( data )
							
							-- NMB-18044: Send info only when some assets are processed
							return data 
									and data.processedCount 
									and data.processedCount > 0
							
						end )
						:finally( function()
							_cleanupFn()
						end ),
					
					close = function()
					
						if _exportTask then
							_exportTask:dispose()
							_exportTask = nil
						end
						
					end,
				}
			end,
			
			--------------------------------------------------
			-- maybeGetOriginalPaths
			--
			-- TODO: Ensure nothing harmful will happen if
			-- an export is going on for the same assets at
			-- the same time as another feature is requiring
			-- local originals via this submodel.
			--------------------------------------------------
			maybeGetOriginalPaths = function( _, assetIds, oneByOne )

				local volumeAgent
			
				return {
					
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
								
									local ppps = pixelProviderProcessorStream
									local provider = require "WFCatExportPixelProviderTask"( ppps )
									
									local fileSpec = wfCatalog:fileSpec()
									
									local originalsVolumeGuid = fileSpec:originalsPathInfo().volumeGuid

									volumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( originalsVolumeGuid )
									
									volumeAgent:protectFromPurge( assetIds )

									local function getOriginals()
										return Observable.from( assetIds )
										:flatMap( function( assetId )
											return provider:getOriginal( assetId )
											:flatMap( function( _, originalPath )
												if WFFileUtils.exists( originalPath ) then
													return Observable.just { path = originalPath, imageId = assetId }
												else
													return provider:getProxy( assetId )
													:map( function( _, proxyPath )
														-- Return what we have, even if it's just the asset id.
														return { proxyPath = proxyPath, imageId = assetId, downloadError = originalPath }
													end )
												end
											end )
										end )
									end
				

									if not oneByOne then									
										return getOriginals()
										:toArray()
									else
										return getOriginals()
									end
									
								end ),
								
					close = function()
						volumeAgent:releaseProtectionFromPurge( assetIds )
					end
				}
			
			end,
			
			--------------------------------------------------
			-- getProxyPaths
			--------------------------------------------------

			getProxyPaths = function( _, assetIds )

				local volumeAgent
			
				return {
					
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
								
									local ppps = pixelProviderProcessorStream
									local provider = require "WFCatExportPixelProviderTask"( ppps )
									
									local fileSpec = wfCatalog:fileSpec()
									
									local originalsVolumeGuid = fileSpec:originalsPathInfo().volumeGuid

									volumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( originalsVolumeGuid )
									
									volumeAgent:protectFromPurge( assetIds )

									return Observable.from( assetIds )
									:flatMap( function( assetId )
										return provider:getProxy( assetId )
										:map( function( _, proxyPath )
											return { proxyPath = proxyPath, imageId = assetId }
										end )
									end )
									:toArray()

								end ),
								
					close = function()
						volumeAgent:releaseProtectionFromPurge( assetIds )
					end
				}
			
			end,
					
			--------------------------------------------------
			-- forgetVolume
			--
			-- High fidelity because need update per asset
			--------------------------------------------------
			forgetVolume = function( _, volume )

				return {
				
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
								
									local fileMover = wfCatalog:getFileMover()
								
									if fileMover then
										return fileMover:forgetVolume( volume )
									else
										return Observable.just( true )
									end
									
								end ),
				
				}
				
			end,
			
			--------------------------------------------------
			-- ongoingDevelopContents
			-- 
			-- high fidelity because we want to get updates
			-- for all of the asset ids and they may come
			-- in bursts
			--------------------------------------------------
			ongoingDevelopContents = function()
			
				local ongoingDevelopContentsModel =
						WFCatDevelopSettings.ongoingDevelopContentsModel(
							getNonNilWFCatalogStream( wfCatalogStream ) )

				-- ongoingDevelopContentsModel:value() is a "bursty" stream and
				-- will cause "daemon send" backlogs unless buffered. See
				-- NMB-21223
				ongoingDevelopContentsModel.value = 
						makeRateLimitedBufferedStream(
							"ongoingDevelopContents",
							ongoingDevelopContentsModel.value, 6 )

				return ongoingDevelopContentsModel
				
			end,
			
			--------------------------------------------------
			-- setCameraRawSettings
			-- 
			-- high fidelity because we want to get the result
			-- every time we send a save event so that the 
			-- client knows when there are no more saves 
			-- pending
			--------------------------------------------------
			setCameraRawSettings = function()

				return WFCatDevelopSettings.setCameraRawSettings(
							getNonNilWFCatalogStream( wfCatalogStream ) )
				
			end,

			--------------------------------------------------
			-- pixel path stream API
			--
			-- These are all high fidelity because we want
			-- to deliver all paths that are requested.
			--------------------------------------------------

			--------------------------------------------------
			-- pixelPaths
			--------------------------------------------------
			pixelPaths = function( _, ozCatalogId, fps )

				return PixelProviderPixelPathsModel.make( 
							wfCatalogStream, ozCatalogId, fps )

			end,

			--------------------------------------------------
			-- idlePixelPaths
			--------------------------------------------------

			idlePixelPaths = function( _, pixStreamId, longSide )

				return PixelProviderIdlePixelPathsModel.make( 
						wfCatalogStream, pixStreamId, longSide )

			end,
			
			downloadPersonThumbnailResponseStream = 
				function()

					local personPathTableStream =
						wfCatSynchronizerStream
							:filter( isNonNil ) 
							:take ( 1 )
							:flatMapLatest( 
								require 'WFCatPerson'.downloadPersonThumbnailResponseStream )

					local personPathTablesStream =
						makeRateLimitedBufferedStream(
							"downloadPersonThumbnailResponseStream",
							personPathTableStream, 24 )

					return {

						value = personPathTablesStream 
					}

				end,

			--------------------------------------------------
			-- getAlbumListForImages
			--------------------------------------------------
			
			getAlbumListForImages = function( _, assetIds )

				return {

					value = wfCatalogStream
								:filter( function( wfCatalog )
									return wfCatalog ~= nil
								end )
								:flatMapLatest( function( wfCatalog )
									return require 'WFCatAlbums'.getAlbumListForAssetIds( wfCatalog, assetIds )
								end )
				}

			end,
			
			--------------------------------------------------
			-- getOzAssetIdsForLocalIds
			--------------------------------------------------
			
			getOzAssetIdsForLocalIds = function( _, localIds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMapLatest( function( wfCatalog )
									return require 'WFCatIdUtils'.getOzAssetIdsForLocalIds( wfCatalog, localIds )
								end )
				}

			end,

			--------------------------------------------------
			-- _DEBUG_getLocalIdAndSubtypeForOzAssetId
			--------------------------------------------------
			
			_DEBUG_getLocalIdAndSubtypeForOzAssetId = function( _, ozAssetId )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMapLatest( function( wfCatalog )
									return require 'WFCatIdUtils'._DEBUG_getLocalIdAndSubtypeForOzAssetId( wfCatalog, ozAssetId )
								end )
				}

			end,

			--------------------------------------------------
			-- getCollaboratorAvatar
			--------------------------------------------------
			getCollaboratorAvatar = function( _, userId, optPreferredResolution )
				return {
						
					value = Observable.combineLatest( getNonNilWFCatalogStream( wfCatalogStream ),
													wfCatSynchronizerStream:filter( isNonNil ):take( 1 ) )
								:flatMapLatest( function( wfCatalog, wfCatSynchronizer )
									
									local NmbCollaborator = require 'NmbCollaborator'
									return NmbCollaborator.getAvatar( wfCatalog, wfCatSynchronizer, userId, optPreferredResolution )
									
								end ),
				}
			end,
			
			--------------------------------------------------
			-- waitForPendingDevelopSettingsSaveForAssetIds
			--------------------------------------------------
			waitForPendingDevelopSettingsSaveForAssetIds = function( _, ozAssetIds )
			
				return {
				
					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMapLatest( function()
									return require 'WFCatDevelopSettings'.waitForPendingDevelopSettingsSaveForAssetIds( ozAssetIds )
								end )
				
				}
			
			end,

			--------------------------------------------------
			-- dashboard reporting
			--------------------------------------------------
			generateDashboard = function( _, ... )

				return require 'NmbCatalogDashboard'.makeModel( 
								getNonNilWFCatalogStream( wfCatalogStream ) )

			end,

			--------------------------------------------------
			-- basicSearchAndSort
			--------------------------------------------------
			basicSearchAndSort = function( _, localSearchTerms, onlineSearchTerms, sortTerms )

				return require 'WFCatSearchAndSort'.makeModel(
						wfCatalogStream, localSearchTerms, onlineSearchTerms,
						sortTerms )

			end,

			--------------------------------------------------
			-- trackedData
			--------------------------------------------------
			trackedData = function( _ )
				return {
					value = NmbTrackedDataBroadcaster.broadcastStream()
				}
			end,

		},

		-- mostRecentValueModels are submodels for which only the most recent
		-- value on the subModel's Rx.Observable value field is guaranteed to
		-- be delivered. i.e. mostRecentValueSubModel:value():subscribe( ... ),
		-- the subscription's onData handler may drop events on the stream if
		-- many events are sent in a short period, but it eventually 
		-- send the most recent value.
		mostRecentValueModels = {

			--------------------------------------------------
			-- restartRequiredForDbIssue
			-- 
			-- refer to handleCorruptNmbCatalogDerivedDbResultStream
			-- in NmbDbCorruptionUtils for details, but this model's value will
			-- emit at most one result and the client app should quit and
			-- possibly restart if an event is emitted.
			--------------------------------------------------
			restartRequiredForDbIssue = function()

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest(
									function( wfCatalog, _, profileCatalog, wfCatSynchronizer )
										
										return NmbDbCorruptionUtils.handleCorruptNmbCatalogDerivedDbResultStream(
												dbCorruptionEventStream,
												wfCatalog, profileCatalog,
												wfCatSynchronizer._metadataSyncClient )

									end )
								:map(
									function( errorType, canBeSolvedByRestart, dbName, errStr )
									
										return { errorType, canBeSolvedByRestart, dbName, errStr }

									end )

				}

			end,


			--------------------------------------------------
			-- assetSyncStatus
			--------------------------------------------------
			assetSyncStatus = function( _, assetIds )

				return require 'WFCatSyncStatusModel'.makeAssetSyncStatusModel( wfCatalogStream, assetIds )

			end,

			--------------------------------------------------
			-- syncStatus
			--------------------------------------------------
			syncStatus = function()

				return require 'WFCatSyncStatusModel'.makeSyncStatusModel( wfCatalogStream )

			end,

			--------------------------------------------------
			-- estimatedTimeRemainingForUploads
			--------------------------------------------------
			estimatedTimeRemainingForUploads = function()

				-- Returns the estimated time remaining for uploads, and copies, in seconds.
				-- -1 is returned in the event there is nothing to upload.

				return {

					value = wfCatSynchronizerStream
								:filter( isNonNil )
								:take( 1 )
								:flatMap( function( wfCatSynchronizer )
									return wfCatSynchronizer:getEstimatedTimeRemainingForUploads()
								end )

				}

			end,


			--------------------------------------------------
			-- areAssetsFullySyncedWithOz
			--------------------------------------------------
			
			-- The following function resolves to `true` if all of the assets
			-- are fully synced with Oz, and there are no pending local changes
			-- that have yet to sync.
			
			areAssetsFullySyncedWithOz = function( _, assetIds )

				return {
				
					value = getNonNilWFCatalogStream( wfCatalogStream )
					
							:flatMap( function( wfCatalog )
							
								return WFOzDocUtils.areAssetsFullySyncedWithOz( wfCatalog, assetIds )
								
							end )
				}
				
			end,

			--------------------------------------------------
			-- waitUntilAssetsAreFullySyncedWithOz
			--------------------------------------------------
	
			-- The following function resolves to `true` once all of the assets
			-- are fully synced with Oz.
	
			waitUntilAssetsAreFullySyncedWithOz = function( _, assetIds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )

							:flatMap( function( wfCatalog )

								return WFOzDocUtils.waitUntilAssetsAreFullySyncedWithOz( wfCatalog, assetIds )

							end )
				}

			end,

			--------------------------------------------------
			-- storage
			--------------------------------------------------
			storage = function()

				return require 'WFCatSyncStatusModel'.makeStorageModel( wfCatalogStream )

			end,
			
			--------------------------------------------------
			-- checkAndReclaimSpaceForNewImages
			--------------------------------------------------
			
			--[[ 
				Checks if there is enough free space for incoming images. If the free space is
				not sufficient, it will try to free up space by purging. Checks for free space
				both on the main drive (for thumbnails, proxies etc) and originals drive (for
				originals). If the originals drive is the main drive, make enough space for both.
				If the drives are different, but the originals drive is offline, then also make
				space for both on the main drive.
				Returns a table {
					originalsAreOnMainVolume = boolean,
					originalsVolumeIsOffline = boolean,

					mainVolumeName = string,
					mainVolumePath = string,
					mainVolumeSpaceRequired = number,
					mainVolumeSpaceAvailable = number,
					
					originalsVolumeName = string,
					originalsVolumePath = string,
					originalsVolumeSpaceRequired = number,
					originalsVolumeSpaceAvailable = number,
				} ]]
			
			checkAndReclaimSpaceForNewImages = function( _, numImages, spaceNeeded )
				return {
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
								
									local fileSpec = wfCatalog:fileSpec()
									
									local mainVolumeGuid = fileSpec:catalogPathInfo().volumeGuid
									local mainVolumeName = WFFileUtils.getVolumeNameFromGuid( mainVolumeGuid )
									local mainVolumePath = WFFileUtils.getVolumeFromGuid( mainVolumeGuid )
									
									local originalsVolumeGuid = fileSpec:originalsPathInfo().volumeGuid

									-- For now, be VERY conservative about the estimated space
									-- requirement for previews, smart previews, catalog info, etc.
								
									-- Post-1.0 we can get more clever here and disable
									-- smart preview downloading during the migration triggered
									-- import, then re-enable it, for example. That way, any space issues
									-- encountered due to smart previews can be dealt with via
									-- the normal mechanisms provided by the purge agent, and we
									-- never have to account for smart preview storage requirement
									-- in this calculation.
									
									-- See NMB-5937.

									local gig = 1024 * 1024 * 1024
									local additionalSpaceNeeded
									
									if NmbFileManagementSettings.keepAllProxies() then
										additionalSpaceNeeded = math.ceil( ( numImages / 1000 ) * 2 * gig )
									else
										-- Don't need to account for smart previews, which makes the space
										-- requirement smaller.
										additionalSpaceNeeded = math.ceil( ( numImages / 1000 ) * 0.75 * gig )
									end
									
									local volumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( originalsVolumeGuid )
									
									if volumeAgent then
										return volumeAgent:getVolumeStatusStream()
											:take( 1 )
											:flatMapLatest( function( volumeStatus )
											
												local originalsVolumeIsOnline = volumeStatus.isOnline
												local originalsAreOnMainVolume = mainVolumeGuid == originalsVolumeGuid
												
												if originalsAreOnMainVolume and not originalsVolumeIsOnline then
													return Observable.throw( 'noDestinationVolume' )
												end
												
												local spaceNeededOnMainVolume
												local spaceNeededOnOriginalsVolume
												local spaceCheckObservable
												
												if not originalsAreOnMainVolume and originalsVolumeIsOnline then
													-- Looks for space on separate volumes
													local mainVolumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( mainVolumeGuid )
													local originalsVolumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( originalsVolumeGuid )
													
													spaceNeededOnMainVolume = additionalSpaceNeeded
													spaceNeededOnOriginalsVolume = spaceNeeded
													
													spaceCheckObservable = Observable.combineLatest(
														mainVolumeAgent:tryToClaimSpace( spaceNeededOnMainVolume ),
														originalsVolumeAgent:tryToClaimSpace( spaceNeededOnOriginalsVolume )
													)
													
												else
													-- Look for space only on the main volume
													local mainVolumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( mainVolumeGuid )
													
													spaceNeededOnMainVolume = spaceNeeded + additionalSpaceNeeded
													spaceNeededOnOriginalsVolume = 0

													spaceCheckObservable = Observable.combineLatest(
														mainVolumeAgent:tryToClaimSpace( spaceNeededOnMainVolume ),
														Observable.just( 0 )
													)
												end
												
												return spaceCheckObservable
													:map( function( spaceAvailableOnMainVolume, spaceAvailableOnOriginalsVolume )

														local originalsVolumeName = originalsVolumeIsOnline
															and WFFileUtils.getVolumeNameFromGuid( originalsVolumeGuid )

														local originalsVolumePath = originalsVolumeIsOnline
															and WFFileUtils.getVolumeFromGuid( originalsVolumeGuid )

														return {
															originalsAreOnMainVolume = originalsAreOnMainVolume,
															originalsVolumeIsOffline = not originalsVolumeIsOnline,

															mainVolumeName = mainVolumeName,
															mainVolumePath = mainVolumePath,
															mainVolumeSpaceRequired = spaceNeededOnMainVolume,
															mainVolumeSpaceAvailable = spaceAvailableOnMainVolume,
															
															originalsVolumeName = originalsVolumeName,
															originalsVolumePath = originalsVolumePath,
															originalsVolumeSpaceRequired = spaceNeededOnOriginalsVolume,
															originalsVolumeSpaceAvailable = spaceAvailableOnOriginalsVolume,
														}
													end )
											end )

									else
										return Observable.throw( 'errorFindingVolume' )
									end
									
								end )
				}
			end,

			--------------------------------------------------
			-- checkAndReclaimSpaceForExport
			--------------------------------------------------
			checkAndReclaimSpaceForExport = function( _, assetIds, settings )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )

							return WFCatExport.checkAndReclaimSpace(
								wfCatalog,
								assetIds,
								settings )

						end )

				}
			end,

			--------------------------------------------------
			-- checkAndReclaimSpaceForLocalAlbums
			--------------------------------------------------
			checkAndReclaimSpaceForLocalAlbums = function( _, albumIds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )

							return require 'NmbOfflineAssets'.checkAndReclaimSpaceForAlbums(
								wfCatalog,
								albumIds )

						end )

				}
			end,

			--------------------------------------------------
			-- deleteAssets
			--------------------------------------------------
			deleteAssets = function( _, assetIds )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( wfCatalog, pixelProvider, _, wfCatSynchronizer )
							return require 'WFCatDelete'.deleteAssets( wfCatalog, assetIds,
								wfCatSynchronizer, pixelProvider, wfCatalog:getDeletionBacklog() )
						end )
						:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- restoreTrashAssets
			--------------------------------------------------
			restoreTrashAssets = function( _, assetIds )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( wfCatalog, _, _, wfCatSynchronizer )
							return require 'WFCatDelete'.restoreTrashAssets( wfCatalog, assetIds,
								wfCatSynchronizer )
						end )
						:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- deleteTrashAssets
			--------------------------------------------------
			deleteTrashAssets = function( _, assetIds )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( wfCatalog, pixelProvider, _, wfCatSynchronizer )
							return require 'WFCatDelete'.deleteTrashAssets( wfCatalog, assetIds,
								wfCatSynchronizer, pixelProvider, wfCatalog:getDeletionBacklog() )
						end )
						:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- trashAssetList
			--------------------------------------------------
			trashAssetList = function( _ )

				return require 'WFCatDelete'.trashAssetListModel( wfCatalogStream )

			end,

			--------------------------------------------------
			-- trashAssetCount
			--------------------------------------------------
			trashAssetCount = function( _ )

				return require 'WFCatDelete'.trashAssetCountModel( wfCatalogStream )

			end,

			--------------------------------------------------
			-- trashAssetsTotalFileSize
			--------------------------------------------------
			trashAssetsTotalFileSize = function( _ )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( wfCatalog )
							return require 'WFCatTrash'.trashAssetsTotalFileSize( wfCatalog )
						end )
				}

			end,
			
			--------------------------------------------------
			-- notificationUnreadCount
			--------------------------------------------------
			notificationUnreadCount = function( _ )
				
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( _, _, _, wfCatSynchronizer )
							return wfCatSynchronizer._metadataSyncClient:notificationUnreadCount()
						end )
				}
			end,
			
			--------------------------------------------------
			-- notificationList
			--------------------------------------------------
			notificationList = function( _ )
			
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
							:flatMap( function( wfCatalog, _, _, wfCatSynchronizer )
								return wfCatSynchronizer._metadataSyncClient:notificationList()
								:flatMap( function( notificationList )
								
									-- use set to elimiate the duplicate ids
									local assetAndAlbumIdSet = {}
									for _, item in ipairs( notificationList ) do
										if item.asset then
											assetAndAlbumIdSet[ item.asset.id ] = true
										end

										if item.album then
											assetAndAlbumIdSet[ item.album.id ] = true
										end

										-- NOTE [immisch 2020-11-24]: person doesn't use localIds

										for _, targetName in ipairs{ 'primaryImageUI', 'secondaryImageUI' } do
											local listOfIdTables = item[ item[ targetName ] ]

											if listOfIdTables then
												for _, idTable in ipairs( listOfIdTables ) do
													if type( idTable ) == 'table' and idTable.id then
														assetAndAlbumIdSet[ idTable.id ] = true
													end
												end
											end
										end
									end

									local assetAndAlbumIds = KSTableUtils.extractKeys( assetAndAlbumIdSet )

									return wfCatalog:docStore():bulkDocIdToLocalIdAsMap( assetAndAlbumIds )
										:map( function( assetIdToLocalIdAsMap )
											for _, item in ipairs( notificationList ) do
												if item.asset then
													item.asset.localId = assetIdToLocalIdAsMap[ item.asset.id ]
												end

												if item.album then
													item.album.localId = assetIdToLocalIdAsMap[ item.album.id ]
												end

												for _, targetName in ipairs{ 'primaryImageUI', 'secondaryImageUI' } do
													local listOfIdTables = item[ item[ targetName ] ]

													if listOfIdTables then
														for _, idTable in ipairs( listOfIdTables ) do
															if type( idTable ) == 'table' and idTable.id then
																idTable.localId = assetIdToLocalIdAsMap[ idTable.id ]
															end
														end
													end
												end
											end

											return notificationList
										end )
									end )
								end )
				}
				
			end,


			--------------------------------------------------
			-- setReadNotification
			--------------------------------------------------
			setReadNotification = function( _, notificationMarkRead, optNotificationMarkUnread )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( _, _, _, wfCatSynchronizer )
							return wfCatSynchronizer:isSyncRunningStream()
								:take( 1 )
								:filter( function( isSyncRunning )
									return isSyncRunning
								end )
								:flatMap( function()
									return wfCatSynchronizer._metadataSyncClient:setReadNotification( notificationMarkRead, optNotificationMarkUnread )
								end )
						end )
				}

			end,

			--------------------------------------------------
			-- setVisitedNotification
			--------------------------------------------------
			setVisitedNotification = function( _, notificationMarkVisited, optNotificationMarkUnvisited )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( _, _, _, wfCatSynchronizer )
							return wfCatSynchronizer:isSyncRunningStream()
								:take( 1 )
								:filter( function( isSyncRunning )
									return isSyncRunning
								end )
								:flatMap( function()
									return wfCatSynchronizer._metadataSyncClient:setVisitedNotification( notificationMarkVisited, optNotificationMarkUnvisited )
								end )
						end )
				}

			end,

			--------------------------------------------------
			-- notificationKillSwitch
			--------------------------------------------------
			notificationKillSwitch = function( _ )
				
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( _, _, _, wfCatSynchronizer )
							return wfCatSynchronizer._metadataSyncClient:notificationKillSwitch()
						end )
				}
			end,
			
			--------------------------------------------------
			-- performUndo
			--------------------------------------------------
			performUndo = function( _, undoToken )
			
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function()
							return require 'WFCatUndo'.performUndo( undoToken )
						end )
				}

			end,
			
			--------------------------------------------------
			-- allStacks
			--------------------------------------------------
			allStacks = function( _ )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							return require 'WFCatStacks'.allStacks( wfCatalog )
						end )
				}

			end,
			
			--------------------------------------------------
			-- allCollaborators
			--------------------------------------------------
			allCollaborators = function( _ )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( wfCatalog )
							return require 'WFCatCollaborator'.getAllCollaborators( wfCatalog )
						end )
				}

			end,
			
			--------------------------------------------------
			-- createStack
			--------------------------------------------------
			createStack = function( _, assetIds, maybeAlbumId )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							return require 'WFCatStacks'.createStack( wfCatalog, assetIds, maybeAlbumId )
								:map( function( _, localStackId )
									return localStackId
								end )
						end )
						:call( WFRxRunUntilDone )
						
				}
			end,
			
			--------------------------------------------------------------------------------
			-- ungroupStack (fully delete the stack)
			--------------------------------------------------------------------------------
			ungroupStack = function( _, stackId )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							return require 'WFCatStacks'.deleteStack( wfCatalog, stackId )
						end )
						:call( WFRxRunUntilDone )
				}
			end,
			
			--------------------------------------------------------------------------------
			-- addAssetsToStack
			--------------------------------------------------------------------------------
			addAssetsToStack = function( _, stackId, imageIds )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							return require 'WFCatStacks'.addAssetsToStack( wfCatalog, stackId, imageIds )
						end )
						:call( WFRxRunUntilDone )
				}
			end,
			
			--------------------------------------------------------------------------------
			-- removeAssetsFromStack
			--------------------------------------------------------------------------------
			removeAssetsFromStack = function( _, stackId, imageIds )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							return require 'WFCatStacks'.removeAssetsFromStack( wfCatalog, stackId, imageIds )
						end )
						:call( WFRxRunUntilDone )
				}
			end,
			
			--------------------------------------------------------------------------------
			-- setStackLeader
			--------------------------------------------------------------------------------
			setStackLeader = function( _, stackId, imageId )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							return require 'WFCatStacks'.setCover( wfCatalog, stackId, imageId )
						end )
						:call( WFRxRunUntilDone )
				}
			end,
			
			--------------------------------------------------
			-- createCopiesOfAssets
			--------------------------------------------------
			createCopiesOfAssets = function( _, assetIds, maybeAlbumIdArray, maybeStackIdsByAssetIds )
				local WFCatCopies = require 'WFCatCopies'
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( wfCatalog )
							return WFCatCopies.createCopiesOfAssets( wfCatalog, assetIds, maybeAlbumIdArray, maybeStackIdsByAssetIds )
							:flatMap( function( theAssetIds )
								return wfCatalog:docStore():bulkDocIdToLocalIdAsMap( theAssetIds )
								:map( KSTableUtils.extractValues )
							end )
						end )
						:call( WFRxRunUntilDone )
				}

			end,

			--------------------------------------------------
			-- enableOfflineUseForAlbums
			--------------------------------------------------
			enableOfflineUseForAlbums = function( _, albumIds )

				return require 'NmbOfflineAssets'.offlineAlbumsModel( wfCatalogStream, albumIds, true )

			end,

			--------------------------------------------------
			-- disableOfflineUseForAlbums
			--------------------------------------------------
			disableOfflineUseForAlbums = function( _, albumIds )

				return require 'NmbOfflineAssets'.offlineAlbumsModel( wfCatalogStream, albumIds, false )

			end,

			--------------------------------------------------
			-- ozACRVersion
			--------------------------------------------------

			ozACRVersion = function()
			
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							local urlTail = "/v2/version?level=full&indent=1"
							local ozUser = wfCatalog:ozUser()
							local urlHead = ozUser:serverBaseUrl()
							local url = string.format( "%s/%s", urlHead, urlTail )
							return ozUser:get( url )
							:map( function( result, headers )
								if headers and tostring( headers.status_code ) == "200" then
									local resultTable = KSJsonDecoder.decodeWebSafeJson( result )
									return resultTable
								end
								
								return "--"
							end )
						end )
				}
			
			end,

			--------------------------------------------------
			-- allRenderableLocalAssetIds
			--------------------------------------------------
			allRenderableLocalAssetIds = 
				makeSimpleSubmodelForValidWFCatalog(
					WFCatAssets.allRenderableLocalAssetIdsStream,
					wfCatalogStream ),

			--------------------------------------------------
			-- facetsValueOnlineSearch
			--------------------------------------------------
			facetsValueOnlineSearch = function( _, onlineSearchTerms )
			
				return require 'WFCatSearchAndSort'.getFacetValues( wfCatalogStream, onlineSearchTerms )
				
			end,

			--------------------------------------------------
			-- categoryKeys
			--------------------------------------------------
			categoryKeys = function( _, onlineSearchTerms )
			
				return require 'WFCatSearchSortOnlineHandler'.categoryKeys( wfCatalogStream, onlineSearchTerms )
			
			end,

			-- autoCompleteSearch
			--------------------------------------------------
			autoCompleteSearch = function( _, onlineSearchTerms )
			
				return require 'WFCatSearchSortOnlineHandler'.getAutoCompleteValues( wfCatalogStream, onlineSearchTerms )
				
			end,

			--------------------------------------------------
			-- basicSearchCount
			--------------------------------------------------
			basicSearchCount = function( _, searchDescriptor )

				return require 'WFCatSearch'.makeCountModel( wfCatalogStream, searchDescriptor )

			end,

			--------------------------------------------------
			-- queryTerms
			--------------------------------------------------
			queryTerms = function( _, termCategory, searchDescription )

				return require 'WFCatSearch'.makeQueryTermsModel( wfCatalogStream, termCategory, searchDescription )

			end,

			--------------------------------------------------
			-- queryTermCategories
			--------------------------------------------------
			queryTermCategories = function()

				return require 'WFCatSearch'.makeQueryTermCategoriesModel( wfCatalogStream )

			end,
			
			--------------------------------------------------
			-- collectKeywordDetailsOfRequiredAssets
			--------------------------------------------------
			collectKeywordDetailsOfRequiredAssets = 
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectKeywordDetailsOfRequiredAssets, {} ),
			
			--------------------------------------------------
			-- collectLCKeywordDetailsOfAllAssets
			--------------------------------------------------
			collectLCKeywordDetailsOfAllAssets = 
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectLCKeywordDetailsOfAllAssets, {} ),
					
			--------------------------------------------------
			-- collectKeywordDetailsOfAllAssets
			--------------------------------------------------
			collectKeywordDetailsOfAllAssets = 
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectKeywordDetailsOfAllAssets, {} ),
					
			--------------------------------------------------
			-- collectCameraDetailsOfRequiredAssets
			--------------------------------------------------
			collectCameraDetailsOfRequiredAssets = 
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectCameraDetailsOfRequiredAssets, {} ),

			--------------------------------------------------
			-- collectCameraDetailsOfAllAssets
			--------------------------------------------------
			collectCameraDetailsOfAllAssets = 
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectCameraDetailsOfAllAssets, {} ),
					
			
			--------------------------------------------------
			-- collectLocationDetailsOfRequiredAssets
			--------------------------------------------------
			collectLocationDetailsOfRequiredAssets = 
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectLocationDetailsOfRequiredAssets, {} ),
			
			--------------------------------------------------
			-- collectLocationDetailsOfAllAssets
			--------------------------------------------------
			collectLocationDetailsOfAllAssets = 
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectLocationDetailsOfAllAssets, {} ),
					
			--------------------------------------------------
			-- collectSyncStatusDetailsOfRequiredAssets
			--------------------------------------------------
			collectSyncStatusDetailsOfRequiredAssets = 
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectSyncStatusDetailsOfRequiredAssets, {} ),
			
			--------------------------------------------------
			-- collectSyncStatusDetailsOfAllAssets
			--------------------------------------------------
			collectSyncStatusDetailsOfAllAssets = 
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectSyncStatusDetailsOfAllAssets, {} ),
					
			--------------------------------------------------
			-- collectAssetTypeDetailsOfRequiredAssets
			--------------------------------------------------
			collectAssetTypeDetailsOfRequiredAssets = 
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectAssetTypeDetailsOfRequiredAssets, {} ),
			
			--------------------------------------------------
			-- collectAssetTypeDetailsOfAllAssets
			--------------------------------------------------
			collectAssetTypeDetailsOfAllAssets = 
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectAssetTypeDetailsOfAllAssets, {} ),
					
			--------------------------------------------------
			-- collectUniqueImportTimeOnAssets
			--------------------------------------------------
			collectUniqueImportTimeAndCountOfAssets = 
				makeSimpleSubmodelWithDefaultValue( 
					wfCatalogStream, 
					require 'WFCatAsset'.collectSortedUniqueImportTimeAndCountOfAssets,
					{} ),
			
			--------------------------------------------------
			-- collectUniqueCaptureDatesOfAssets
			--------------------------------------------------
			collectUniqueCaptureDatesAndCountOfAssets = 
				makeSimpleSubmodelWithDefaultValue( 
					wfCatalogStream, 
					require 'WFCatAsset'.collectSortedUniqueCaptureDatesAndCountOfAssets,
					{} ),
			
			--------------------------------------------------
			-- collectInfoForAssetsFromIndexStore
			--------------------------------------------------
			collectInfoForAssetsFromIndexStore = 
				makeSimpleSubmodelWithDefaultValue( 
					wfCatalogStream, 
					require 'WFCatAsset'.collectInfoForAssetsFromIndexStore, 
					{} ),
			
			--------------------------------------------------
			-- collectInfoForAssetsFromDocStore
			--------------------------------------------------
			collectInfoForAssetsFromDocStore = 
				makeSimpleSubmodelWithDefaultValue( 
					wfCatalogStream, 
					require 'WFCatAsset'.collectInfoForAssetsFromDocStore, 
					{} ),
					
			--------------------------------------------------
			-- collectPathsForAssetFromDocStore
			--------------------------------------------------
			collectPathsForAssetFromDocStore = 
				makeSimpleSubmodelWithDefaultValue( 
					wfCatalogStream, 
					require 'WFCatAsset'.collectPathsForAssetFromDocStore, 
					{} ),
			
			--------------------------------------------------
			-- collectCaptureDateTimeRangeForAssets
			--------------------------------------------------
			collectCaptureDateTimeRangeForAssets =
				makeSimpleSubmodelWithDefaultValue(
					wfCatalogStream,
					require 'WFCatAsset'.collectCaptureDateTimeRangeForAssets,
					{} ),
					
			--------------------------------------------------
			-- collectImportedByUserOfAllAssets
			--------------------------------------------------
			collectImportedByUserOfAllAssets = 
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatSearchByImportedByUser'.collectImportedByUserOfAllAssets, {} ),
					
			
			-- collectImportedByUserOfRequiredAssets
			--------------------------------------------------
			collectImportedByUserOfRequiredAssets = 
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatSearchByImportedByUser'.collectImportedByUserOfRequiredAssets, {} ),
			
			--------------------------------------------------
			-- setRatings
			--------------------------------------------------
			setRatings = function( _, imageIds, newRating, optUndoStateToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest( 
									function( wfCatalog )

										return require 'WFCatAsset'.setRatings( wfCatalog, imageIds, newRating, optUndoStateToken )

									end )

				}

			end,

			--------------------------------------------------
			-- setFlags
			--------------------------------------------------
			setFlags = function( _, imageIds, newFlag, optUndoToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest( 
									function( wfCatalog )

										return require 'WFCatAsset'.setFlags( wfCatalog, imageIds, newFlag, optUndoToken )

									end )

				}
			end,
			
			--------------------------------------------------
			-- toggleFlags
			--------------------------------------------------
			toggleFlags = function( _, imageIds, flag, optUntoToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest( 
									function( wfCatalog )

										return require 'WFCatAsset'.toggleFlags( wfCatalog, imageIds, flag, nil, optUntoToken )

									end )

				}
			end,

			--------------------------------------------------
			-- adjustFlags
			--------------------------------------------------
			adjustFlags = function( _, imageIds, direction, optUntoToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest( 
									function( wfCatalog )

										return require 'WFCatAsset'.adjustFlags( wfCatalog, imageIds, direction, nil, optUntoToken )

									end )

				}
			end,
			
			--------------------------------------------------
			-- rotateOrFlipImages
			--------------------------------------------------
			rotateOrFlipImages = function( _, imageIds, rotation, flip, optUndoStateToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest( 
									function( wfCatalog )

										return require 'WFCatAsset'.rotateOrFlipImages( wfCatalog, imageIds, rotation, flip, optUndoStateToken )

									end )

				}

			end,
			
			--------------------------------------------------
			-- setKeywords
			--------------------------------------------------
			setKeywords = function( _, imageIds, keywords, delete, optUndoStateToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest( 
									function( wfCatalog )

										return require 'WFCatAsset'.setKeywords( wfCatalog, imageIds, keywords, delete, optUndoStateToken )

									end )

				}
			end,
			
			--------------------------------------------------
			-- reorderAssetsInAlbum
			--------------------------------------------------
			reorderAssetsInAlbum = function( _, albumId, anchorAssetId, assetIdsToReorder, insertWhere, maybeAllAssetIds, optUndoStateToken )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:take( 1 )
								:flatMapLatest( 
									function( wfCatalog )

										return require 'WFCatAlbums'.reorderAssetsInAlbum( wfCatalog, albumId, anchorAssetId, assetIdsToReorder, insertWhere, maybeAllAssetIds, optUndoStateToken )

									end )

				}
			end,
			
			-------------------------------------------------
			-- forceReorderAssetsInAlbum
			--------------------------------------------------
			forceReorderAssetsInAlbum = function( _, albumId, anchorAssetId, assetIdsToReorder, insertWhere, maybeAllAssetIds, optUndoStateToken )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:take( 1 )
								:flatMapLatest( 
									function( wfCatalog )

										return require 'WFCatAlbums'.forceReorderAssetsInAlbum( wfCatalog, albumId, anchorAssetId, assetIdsToReorder, insertWhere, maybeAllAssetIds, optUndoStateToken )

									end )

				}
			end,
			
			--------------------------------------------------
			-- copyMetadataAndSetEnhanceDetailsForImages
			--------------------------------------------------
			copyMetadataAndSetEnhanceDetailsForImages = function( _, imageIdMap )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest( 
									function( wfCatalog )

										return require 'WFCatAsset'.copyMetadataAndSetEnhanceDetailsForImages( wfCatalog, imageIdMap )

									end )

				}
			end,
			
			--------------------------------------------------
			-- editCaptureDate
			--------------------------------------------------
			editCaptureDate = function( _, imageIds, timeDelta, newDate, optUndoStateToken )

				return {

					value = wfCatalogStream
						:take( 1 )
						:flatMapLatest( 
							function( wfCatalog )

								return require 'WFCatAsset'.editCaptureDate( wfCatalog, imageIds, timeDelta, newDate, optUndoStateToken )

							end )

				}
			end,

			--------------------------------------------------
			-- albums
			--------------------------------------------------
			albums = function()

				return require 'NmbAlbums'.makeAlbumsModel( wfCatalogStream )

			end,
			
			--------------------------------------------------
			-- upload priority
			--------------------------------------------------
			flagUploadAsHighPriority = function( _, assetId, isTimeCritical )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatPhotoImportance'.increaseUploadPriorityForAsset, assetId, isTimeCritical )

			end,
			
			--------------------------------------------------------------------------------
			
			removeHighPriorityUploadFlag = function( _, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatPhotoImportance'.removeUploadPriorityModifier, assetId )

			end,
			
			--------------------------------------------------
			-- shared-with-you albums
			--------------------------------------------------
			sharedWithYouAlbums = function()

				return require 'WFCatSharedWithYouAlbums'.makeModel( wfCatalogStream )

			end,
			
			--------------------------------------------------
			-- Oz Maintenance
			--------------------------------------------------

			getOzMaintenanceNotificationStream = function()

				return {

					value = wfCatSynchronizerStream:filter( isNonNil )

						:take( 1 )

						:flatMap( function( wfCatSynchronizer )

							return wfCatSynchronizer:getOzMaintenanceNotificationStream()

						end )

				}

			end,

			--------------------------------------------------
			-- syncDiagnostics
			--------------------------------------------------

			--[[! This model that can be used gather real-time sync diagnostic data.

				Example response structure:
					
					{
					
						metadataSyncDiagnostics = {
						
							catalogPullerSyncDiagnostics = { < diagnostic data > },
							
							catalogPusherSyncDiagnostics = { < diagnostic data > },
							
							spaceFeedsManagerSyncDiagnostics = {
							
								spacePusherSyncDiagnostics = { < diagnostic data > },
								
								feedOfFeedsPullerSyncDiagnostics = { < diagnostic data > },
								
								spacePullersSyncDiagnostics = { 
								
									{ < space 1 diagnostic data > },
									
									{ < space 2 diagnostic data > },
									
									...
								}
								
							}
							
						},
						
						binaryUploaderSyncDiagnostics = { < diagnostic data > },
						
						idleDownloaderSyncDiagnostics = { < diagnostic data > }
						
					}
					
					For more information on the diagnostic data, please see the syncDiagnostics
					functions with the associated sync classes.

			--]]

			syncDiagnostics = function( _, optCalm )

				return {

					value = wfCatSynchronizerStream:filter( isNonNil )

						:take( 1 )

						:flatMap( function( wfCatSynchronizer )

							return wfCatSynchronizer:syncDiagnostics( optCalm )

						end )

				}

			end,

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

			-- Get a stream that returns asset document IDs that the system believes are in
			-- a stuck sync state due to non-uploaded metadata.

			getDocsStuckSyncingMetadataStream = function( _, optCalm, optStuckReportingTimeThreshold )

				return {

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ), wfCatSynchronizerStream:filter( isNonNil ) )
								:take( 1 )
								:flatMap( function( wfCatalog, wfCatSynchronizer )

										local isNormalSyncRunningStream = wfCatSynchronizer:isNormalSyncRunningStream()

										return require 'WFCatStuckSyncUtils'.getDocsStuckSyncingMetadataStream( 
												wfCatalog, isNormalSyncRunningStream, optCalm, optStuckReportingTimeThreshold )
										
								end )
				}

			end,

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

			-- Get a stream that returns asset document IDs that the system believes are in
			-- a stuck sync state due to an non-uploaded original.

			getDocsStuckUploadingOriginalStream = function( _, optCalm, optStuckReportingTimeThreshold )

				return {

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ), wfCatSynchronizerStream:filter( isNonNil ) )
								:take( 1 )
								:flatMap( function( wfCatalog, wfCatSynchronizer )

										local isNormalSyncRunningStream = wfCatSynchronizer:isNormalSyncRunningStream()

										return require 'WFCatStuckSyncUtils'.getDocsStuckUploadingOriginalStream( 
												wfCatalog, isNormalSyncRunningStream, optCalm, optStuckReportingTimeThreshold )
										
								end )
				}

			end,
			
			--------------------------------------------------
			-- Versions
			--------------------------------------------------
			
			createVersionForAsset = function( _, assetId, createdManually, versionName )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.createVersionForAsset, assetId, createdManually, versionName )

			end,

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

			createVersionForAssetWithSettings = function( _, assetId, createdManually, versionName, developSettings )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.createVersionForAssetWithSettings, 
								assetId, createdManually, versionName, developSettings )

			end,

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

			createVersionForAssetWithExternalSettings = function( _, assetId, createdManually, versionName, developSettings )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.createVersionForAssetWithExternalSettings, 
								assetId, createdManually, versionName, developSettings )

			end,
			
			--------------------------------------------------------------------------------

			duplicateVersion = function( _, sourceVersionId, createdManually, versionName )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.duplicateVersion, sourceVersionId, createdManually, versionName )

			end,

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

			getAutoVersionCountForAsset = function( _, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.getAutoVersionCountForAsset, assetId )

			end,

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

			getAllVersionsStreamForAsset= function( _, assetId, includeFakeImportStateVersion )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.getAllVersionsStreamForAsset, assetId, includeFakeImportStateVersion )

			end,
			
			--------------------------------------------------------------------------------

			getManualVersionsStreamForAsset= function( _, assetId, includeFakeImportStateVersion )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.getManualVersionsStreamForAsset, assetId, includeFakeImportStateVersion )

			end,
			
			--------------------------------------------------------------------------------

			getAutoVersionsStreamForAsset= function( _, assetId, includeFakeImportStateVersion )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.getAutoVersionsStreamForAsset, assetId, includeFakeImportStateVersion )

			end,

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

			renameVersion = function( _, versionId, versionName )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.renameVersion, versionId, versionName )

			end,

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

			getVersionsAnalytics = function()

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.getVersionsAnalytics )

			end,

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

			deleteVersion = function( _, versionId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.deleteVersion, versionId )

			end,

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

			deleteAllVersionsForAsset = function( _, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.deleteAllVersionsForAsset, assetId )

			end,
			
			--------------------------------------------------------------------------------

			deleteManualVersionsForAsset = function( _, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.deleteManualVersionsForAsset, assetId )

			end,
			
			--------------------------------------------------------------------------------

			deleteAutoVersionsForAsset = function( _, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.deleteAutoVersionsForAsset, assetId )

			end,

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

			compareCurrentSettings = function( _, docId, developSettings )
			
				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.compareCurrentSettings, docId, developSettings )

			end,

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

			downloadExternalizedVersionXmp = function( _, versionId )
			
				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.downloadExternalizedXmpForVersion, versionId,
								WFCatDevelopSettings.saveExternalizedXmp )
						
			end,
			
			--------------------------------------------------------------------------------

			maybeDeleteTempAutoVersion = function( _, assetId, withinSeconds )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
						require 'WFCatVersions'.maybeDeleteTempAutoVersion, assetId, withinSeconds )

			end,

			--------------------------------------------------
			-- EditDefaults
			--------------------------------------------------

			getEditDefaultsEnabled = function()

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.getEditDefaultsEnabled )

			end,

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

			setEditDefaultsEnabled = function(_, enabled)

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.setEditDefaultsEnabled, enabled )

			end,

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

			getDefaultImportPreset = function()

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.getDefaultImportPreset )

			end,

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

			setDefaultImportPreset = function( _, presetName, presetDigest )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.setDefaultImportPreset, presetName, presetDigest )

			end,

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

			getEditDefaultsEnabledWithImportPreset = function()

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.getEditDefaultsEnabledWithImportPreset )


			end,

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

			getPresetForCameraModel = function( _, modelName )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.getPresetForCameraModel, modelName )

			end,

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

			setPresetForCameraModel = function( _, modelName, presetName, presetDigest )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.setPresetForCameraModel, modelName, presetName, presetDigest )

			end,

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

			removePresetForCameraModel = function(_, modelName)

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.removePresetForCameraModel, modelName )

			end,

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

			listPresetsByCameraModel = function()

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.listPresetsByCameraModel )

			end,
			
			--------------------------------------------------
			-- getPeopleForImageIds
			--------------------------------------------------
			getPeopleForImageIds = function( _, assetIds )

				return {
				
					value = getNonNilWFCatalogStream( wfCatalogStream )
    							:flatMap( function( wfCatalog )
								
										return require 'WFCatFaces'.getPeopleForImageIds( wfCatalog, assetIds )
										
									end 
								)
				}
			end,

			--------------------------------------------------
			-- getDistinctPeopleForImageIds
			--------------------------------------------------
			getDistinctPeopleForImageIds = function( _, assetIds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
    							:flatMapLatest( function( wfCatalog )
								
										return require 'WFCatFaces'.getDistinctPeopleForImageIds( wfCatalog, assetIds )
										
									end 
								)
				}

			end,
			
			--------------------------------------------------
			-- getDistinctPeopleForAllPhotos
			--------------------------------------------------
			getDistinctPeopleForAllPhotos = function( _ )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
    							:flatMapLatest( function( wfCatalog )
								
										return require 'WFCatFaces'.getDistinctPeopleForAllPhotos( wfCatalog )
										
									end 
								)
				}

			end,
			
			--------------------------------------------------
			-- getPeopleInfoForAllAssets
			--------------------------------------------------
			getPeopleInfoForAllAssets = function( _, minimumImagesCount, dontIncludeHidden, namedOnly )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
    							:flatMap( function( wfCatalog )
								
										return require 'WFCatFaces'.getPeopleInfoForAllAssets( wfCatalog, minimumImagesCount, dontIncludeHidden, namedOnly )
										
									end 
								)
				}

			end,
			
			--------------------------------------------------
			-- getPhotoCountforPersonIds
			--------------------------------------------------
			getPhotoCountforPersonIds = function( _, personIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatFaces'.getPhotoCountforPersonIds( wfCatalog, personIds )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- addPerson
			--------------------------------------------------
			addPerson = function( _, person )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.addPerson( wfCatalog, person )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- updatePersonName
			--------------------------------------------------
			updatePersonName = function( _, personId, personName )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.updatePersonName( wfCatalog, personId, personName )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- updatePeopleName
			--------------------------------------------------
			updatePeopleName = function( _, peopleIdNameList )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.updatePeopleName( wfCatalog, peopleIdNameList )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- deletePerson
			--------------------------------------------------
			deletePerson = function( _, personIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.deletePerson( wfCatalog, personIds )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- hidePerson
			--------------------------------------------------
			hidePerson = function( _, personId, hide )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.hidePerson( wfCatalog, personId, hide )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- hidePeople
			--------------------------------------------------
			hidePeople = function( _, peopleHideSet )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.hidePeople( wfCatalog, peopleHideSet )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- downloadPersonThumbnail
			--------------------------------------------------
			downloadPersonThumbnail = function( _, personId )
				return {

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ), wfCatSynchronizerStream:filter( isNonNil ) )
								:take( 1 )
								:flatMap( function( wfCatalog, wfCatSynchronizer )

										return require 'WFCatPerson'.downloadPersonThumbnail( wfCatalog, wfCatSynchronizer, personId )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- downloadPeopleThumbnails
			--------------------------------------------------
			downloadPeopleThumbnails = function( _, personIds )
				return {

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ), wfCatSynchronizerStream:filter( isNonNil ) )
								:take( 1 )
								:flatMap( function( wfCatalog, wfCatSynchronizer )

										return require 'WFCatPerson'.downloadPeopleThumbnails( wfCatalog, wfCatSynchronizer, personIds )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- shallowMergePeople
			--------------------------------------------------
			shallowMergePeople = function( _, parentId, personIds, personName )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.shallowMergePeople( wfCatalog, parentId, personIds, personName )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- splitPeople
			--------------------------------------------------
			splitPeople = function( _, peopleInfoList )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.splitPeople( wfCatalog, peopleInfoList )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- rejectSimilarPerson
			--------------------------------------------------
			rejectSimilarPerson = function( _, personId, rejectedPersonId, undo )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.rejectSimilarPerson( wfCatalog, personId, rejectedPersonId, undo )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- rejectSimilarPeople
			--------------------------------------------------
			rejectSimilarPeople = function( _, personId, rejectedPersonIds, undo )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.rejectSimilarPeople( wfCatalog, personId, rejectedPersonIds, undo )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- getSimilarPeopleForPersonId
			--------------------------------------------------
			getSimilarPeopleForPersonId = function( _, personId )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.getSimilarPeopleForPersonId( wfCatalog, personId )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- setPersonCover
			--------------------------------------------------
			setPersonCover = function( _, personId, assetId )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.setPersonCover( wfCatalog, personId, assetId )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- setPersonCoverReturnFaceInfo
			--------------------------------------------------
			setPersonCoverReturnFaceInfo = function( _, personIds, assetId )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.setPersonCoverReturnFaceInfo( wfCatalog, personIds, assetId )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- setPersonCoverReturnFaceInFoExcept
			--------------------------------------------------
			setPersonCoverReturnFaceInfoExcept = function( _, personIds, assetIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatPerson'.setPersonCoverReturnFaceInfoExcept( wfCatalog, personIds, assetIds )
										
									end 
								)
				}
			end,
			
			--------------------------------------------------
			-- removePeopleFromAssets
			--------------------------------------------------
			removePeopleFromAssets = function( _, personIds, assetIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatFaces'.removePeopleFromAssets( wfCatalog, personIds, assetIds )
										
									end )
									:call( WFRxRunUntilDone )
				}
			end,
			
			--------------------------------------------------
			-- removePersonFromTagsPanel
			--------------------------------------------------
			removePersonFromTagsPanel = function( _, ozFaceIdsAssetIdsPersonIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatFaces'.removePersonFromTagsPanel( wfCatalog, ozFaceIdsAssetIdsPersonIds )
										
									end )
									:call( WFRxRunUntilDone )
				}
			end,
			
			--------------------------------------------------
			-- moveAssetsToAnotherPerson
			--------------------------------------------------
			moveAssetsToAnotherPerson = function( _, personIdTo, personIds, assetIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatFaces'.moveAssetsToAnotherPerson( wfCatalog, personIdTo, personIds, assetIds )
										
									end )
									:call( WFRxRunUntilDone )
				}
			end,
			
			--------------------------------------------------
			-- moveAssetsToAnotherPersonTable
			--------------------------------------------------
			moveAssetsToAnotherPersonTable = function( _, personIdTo, ozFaceIdsAssetIdsPersonIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatFaces'.moveAssetsToAnotherPersonTable( wfCatalog, personIdTo, ozFaceIdsAssetIdsPersonIds )
										
									end )
									:call( WFRxRunUntilDone )
				}
			end,
			
			--------------------------------------------------
			-- addFaceToAsset
			--------------------------------------------------
			addFaceToAsset = function( _, assetId, faceId, personId )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatFaces'.addFaceToAsset( wfCatalog, assetId, faceId, personId )
										
									end )
									:call( WFRxRunUntilDone )
				}
			end,
			
			--------------------------------------------------
			-- addFacesToAsset
			--------------------------------------------------
			addFacesToAsset = function( _, assetId, appliedToDimensions, faceDetailsArray )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatFaces'.addFacesToAsset( wfCatalog, assetId, appliedToDimensions, faceDetailsArray )
										
									end )
									:call( WFRxRunUntilDone )
				}
			end,
			
			--------------------------------------------------
			-- updateFaceRegion
			--------------------------------------------------
			updateFaceRegion = function( _, assetId, faceId, faceArea )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatFaces'.updateFaceRegion( wfCatalog, assetId, faceId, faceArea )
										
									end )
									:call( WFRxRunUntilDone )
				}
			end,
			
			--------------------------------------------------
			-- removeFaceRegionsFromAssets
			--------------------------------------------------
			removeFaceRegionsFromAssets = function( _, assetFaceMap )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( 
									function( wfCatalog )

										return require 'WFCatFaces'.removeFaceRegionsFromAssets( wfCatalog, assetFaceMap )
										
									end )
									:call( WFRxRunUntilDone )
				}
			end,
			
			--------------------------------------------------
			-- Style favorite info
			--------------------------------------------------			
			styleFavoriteInfo = function()
				
				return {
				
					value = wfCatalogStream:filter( isNonNil )
								:flatMapLatest( function( wfCatalog )
						
									return require "NmbStyleFavorites".favoriteInfoStream( wfCatalog )
									
						end )
				
				}
			end,
			
			--------------------------------------------------
			-- developContent
			--------------------------------------------------
			developContent = function( _, assetId )
			
				return WFCatDevelopSettings.developContentModel(
							getNonNilWFCatalogStream( wfCatalogStream ), assetId )
				
			end,

			--------------------------------------------------
			-- developContentsWithLocalNegativePath
			-- 
			--------------------------------------------------
			developContentWithLocalNegativePath = function( _, assetId )
			
				return WFCatDevelopSettings.developContentWithLocalNegativePathModel(
							getNonNilWFCatalogStream( wfCatalogStream ), assetId )
				
			end,
			
			--------------------------------------------------
			-- ongoingDevelopContent
			--------------------------------------------------
			ongoingDevelopContent = function()
			
				return WFCatDevelopSettings.ongoingDevelopContentModel(
							getNonNilWFCatalogStream( wfCatalogStream ) )
				
			end,

			--------------------------------------------------
			-- purgeBlackout
			--------------------------------------------------
			purgeBlackout = function()
			
				return NmbPurgeControl.makeModel(
							getNonNilWFCatalogStream( wfCatalogStream ) )
				
			end,

			--------------------------------------------------
			-- Undo state management
			--------------------------------------------------

			deleteUndoState = function( _, undoStateToken )
				
				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest( function()
									return require'WFCatUndo'.deleteUndoState( undoStateToken )
								end )
								:call( WFRxRunUntilDone )
				}
				
			end,

			getUndoStateCount = function()
				
				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest( 
									function()

										local count = require'WFCatUndo'.getUndoStateCount()
										return Observable.just( count )

									end )
				}
				
			end,
			
			--------------------------------------------------
			-- setMetadata
			--------------------------------------------------
			setMetadata = function( _, imageIds, formatterId, value, optUndoStateToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest( 
									function( wfCatalog )

										return require'WFCatFormattedMetadata'.setMetadata( wfCatalog, imageIds, formatterId, value, optUndoStateToken )

									end )

				}

			end,
			
			--------------------------------------------------
			-- harvestedMetadata
			--------------------------------------------------
			harvestedMetadata = function( _, imageIds, formatterIds )
			
				return require 'WFCatFormattedMetadata'.harvestedMetadataModel( wfCatalogStream, imageIds, formatterIds )
			
			end,

			--------------------------------------------------
			-- gridPixelPaths
			--------------------------------------------------
			gridPixelPaths = function( _, pixStreamId )

				return PixelProviderGridPixelPathsModel.make( 
						wfCatalogStream, pixStreamId )

			end,

			--------------------------------------------------
			-- bestLoupeRenditionPaths
			--------------------------------------------------
			bestLoupeRenditionPaths = function( _, pixStreamId,
												defaultLoupeRenditionSize )

				return PixelProviderLoupePixelPathsModel.make( 
						wfCatalogStream, pixStreamId,
						defaultLoupeRenditionSize )

			end,

			--------------------------------------------------
			-- getNegativePathASAP
			--------------------------------------------------
			getNegativePathASAP = function( _, pixStreamId )

				return PixelProviderDownloadNegativeModel.getNegativePathASAP( 
						pixelProviderProcessorStream, pixStreamId )

			end,

			--------------------------------------------------
			-- videoLoupe 
			--------------------------------------------------
			videoLoupe = function( _, pixStreamId )

				return PixelProviderVideoLoupeModel.make( 
						pixelProviderProcessorStream, pixStreamId )

			end,

			--------------------------------------------------
			-- ensureLocalNegativePathForPrefetchImageIds
			--------------------------------------------------
			ensureLocalNegativePathForPrefetchImageIds= function( _, pixStreamId )

				return PixelProviderDownloadNegativeModel.ensureLocalNegativePathForPrefetchImageIds( 
						pixelProviderProcessorStream, pixStreamId )

			end,

			--------------------------------------------------
			-- _DEBUG_isSyncRunning
			--------------------------------------------------
			_DEBUG_isSyncRunning = function( _ )
				return {
					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
														wfCatSynchronizerStream )
						:flatMapLatest( function( _, wfCatSynchronizer )
							return wfCatSynchronizer:isSyncRunningStream()
						end )
				}
			end,

			--------------------------------------------------
			-- _DEBUG_enableDetailedPixelProviderStats
			--------------------------------------------------
			_DEBUG_enableDetailedPixelProviderStats= function()
			
				return {

					value = pixelProviderProcessorStream
								:flatMapLatest(
									function( pixelProviderProcessor )

										if not pixelProviderProcessor then

											return Rx.Observable.empty()

										end

										return pixelProviderProcessor:runUntilDone(
												'_DEBUG_enableDetailedPixelProviderStats' )

									end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_pixelRequestStats
			--------------------------------------------------
			_DEBUG_pixelRequestStats = function()
			
				return {

					value = pixelProviderProcessorStream
								:flatMapLatest(
									function( pixelProviderProcessor )

										return pixelProviderProcessor
												:_DEBUG_requestStatsStream()

									end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_localAssetIdPixelProviderStates
			--------------------------------------------------
			_DEBUG_localAssetIdPixelProviderStates = function( _, localAssetId )
			
				return {

					value = pixelProviderProcessorStream
								:flatMapLatest(
									function( pixelProviderProcessor )

										return pixelProviderProcessor
												:_DEBUG_localAssetIdStatesStream(
													localAssetId )

									end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_tryCreateRendition
			--------------------------------------------------
			_DEBUG_tryCreateRendition = function( _, localAssetId )

				local pixStreamId = string.format( "_DEBUG_tryCreateRendition %s", localAssetId )
			
				return {

					value = pixelProviderProcessorStream
								:flatMapLatest(
									function( pixelProviderProcessor )

										pixelProviderProcessor
											:runUntilDone(
												"requestFullSizeRenditionForExport", 
													pixStreamId,
													localAssetId )

										return pixelProviderProcessor
												:dispatch(
													"pixStreamIdPixelPathStream",
														pixStreamId )
												:map(
													function( ... )

														return { ... }

													end )

									end ),

					close = function()

						WFRxRunUntilDone(
							pixelProviderProcessorStream
								:flatMapLatest(
									function( pixelProviderProcessor )

										pixelProviderProcessor
											:runUntilDone(
												"abandonAllPixStreamRequests", 
													pixStreamId )
									end ) )

					end,

				}

			end,

			--------------------------------------------------
			-- _DEBUG_getSimulateDbCorruptionDbList
			--------------------------------------------------
			_DEBUG_getSimulateDbCorruptionDbList = function()

				return {

					value = Rx.Observable.just(
								NmbDbCorruptionUtils.simulateCorruptionDbList );
								
				}

			end,

			--------------------------------------------------
			-- _DEBUG_simulateDbCorruption
			--------------------------------------------------
			_DEBUG_simulateDbCorruption = function( _, dbName )

				return {

					value = 
						wfCatalogStream
							:take( 1 )
							:flatMapLatest(
								function( wfCatalog, _, profileCatalog, wfCatSynchronizer )

									return 
										NmbDbCorruptionUtils.makeSimulateCorruptionForDbResultStream(
											wfCatalog, profileCatalog,
											wfCatSynchronizer._metadataSyncClient,
											dbName )

								end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_bulkRequestInfoStream
			--------------------------------------------------
			_DEBUG_bulkRequestInfoStream = function( _, refreshSeconds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )

							return wfCatalog:docStore():_DEBUG_bulkRequestInfoStream( refreshSeconds )

						end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_changesFeedInfoStream
			--------------------------------------------------
			_DEBUG_changesFeedInfoStream = function( _, refreshSeconds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )

							return wfCatalog:docStore():_DEBUG_changesFeedInfoStream( refreshSeconds )

						end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_changeCounterAndSizeOfIndexQueue
			--------------------------------------------------
			_DEBUG_changeCounterAndSizeOfIndexQueue = function( _, refreshSeconds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )

							return Observable.combineLatest(
								wfCatalog:docStore():changeCounter(),
								wfCatalog:indexStore():sizeOfIndexQueue()
							)
							:map( function( changeCounter, indexedSoFar, totalDocuments )
								return {
									changeCounter = changeCounter,
									indexedSoFar = indexedSoFar,
									totalDocuments = totalDocuments,
								}
							end )
							:calm( refreshSeconds, WFRxScheduler.sharedScheduler() )

						end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_stopMetadataPuller
			--
			-- ################## WARNING ####################
			-- This is for DEBUG use only and MUST be used
			-- with caution and specific purpose. Stopping the
			-- metadata puller without also stopping the
			-- binary uploader can lead to very undesirable
			-- behavior. For example a binary can be uploaded,
			-- and no confirmation is received (because we're
			-- not pulling updates) and so the binary will be
			-- uploaded again and again (with 'blackout'
			-- delays between each upload).
			-- ###############################################
			--------------------------------------------------			
			_DEBUG_stopMetadataPuller = function()
				return {
				
					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )

									if wfCatSynchronizer._metadataSyncClient then
										return wfCatSynchronizer._metadataSyncClient:stopSyncInResidualMode()
											-- Stops metadata puller only.
									end
									
									return Observable.empty()
								end )
				
				}
			end,
			
			--------------------------------------------------
			-- _DEBUG_startMetadataPuller
			--------------------------------------------------			
			_DEBUG_startMetadataPuller = function()
				return {
				
					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )
								
									if wfCatSynchronizer._metadataSyncClient then
										return wfCatSynchronizer._metadataSyncClient:startSyncInResidualMode()
											-- Starts metadata puller only.
										end
									
									return Observable.empty()
								end )
				
				}
			end,

			--------------------------------------------------
			-- _DEBUG_stopMetadataPusher
			--------------------------------------------------			
			_DEBUG_stopMetadataPusher = function()
				return {
				
					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )
								
									if wfCatSynchronizer._metadataSyncClient then
										return wfCatSynchronizer._metadataSyncClient:_DEBUG_stopMetadataPusher()
									end
									
									return Observable.empty()
						
								end )
				
				}
			end,

			--------------------------------------------------
			-- _DEBUG_startMetadataPusher
			--------------------------------------------------			
			_DEBUG_startMetadataPusher = function()
				return {
				
					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )
								
									if wfCatSynchronizer._metadataSyncClient then
										return wfCatSynchronizer._metadataSyncClient:_DEBUG_startMetadataPusher()
									end
									
									return Observable.empty()
						
								end )
				
				}
			end,

			--------------------------------------------------
			-- _DEBUG_startBinaryUploader
			--------------------------------------------------			
			_DEBUG_startBinaryUploader = function()
				return {
				
					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )
								
									if wfCatSynchronizer._binaryUploader then
										wfCatSynchronizer._binaryUploader:startContinuousUploads()
									end
									
									return Observable.empty()
						
								end )
				
				}
			end,

			--------------------------------------------------
			-- _DEBUG_stopBinaryUploader
			--------------------------------------------------			
			_DEBUG_stopBinaryUploader = function()
				return {
				
					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )
								
									if wfCatSynchronizer._binaryUploader then
										wfCatSynchronizer._binaryUploader:stopContinuousUploads()
									end
									
									return Observable.empty()
						
								end )
				
				}
			end,
			
			--------------------------------------------------
			-- _DEBUG_sendBinarySyncDiagnosticLogData
			--------------------------------------------------	

			_DEBUG_sendBinarySyncDiagnosticLogData = function()
				return {
					value = wfCatSynchronizerStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( 
									function( wfCatSynchronizer )
										
										if wfCatSynchronizer._binaryUploader then
										
											return Observable.combineLatest ( 
													wfCatSynchronizer:isUploadingBinaries(),
													wfCatSynchronizer._binaryUploader:uploadStatusChangeStream(),
													wfCatSynchronizer._binaryUploader:binaryUploadCompletionStream() )
												:map( function()
													
													return wfCatSynchronizer._binaryUploader:sendDiagnosticLogData()
										
											end )
											
										end
										
										return Observable.empty()
										
								end )
				}
			end,
			
			
			

			
			--------------------------------------------------
			-- libraryPath
			--------------------------------------------------
			libraryPath = function()
			
				return {
				
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:map(  
									function( wfCatalog )

										local fileSpec = wfCatalog:fileSpec()
										
										return fileSpec:rootPath()

									end )
					
				}
			
			end,

			--------------------------------------------------
			-- catalogFolderPath
			--------------------------------------------------
			crUserPathInfo = function()
			
				return {
				
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:map(  
									function( wfCatalog )

										local fileSpec = wfCatalog:fileSpec()
										
										local catalogPath = fileSpec:catalogPath()
										local crUserPathInfo = { require "NmbCrPathOverrides".get( catalogPath ) }
										
										return { crUserPathInfo, catalogPath }

									end )
					
				}
			
			end,

			--------------------------------------------------
			-- pixelsPath
			--------------------------------------------------
			pixelsPath = function()
			
				return {
					priority = 'high',
					
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:map(  
									function( wfCatalog )

										local fileSpec = wfCatalog:fileSpec()
										
										return fileSpec:pixelsPath()

									end )					
				}
			
			end,

			--------------------------------------------------
			-- assetListInfo
			--------------------------------------------------
			assetListInfo = function( _ )
			
				return {
				
					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMapLatest(  
									function( wfCatalog )

										return require 'WFCatImportDuplicateFiles'.assetListInfo( wfCatalog )

									end )
					
				}
			
			end,
			
			--------------------------------------------------
			-- findExistingFilesBySha256
			--------------------------------------------------
			findExistingFilesBySha256 = function( _, rawImageInfoArray )
			
				return {
				
					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMapLatest(  
									function( wfCatalog )

										return require 'WFCatImportDuplicateFiles'.findExistingFilesBySha256( wfCatalog, rawImageInfoArray )

									end )
					
				}
			
			end,

			--------------------------------------------------
			-- getMigratedLightroomCatalogs
			--------------------------------------------------
			getMigratedLightroomCatalogs = function( _ )
			
				return {
				
					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMapLatest(  
									function( wfCatalog )

										return require 'WFCatImportManifestMigratedCatalogs'.getMigratedCatalogs( wfCatalog )

									end )
					
				}
			
			end,

			--------------------------------------------------
			-- _debugAssetFileInfoModel
			--------------------------------------------------
			_debugAssetFileInfoModel = 
				makeSimpleSubmodelWithPriorityAndDefaultValue( 
					wfCatalogStream, 'low', DebuggingStreams.makeAssetFileInfoStream, {} ),
					
			
			--------------------------------------------------
			-- _nmb5582fixupInvalidOriginals
			--------------------------------------------------
			_nmb5582fixupInvalidOriginals = function ( _ )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest(
							function( wfCatalog )
								require 'WFCatInvalidOriginalHandler'.fixInvalidOriginals( wfCatalog )
								return Observable.empty()
							end )
				}
			end,
			
			--------------------------------------------------
			-- MapBox Access Token
			--------------------------------------------------
			
			getMapBoxAccessToken = function()

				return {
				
					value = getNonNilWFCatalogStream( wfCatalogStream )
					
						:flatMapLatest( function( wfCatalog )
							
							local catalogSpec = wfCatalog:ozCatalogSpec()
						
							local requestUrl = string.format( '%stokens/mapbox', catalogSpec:baseHref() )
						
							return catalogSpec:get( requestUrl, {
								returnHttpErrorsAsData = true
							} )
							
						end )
						
						:map( function( data, headers )
						
							if headers and headers.status_code == "200" then
							
								local decodedJson = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( data )
							
								local accessToken = decodedJson and derefOpt( decodedJson, 'access_token' )
							
								return accessToken
							
							end
						
							return nil
						
						end )
						
						:catch( function()
							return Observable.just()
						end )
						
				}

			end,
			

			setCodecEntitlement = function ( _, entitlement )
				return {
					value = require 'WFCodecUsage'.setEntitlement( entitlement )
				}
			end,


			codecUsage = function ( _ )
				return {
					value = require 'WFCodecUsage'.getCodecUsageStream()
				}
			end,
			
			--------------------------------------------------
			-- ensureProfilesExistsForCamera
			--------------------------------------------------
			ensureProfilesExistsForCamera = function( )
				
				return require "NmbEnsureProfileExistsModel".make( wfCatSynchronizerStream )
			
			end,
			
			--------------------------------------------------
			-- getFavoritesForAsset
			--------------------------------------------------
			getFavoritesForAsset = function( _, albumId, assetId )
				
				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
					require 'WFCatCollaborationActivity'.getFavoritesForAsset, albumId, assetId )
				
			end,

			--------------------------------------------------
			-- addFavorite
			--------------------------------------------------
			addFavorite = function( _, albumId, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
					require 'WFCatCollaborationActivity'.addFavorite, albumId, assetId )
				
			end,

			--------------------------------------------------
			-- removeFavorite
			--------------------------------------------------
			removeFavorite = function( _, albumId, assetId )
				
				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
					require 'WFCatCollaborationActivity'.removeFavorite, albumId, assetId )
				
			end,
			
			--------------------------------------------------
			-- getFavoritedAlbumsForAssetId
			--------------------------------------------------
			getFavoritedAlbumsForAssetId = function( _, assetId )
				
				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
					require 'WFCatCollaborationActivity'.getFavoritedAlbumsForAssetId, assetId )
					
			end,
			
			--------------------------------------------------
			-- getCommentsForAsset
			--------------------------------------------------
			getCommentsForAsset = function( _, albumId, assetId )
				
				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
					require 'WFCatCollaborationActivity'.getCommentsForAsset, albumId, assetId )
				
			end,
			
			--------------------------------------------------
			-- addCommentForAsset
			--------------------------------------------------
			addCommentForAsset = function( _, albumId, assetId, body )
				
				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
					require 'WFCatCollaborationActivity'.addCommentForAsset, albumId, assetId, body )
				
			end,
			
			--------------------------------------------------
			-- deleteComment
			--------------------------------------------------
			deleteComment = function( _, commentId )
				
				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
					require 'WFCatCollaborationActivity'.deleteComment, commentId )
				
			end,
			
			--------------------------------------------------
			-- getCommentedAlbumsForAssetId
			--------------------------------------------------
			getCommentedAlbumsForAssetId = function( _, assetId )
				
				return WFModelsUtils.createModelForFunction( wfCatalogStream, 
					require 'WFCatCollaborationActivity'.getCommentedAlbumsForAssetId, assetId )
					
			end,

		},

		-- use events to send "global" commands to anything that's created using
		-- the wfCatalogStream
		events = {

			startRenderBlackout = makeForwardEventToPixelProviderProccessor(
									"startRenderBlackout" ),
			
			--------------------------------------------------------------------

			endRenderBlackout = makeForwardEventToPixelProviderProccessor(
									"endRenderBlackout" ),
			
			--------------------------------------------------------------------

			setPreferRenderForNewRenditions = 
				makeForwardEventToPixelProviderProccessor(
					"setPreferRenderForNewRenditions" ),

		},

		close = function()

			logger:info( "Closing" )

		end,

	}

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

	-- SPAM [matthewj 2018-08-02]: Formalize this builder, probably in NmbModelUtils
	local modelBuilder = { 
		
		addCatalogBasedMostRecentValueModel = function( subModelName, fn )
			nmbModelDefinition.mostRecentValueModels[ subModelName ] = makeSimpleSubmodelForValidWFCatalog( fn, wfCatalogStream )
		end,
		
		addMostRecentValueModelWithDefaultValue = function( subModelName, fn, ... )
			nmbModelDefinition.mostRecentValueModels[ subModelName ] = makeSimpleSubmodelWithDefaultValue( fn, wfCatalogStream, ... )
		end
	}

	NmbConnectors.addConnectorModels( modelBuilder )
	
	modelBuilder.addCatalogBasedMostRecentValueModel( 'dumpSyncingDocsToPath', 
		function( wfCatalog, dbPath )
			
			-- get list of images that are in 'incomplete' states
			return require 'WFCatSyncStatus'.getLocalIdsWithSyncPendingOrError( wfCatalog )
				:take( 1 )
				:flatMap( function( syncingDocIds )
					return wfCatalog:docStore():dumpDocsToDbAtPath( syncingDocIds, dbPath )
						:map( function()
							return dbPath
						end )
				end )
		end )
			
	return NmbModelUtils.makeWFModelDefinition( nmbModelDefinition )

end

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

return NmbRootModel
