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

NmbUserModel.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.

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

	-- Rx
local Rx = require 'Rx'
local BehaviorSubject = Rx.BehaviorSubject
local ReplaySubject = Rx.ReplaySubject
local Observable = Rx.Observable

	-- WF.core.rxlua
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local WFRxScheduler = require 'WFRxScheduler'

	-- WF.core.diagnostic_log
local WFDiagnosticLog = require 'WFDiagnosticLog'

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

	-- WF.core.os
local KSPathUtils = require 'KSPathUtils'
local WFFileUtils = require 'WFFileUtils'
local WFInsomnia = require 'WFInsomnia'

	-- WF.core.utils

local KSData = require 'KSData'
local KSTableUtils = require 'KSTableUtils'
local WFRxLowFidelityTimer = require 'WFRxLowFidelityTimer'
local WFConfiguration = require 'WFConfiguration'
local KSIdGenerator = require "KSIdGenerator"
local KSPrefs = require 'KSPrefs'
local WFArguments = require 'WFArguments'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFCloseSupport = require 'WFCloseSupport'

	-- WF.oz_client.managed_catalog
local NmbCatalog = require 'NmbCatalog'
local NmbFreemiumUtils = require "NmbFreemiumUtils"
local NmbFileManagementSettings = require 'NmbFileManagementSettings'
local WFCatSpec = require 'WFCatSpec'
local WFCatFileSpec = require 'WFCatFileSpec'
local WFCatStorage = require 'WFCatStorage'
local WFCatSynchronizer = require 'WFCatSynchronizer'
local WFCatSyncStatus = require 'WFCatSyncStatus'
local WFCatUser = require 'WFCatUser'
local WFCatUserSynchronizer = require 'WFCatUserSynchronizer'
local NmbAppMessaging = require 'NmbAppMessaging'
local WFCatAvatar = require 'WFCatAvatar'
local NmbUserModel_LocalStorage = require 'NmbUserModel_LocalStorage'
local NmbCrPathOverrides = require 'NmbCrPathOverrides'
local WFCatAlbums = require 'WFCatAlbums'
local NmbTrackedDataBroadcaster = require 'NmbTrackedDataBroadcaster'
local NmbDbCorruptionUtils = require "NmbDbCorruptionUtils"

	-- WF.oz_client.ims
local WFImsAppIdentity = require 'WFImsAppIdentity'

	-- WF.oz_client.managed_catalog.pixel_provider
local PixelProviderProcessor = require 'PixelProviderProcessor'
	
	-- WF.oz_client.sync_engine
local WFOzUserLogin = require 'WFOzUserLogin'

	-- WF.web.http_client
local WFReachability = require 'WFReachability'

	-- WF.web
local KSJsonDecoder = require "KSJsonDecoder"

	-- WF trash
local WFCatTrash = require 'WFCatTrash'

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

local kGlobalDocumentId = '00000000000000000000000000000000'

local check = assert( WFArguments.check )

local runUntilDone = WFRxRunUntilDone

local logger = WFLogger 'NmbUserModel'

local POSTPONE_DURATION = WFConfiguration.FileMover.postponeDurationInSeconds

local DEBUG_OZ_IS_DOWN = WFConfiguration.Debug.debugOzIsDown

local USE_NON_CC_CREDENTIALS = WFConfiguration.LrMobile.useNonCCCredentials

local kClientUpgradeRequired = "upgrade_required"

local kReadOnlyThreadCount = WFConfiguration.WFRxIndexStore.readOnlyThreadCount or 2

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

local function setupPixelProccessor( wfCatUser, wfCatalogSpec,
									rootCatalogDataDir, catalogOptions,
									pixelProviderSettings )

	local pixelProviderProcessor = PixelProviderProcessor( wfCatUser,
																wfCatalogSpec, 
																rootCatalogDataDir, 
																catalogOptions,
																pixelProviderSettings)
	
	local scanAndPurgeDelay = WFConfiguration.PixelProvider.RenditionCachePurgeDelay

	scanAndPurgeDelay = type( scanAndPurgeDelay ) == "number" and scanAndPurgeDelay > 0 and
							scanAndPurgeDelay or 5 * 60

	local delayedScanAndPurgeRenditionCache =
			WFRxLowFidelityTimer.timer( scanAndPurgeDelay )
				:take( 1 )
				:flatMap( 
					function()

						return pixelProviderProcessor:scanAndPurge()

					end )

	local scanAndPurgeRenditionCacheSubscription =
				delayedScanAndPurgeRenditionCache:subscribe(
					function()

						logger:info( "Scan and purge requested" )

					end )

	pixelProviderProcessor:addCloseAction( 
		function() 

			scanAndPurgeRenditionCacheSubscription:dispose()

		end )

	return pixelProviderProcessor

end

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

local function addModelValueToDiagnosticLog( modelValue )
	
	WFDiagnosticLog.addLogSectionFunc( function()

		local modelValue_copy = {}

		for k, v in pairs( modelValue ) do
			if type( v ) == 'function' then
				modelValue_copy[ k ] = v()
			else
				modelValue_copy[ k ] = v
			end
		end

		modelValue_copy.email = nil
		modelValue_copy.fullName = nil
		modelValue_copy.firstName = nil
		modelValue_copy.lastName = nil

		WFDiagnosticLog.addSectionToLog
		{
			name = "Oz logged in user",
			id = 'NmbUserModel',
			weight = -100,

			body = modelValue_copy,
		}

	end )
	
end

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

local function takeOneValueAndRunFunc( observable, functionToRun )

	observable
		:take( 1 )
		:tap( functionToRun )
		:call( runUntilDone )
		
end

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

local function logoutUser( closingUser )

	closingUser:logout()

		:map( function()
			return closingUser:close()
		end )

		:call( runUntilDone )

end

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

local wfCatUser_now

local function createCurrentUserStream( serverBaseUrlStream, userIdStream, userInfoCachePath )
	
	local previousUser
	
	return Observable.combineLatest( function( serverBaseUrl, userId )

		logger:debugf( "currentUser from serverBaseUrl %s, userId %s via %s",
					   serverBaseUrl, userId, ':traceback:' )

		local newUser
		
		if serverBaseUrl and userId then
			newUser = WFCatUser( serverBaseUrl, userId, userInfoCachePath )
		end

		if previousUser and previousUser ~= newUser then
			logoutUser( previousUser )
		end

		previousUser = newUser

		if not newUser then
			KSPrefs.lastDaemonUser = nil
		end

		wfCatUser_now = newUser

		return newUser

	end,
	serverBaseUrlStream:distinctUntilChanged(), userIdStream:distinctUntilChanged() )
	:replay( nil, 1 )
	:refCount()
		
end

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

local function createHasValidConnectionToOz( currentUserStream )

	return currentUserStream
	
		:flatMap( function( currentUser )

			if currentUser then
				return currentUser:hasValidConnectionToOz()
			else
				return Observable.just( false )
			end

		end )
		
end

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

local function getUserIdStream( imsOverrides )

	if imsOverrides and imsOverrides.imsClientId and imsOverrides.imsClientSecret then
		WFImsAppIdentity.setClientIdAndSecret( imsOverrides.imsClientId,
														 imsOverrides.imsClientSecret )
	end

	local userIdStream = ReplaySubject( 1 )

	userIdStream
		:call( runUntilDone )

	return userIdStream

end

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

local wfCatSynchronizer

local function startSyncingUserAndCatalog( modelData )

	modelData._shouldSyncUserInfoStream:onNext( true )

	if wfCatSynchronizer then
		wfCatSynchronizer:startSyncing()
	end

end

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

local function stopSyncingUserAndCatalog( modelData )

	modelData._shouldSyncUserInfoStream:onNext( false )

	if wfCatSynchronizer then
		wfCatSynchronizer:stopSyncing()
	end

end

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

local function activateOnlineMode( modelData )

	logger:info( "Online mode activated" )

	modelData._isOnlineStream:onNext( true )

	if not KSPrefs.syncPaused then
		startSyncingUserAndCatalog( modelData )
	end

end

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

local function activateOfflineMode_imp( modelData, userId )

	modelData._isOnlineStream:onNext( false )

	stopSyncingUserAndCatalog( modelData )

	if not userId then
		logger:infof( "Offline mode requested, but no stored user ID. Ignoring." )
		return
	end

	modelData._userIdStream:onNext( userId )

end

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

local function activateOzLoginFailureMode( modelData, userId )

	logger:warnf( "offline mode activated with user id %s due to Oz login failure", userId )
	activateOfflineMode_imp( modelData, userId )

end

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

local function activateOfflineMode( modelData, userId )

	logger:warnf( "offline mode activated with user id %s due to network network connectivity problem", userId )
	activateOfflineMode_imp( modelData, userId )

end

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

local function monitorLoginTask( modelData, loginTaskObservable )

	modelData._loginStatusStream:onNext( { status = 'pending' } )

	local observable = loginTaskObservable

	if DEBUG_OZ_IS_DOWN then
		observable = Observable.throw( 'Oz is down!', 'server_error' )
	end

	observable

		:flatMapLatest( function( wfCatUser, userInfo, accessToken, refreshToken, optDeviceToken, optPerfData )

			if optPerfData then
			
				NmbTrackedDataBroadcaster.broadcastMessage( 'OzAuthenticationPerfData', optPerfData )
				
			end

			logger:debugf( "Storing tokens for new user %s", userInfo )

			return wfCatUser:storeTokens( accessToken, refreshToken, optDeviceToken )
				:concat( Observable.just( wfCatUser ) )

		end )

		:tap(

			function( wfCatUser )

				local _userId = wfCatUser:userId()

				logger:infof( "Successfully logged in to Oz with user ID %s",
							  _userId  )

				modelData._userIdStream:onNext( _userId )

				-- clear out any prior error if we are successful
				modelData._ozLoginServerErrorStream:onNext( nil )

				modelData._loginStatusStream:onNext( { status = "success" } )

			end,

			function( message, errCode, isRetryable, retryDelay, errorData )

				logger:errorf( "Failed to log in to Oz: %s, ( code: %s)", message, errCode )

				if isRetryable == false then
					
					local errorMessage = message
		
					local ok, errorInfo = pcall( KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty, message )
						
					if ok then
						errorMessage = KSTableUtils.derefOpt( errorInfo, "code" ) or message
					end
					
					modelData._loginStatusStream:onNext( { status = "error", errorMessage = errorMessage } )

				elseif errCode == 'server_error' and errorData then

					local ok, errorInfo = pcall( KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty, errorData )

					if ok then
						local code = KSTableUtils.derefOpt( errorInfo, "code" )
						if code == 'MaintenanceError' then

							local outageInfo = {
								id = 'catalog',
								isUnderMaintenance = true,
								accountAndErrorInfo = errorInfo,
							}

							modelData._isOutageOccurringStream:onNext( outageInfo )

						end
					end

				end

				--[[
					SPAM [jandrew 2018-07-24]: When Oz or IMS is down, the call to store tokens
					will throw an error. The error codes can be found in WFOzUserLogin. The possible
					values are:
				
					"client_error" -- Occurs when the login credentials are wrong ( HTTP 400 -> 500 )
					"server_error" -- Occurs when there is an unexpected Oz error.
					"connection_error" -- General network failures.
					"failure" -- Occurs when the response from IMS/Oz is invalid.
					
					In all the cases but the "client_error", we want to put the user in offline mode.
				]]--

				if errCode ~= 'client_error' then
					modelData._ozLoginServerErrorStream:onNext( string.format( "login server error '%s': %s", errCode, message ) )
				elseif isRetryable == false then
					modelData._userIdStream:onNext( nil )
				end

			end )

			:retryWhen( function( failure )
				return failure:flatMap( function( errorMessage, code, isRetryable, retryDelay )

					if isRetryable then

						-- Default to retry every minute if not present
						retryDelay = retryDelay or 60

						return Observable.just( true )
							:delay( retryDelay, WFRxScheduler.sharedScheduler() )

					end

					return Observable.throw( errorMessage, code, isRetryable, retryDelay )

				end )

			end )

		:call( runUntilDone )

	-- TODO [scouten 2015-07-22]: This subscription should probably
	-- have a lifetime so we can cancel it.

end

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

local function loginFromTokens( modelData, _,
								baseServerUrl,
								accessToken,
								refreshToken,
								optDeviceToken )

	logger:infof( "loginFromTokens url = %s", baseServerUrl )

	if optDeviceToken and #optDeviceToken == 0 then
		optDeviceToken = nil
	end

	local loginResult = WFOzUserLogin.loginFromTokens( modelData._userInfoCachePath, 
														baseServerUrl,
														accessToken,
														refreshToken,
														optDeviceToken )

	monitorLoginTask( modelData, loginResult )

end

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

local function currentUserFieldValueForPath( modelData, memberFieldPath )

	return modelData._currentUserStream and modelData._currentUserStream

		:map( function( wfCatUser )

			if wfCatUser then
				return wfCatUser:userInfoCache():ongoingFieldValue( wfCatUser:userId(), memberFieldPath )
			else
				return Observable.just( nil )
			end

		end )

		:switch()

end

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

-- When new assets are synced from Oz, add them to the file access database.

local function trackNewlySyncedFileAccess( fileAccessDb, latestDownloadedAssetIdsStream )

	local trackNewlySyncedFileAccessSub = latestDownloadedAssetIdsStream
		:subscribe( function( assetIds )
			fileAccessDb:touchMultipleIfNew( assetIds )
		end )

	fileAccessDb:addCloseAction( function()
		trackNewlySyncedFileAccessSub:dispose()
		trackNewlySyncedFileAccessSub = nil
	end )

end

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

local function mapVolumeGuidToName( volumesToMap )

	local mappedVolumes = {}

	for volumeGuid, value in pairs( volumesToMap ) do
	
		local volumeIdentifier

		if WFPlatformInfo.isMac then
			volumeIdentifier = WFFileUtils.getVolumeNameFromGuid( volumeGuid )
				-- On Mac, use the volume name.
		else 
			volumeIdentifier = WFFileUtils.getVolumeFromGuid( volumeGuid )
				-- On Windows, use the drive letter.
		end

		if not volumeIdentifier then
	
			-- If the drive identifier cannot be obtained ( likely because the drive is offline ),
			-- use the cached value.
	
			volumeIdentifier = NmbFileManagementSettings.getReadableIdentifierForVolumeGuid( volumeGuid )
					or volumeGuid
			
		end
		
		mappedVolumes[ volumeIdentifier ] = value
		
	end

	return mappedVolumes

end

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

local function distinctUserCompare( _, user1, _, user2 )

	return KSTableUtils.deepEquals( user1, user2 )
	
end

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

local function watchForRequiredUpdate( modelData, myWFCatSynchronizer )

	local requiredUpdateStreamSub = myWFCatSynchronizer:getRequiredUpdateStream()
		:subscribe( function( updateRequired )
		
			if updateRequired then
			
				modelData._isUpdateRequiredStream:onNext( true )
				
				if myWFCatSynchronizer then
					KSPrefs.syncPaused = true
					stopSyncingUserAndCatalog( modelData )
				end
				
			end
		
		end )

	myWFCatSynchronizer:addCloseAction( function()
		requiredUpdateStreamSub:dispose()
		requiredUpdateStreamSub = nil
	end )

end

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

local function restrictedCountryMode( modelData )

	KSPrefs.restrictedCountryMode = true

	modelData._restrictedCountryModeStream:onNext( true )

	wfCatSynchronizer:enableRestrictedCountryMode()

	wfCatSynchronizer:stopSyncing()
		
	if KSPrefs.syncPaused then
		KSPrefs.syncPaused = false
	end

end

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

local function checkForRestrictedCountry( wfCatUser, modelData )

	local chineseAccountCheckSub = wfCatUser:getAccountDocFromOz()
			:catch( Observable.just( {} ) )
			:pluck( 'country' )
			:take( 1 )
			:subscribe( function( countryCode )
			
				local chinaCountryCode = 'CN'
				
				if countryCode == chinaCountryCode then
					restrictedCountryMode( modelData )
				else
					KSPrefs.restrictedCountryMode = false
				end
				
			end )
						
	wfCatUser:addCloseAction( function()
		chineseAccountCheckSub:dispose()
		chineseAccountCheckSub = nil
	end )

end

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

local function getFreemiumCatalogBaseDir( rootCatalogDataDir )

	return KSPathUtils.child( rootCatalogDataDir, kGlobalDocumentId )

end

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

local function maybeMigrateFreemiumCatalog ( wfCatalogSpec, rootCatalogDataDir, simulateMoveError)

	local catalogID = wfCatalogSpec:catalogId()
	
	if catalogID ~= kGlobalDocumentId then
	
		local migratedCatalogBaseDir = KSPathUtils.child( rootCatalogDataDir, catalogID)
		local freemiumCatalogBaseDir = getFreemiumCatalogBaseDir( rootCatalogDataDir )
		
		if not WFFileUtils.exists( migratedCatalogBaseDir ) and WFFileUtils.exists( freemiumCatalogBaseDir ) then
			
			-- we are guaranteed that we are on the same Volume for large file moves
			local mkdirSuccess, mkdirMaybeError = pcall(WFFileUtils.mkdir, migratedCatalogBaseDir)
			
			if mkdirSuccess and WFFileUtils.exists( migratedCatalogBaseDir ) then
				
				-- MOVE FILES
				
				local filesAndFoldersToMove = {
					-- Files -- 
					"Managed Catalog.mcat",
					"Managed Catalog.mcat-wal",
					"Managed Catalog.wfindex",
					"Managed Catalog.wfindex-wal",
					"Managed Catalog.mru",
					-- Folders --
					"settings",
					"cr_settings",
					"cr_defaults",
					"cr_cameraprofiles",
					"people",
					"previews",
					"xmp"
				}
				
				-- check if files/folder exist and move
				for _, item in ipairs ( filesAndFoldersToMove ) do
					local itemPath = KSPathUtils.child( freemiumCatalogBaseDir, item )
					-- check if files exist and copy
					if WFFileUtils.exists( itemPath ) then
						local fileMoveSuccess
						local fileMoveMaybeError
						if not simulateMoveError then
							fileMoveSuccess, fileMoveMaybeError = pcall(WFFileUtils.move, itemPath, KSPathUtils.child( migratedCatalogBaseDir, item))
						else
							fileMoveSuccess = false
							fileMoveMaybeError = "SimulatedMoveError"
						end
						if not fileMoveSuccess then
							logger:errorf("Error during catalog transfer: %s, %s", item, fileMoveMaybeError)
						end
						
					else
						logger:warnf("Catalog migration could not find and move file: %s.", item)
					end
				end
			
				-- DELETE FILES
				
				-- Some files cannot move because they depend on the absolute paths
				-- We need to clear these files from the local catalog to ensure the 
				-- next local catalog user does not use old database.
				local filesAndFolderToDelete = {
					"previews.db"
				}
				
				-- Delete files and folders that cannot be transfered
				for _, item in ipairs ( filesAndFolderToDelete ) do
					local itemPath = KSPathUtils.child( freemiumCatalogBaseDir, item )
					-- check if files exist and move
					if WFFileUtils.exists( itemPath ) then
					
						if WFFileUtils.exists( itemPath ) == 'directory' and not ( WFFileUtils.empty( itemPath ) ) then
							
							logger:warnf("Error during catalog transfer: %s %s", item, "directory is not empty. Will not delete.")
						
						elseif WFFileUtils.exists( itemPath ) == 'directory' and ( WFFileUtils.empty( itemPath ) ) then
							-- deletable directory.
							local moveToTrashSuccess, moveToTrashMaybeError = pcall(WFFileUtils.delete, itemPath, true)
					
							if not moveToTrashSuccess then
								logger:errorf("Error during catalog transfer: %s, %s", item, moveToTrashMaybeError)
							end
							
						else
							-- is not a directory
							local moveToTrashSuccess, moveToTrashMaybeError = pcall(WFFileUtils.moveToTrash, itemPath)
							
							if not moveToTrashSuccess then
								logger:errorf("Error during catalog transfer: %s, %s", item, moveToTrashMaybeError)
							end
						end
						
					else
						logger:warnf("Catalog migration could not find and remove file: %s.", item)
					end
				end
				
				-- BACKUP RESIDUAL FILES
				
				-- Check and clear the local catalog in order to have a clean directory for the next freemium user
				local backupFolderBaseDir = KSPathUtils.child( freemiumCatalogBaseDir, "backup" )
				local backupMkdirSuccess, backupMkdirMaybeError = pcall(WFFileUtils.mkdir, backupFolderBaseDir )
				
				-- either succesfully created or already exists
				if backupMkdirSuccess or WFFileUtils.exists( backupFolderBaseDir ) then
					-- Check in the move list
					for _, item in ipairs ( filesAndFoldersToMove ) do
						local itemPath = KSPathUtils.child( freemiumCatalogBaseDir, item )
						-- check if files exist and move to backup
						if WFFileUtils.exists( itemPath ) then
							local maybeUniqueFileName = WFFileUtils.ensureFileNameIsUnique(item)
							local fileBackupSuccess, fileBackupMaybeError = pcall(WFFileUtils.move, itemPath, KSPathUtils.child( backupFolderBaseDir, maybeUniqueFileName))

							if not fileBackupSuccess then
								logger:errorf("Error during catalog transfer backup: %s, %s", item, fileBackupMaybeError)
							end
							
						else
							logger:infof("Catalog migration could not find and backup file: %s.", item)
						end
					end
					
					-- Check in the delete list
					for _, item in ipairs ( filesAndFolderToDelete ) do
						local itemPath = KSPathUtils.child( freemiumCatalogBaseDir, item )
						if WFFileUtils.exists( itemPath ) then
						
							local maybeUniqueFileName = WFFileUtils.ensureFileNameIsUnique(item)
							local fileBackupSuccess, fileBackupMaybeError = pcall(WFFileUtils.move, itemPath, KSPathUtils.child( backupFolderBaseDir, maybeUniqueFileName))

							if not fileBackupSuccess then
								logger:errorf("Error during catalog transfer backup: %s, %s", item, fileBackupMaybeError)
							end
							
						else
							logger:warnf("Catalog migration could not find and backup file: %s.", item)
						end
					end
					
				else
				
					logger:errorf("Error during catalog transfer: %s", backupMkdirMaybeError)

				end
				
			else
				-- Something Went Wrong creating the new catalog base directory
				logger:errorf("Error during catalog transfer: %s", mkdirMaybeError)
			end
					
			return true
			
		end
	end
	
	return false
	
end

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

local function createOzCatalog( wfCatUser, wfCatalogSpec, maybeProfileCatalogSpec, 
		rootCatalogDataDir, pixelProviderSettings, modelData )

	local catalogOptions = {
		indexSpecScript = 'WFCatIndexDefinitions.lua',
		indexStoreReadThreads = kReadOnlyThreadCount,
		docStoreInitProgressRequested = true,
	}
	
	-- Set Up Freemium
	
	local freemiumCatalogMigrated = maybeMigrateFreemiumCatalog( wfCatalogSpec, rootCatalogDataDir )
	
	local isFreemium = wfCatalogSpec:catalogId() == kGlobalDocumentId
	
	local wfCatalog = NmbCatalog( wfCatalogSpec, rootCatalogDataDir, catalogOptions )
	NmbCrPathOverrides.set( wfCatalog:fileSpec():catalogPath() )
	
	-- If a catalog is freemium. We do not get a local document back from Oz.
	-- Instead, create a dummy document so that docValue._empty == false.
	if isFreemium then
		local catalogDoc = {
			_id = kGlobalDocumentId,
			type = 'catalog',
			subtype = 'lightroom',
			}
		wfCatalog:docStore():setDocValue( catalogDoc ):call( runUntilDone )
	end
	
	local pixelProviderProcessor = 
			setupPixelProccessor( wfCatUser, wfCatalogSpec,
									rootCatalogDataDir, catalogOptions, pixelProviderSettings )

	wfCatalog:addCloseTarget( pixelProviderProcessor )

	local maybeProfileCatalogOptions
	local maybeProfileCatalogIdOrDoc
	local maybeProfileCatalog
	if maybeProfileCatalogSpec then
		
		local profilesDir = KSPathUtils.child( rootCatalogDataDir, 'profiles' )
		local profileCatalogPath = KSPathUtils.child( profilesDir, maybeProfileCatalogSpec:catalogId() .. '.mcat' )
		maybeProfileCatalogOptions = { 
			docStorePath = profileCatalogPath,
			indexSpecScript = 'NmbProfilesIndexDefinitions.lua',
			noIdleDownloader = true,
			noCatalogSearch = true,
		}
		
		maybeProfileCatalog = require 'NmbProfileCatalog'( maybeProfileCatalogSpec, rootCatalogDataDir, 
							maybeProfileCatalogOptions )
		if isFreemium then
			maybeProfileCatalogIdOrDoc = maybeProfileCatalogSpec:catalogDoc() or maybeProfileCatalogSpec:catalogId()
		else
			maybeProfileCatalogIdOrDoc = maybeProfileCatalogSpec:catalogDoc() or maybeProfileCatalogSpec:ozCatalogId()
		end
	end

	
	local workerThreadArgs = { wfCatUser:serverBaseUrl(),
								wfCatUser:userId(),
								wfCatUser:userInfoCacheFolder(),
								wfCatalogSpec:catalogDoc() or wfCatalogSpec:catalogId(),
								rootCatalogDataDir, 
								catalogOptions,
								maybeProfileCatalogIdOrDoc,
								maybeProfileCatalogOptions,
							}

	wfCatSynchronizer = WFCatSynchronizer( wfCatalog, maybeProfileCatalog, pixelProviderProcessor, workerThreadArgs,
		not KSPrefs.syncPaused, KSPrefs.residualMode, KSPrefs.freemiumMode, modelData._locale )

	wfCatalog:addCloseAction( function()
	
		wfCatSynchronizer:close()
		wfCatSynchronizer = nil
		
	end )

	local docStoreUpdater = wfCatalog:getDocStoreUpdater()
	docStoreUpdater:setSynchronizer( wfCatSynchronizer )
	docStoreUpdater:start()
	
	local currentDaemonDownloadStatusSub = Observable.combineLatest( modelData._currentDaemonDownloadStatusStream,
				 modelData._catalogValidationStatusStream )
		:subscribe( function( statusTable, catalogValidationStatus )
			if not catalogValidationStatus then
				statusTable.catalogStatus = 'does_not_match'
			end
			pixelProviderProcessor:runUntilDone( 'setDaemonDownloadStatus', statusTable )
		end )
	
	local catalogValidationSub = wfCatSynchronizer:catalogValidationStream()
		:subscribe( function( isValid )
			modelData._catalogValidationStatusStream:onNext( isValid )
		end )
		
	local isUploadingMetadataSub = wfCatSynchronizer:isUploadingMetadata()
		:subscribe( function( isUploading )
			modelData._isSyncUploadingMetadataStream:onNext( isUploading )
		end )

	local isDownloadingMetadataSub = wfCatSynchronizer:isDownloadingMetadata()
		:subscribe( function( isDownloading )
			modelData._isSyncDownloadingMetadataStream:onNext( isDownloading )
		end )

	local isUploadingBinariesSub = wfCatSynchronizer:isUploadingBinaries()
		:subscribe( function( isUploading )
			modelData._isSyncUploadingBinariesStream:onNext( isUploading )
		end )

	local isDownloadingOriginalsOrProxiesSub = wfCatSynchronizer:isDownloadingRequiredOriginalsOrProxies()
		:subscribe( function( isDownloading )
			modelData._isSyncDownloadingOriginalsOrProxiesStream:onNext( isDownloading )
		end )
	
	local isDownloadingStylesSub = wfCatSynchronizer:isDownloadingStyles()
		:subscribe( function( isDownloadingStyles )
			modelData._isSyncDownloadingStylesStream:onNext( isDownloadingStyles )
		end )

	local isSyncPendingSub = WFCatSyncStatus.syncPendingForAnyAsset( wfCatalog )
		:subscribe( function( isPending )
			modelData._isSyncPendingStream:onNext( isPending )
		end )
		
	local isPendingRestoreSub = WFCatSyncStatus.getNumAssetsWithRestorationPending( wfCatalog )
		:subscribe( function( isPendingRestore )
			modelData._isPendingRestoreStream:onNext( isPendingRestore )
		end )

	local getOzMaintenanceNotificationStreamSub = wfCatSynchronizer:getOzMaintenanceNotificationStream()
		:subscribe( function( ozMaintenanceInfo )

			local catalogId = ozMaintenanceInfo and ozMaintenanceInfo.id
			local maintenanceActive = ozMaintenanceInfo and ozMaintenanceInfo.isUnderMaintenance
			local errorAndAccountInfo = ozMaintenanceInfo and ozMaintenanceInfo.accountAndErrorInfo
			
			if catalogId == 'catalog' then

				local outageInfo -- table of errorAndAccountInfo or false

				if maintenanceActive then
					outageInfo = errorAndAccountInfo or {}
				else
					outageInfo = false
				end

				modelData._isOutageOccurringStream:onNext( outageInfo )

			else

				if maintenanceActive then
					modelData._spaceOutages[ catalogId ] = true
				else
					modelData._spaceOutages[ catalogId ] = nil
				end

				modelData._spaceOutageOccurringStream:onNext( modelData._spaceOutages )

			end

		end )

	local havePersistentSyncErrorSub = WFCatSyncStatus.persistentSyncErrorForAnyAsset( wfCatalog )
		:subscribe( function( haveError )
			modelData._havePersistentSyncErrorStream:onNext( haveError )
		end )

	local isInitialSyncSub = WFCatSyncStatus.isDoingInitialSync( wfCatalog )
		:subscribe( function( isInitialSyncActive )
			modelData._isInitialSyncStream:onNext( isInitialSyncActive )
		end )
	
	local isOutOfStorageSub = WFCatStorage.overStorageQuota( wfCatalog )
		:subscribe( function( isOverQuota )
			modelData._isOutOfOzStorageStream:onNext( isOverQuota )
		end )

	local isLowOnStorageSub = WFCatStorage.lowOnStorage( wfCatalog )
		:subscribe( function( isLow )
			modelData._isLowOnOzStorageStream:onNext( isLow )
		end )

	local deletedCatalogSub = wfCatSynchronizer:isOzCatalogDeleted()
		:filter( function( isDeleted )
			return isDeleted
		end )
		:subscribe( function( isDeleted )
			modelData._deletedCatalogStream:onNext( isDeleted )
		end )

	local sleepPreventionSubscription = Observable.combineLatest( 
			modelData._isActivelySyncingStream, modelData._shouldPreventSleepWhileSyncingStream )
		:filter( function( isSyncing, shouldPreventSleepWhileSyncing )
			return isSyncing and shouldPreventSleepWhileSyncing ~= false
		end )
		:subscribe( function()
			WFInsomnia.preventSystemSleep( "Syncing" )
				-- Prevent the system from sleeping when syncing while
				-- on AC Power. This will not prevent sleep if the user
				-- is on Battery Power.
				-- preventSystemSleep prevents the system from
				-- sleeping for ~30 seconds.
		end )

	-- Set up Trash Auto Purge
		
	local _cloudTrashAutoPurge = nil
	local checkAutoPurgeSubscription = wfCatUser:isSubscriptionExpired()
		:map(function( isSubscriptionExpired )
				-- Allow trash purging for freemium and residual (trial_exired and subscription_expired) accounts
				if isSubscriptionExpired or isFreemium then
					return true
				end
				return false
			end
		)
		:distinctUntilChanged()
		:subscribe( function( shouldAutoPurge )
			if _cloudTrashAutoPurge then
				_cloudTrashAutoPurge:dispose()
				_cloudTrashAutoPurge = nil
			end
			if shouldAutoPurge then
				_cloudTrashAutoPurge = WFCatTrash.autoPurge( wfCatalog ):subscribe()
			end
		end )
		
	wfCatSynchronizer:addCloseAction( function()
		
		currentDaemonDownloadStatusSub:dispose()
		currentDaemonDownloadStatusSub = nil
		
		catalogValidationSub:dispose()
		catalogValidationSub = nil
		
		isUploadingMetadataSub:dispose()
		isUploadingMetadataSub = nil

		isDownloadingMetadataSub:dispose()
		isDownloadingMetadataSub = nil

		isUploadingBinariesSub:dispose()
		isUploadingBinariesSub = nil

		isDownloadingOriginalsOrProxiesSub:dispose()
		isDownloadingOriginalsOrProxiesSub = nil

		isDownloadingStylesSub:dispose()
		isDownloadingStylesSub = nil
		
		isSyncPendingSub:dispose()
		isSyncPendingSub = nil
		
		isPendingRestoreSub:dispose()
		isPendingRestoreSub = nil

		getOzMaintenanceNotificationStreamSub:dispose()
		getOzMaintenanceNotificationStreamSub = nil

		havePersistentSyncErrorSub:dispose()
		havePersistentSyncErrorSub = nil

		isInitialSyncSub:dispose()
		isInitialSyncSub = nil
		
		isOutOfStorageSub:dispose()
		isOutOfStorageSub = nil

		isLowOnStorageSub:dispose()
		isLowOnStorageSub = nil
		
		deletedCatalogSub:dispose()
		deletedCatalogSub = nil
		
		sleepPreventionSubscription:dispose()
		sleepPreventionSubscription = nil
		
		checkAutoPurgeSubscription:dispose()
		checkAutoPurgeSubscription = nil
		
		modelData:onSynchronizerClose()
		
	end )

	local fileMover = wfCatalog:getFileMover()
	fileMover:setFreemiumMigration( freemiumCatalogMigrated )
	fileMover:setSynchronizer( wfCatSynchronizer )

	local mainVolumeGuid = modelData._userModelLocalStorage:setupVolumeAgents( wfCatalog, 
			wfCatSynchronizer:getIdleDownloader(), fileMover, 
			wfCatSynchronizer:getBinaryUploader(), 
			wfCatSynchronizer:getStyleSynchronizer(),
			modelData._isOnlineStream, modelData._isSyncPausedStream )

	modelData._mainVolumeGuidStream:onNext( mainVolumeGuid )

	trackNewlySyncedFileAccess( wfCatalog:fileAccessDb(), wfCatSynchronizer:getLatestDownloadedAssetIdsStream() )
	
	watchForRequiredUpdate( modelData, wfCatSynchronizer )
	
	NmbAppMessaging.setupUpdateAvailableCheck( wfCatUser, modelData._isUpdateAvailableStream )

	checkForRestrictedCountry( wfCatUser, modelData )

	modelData._originalStorageLocationInfoStream:onNext( NmbFileManagementSettings.originalStorageLocationInfo() )
	modelData._proxyStorageLocationInfoStream:onNext( NmbFileManagementSettings.proxyStorageLocationInfo() )

	WFCatAlbums.addDiagnosticLogSectionFunc( wfCatalog )
		-- Track album cycles in the diagnostic log.
		
	return wfCatalog, pixelProviderProcessor, maybeProfileCatalog, wfCatSynchronizer

end

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

local function bootstrapFreemiumCatalogResultStream( 
				wfCatUser, nmbCatalogFoldersParentDir, pixelProviderSettings,
				modelData )

	local freemiumCatalogSpec = WFCatSpec( wfCatUser, kGlobalDocumentId )

	local profileSpec = WFCatSpec( wfCatUser, wfCatUser:userId())
	
	local managedCatalogDoc = freemiumCatalogSpec:catalogDoc()

	if managedCatalogDoc then

		managedCatalogDoc._replace = true

	end
	
	local profileCatalogDoc = profileSpec:catalogDoc()
	
	if profileCatalogDoc then

		profileCatalogDoc._replace = true

	end

	local updateUserCacheForFreemiumUserResultStream =
			wfCatUser._userInfoCache
				:applyOverlay(
					{
						_id = wfCatUser:userId(),
						_localOnly = {
							managedCatalog = managedCatalogDoc,
							managedCatalogId = freemiumCatalogSpec:catalogId(),
							profileCatalog = profileCatalogDoc,
							profileCatalogId = profileSpec and profileSpec:catalogId()
						}
					} )

	local freemiumCatalogFolder = 
			getFreemiumCatalogBaseDir( nmbCatalogFoldersParentDir )

	local freemiumDocStorePath = 
			WFCatFileSpec.getDefaultDocstorePath( freemiumCatalogFolder )

	local backupDocstoreResultStream =
			NmbFreemiumUtils.maybeBackupDocstoreResultStream(
								freemiumDocStorePath )
				:catch(
					function( errStr )

						-- 2020.09.14 bkruse
						--
						-- By design, we are keeping freemium docstore backup
						-- logic very simple. We are only backing up the
						-- freemium docstore as a hedge against sqlite db
						-- corruption, similar to our strategy in LrClassic.
						--
						-- We only do this backup for Freemium catalogs. For 
						-- "normal" catalogs, the backup is sync to Oz.
						--
						-- Given this policy, we will simply log backup failures but
						-- take no further action.
						--
						local errMsg = string.format( "Failed to backup freemium docStore: %s", errStr )

						logger:warn( errMsg )

						return Rx.Observable.just( false, errMsg )

					end )
				:tap(
					function( didBackup, reason )

						if didBackup then

							logger:infof( "Did backup: %s", freemiumDocStorePath )

							return

						end

						logger:infof( "Did not backup %s becaues %s", freemiumDocStorePath, reason )

					end )


	local runCreateOzCatalogResultStream =
		Rx.Observable.create(
			function( observer )

				-- We use an anonymous observable to delay invoking 
				--
				-- 		createOzCatalog( ... )
				--
				-- until :subscribe(). This allows us to sequence the
				-- freemium catalog bootstrap via Rx.Observable.concat()
				observer
					:onNext(
						createOzCatalog(
							wfCatUser,
							freemiumCatalogSpec,
							profileSpec, 
							nmbCatalogFoldersParentDir,
							pixelProviderSettings,
							modelData ) )

				observer:onCompleted()

			end )

	-- this concat stream only emits the return value of createOzCatalog( ... )
	--
	-- also, Rx.Observable.concat runs each step to completion before running
	-- the next.
	--
	-- So in summary, we are running 3 events in sequence, even if one or more
	-- of the events are async.
	return Rx.Observable.concat(
			updateUserCacheForFreemiumUserResultStream
				:takeLast( 0 ), -- discards all the results (if any)
			backupDocstoreResultStream
				:takeLast( 0 ), -- discards all the results (if any)
			runCreateOzCatalogResultStream )


end

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

local _wfOzCatalogStream, _wfOzCatalog_now

local _lastWFCatUserId, _lastIsDeletedCatalogVal

local function getWFCatalog( rootCatalogDir, pixelProviderSettings, modelData )

	if _wfOzCatalogStream then

		return _wfOzCatalogStream

	end

	_wfOzCatalogStream = Observable.combineLatest( modelData._currentUserStream, modelData._userInfoStream, modelData._deletedCatalogStream, modelData._freemiumEnabledStream )

		:filter( function( wfCatUser )
			return wfCatUser
		end )
		
		:distinctUntilChanged(nil, distinctUserCompare)
		:flatMapLatest( function( wfCatUser, userInfo, isDeletedCatalog, isFreemiumEnabled)

			logger:infof( "Got new user %s, creating / finding catalog",
								wfCatUser
									and tostring( wfCatUser )
									or ":unknown:" )

			local newUserId = type( wfCatUser ) == "table" and 
								type( wfCatUser.userId ) == "function" and
								wfCatUser:userId()

			if _wfOzCatalog_now then

				logger:errorf( "Daemon is unlikely to continue working properly because it closed and reopened the the active NmbCatalog instance due to userId change from %s -> %s and/or deleted Oz catalog state changed from %s -> %s",
								newUserId, _lastWFCatUserId,
								isDeletedCatalog, _lastIsDeletedCatalogVal )

				_wfOzCatalog_now:close()
				_wfOzCatalog_now = nil
			end

			_lastWFCatUserId = newUserId

			_lastIsDeletedCatalogVal = isDeletedCatalog

			-- TODO [scouten 2015-08-25]: If we have the cached catalog ID
			-- available locally, we'll launch faster. Consider using that before
			-- going out to the network.
			
			-- This behavior is specific to freemium-enabled machines for NMB-freemium test.
			if isFreemiumEnabled then
				
				local entitlement = userInfo.entitlement
				
				if type( entitlement ) == "table" and entitlement.status then
				
					-- Oz Communication was OK
					if entitlement.status == 'created' then
					
						return bootstrapFreemiumCatalogResultStream( 
							wfCatUser, rootCatalogDir, pixelProviderSettings,
							modelData )
						
					end
					
					-- If entitlement is anything else, we do the normal catalog creation
					
				else
				
					-- Something went wrong with Oz Communication and we should try to use Cached Profile
					return Observable.combineLatest( wfCatUser:hasValidConnectionToOz(), modelData._isOnlineStream )
					
                        :take( 1 )
                    
                        :flatMapLatest( function( hasValidConnection, isOnlineStream )

                            -- if there is valid connection or the current model is offline mode
                            -- we will enable the cached catalog.
                            if hasValidConnection or not isOnlineStream then

								return wfCatUser._userInfoCache:docValue( wfCatUser:userId() )
									:map( function( userDoc )
										
										local managedCatalog = KSTableUtils.derefOpt( userDoc, '_localOnly', 'managedCatalog' )
										local managedCatalogId = KSTableUtils.derefOpt( userDoc, '_localOnly', 'managedCatalogId' )
										local profileCatalog = KSTableUtils.derefOpt( userDoc, '_localOnly', 'profileCatalog' )
										local profileCatalogId = KSTableUtils.derefOpt( userDoc, '_localOnly', 'profileCatalogId' )
											
										logger:infof( "Found cached catalog ID %s", managedCatalogId )

										if managedCatalog or managedCatalogId then

											local catalogSpec = WFCatSpec( wfCatUser, managedCatalog or managedCatalogId )
																									
											local profileSpec
											if profileCatalog or profileCatalogId then
												profileSpec = WFCatSpec( wfCatUser, profileCatalog or profileCatalogId )
											end
											
											return createOzCatalog( wfCatUser, catalogSpec,
																	profileSpec, rootCatalogDir,
																	pixelProviderSettings,
																	modelData )

										else

											modelData._userIdStream:onNext( nil ) -- force logout
											error( "Could not get entitlement and unable to get cached catalog" )

										end

									end )

							else

								modelData._userIdStream:onNext( nil ) -- force logout
								error( "Failed to connect to services" )

							end

					end )
					
				end
				
			end

			return Observable.combineLatest(
					wfCatUser:findOrCreateCatalog( "lightroom" ),
					wfCatUser:findFirstCatalogWithSubtype( 'profile', 'any' ) )
				:take( 1 )
				
				:flatMapLatest( function( catalogSpec, profileSpec )
					
					local managedCatalogDoc = catalogSpec:catalogDoc()
					if managedCatalogDoc then
						managedCatalogDoc._replace = true
					end
					
					local profileCatalogDoc = profileSpec:catalogDoc()
					if profileCatalogDoc then
						profileCatalogDoc._replace = true
					end
					
					return wfCatUser._userInfoCache:applyOverlay
						{
							_id = wfCatUser:userId(),
							_localOnly = {
								managedCatalog = managedCatalogDoc,
								managedCatalogId = catalogSpec:catalogId(),
								profileCatalog = profileCatalogDoc,
								profileCatalogId = profileSpec and profileSpec:catalogId()
							}
						}

						:map( function( ... )

							return createOzCatalog( wfCatUser,
													catalogSpec,
													profileSpec, 
													rootCatalogDir,
													pixelProviderSettings,
													modelData )

						end )

				end )

				:catch( function( whatWentWrong )

					logger:warnf( "Failed to get catalog ID from Oz( %s ), trying cached reference",
								  whatWentWrong )

					return Observable.combineLatest( wfCatUser:hasValidConnectionToOz(), modelData._isOnlineStream )
                    
                        :take( 1 )
                    
                        :flatMapLatest( function( hasValidConnection, isOnlineStream )

                            -- if there is valid connection or the current model is offline mode
                            -- we will enable the cached catalog.
                            if hasValidConnection or not isOnlineStream then

								return wfCatUser._userInfoCache:docValue( wfCatUser:userId() )
									:map( function( userDoc )
										
										local managedCatalog = KSTableUtils.derefOpt( userDoc, '_localOnly', 'managedCatalog' )
										local managedCatalogId = KSTableUtils.derefOpt( userDoc, '_localOnly', 'managedCatalogId' )
										local profileCatalog = KSTableUtils.derefOpt( userDoc, '_localOnly', 'profileCatalog' )
										local profileCatalogId = KSTableUtils.derefOpt( userDoc, '_localOnly', 'profileCatalogId' )
											
										logger:infof( "Found cached catalog ID %s", managedCatalogId )

										if managedCatalog or managedCatalogId then

											local catalogSpec = WFCatSpec( wfCatUser, managedCatalog or managedCatalogId )
																									
											local profileSpec
											if profileCatalog or profileCatalogId then
												profileSpec = WFCatSpec( wfCatUser, profileCatalog or profileCatalogId )
											end
											
											return createOzCatalog( wfCatUser, catalogSpec,
																	profileSpec, rootCatalogDir,
																	pixelProviderSettings,
																	modelData )

										else

											modelData._userIdStream:onNext( nil ) -- force logout
											error( whatWentWrong )

										end

									end )

							else

								modelData._userIdStream:onNext( nil ) -- force logout
								error( whatWentWrong )

							end

					end )

				end )

		end )
		:map( function( wfOzCatalog, pixelProviderProcessor, profileCatalog, myWFCatSynchronizer )
		
			_wfOzCatalog_now = wfOzCatalog
			
			modelData._catalogIdStream:onNext( wfOzCatalog:ozCatalogId() )
			
			return wfOzCatalog, pixelProviderProcessor, profileCatalog, myWFCatSynchronizer
			
		end )
		:replay( nil, 1 )
		:refCount()

	return _wfOzCatalogStream

end

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

local function getAppModel( rootCatalogDir, pixelProviderSettings, modelData, _, namespace, func, isTiedToCatalog, ... )

	local packedN = select( '#', ... )
	local packed = { ... }

	local wfCatalogObservable = getWFCatalog( rootCatalogDir, pixelProviderSettings, modelData )

	if isTiedToCatalog == nil then
		isTiedToCatalog = true
	end

	if isTiedToCatalog then
		wfCatalogObservable = wfCatalogObservable:take( 1 )
	end

	local value = wfCatalogObservable
		:flatMapLatest( function( wfOzCatalog )
			if wfOzCatalog then
				return require( namespace )[ func ]( wfOzCatalog,
													 unpack( packed, 1, packedN ) )
			else
				return Observable.just( {} )
			end
		end )

	return {
	
		value = value,

		close = function()
			logger:debugf( "Model %s.%s CLOSED", namespace, func )
		end,
		
	}

end


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

-- @mark NmbModelData class

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

local NmbModelDataImp = {

	-- User Info
	
	_userInfoCachePath = false,
	_userIdStream = false,
	_catalogIdStream = false,
	_deletedCatalogStream = false,
	_currentUserStream = false,
	_loginStatusStream = false,
	_isLoggedInStream = false,
	_userInfoStream = false,
	_shouldSyncUserInfoStream = false,
	_isSyncingUserInfoStream = false,
	
	-- Local Storage
	
	_userModelLocalStorage = false,
	_volumesOutOfSpaceStream = false,
	_offlineVolumesStream = false,
	_volumesRequiringSyncingStream = false,
	_readOnlyVolumesStream = false,
	
	-- Sync

	_serverBaseUrlStream = false,
	_hasValidConnectionToOzStream = false,
	_isOnlineStream = false,
	_isSyncPendingStream = false,
	_isPendingRestoreStream = false,
	_havePersistentSyncErrorStream = false,
	_isInitialSyncStream = false,
	_isSyncUploadingMetadataStream = false,
	_isSyncDownloadingMetadataStream = false,
	_isSyncUploadingBinariesStream = false,
	_isSyncDownloadingOriginalsOrProxiesStream = false,
	_isSyncDownloadingStylesStream = false,
	_shouldPreventSleepWhileSyncingStream = false,
	_isSyncPausedStream = false,
	_isActivelySyncingStream = false,
	_currentSyncStatusStream = false,
	_hostIsReachableStream = false,
	_ozLoginServerErrorStream = false,
	_currentDaemonDownloadStatusStream = false,
	_catalogValidationStatusStream = false,
	_isOutageOccurringStream = false,
	_spaceOutageOccurringStream = false,
	_spaceOutages = false,

	-- Cloud Storage
	
	_isOutOfOzStorageStream = false,
	_isLowOnOzStorageStream = false,

	-- File Management

	_mainVolumeGuidStream = false,
	_keepAllOriginalsStream = false,
	_keepAllProxiesStream = false,
	_targetDiskSpacePercentageStream = false,
	_originalStorageLocationInfoStream = false,
	_proxyStorageLocationInfoStream = false,
	_fileMoverPostponedUntilDateStream = false,
	
	-- App Updates / Residual / Trial Mode

	_isUpdateAvailableStream = false,
	_isUpdateRequiredStream = false,
	_residualModeStream = false,
	_freemiumModeStream = false,
	_freemiumEnabledStream = false,
	_restrictedCountryModeStream = false,
	_trialEndDateStream = false,
	_deletionDateStream = false,
	_updateCommunicationPrefSubscription = false,
	
	_purgeAssetsAfterStream = false,
	
	_userModelIdStream = false,

	_dbCorruptionEventStream = false,
	_dbCorruptionEventStreamSubscription = false,
	
	_locale = false,
	
	WFCloseSupport.addCloseSupportMethods,
	
}

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

local function createUserInfoStream( currentUserStream )
	
	return currentUserStream
		:flatMap( function( wfCatUser )

			if wfCatUser then
				return wfCatUser:userInfoCache():docValue( wfCatUser:userId() )
			else
				return Observable.just( nil )
			end

		end )
		:replay( nil, 1 )
		:refCount()

end

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

local userInfoSynchronizer_now

local function createIsSyncingUserInfoStream( shouldSyncUserInfoStream, currentUserStream )

	return Observable.combineLatest( function( shouldSync, wfCatUser )
	
		if not wfCatUser then 
			if userInfoSynchronizer_now then
				userInfoSynchronizer_now:close()
				userInfoSynchronizer_now = nil
			end
			return false
		end
		
		if not userInfoSynchronizer_now or 
			( userInfoSynchronizer_now and 
			( userInfoSynchronizer_now:userId() ~= wfCatUser:userId() ) ) then 
			
			if userInfoSynchronizer_now then
				userInfoSynchronizer_now:close()
			end
			
			userInfoSynchronizer_now = WFCatUserSynchronizer( wfCatUser )
		
		end
			
		if shouldSync then
			userInfoSynchronizer_now:startContinuousUpdates()
		else
			userInfoSynchronizer_now:stopContinuousUpdates()
		end
		
		return shouldSync
		
	end,
	shouldSyncUserInfoStream, currentUserStream )

end

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

local function getTrialEndDateStream( userInfoStream )

	return userInfoStream
		:map( function( userInfo )

			local status = KSTableUtils.derefOpt( userInfo,
												  'entitlement',
												  'status' )

			local trialEnd = KSTableUtils.derefOpt( userInfo,
													'entitlement',
													'trial',
													'end' )

			if status == 'trial' then
				return trialEnd
			else
				return nil
			end

		end )

end

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

local function getDeletionDateStream( userInfoStream )

	return userInfoStream
		:map( function( userInfo )

			local deletionDate = KSTableUtils.derefOpt( userInfo,
													'entitlement',
													'deletion_date' )

			return deletionDate

		end )
	
end

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

local function getPurgeAssetsAfterStream( userInfoStream )

	return userInfoStream:map( function( userInfo )
		return KSTableUtils.derefOpt( userInfo,  'config', 'purge_assets_after' )
	end )

end

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

local function getIsSyncPausedStream( shouldSyncUserInfoStream )

	return shouldSyncUserInfoStream
		:map( function()

			-- Could use shouldSyncUserInfo, but re-inforce that the pref
			-- controls the paused state.
			-- Using shouldSyncUserInfo also updates observable.
			return KSPrefs.syncPaused

		end )

end

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

local function getCurrentDaemonDownloadStatusStream( userModelLocalStorage,
		isSyncPausedStream, isOnlineStream, volumesOutOfSpaceStream, 
		offlineVolumesStream )

	local kOkay = 'okay'
	local kOutOfDiskSpace = 'out_of_disk_space'
	local kVolumeOffline = 'volume_offline'

	return Observable.combineLatest(
	
		function( syncPaused, online, volumesOutOfSpace, offlineVolumes )

			local outOfLocalStorageForOriginals, outOfLocalStorageForProxies, outOfLocalStorageOnMainVolume
					= userModelLocalStorage:whichVolumes( volumesOutOfSpace )

			local offlineVolumeForOriginals, offlineVolumeForProxies = 
					userModelLocalStorage:whichVolumes( offlineVolumes )

			local statusTable = {
				syncStatus = kOkay,
				mainVolumeStatus = kOkay,
				originalVolumeStatus = kOkay,
				proxyVolumeStatus = kOkay
			}
			
			if syncPaused then
				statusTable.syncStatus = 'sync_paused'
			elseif not online then
				statusTable.syncStatus = 'offline'
			end
			
			if outOfLocalStorageOnMainVolume then
				statusTable.mainVolumeStatus = kOutOfDiskSpace
			end
			
			if outOfLocalStorageForOriginals then
				statusTable.originalVolumeStatus = kOutOfDiskSpace
			elseif offlineVolumeForOriginals then
				statusTable.originalVolumeStatus = kVolumeOffline
			end
			
			if outOfLocalStorageForProxies then
				statusTable.proxyVolumeStatus = kOutOfDiskSpace
			elseif offlineVolumeForProxies then
				statusTable.proxyVolumeStatus = kVolumeOffline
			end

			return statusTable

		end,
		isSyncPausedStream, isOnlineStream, 
		volumesOutOfSpaceStream, offlineVolumesStream 
		
	)

end

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

local function getIsActivelySyncingStream( modelData )
	
	local self = modelData
	
	-- This observable emits a boolean at least once every 15 seconds that
	-- indicates whether or not the system is actively syncing.
	
	return Observable.combineLatest(
	
		function( syncPaused, online, loggedIn, 
			syncMetaUp, syncMetaDown, 
			syncOrigUp, syncOrigOrProxyDown,
			syncStylesDown )

			if not syncPaused and online and loggedIn then
				
				if syncMetaUp or
					syncMetaDown or
					syncOrigUp or
					syncOrigOrProxyDown or
					syncStylesDown then
					
					return true
					
				end
				
			end
			
			return false
			
		end,
		self._isSyncPausedStream, self._isOnlineStream, self._isLoggedInStream,
		self._isSyncUploadingMetadataStream, self._isSyncDownloadingMetadataStream,
		self._isSyncUploadingBinariesStream, self._isSyncDownloadingOriginalsOrProxiesStream,
		self._isSyncDownloadingStylesStream,
		Observable.timer( 0, 15, WFRxScheduler.sharedScheduler() )
		
	)

end

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

local function getCurrentSyncStatusStream( modelData )
	
	local self = modelData
	
	return Observable.combineLatest(
	
		function( syncPaused, online, loggedIn, syncPending,
			initialSync, syncMetaUp, syncMetaDown,
			syncOrigUp, syncOrigOrProxyDown, syncStylesDown,
			outOfStorage, lowOnStorage,
			volumesOutOfSpace, offlineVolumes, volumesRequiringSyncing,
			updateRequired, persistentSyncError,
			catalogValidationStatus, outageOccurring )

			local outOfLocalStorageForOriginals, outOfLocalStorageForProxies, outOfLocalStorageOnMainVolume
					= self._userModelLocalStorage:whichVolumes( volumesOutOfSpace )

			local offlineVolumeForOriginals, offlineVolumeForProxies = self._userModelLocalStorage:whichVolumes( offlineVolumes )
	
			local offlineVolumeRequiresSync = self._userModelLocalStorage:offlineVolumeRequiresSyncing( offlineVolumes, volumesRequiringSyncing )
	
			local status

			if not loggedIn then
				status = nil -- no status available when not signed in
			elseif updateRequired then
				status = 'SyncPausedUpdateRequired'
			elseif not catalogValidationStatus then
				status = 'SyncDisconnectedInvalidCatalog'
			elseif outageOccurring then
				status = 'SyncPausedForOutage'
			elseif syncPaused then
				status = 'SyncPaused'
			elseif not online then
				status = 'SyncDisconnectedNoNetwork'
			elseif offlineVolumeRequiresSync then
				status = 'SyncVolumeOfflineRequiresSyncing'
			elseif outOfLocalStorageOnMainVolume then
				status = 'SyncErrorNoDiskSpaceAvailableOnMainVolume'
			elseif outOfLocalStorageForOriginals then
				status = 'SyncErrorNoDiskSpaceAvailableForOriginals'
			elseif outOfLocalStorageForProxies then
				status = 'SyncErrorNoDiskSpaceAvailableForProxies'
			elseif outOfStorage then
				status = 'SyncErrorOutOfStorage'
			elseif initialSync and ( syncMetaUp or syncOrigUp ) then
				status = 'SyncInitialAndUp'
			elseif initialSync then
				status = 'SyncingInitial'
			elseif ( syncMetaDown or syncOrigOrProxyDown ) and ( syncMetaUp or syncOrigUp ) then
				status = 'SyncingUpAndDown'
			elseif syncMetaUp or syncOrigUp then
				status = 'SyncingUp'
			elseif syncMetaDown or syncOrigOrProxyDown or syncStylesDown then
				status = 'SyncingDown'
			elseif syncPending then
				status = 'SyncPending'
			elseif lowOnStorage then
				status = 'SyncLowOnStorage'
			elseif offlineVolumeForOriginals then
				status = 'SyncVolumeOfflineForOriginals'
			elseif offlineVolumeForProxies then
				status = 'SyncVolumeOfflineForProxies'
			elseif persistentSyncError then
				status = 'SyncCompletedWithError'
			else
				status = 'SyncNull'
			end

			return status

		end,
		self._isSyncPausedStream, self._isOnlineStream, self._isLoggedInStream, self._isSyncPendingStream,
		self._isInitialSyncStream, self._isSyncUploadingMetadataStream, self._isSyncDownloadingMetadataStream,
		self._isSyncUploadingBinariesStream, self._isSyncDownloadingOriginalsOrProxiesStream,
		self._isSyncDownloadingStylesStream,
		self._isOutOfOzStorageStream, self._isLowOnOzStorageStream, self._volumesOutOfSpaceStream, 
		self._offlineVolumesStream, self._volumesRequiringSyncingStream, 
		self._isUpdateRequiredStream, self._havePersistentSyncErrorStream,
		self._catalogValidationStatusStream, self._isOutageOccurringStream )

	:distinctUntilChanged()

	:tap( function( status )
		logger:infof( "Sync Status: %s", status )
	end )

end

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

function NmbModelDataImp:__init( serverBase, rootCatalogDir, imsOverrides, locale, userModelId )

	check( 'NmbModelData', 'string', '?string', '?table', '?string' )

	-- User Info
	
	local userIdStream = getUserIdStream( imsOverrides )

	self._userIdStream = userIdStream
	
	self._userModelIdStream = ReplaySubject( 1 )
	self._userModelIdStream:onNext( userModelId )

	self._catalogIdStream = ReplaySubject( 1 )

	self._deletedCatalogStream = BehaviorSubject( false )
	
	self._userInfoCachePath = rootCatalogDir == ':memory:' and rootCatalogDir or 
										KSPathUtils.child( rootCatalogDir, 'user' )

	self._serverBaseUrlStream = BehaviorSubject( serverBase )

	self._currentUserStream = createCurrentUserStream( self._serverBaseUrlStream, 
			self._userIdStream, self._userInfoCachePath )

	self._locale = locale
	self._updateCommunicationPrefSubscription = self._currentUserStream
	
		:subscribe( function( wfCatUser )
			
				local globalDocumentId = '00000000000000000000000000000000'
				
				if wfCatUser and wfCatUser:userId() ~= globalDocumentId then
				
					wfCatUser:updateCommunicationPreference( locale )
						:call( runUntilDone )
				
				end

			end )

	self._userInfoStream = createUserInfoStream( self._currentUserStream )

	self._shouldSyncUserInfoStream = BehaviorSubject( false )

	self._isSyncingUserInfoStream = createIsSyncingUserInfoStream( 
			self._shouldSyncUserInfoStream, self._currentUserStream )

	self._loginStatusStream = BehaviorSubject( { status = "init" } )

	self._isLoggedInStream = self._userIdStream
		:map( function( userId )
			return not not userId
		end )

	-- Local Storage
	
	local userModelLocalStorage = NmbUserModel_LocalStorage()
	self._userModelLocalStorage = userModelLocalStorage
	self._volumesOutOfSpaceStream = userModelLocalStorage:getVolumesOutOfSpaceStream()
	self._offlineVolumesStream = userModelLocalStorage:getOfflineVolumesStream()
	self._volumesRequiringSyncingStream = userModelLocalStorage:getVolumesRequiringSyncingStream()
	self._readOnlyVolumesStream = userModelLocalStorage:getReadOnlyVolumesStream()
	
	-- Sync

	self._hasValidConnectionToOzStream = createHasValidConnectionToOz( self._currentUserStream )
	self._catalogValidationStatusStream = BehaviorSubject( true )
	
	if USE_NON_CC_CREDENTIALS then
	
		-- For automation, start the daemon as if it's online.
		self._isOnlineStream = BehaviorSubject( true )
		
	else
	
		-- For standard thor login, start the daemon as if it's offline.
		self._isOnlineStream = BehaviorSubject( false )
		
	end
	
	self._isSyncPendingStream = BehaviorSubject( false )
	self._isPendingRestoreStream = BehaviorSubject( false )
	self._havePersistentSyncErrorStream = BehaviorSubject( false )
	self._isInitialSyncStream = BehaviorSubject( true )
	self._isSyncUploadingMetadataStream = BehaviorSubject( false )
	self._isSyncDownloadingMetadataStream = BehaviorSubject( false )
	self._isSyncUploadingBinariesStream = BehaviorSubject( false )
	self._isSyncDownloadingOriginalsOrProxiesStream = BehaviorSubject( false )
	self._isSyncDownloadingStylesStream = BehaviorSubject( false )
	self._shouldPreventSleepWhileSyncingStream = BehaviorSubject( KSPrefs.shouldPreventSleepWhileSyncing )
	self._isSyncPausedStream = getIsSyncPausedStream( self._shouldSyncUserInfoStream )
	self._isActivelySyncingStream = getIsActivelySyncingStream( self )
	self._isOutageOccurringStream = BehaviorSubject( false )

	self._spaceOutages = {}
	self._spaceOutageOccurringStream = BehaviorSubject( self._spaceOutages )
	

	self._hostIsReachableStream = self._currentUserStream
		:flatMap(function( wfCatUser )

			if wfCatUser == nil then
				return Observable.just( nil )
			end

			return WFReachability.hostIsReachable( wfCatUser:serverVersionUrl() or wfCatUser:serverBaseUrl() )

		end)
	
	-- Cloud Storage
	
	self._isOutOfOzStorageStream = BehaviorSubject( false )
	self._isLowOnOzStorageStream = BehaviorSubject( false )

	-- File Management

	-- The location of the user's catalog directory is given to the daemon when it's started
	-- by the GUI so it has to be set here as it can't be determined by generic code
	local prefsFilename = KSPathUtils.child( rootCatalogDir, "user" )
	prefsFilename = KSPathUtils.child( prefsFilename, "FileManagementSettings.lrpref" )

	NmbFileManagementSettings.setSecondaryPrefsLocation( prefsFilename )

	-- if the secondary prefs were updated more recently than the standard prefs then load them
	NmbFileManagementSettings.maybeLoadSecondaryPrefs( )

	self._mainVolumeGuidStream = BehaviorSubject( false )
	self._keepAllOriginalsStream = BehaviorSubject( NmbFileManagementSettings.keepAllOriginals() )
	self._keepAllProxiesStream = BehaviorSubject( NmbFileManagementSettings.keepAllProxies() )
	self._targetDiskSpacePercentageStream = BehaviorSubject( NmbFileManagementSettings.targetNimbusDiskSpacePercentage() )
	self._originalStorageLocationInfoStream = BehaviorSubject( false )
	self._proxyStorageLocationInfoStream = BehaviorSubject( false )
	self._fileMoverPostponedUntilDateStream = BehaviorSubject( KSPrefs.postponeFileMoveUntilDate )
	
	-- App Updates / Residual / Trial Mode

	self._isUpdateAvailableStream = BehaviorSubject( false )
	self._isUpdateRequiredStream = BehaviorSubject( false )
	self._residualModeStream = BehaviorSubject( not not KSPrefs.residualMode )
	self._freemiumModeStream = BehaviorSubject( not not KSPrefs.freemiumMode )
	self._freemiumEnabledStream = BehaviorSubject( false )
	self._restrictedCountryModeStream = BehaviorSubject( false )
	self._trialEndDateStream = getTrialEndDateStream( self._userInfoStream )
	self._deletionDateStream = getDeletionDateStream( self._userInfoStream )
	
	self._purgeAssetsAfterStream = getPurgeAssetsAfterStream( self._userInfoStream )
		
	-- The following streams rely on data from both file management and sync streams.

	self._currentDaemonDownloadStatusStream = getCurrentDaemonDownloadStatusStream( 
			userModelLocalStorage, self._isSyncPausedStream, self._isOnlineStream, 
			self._volumesOutOfSpaceStream, self._offlineVolumesStream )

	self._currentSyncStatusStream = getCurrentSyncStatusStream( self )
	
	self._ozLoginServerErrorStream = ReplaySubject( 1 )

	-- db corruption handling setup.
	--
	-- There are multiple sqlite dbs associated with a user. 
	--
	-- our policy is to shutdown the app when a corruption event is seen is any
	-- of them. Therefore we need only capture one corruption event
	self._dbCorruptionEventStream = ReplaySubject( 1 )

	-- We subscribe to the db corruption stream before we have even launched
	-- any of the SQLite db threads associated with the user. This ensures that
	-- corruption events don't occur before we have a chance to observe them.
	self._dbCorruptionEventStreamSubscription =
		NmbDbCorruptionUtils.corruptionEventOnce()
			:take( 1 )
			:subscribe( self._dbCorruptionEventStream )

	
end

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

function NmbModelDataImp:onSynchronizerClose()

	self._isSyncUploadingMetadataStream:onNext( false )

	self._isSyncDownloadingMetadataStream:onNext( false )

	self._isSyncUploadingBinariesStream:onNext( false )

	self._isSyncDownloadingOriginalsOrProxiesStream:onNext( false )
	
	self._isSyncDownloadingStylesStream:onNext( false )

	self._isSyncPendingStream:onNext( false )
	
	self._isPendingRestoreStream:onNext( false )

	self._havePersistentSyncErrorStream:onNext( false )

	self._isInitialSyncStream:onNext( false )
	
	self._isOutOfOzStorageStream:onNext( false )

	self._isLowOnOzStorageStream:onNext( false )

	self._volumesOutOfSpaceStream:onNext( {} )
	self._offlineVolumesStream:onNext( {} )
	self._volumesRequiringSyncingStream:onNext( {} )
	self._readOnlyVolumesStream:onNext( {} )

end

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

function NmbModelDataImp:close()

	self._userIdStream:dispose()

	self._catalogIdStream:dispose()

	if self._updateCommunicationPrefSubscription then
	
		self._updateCommunicationPrefSubscription:dispose()
		self._updateCommunicationPrefSubscription = nil
		
	end

	self._dbCorruptionEventStreamSubscription:dispose()

end

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

local NmbModelData = Rx.Class 'NmbModelData'( NmbModelDataImp )


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

-- @mark NmbUserModel

local NmbUserModel = {}

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

NmbUserModel.maybeMigrateFreemiumCatalog = maybeMigrateFreemiumCatalog

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

function NmbUserModel.makeUserModel( serverBase, rootCatalogDir, processId, 
		imsOverrides, pixelProviderSettings, locale )

	check( 'string', '?string', '?string', '?table', '?table', '?string' )

	pixelProviderSettings = pixelProviderSettings or {}
	
	imsOverrides = imsOverrides or {}
	
	logger:infof( "NmbUserModel.makeUserModel( %s, %s, %s, %s, %s )",
				  serverBase, rootCatalogDir, processId, 
				  pixelProviderSettings.maxSaves, pixelProviderSettings.maxRenders )
				  
	local userModelId = KSIdGenerator.uuidInLowercaseHex()

	local modelData = NmbModelData( serverBase, rootCatalogDir, imsOverrides, locale, userModelId )

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

	local modelValue = {

		-- model info
		
		userModelId = Observable.just( userModelId ),

		-- basic user info

		ozUserId = modelData._userIdStream,
		catalogId = modelData._catalogIdStream:startWith( nil ),
		isLoggedIn = modelData._isLoggedInStream,
		loginStatus = modelData._loginStatusStream,
		email = currentUserFieldValueForPath( modelData, 'email' ),
		fullName = currentUserFieldValueForPath( modelData, 'full_name' ),
		firstName = currentUserFieldValueForPath( modelData, 'first_name' ),
		lastName = currentUserFieldValueForPath( modelData, 'last_name' ),
		
		-- sync
		
		isSyncingUserInfo = modelData._isSyncingUserInfoStream,
		syncPaused = modelData._isSyncPausedStream,
		isOnline = modelData._isOnlineStream,
		hasValidConnectionToOz = modelData._hasValidConnectionToOzStream,
		syncStatus = modelData._currentSyncStatusStream,
		syncUploadingMetadata = modelData._isSyncUploadingMetadataStream,
		syncDownloadingMetadata = modelData._isSyncDownloadingMetadataStream,
		syncUploadingBinaries = modelData._isSyncUploadingBinariesStream,
		syncDownloadingOriginalsOrProxies = modelData._isSyncDownloadingOriginalsOrProxiesStream,
		syncDownloadingStyles = modelData._isSyncDownloadingStylesStream,
		syncDownloadingMetadataInitial = modelData._isInitialSyncStream,
		syncPending = modelData._isSyncPendingStream,
		pendingRestore = modelData._isPendingRestoreStream,
		outageOccurring = modelData._isOutageOccurringStream,
		spaceOutageOccurring = modelData._spaceOutageOccurringStream,

		-- oz storage

		isOutOfOzStorage = modelData._isOutOfOzStorageStream,
		isLowOnOzStorage = modelData._isLowOnOzStorageStream,

		-- local storage

		volumesOutOfSpace = modelData._volumesOutOfSpaceStream:map( mapVolumeGuidToName ),
		offlineVolumes = modelData._offlineVolumesStream:map( mapVolumeGuidToName ),
		volumesRequiringSyncing = modelData._volumesRequiringSyncingStream:map( mapVolumeGuidToName ),
		readOnlyVolumes = modelData._readOnlyVolumesStream:map( mapVolumeGuidToName ),
		
		-- file management settings

		fmMainVolumeGuid = modelData._mainVolumeGuidStream,
		fmKeepAllOriginals = modelData._keepAllOriginalsStream,
		fmKeepAllProxies = modelData._keepAllProxiesStream,
		fmDiskSpacePercentage = modelData._targetDiskSpacePercentageStream,
		fmOriginalStorageLocationInfo = modelData._originalStorageLocationInfoStream,
		fmProxyStorageLocationInfo = modelData._proxyStorageLocationInfoStream,
		fmFileMoverPostponedUntilDate = modelData._fileMoverPostponedUntilDateStream,
		
		-- account status
		
		isInResidualMode = modelData._residualModeStream,
		isInFreemiumMode = modelData._freemiumModeStream,
		isFreemiumEnabled = modelData._freemiumEnabledStream,
		isInRestrictedCountryMode = modelData._restrictedCountryModeStream,
		accountStatus = currentUserFieldValueForPath( modelData, 'entitlement/status' ),
		trialEndDate = modelData._trialEndDateStream,
		deletionDate = modelData._deletionDateStream,
		
		autoPurgeWindow = modelData._purgeAssetsAfterStream,
		
		-- upgrades
		
		isLightroomCCUpdateAvailable = modelData._isUpdateAvailableStream,
		isLightroomCCUpdateRequired = modelData._isUpdateRequiredStream,
	
	}

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

	addModelValueToDiagnosticLog( modelValue )

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

	return {

		_imsOverrides = imsOverrides,

		value = modelValue,

		models = {

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

			appModel = function( ... )
				return getAppModel( rootCatalogDir, pixelProviderSettings, modelData, ... )
			end,
			
			--------------------------------------------------------------------
			
			indexStatus = function()
			
				return require 'NmbForceReindexing'.makeModel( getWFCatalog( rootCatalogDir, pixelProviderSettings, modelData ) )
				
			end,
			
			--------------------------------------------------------------------
			
			catalog = function( self )

				local wfCatalogStream = 
						getWFCatalog( rootCatalogDir, pixelProviderSettings, modelData )
			
				self._catalogModel = 
					require 'NmbRootModel'.makeModel( 
								wfCatalogStream,
								modelData._dbCorruptionEventStream )

				return self._catalogModel
				
			end,
			
			--------------------------------------------------------------------
			
			hostIsReachable = function()
				return {
					value = modelData._hostIsReachableStream
				}
			end,
			
			--------------------------------------------------------------------

			ozLoginServerError = function()
				return {
					value = modelData._ozLoginServerErrorStream
				}
			end,
			
			--------------------------------------------------------------------
			
			appNeedsRestart = function()
				return {
					value = modelData._deletedCatalogStream
				}
			end,

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

			userModelId = function()
				return {
					value = modelData._userModelIdStream	
				}
			end,
			
			--------------------------------------------------------------------

			tempFolderPathForOriginals = function()
				return {
					value = Observable.combineLatest( getWFCatalog( rootCatalogDir, pixelProviderSettings, modelData ), 
							modelData._originalStorageLocationInfoStream )
						:map( function( wfCatalog, locationInfo )
						
							if locationInfo and locationInfo.volumeGuid then
								return wfCatalog:fileSpec():tempFolderPathForVolume( locationInfo.volumeGuid )
							end
							
							return false
							
						end )
						:take( 1 )
				}
			end,
			
			--------------------------------------------------------------------
			
			diagnosticLog = function( _, logName, logFileName )
				return require 'WFDiagnosticLog'.makeSimpleModel( logName, logFileName )
			end,
			
			--------------------------------------------------------------------
			
			innerHTMLDiagnosticLog = function( _, logName, logFileName )

				return {

					value = require 'WFDiagnosticLog'.makeInnerHTMLReportAsFile( 
														logName, logFileName )
								:map(
									function( ... )

										return { ... }

									end )

				}

			end,

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

			getImsAuthUrl = function( _, optOverrides )

				logger:infof( "getImsAuthUrl original serverBase = %s, optOverrides = %s",
							  serverBase, optOverrides )

				optOverrides = optOverrides or {}

				if not optOverrides.baseServerUrl then
					optOverrides.baseServerUrl = serverBase
				end

				optOverrides.apiVersion = 'v2'

				if optOverrides.imsClientId and optOverrides.imsClientSecret then
					require 'WFImsAppIdentity'.setClientIdAndSecret( optOverrides.imsClientId,
																	 optOverrides.imsClientSecret )
				end

				return {
					value = require 'WFOzClient'.getImsAuthUrl( optOverrides )
				}

			end,

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

			getJumpUrlForUrl = function( _, url, targetClientId, optOverrides )

				logger:infof( "getJumpUrlForUrl url = %s, webCid = %s",
							  url, targetClientId )

				optOverrides = optOverrides or {}

				if not optOverrides.baseServerUrl then
					optOverrides.baseServerUrl = serverBase
				end

				return {
				
					value = getWFCatalog( rootCatalogDir, pixelProviderSettings, modelData )

						:flatMap( function( catalog )
							return catalog:ozUser():fetchTokens()
						end )

						:flatMap( function( accessToken )
							return require 'WFOzClient'.getJumpUrlForUrl( url, targetClientId, accessToken, optOverrides )
						end )
					
				}

			end,

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

			loginFromTokens = function( ... )
				loginFromTokens( modelData, ... )
				return {
					value = true,
				}
			end,

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

			avatar = function( _, avatarDir, adobeId )
			
				return {
					value = modelData._currentUserStream
								:flatMapLatest( function( wfCatUser )
									if wfCatUser then
										return WFCatAvatar.retrieveAvatar( wfCatUser, adobeId, avatarDir )
									else
										return Observable.empty()
									end
								end )
				}
				
			end,
			
			--------------------------------------------------------------------
			
			startProxyAuthErrorMonitoring = function()
			
				return {
					value = require "NmbProxyAuthErrorMonitor".startProxyAuthErrorMonitor()
				}
				
			end,
			
			
			--------------------------------------------------------------------

			isFaceDetectionAllowed = function()
			
				return {
					value = modelData._currentUserStream
								:flatMapLatest( function( wfCatUser )
									if wfCatUser then
										return wfCatUser:isFaceDetectionAllowed()
									else
										return Observable.empty()
									end
								end )
				}
			end,

			--------------------------------------------------
			-- restartRequiredForDbIssue
			-- 
			-- refer to NmbDbCorruptionUtils.makeCleanupCorruptDbResultStream
			-- for details, but it will emit at most one result
			-- and the client app should quit and possibly restart
			-- if an event is emitted.
			--------------------------------------------------
			restartRequiredForDbIssue = function()

				return {

					value = Rx.Observable.combineLatest(
									Rx.Observable.just( modelData._dbCorruptionEventStream ),
									modelData._currentUserStream )
								:take( 1 )
								:flatMapLatest(
									NmbDbCorruptionUtils.handleCorruptUserInfoCacheDbResultStream )
								:map(
									function( errorType, canBeSolvedByRestart, dbName, errStr )
									
										return { errorType, canBeSolvedByRestart, dbName, errStr }

									end )

				}

			end,


			--------------------------------------------------
			-- _DEBUG_simulateUserInfoCacheDbCorruption
			--------------------------------------------------
			_DEBUG_simulateUserInfoCacheDbCorruption = function()

				return {

					value = modelData._currentUserStream
								:flatMapLatest( 
									NmbDbCorruptionUtils.makeSimulateCorruptUserInfoCacheStream )

				}

			end,


			--------------------------------------------------
			-- _DEBUG_emitDataThatCannotTransit
			--------------------------------------------------
			_DEBUG_emitDataThatCannotTransit = function()

				return {

					value = Rx.Observable.concat(
								Rx.Observable.just( "Can transit" ),
								Rx.Observable.just( KSData.make( "Can't transit" ) ) )

				}

			end,

			--------------------------------------------------------------------
			-- _DEBUG_echoAtInterval
			--------------------------------------------------------------------

			_DEBUG_echoAtInterval = function( _, msg, interval )

				return {

					value = Rx.Observable.timer( 
									0,
									tonumber( interval ) or 1,
									WFRxScheduler.sharedScheduler() )
								:map( 
									function() 

										return msg 

									end )


				}
			end,

		},

		events = {

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

			activateOfflineMode = function( _, userId, reason )
				if reason == "ozLoginFailure" then
					activateOzLoginFailureMode( modelData, userId )
				else
					activateOfflineMode( modelData, userId )
				end
			end,

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

			activateOnlineMode = function( ... )
				activateOnlineMode( modelData, ... )
			end,
			
			--------------------------------------------------------------------
			
			useUser = function( _, userId )
				modelData._userIdStream:onNext( userId )
			end,

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

			loginFromTokens = function( ...)
				loginFromTokens( modelData, ... )
			end,
			
			--------------------------------------------------------------------
			
			logout = function()
				modelData._userIdStream:onNext( nil )
				modelData._loginStatusStream:onNext( { status = "logout" } )
			end,

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

			startSync = function()
				takeOneValueAndRunFunc( modelData._isUpdateRequiredStream, function( updateRequired )
					if updateRequired then
						logger:info( "Not starting sync, because a client upgrade is required." )
						return
					elseif not KSPrefs.syncPaused or KSPrefs.syncPaused == kClientUpgradeRequired then
					
						-- When KSPrefs.syncPaused is "upgrade_required", we attempt to resume sync,
						-- because the user didn't choose to pause it. If an upgrade is still required,
						-- that will become clear soon enough, and sync will be paused with the
						-- the corresponding notification presented to the user.
					
						logger:infof( "Resuming sync with (previous) syncPaused preference of %s", KSPrefs.syncPaused )
						KSPrefs.syncPaused = false
						startSyncingUserAndCatalog( modelData )
					end
				end )
			end,

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

			stopSync = function()
				stopSyncingUserAndCatalog( modelData )
			end,

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

			pauseSync = function()
				KSPrefs.syncPaused = true
				stopSyncingUserAndCatalog( modelData )
			end,
			
			--------------------------------------------------------------------
			
			preventSleep = function( _, reason, optKeepDisplayAwake )
				WFInsomnia.preventSystemSleep( reason, optKeepDisplayAwake )
			end,
			
			--------------------------------------------------------------------
			
			residualMode = function()
				
				if not KSPrefs.residualMode and not KSPrefs.restrictedCountryMode then
				
					KSPrefs.freemiumMode = false
					modelData._freemiumModeStream:onNext( KSPrefs.freemiumMode )
					
					KSPrefs.residualMode = true
					modelData._residualModeStream:onNext( KSPrefs.residualMode )
				
					-- If wfCatSynchronizer doesn't yet exist,
					-- it will be created using the current value
					-- in KSPrefs.residualMode
				
					if wfCatSynchronizer then
					
						wfCatSynchronizer:stopSyncing()
							-- Stop the normal mode sync.
					
						wfCatSynchronizer:enableResidualMode()
						
						startSyncingUserAndCatalog( modelData )
						
					end
					
					if KSPrefs.syncPaused then
						KSPrefs.syncPaused = false
					end
					
				end
				
			end,
			
			--------------------------------------------------------------------
			
			freemiumMode = function()
				
				if not KSPrefs.freemiumMode and not KSPrefs.restrictedCountryMode then
				
					KSPrefs.residualMode = false
					modelData._residualModeStream:onNext( KSPrefs.residualMode )
					
					KSPrefs.freemiumMode = true
					modelData._freemiumModeStream:onNext( KSPrefs.freemiumMode )
					
					-- If wfCatSynchronizer doesn't yet exist,
					-- it will be created using the current value
					-- in KSPrefs.freemiumMode
				
					if wfCatSynchronizer then
					
						wfCatSynchronizer:stopSyncing()
							-- Stop the normal mode sync.
							
						wfCatSynchronizer:enableFreemiumMode()
						
						startSyncingUserAndCatalog( modelData )
						
					end
					
					if KSPrefs.syncPaused then
						KSPrefs.syncPaused = false
					end
					
				end
				
			end,
			
			--------------------------------------------------------------------
			
			normalMode = function()
			
				if ( KSPrefs.residualMode or KSPrefs.freemiumMode )
						and not KSPrefs.restrictedCountryMode then
			
					KSPrefs.residualMode = false
					modelData._residualModeStream:onNext( KSPrefs.residualMode )
			
					KSPrefs.freemiumMode = false
					modelData._freemiumModeStream:onNext( KSPrefs.freemiumMode )
			
					-- If wfCatSynchronizer doesn't yet exist,
					-- it will be created using the current value
					-- in KSPrefs.residualMode
			
					if wfCatSynchronizer then
					
						wfCatSynchronizer:stopSyncing()
							
						wfCatSynchronizer:disableResidualMode()
						wfCatSynchronizer:disableFreemiumMode()
						
						startSyncingUserAndCatalog( modelData )
						
					end
					
					if KSPrefs.syncPaused then
						KSPrefs.syncPaused = false
					end
				
				end
				
			end,
			
			--------------------------------------------------------------------
			
			freemiumEnabled = function()
				modelData._freemiumEnabledStream:onNext( true )
			end,

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

			resumeSync = function()
				takeOneValueAndRunFunc( modelData._isUpdateRequiredStream, function( updateRequired )
					if updateRequired then
						logger:info( "Not resuming sync, because a client upgrade is required." )
						return
					end
					KSPrefs.syncPaused = false
					startSyncingUserAndCatalog( modelData )
				end )
			end,

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

			resetFileManagementPreferences = function() 
				
				NmbFileManagementSettings.reset()
				
				KSPrefs.postponeFileMoveUntilDate = nil
				modelData._fileMoverPostponedUntilDateStream:onNext( KSPrefs.postponeFileMoveUntilDate )
				
				local keepAllOriginals = NmbFileManagementSettings.keepAllOriginals()
				local keepAllProxies = NmbFileManagementSettings.keepAllProxies()
				local targetDiskSpace = NmbFileManagementSettings.targetNimbusDiskSpacePercentage()
				local newOriginalStorageLocationInfo = NmbFileManagementSettings.originalStorageLocationInfo()
				local newProxyStorageLocationInfo = NmbFileManagementSettings.proxyStorageLocationInfo()
				
				modelData._keepAllOriginalsStream:onNext( keepAllOriginals )
				modelData._keepAllProxiesStream:onNext( keepAllProxies )
				modelData._targetDiskSpacePercentageStream:onNext( targetDiskSpace )
				modelData._originalStorageLocationInfoStream:onNext( newOriginalStorageLocationInfo )
				modelData._proxyStorageLocationInfoStream:onNext( newProxyStorageLocationInfo )
				
			end,

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

			updateFileManagementPreferences = function( _, targetDiskSpace, keepAllOriginals, keepAllProxies ) 
				
				if keepAllOriginals ~= nil then
					NmbFileManagementSettings.setKeepAllOriginals( keepAllOriginals )
					modelData._keepAllOriginalsStream:onNext( keepAllOriginals )
				end

				if keepAllProxies ~= nil then
					NmbFileManagementSettings.setKeepAllProxies( keepAllProxies )
					modelData._keepAllProxiesStream:onNext( keepAllProxies )
				end
				
				if targetDiskSpace then
					NmbFileManagementSettings.setTargetDiskSpacePercentage( targetDiskSpace )
					modelData._targetDiskSpacePercentageStream:onNext( targetDiskSpace )
				end
				
			end,

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

			changeOriginalStorageLocation = function( _, newOriginalStorageLocation ) 
				
				KSPrefs.postponeFileMoveUntilDate = nil
				modelData._fileMoverPostponedUntilDateStream:onNext( KSPrefs.postponeFileMoveUntilDate )
				
				NmbFileManagementSettings.setOriginalStorageLocation( newOriginalStorageLocation )
				modelData._originalStorageLocationInfoStream:onNext( NmbFileManagementSettings.originalStorageLocationInfo() )
				
			end,

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

			changeProxyStorageLocation = function( _, newProxyStorageLocation ) 
				
				KSPrefs.postponeFileMoveUntilDate = nil
				modelData._fileMoverPostponedUntilDateStream:onNext( KSPrefs.postponeFileMoveUntilDate )
				
				NmbFileManagementSettings.setProxyStorageLocation( newProxyStorageLocation )
				modelData._proxyStorageLocationInfoStream:onNext( NmbFileManagementSettings.proxyStorageLocationInfo() )
				
			end,

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

			updateVolumeStorageName = function( _, volumeGuid, readableIdentifier ) 
				
				NmbFileManagementSettings.setVolumeReadableIdentifier( volumeGuid, readableIdentifier )
				
				modelData._originalStorageLocationInfoStream:onNext( NmbFileManagementSettings.originalStorageLocationInfo() )
				modelData._proxyStorageLocationInfoStream:onNext( NmbFileManagementSettings.proxyStorageLocationInfo() )
				
			end,
			
			--------------------------------------------------------------------
			
			togglePreventSleepWhileSyncing = function( _, shouldPreventSleepWhileSyncing ) 
				
				modelData._shouldPreventSleepWhileSyncingStream:onNext( shouldPreventSleepWhileSyncing )
				KSPrefs.shouldPreventSleepWhileSyncing = shouldPreventSleepWhileSyncing
		
			end,
			
			--------------------------------------------------------------------
			
			postponeFileMove = function( _, maybePostponeDurationInSeconds ) 
				
				local daysToSeconds = function( days )
					return days * 24 * 60 * 60
				end
				
				local postponeDelayInSeconds = POSTPONE_DURATION or maybePostponeDurationInSeconds or daysToSeconds( 6 )
					-- Intentionally delaying 6 days instead of 7

				if postponeDelayInSeconds <= 0 then
					KSPrefs.postponeFileMoveUntilDate = nil
				else
					KSPrefs.postponeFileMoveUntilDate = require 'KSDate'.timeNow() + postponeDelayInSeconds
				end
				
				modelData._fileMoverPostponedUntilDateStream:onNext( KSPrefs.postponeFileMoveUntilDate )
				
			end,
			
			--------------------------------------------------------------------
			
			updateAvatar = function( _, newAvatarFilePath, ext )
				
				return modelData._currentUserStream
					:take( 1 )
					:flatMap( function( wfCatUser )
						if wfCatUser then
							return WFCatAvatar.updateAvatar( wfCatUser, newAvatarFilePath, ext )
						else
							return Observable.empty()
						end
					end )
					:call( WFRxRunUntilDone )
						
			end,
			
			--------------------------------------------------------------------
			
			triggerForceLightroomClassicUpgradeFlag = function()
				if userInfoSynchronizer_now then
					userInfoSynchronizer_now:triggerForceLightroomClassicUpgradeFlag()
				end
			end,
			
			--------------------------------------------------------------------
			
			setProxyCredentials = function( _, username, password )
				require "WFRxHttpClient".setProxyCredentials( username, password )
				modelData._serverBaseUrlStream
					:tap( function( baseUrl )
						require "WFReachability".refreshReachabilityStatus( baseUrl )
					end )
					:call( WFRxRunUntilDone )
				
			end,
			
			--------------------------------------------------------------------
			
			allowFaceDetection = function()
				modelData._currentUserStream
					:take( 1 )
					:flatMap( function( wfCatUser )
						if wfCatUser then
							return wfCatUser:allowFaceDetection()
						else
							return Observable.empty()
						end
					end )
					:call( WFRxRunUntilDone )
			end,
			
			--------------------------------------------------------------------
			
			disallowFaceDetection = function()
				modelData._currentUserStream
					:take( 1 )
					:flatMap( function( wfCatUser )
						if wfCatUser then
							return wfCatUser:disallowFaceDetection()
						else
							return Observable.empty()
						end
					end )
					:call( WFRxRunUntilDone )
			end,
			
			--------------------------------------------------------------------
			
			DEBUG_dumpCoverage = function( _, path )
				require 'SetupCoverage'.dumpCoverage( path )
			end,

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

			DEBUG_incStorageUsed = function( _, amountUsed )
				WFCatStorage.debugStorageUsed( amountUsed )
			end,

		},

		close = function()

			logger:info( "Closing" )

			if wfCatUser_now then
				wfCatUser_now:close()
				wfCatUser_now = nil
			end
			
			if modelData then
				modelData:close()
				modelData = nil
			end
			
			if userInfoSynchronizer_now then
				userInfoSynchronizer_now:close()
				userInfoSynchronizer_now = nil
			end
			
			if _wfOzCatalog_now then
				_wfOzCatalog_now:close()
				_wfOzCatalog_now = nil
			end

		end,

	}
	
end

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

return NmbUserModel
