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

WFCatSyncStatus.lua

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

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

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

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

	-- Third Party
local bit = require 'bit'

	-- Rx
local Rx = require 'Rx'

	-- WF.core.utils
local KSTableUtils = require 'KSTableUtils'
local WFLogger = require 'WFLogger'
local WFRxScheduler = require 'WFRxScheduler'
local WFArguments = require 'WFArguments'

	-- WF.oz_client.managed_catalog
local WFCatAssetAux = require 'WFCatAssetAux'
local WFCatAssetUtils = require 'WFCatAssetUtils'
local WFCatBinaryUploader = require 'WFCatBinaryUploader'
local WFCatIndexUtils = require 'WFCatIndexUtils'
local WFCatFileSpec = require 'WFCatFileSpec'
local WFCatOzErrors = require 'WFCatOzErrors'
local WFCatSharedWithYouUtils = require 'WFCatSharedWithYouUtils'
local WFCatTrashUtils = require 'WFCatTrashUtils'
local NmbFileManagementSettings = require 'NmbFileManagementSettings'

	-- WF.oz_client.shared.utils
local LrAssetUtils = require 'LrAssetUtils'
local WFOzDocUtils = require 'WFOzDocUtils'

	-- WF.sqlite.database
local WFSQLiteUtils = require 'WFSQLiteUtils'

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

local assetHasThumbnail = assert( WFCatIndexUtils.assetHasThumbnail )
local isAssetDocument = assert( WFCatIndexUtils.isAssetDocument )
local isValidDisplayableAssetDocument = assert( WFCatIndexUtils.isValidDisplayableAssetDocument )
local isFileBasedAssetDocument = assert( WFCatIndexUtils.isFileBasedAssetDocument )
local isVideoDocument = assert( WFCatIndexUtils.isVideoDocument )

local WFCatTrashUtils_isDeleteTrash = assert( WFCatTrashUtils.isDeleteTrash )

local derefOpt = KSTableUtils.derefOpt

local isLatestMetadataUploaded = assert( LrAssetUtils.isLatestMetadataUploaded )

local logger = WFLogger( "WFCatSyncStatus" )

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

local WFCatSyncStatus = {}

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

-- @mark Status Strings and IDs

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

-- Lightroom Desktop can be in either one of four different configurations:
-- (Note that the configs are independent of the store-album-locally functionality.)

local kKeepNothing = 0 		-- Don't store anything locally. This is always true for Mobile.
local kKeepProxies = 1 		-- Store all proxies locally.
local kKeepOriginals = 2 	-- Store all originals locally.
local kKeepEverything = 3 	-- Store both proxies and originals locally.

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

-- All Asset Sync Status Strings

-- General Status: A general sync status for tracking metadata/errors.
-- Original Status: Sync status related to originals.
-- Proxy Status: Sync status related to proxies.
-- Develop Settings Status: Sync status related to develop settings/aux.

-- General

local kAssetNeedsMetadataUpload = 'NeedsMetadataUploaded'
local kAssetComplete = 'Completed'
local kAssetCompleteNoMaster = 'CompletedNoMaster'
local kAssetCompleteNoNegatives = 'CompletedNoNegatives'
local kAssetCompleteForOffline = 'CompletedForOffline'
local kAssetStorageLimitExceeded = 'StorageLimitExceeded'
local kAssetSyncPaused = 'SyncPaused'
local kAssetSyncNoConnection = 'SyncNoConnection'
local kAssetTrialOrSubscriptionExpired = 'TrialOrSubscriptionExpired'
local kAssetStackComplete = 'StackComplete'
local kAssetInsufficientPermissionError = 'InsufficientPermissionError'

-- Original

local kAssetNeedsOriginalUpload = 'NeedsOriginalUploaded'
local kAssetNeedsOriginalUploadFromOfflineVolume = 'NeedsOriginalUploadedButOnOfflineVolume'
local kAssetNeedsOriginalDownload = 'NeedsOriginalDownloaded'
local kAssetNeedsOriginalDownloadForOffline = 'NeedsOriginalDownloadedForOffline'
local kAssetNeedsOriginalDownloadToOfflineVolume = 'NeedsOriginalDownloadToOfflineVolume'
local kAssetNeedsOriginalDownloadToOutOfSpaceVolume = 'NeedsOriginalDownloadToOutOfSpaceVolume'
local kAssetInvalidOriginal = 'InvalidLocalOriginal'
local kAssetOriginalIsOnOfflineVolume = 'OriginalIsOnOfflineVolume'
local kAssetOriginalDownloadError = 'OriginalDownloadError'
local kAssetNeedsOriginalMigration = 'NeedsOriginalMigration'

-- Proxy

local kAssetNeedsProxyDownload = 'NeedsProxyDownloaded'
local kAssetNeedsProxyDownloadForOffline = 'NeedsProxyDownloadedForOffline'
local kAssetNeedsProxyDownloadToOfflineVolume = 'NeedsProxyDownloadToOfflineVolume'
local kAssetNeedsProxyDownloadToOutOfSpaceVolume = 'NeedsProxyDownloadToOutOfSpaceVolume'
local kAssetProxyDownloadError = 'ProxyDownloadError'
local kAssetProxyIsOnOfflineVolume = 'ProxyIsOnOfflineVolume'
local kAssetNeedsProxyMigration = 'NeedsProxyMigration'

-- Develop Settings

local kAssetNeedsDevelopXmpDownload = 'NeedsDevelopXmpDownloaded'
local kAssetDevelopXmpDownloadError = 'DevelopXmpDownloadError'
local kAssetNeedsDevelopXmpUpload = 'NeedsDevelopXmpUpload'
local kAssetInvalidAux = 'InvalidAuxError'
local kAssetInvalidXmpDevelop = 'InvalidXmpDevelopError'

-- Sync Status IDs are assigned by powers of two, so that we can assign multiple
-- sync statuses to a single asset, store that combination as a number in a single
-- SQLite column, and then leverage bitwise operations in SQLite to determine status.

-- As a note, even though the following creates an array with integer keys that range
-- from 1 to over 2 billion, Lua is smart enough so that a sufficiently sparse array
-- is actually stored as a hash table under the hood; it is not creating a 2b element
-- array! See section 4 in http://www.lua.org/doc/jucs05.pdf.

local kAssetSyncStatusById = {
	[ 1 ] = kAssetNeedsMetadataUpload,							-- 1
	[ 2^1 ] = kAssetNeedsOriginalUpload,						-- 2
	[ 2^2 ] = kAssetNeedsOriginalDownload,						-- 4
	[ 2^3 ] = kAssetNeedsOriginalDownloadForOffline,			-- 8
	[ 2^4 ] = kAssetNeedsProxyDownload,							-- 16
	[ 2^5 ] = kAssetNeedsDevelopXmpDownload,					-- 32
	[ 2^6 ] = kAssetComplete,									-- 64
	[ 2^7 ] = kAssetCompleteNoMaster,							-- 128
	[ 2^8 ] = kAssetStorageLimitExceeded,						-- 256
	[ 2^9 ] = kAssetInvalidOriginal,							-- 512
	[ 2^10 ] = kAssetOriginalIsOnOfflineVolume,            		-- 1024
	[ 2^11 ] = kAssetProxyIsOnOfflineVolume,            		-- 2048
	[ 2^12 ] = kAssetNeedsOriginalDownloadToOfflineVolume, 		-- 4096
	[ 2^13 ] = kAssetNeedsProxyDownloadToOfflineVolume,			-- 8192
	[ 2^14 ] = kAssetNeedsOriginalMigration,					-- 16384
	[ 2^15 ] = kAssetNeedsProxyMigration,						-- 32768
	[ 2^16 ] = kAssetNeedsOriginalUploadFromOfflineVolume,		-- 65536
	[ 2^17 ] = kAssetOriginalDownloadError, 					-- 131072
	[ 2^18 ] = kAssetProxyDownloadError,						-- 262144
	[ 2^19 ] = kAssetDevelopXmpDownloadError,					-- 524288
	[ 2^20 ] = kAssetNeedsOriginalDownloadToOutOfSpaceVolume, 	-- 1048576
	[ 2^21 ] = kAssetNeedsProxyDownloadToOutOfSpaceVolume, 		-- 2097152
	[ 2^22 ] = kAssetSyncPaused,								-- 4194304
	[ 2^23 ] = kAssetSyncNoConnection,							-- 8388608
	[ 2^24 ] = kAssetNeedsProxyDownloadForOffline,				-- 16777216
	[ 2^25 ] = kAssetTrialOrSubscriptionExpired,				-- 33554432
	[ 2^26 ] = kAssetStackComplete,								-- 67108864
	[ 2^27 ] = kAssetCompleteForOffline,						-- 134217728
	[ 2^28 ] = kAssetCompleteNoNegatives,						-- 268435456
	[ 2^29 ] = kAssetInsufficientPermissionError,				-- 536870912
	[ 2^30 ] = kAssetNeedsDevelopXmpUpload,						-- 1073741824
	[ 2^31 ] = kAssetInvalidAux,								-- 2147483648
	[ 2^32 ] = kAssetInvalidXmpDevelop,							-- 4294967296

	-- [ 2^52 ] This is the largest we can go. Lua 5.1s 52-bit mantissa means that
	-- the highest number that can be used with integer precision is 2^52.

}

local kAssetSyncStatus = KSTableUtils.invert( kAssetSyncStatusById )

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

local kNonSyncingStates = {
	kAssetComplete,
	kAssetCompleteNoMaster,
	kAssetCompleteNoNegatives,
	kAssetCompleteForOffline,
	kAssetInvalidOriginal,
	kAssetOriginalDownloadError,
	kAssetProxyDownloadError,
	kAssetDevelopXmpDownloadError,
	kAssetStackComplete,
	kAssetInsufficientPermissionError,
	kAssetStorageLimitExceeded,
	kAssetTrialOrSubscriptionExpired,
	kAssetInvalidAux,
	kAssetInvalidXmpDevelop,
}

local kNonSyncingStateSet = KSTableUtils.mapArrayToSet( kNonSyncingStates )

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

local kAssetHasLocalOriginal = 'OriginalOnDisk'
local kAssetHasLocalOriginalForOffline = 'OriginalOnDiskForOffline'
local kAssetHasLocalProxy = 'ProxyOnDisk'
local kAssetHasNothing = 'NothingOnDisk'

local kAssetLocalFileTypeById = {
	kAssetHasLocalOriginal,				-- 1
	kAssetHasLocalOriginalForOffline,	-- 2
	kAssetHasLocalProxy,				-- 3
	kAssetHasNothing,					-- 4
}

local kAssetLocalFileType = KSTableUtils.invert( kAssetLocalFileTypeById )

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

local kAlbumSyncing = 'syncing'
local kAlbumComplete = 'synced'
local kAlbumSyncPaused = 'paused'
local kAlbumSyncNoConnection = 'offline'
local kAlbumSyncingForOfflineUse = 'syncingForOfflineUse'
local kAlbumCompleteForOfflineUse = 'syncedForOfflineUse'
local kAlbumSyncingForOfflineUseToOfflineVolume = 'syncingForOfflineUseToOfflineVolume'
local kAlbumSyncingForOfflineUseToOutOfSpaceVolume = 'syncingForOfflineUseToOutOfSpaceVolume'

local kAlbumSyncStatusById = {
	kAlbumSyncing,									-- 1
	kAlbumComplete,									-- 2
	kAlbumSyncPaused,   							-- 3
	kAlbumSyncNoConnection, 						-- 4
	kAlbumSyncingForOfflineUse, 					-- 5
	kAlbumCompleteForOfflineUse,					-- 6
	kAlbumSyncingForOfflineUseToOfflineVolume, 		-- 7
	kAlbumSyncingForOfflineUseToOutOfSpaceVolume 	-- 8
}

local kAlbumSyncStatus = KSTableUtils.invert( kAlbumSyncStatusById )

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

local check = assert( WFArguments.check )

local kOzHasOriginal = 'OzHasOriginal'
local kOzHasProxy = 'OzHasSmartPreview'
local kOzHasNothing = 'OzHasNothing'

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

-- @mark Helper Functions

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

local function whatToDownload(
		keepAllOriginals,
		keepAllProxies,
		needsOriginalDownload,
		hasLocalOriginal,
		needsProxyDownload,
		hasLocalProxy,
		needsDevelopXmpDownload,
		needsAuxDownload,
		isMarkedForOffline,
		originalVolumeOfflineOrFull,
		proxyVolumeOfflineOrFull,
		ozHasNonThumbnailRendition,
		waitingForProxyOnOz )

	local maybeBinaryType

	local shouldDownloadOriginal = needsOriginalDownload and not originalVolumeOfflineOrFull
	local shouldDownloadProxy = needsProxyDownload and not proxyVolumeOfflineOrFull and not waitingForProxyOnOz
	local needNegativeForRender = not ozHasNonThumbnailRendition and not hasLocalOriginal and not hasLocalProxy

	if keepAllOriginals or isMarkedForOffline then

		if needsAuxDownload then

			-- 1st Download the auxiliary objects

			maybeBinaryType = 'aux'

		elseif needsDevelopXmpDownload then

			-- 2nd Download the xmp develop settings.

			maybeBinaryType = 'xmp_develop'

		elseif shouldDownloadOriginal then

			-- 3rd Download the original, if we can.

			maybeBinaryType = 'original'

		elseif shouldDownloadProxy then

			-- 4th Download the proxy

			maybeBinaryType = 'proxy'

		end

	elseif needNegativeForRender then

		if needsAuxDownload then

			-- 1st Download the auxiliary objects

			maybeBinaryType = 'aux'

		elseif needsDevelopXmpDownload then

			-- 2nd Download the xmp develop settings.

			maybeBinaryType = 'xmp_develop'

		elseif shouldDownloadProxy then

			-- 3rd First try to download the proxy.

			maybeBinaryType = 'proxy'

		elseif shouldDownloadOriginal then

			-- If we can't download it, download the original.

			maybeBinaryType = 'original'

		end

	else

		if needsAuxDownload then

			-- 1st Download the auxiliary objects

			maybeBinaryType = 'aux'

		elseif needsDevelopXmpDownload then

			-- 2nd Download the xmp develop settings.

			maybeBinaryType = 'xmp_develop'

		elseif needsProxyDownload then

			if not proxyVolumeOfflineOrFull and not waitingForProxyOnOz then

				-- 3rd Download the proxy, if we can.

				maybeBinaryType = 'proxy'

			end

		elseif not hasLocalProxy and shouldDownloadOriginal then

			-- If we can't download the proxy because it's not on Oz, then
			-- download the original.

			maybeBinaryType = 'original'

		end

	end

	-- A binary is "required" if:
	-- it is marked for offline use.
	-- a negative is needed to render
	-- it is an xmp develop settings and we're keeping all originals or proxies.
	-- it is an original and we're keeping all originals.
	-- it is a proxy and we're keeping all originals, but there's no original on Oz.
	-- it is a proxy and we're keeping all proxies.
	-- it is an original and we're keeping all proxies, but there's no proxy on Oz.

	local isBinaryRequired = false

	if isMarkedForOffline or
		needNegativeForRender or
		( maybeBinaryType == 'xmp_develop' and ( keepAllOriginals or keepAllProxies ) ) or
		( maybeBinaryType == 'aux' and ( keepAllOriginals or keepAllProxies ) ) or
		( maybeBinaryType == 'original' and keepAllOriginals ) or
		( maybeBinaryType == 'proxy' and keepAllProxies ) or
		( maybeBinaryType == 'proxy' and keepAllOriginals and not hasLocalOriginal and not shouldDownloadOriginal ) or
		( maybeBinaryType == 'original' and keepAllProxies and not hasLocalProxy and not shouldDownloadProxy ) then
			isBinaryRequired = true
	end

	return maybeBinaryType, isBinaryRequired

end

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

local function assetNeeded( docValue, keepAllProxies, keepAllOriginals )

	local ozHasURIForOriginal = WFCatFileSpec.getOriginalLinkFromDocValue( docValue ) ~= nil
	local haveLocalPathForOriginal = WFCatAssetUtils.hasLocalOriginal( docValue )
	local needsOriginalDownload = not haveLocalPathForOriginal and ozHasURIForOriginal

	local ozHasURIForProxy = WFCatFileSpec.getProxyLinkFromDocValue( docValue ) ~= nil
	local haveLocalPathForProxy = WFCatAssetUtils.hasLocalProxy( docValue )
	local needsProxyDownload = not haveLocalPathForOriginal and not haveLocalPathForProxy and ozHasURIForProxy
		and WFCatSharedWithYouUtils.canEditAsset( docValue )

	local haveLocalPathForXmp = WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue ) ~= nil
	local ozHasURIForXmp = WFCatFileSpec.getXmpDevelopLinkAndSha256FromDocValue( docValue ) ~= nil
	local needsDevelopXmpDownload = not haveLocalPathForXmp and ozHasURIForXmp

	local needsAuxDownload = ozHasURIForXmp and WFCatAssetAux.needsAuxDownloadForDevelop( docValue )

	local isMarkedForOffline = WFCatFileSpec.getMarkedForOfflineUseFromDocValue( docValue )

	local sufficientRenditionFoundOnOz = WFCatFileSpec.checkOzForNonThumbnailRendition( docValue )

	local waitingForProxyOnOz = derefOpt( docValue, '_serverProvidedMetadata', 'startedWaitingForProxyTimestamp' )

	-- SPAM [jandrew 2017-09-12]: We pass `nil` for two of the parameters because they should be
	-- ignored when calculating what we NEED to sync as opposed to what we SHOULD try to sync;
	-- the difference between "need" and "should" in this context is that an asset that needs
	-- syncing still needs to be synced even if there's no disk space available. On the other hand,
	-- an asset that needs to be synced SHOULD NOT be synced if there's no disk space available.

	local binaryType, isBinaryRequired = whatToDownload(
		keepAllOriginals,
		keepAllProxies,
		needsOriginalDownload,
		haveLocalPathForOriginal,
		needsProxyDownload,
		haveLocalPathForProxy,
		needsDevelopXmpDownload,
		needsAuxDownload,
		isMarkedForOffline,
		nil, -- originalVolumeOfflineOrFull.
		nil, -- proxyVolumeOfflineOrFull
		sufficientRenditionFoundOnOz,
		waitingForProxyOnOz )

	-- Sync status is not affected by purely idle downloads. It's only affected by idle
	-- downloads that are required.

	if not isBinaryRequired then
		binaryType = nil
	end

	return binaryType

end

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

local function isStack( docValue )

	return not not ( docValue.subtype == 'stack' )

end

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

local function doesAssetNeedOriginalUploaded( docValue )

	local needsUpload = false

	local needsUploadCallback = function()
		needsUpload = true
	end

	-- Consider the helper function used by the needsBinaryUpload index store
	-- table as the source of truth for whether or not an original needs to
	-- be uploaded.

	local importDate, captureDate, isDenied, priorityForUpload = nil
		-- None of these matter in this context

	local skipAccountErrors = true
		-- Whether or not there are account errors doesn't affect the sync status.

	WFCatBinaryUploader.maybeEmitMasterUploadRecord(
			docValue, needsUploadCallback, importDate, captureDate,
			isDenied, priorityForUpload, skipAccountErrors )

	return needsUpload

end

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

local function isMarkedForOfflineUse( docValue )

	return WFCatFileSpec.getMarkedForOfflineUseFromDocValue( docValue )

end

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

local function doesAssetNeedOriginalDownloaded( docValue, keepAllProxies, keepAllOriginals )

	return assetNeeded( docValue, keepAllProxies, keepAllOriginals ) == 'original'

end

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

local function doesAssetNeedProxyDownloaded( docValue, keepAllProxies, keepAllOriginals )

	return assetNeeded( docValue, keepAllProxies, keepAllOriginals ) == 'proxy'

end

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

local function doesAssetNeedDevelopXmpDownloaded( docValue, keepAllProxies, keepAllOriginals )

	return assetNeeded( docValue, keepAllProxies, keepAllOriginals ) == 'xmp_develop'

end

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

local function doesAssetNeedAuxDownloaded( docValue, keepAllProxies, keepAllOriginals )

	return assetNeeded( docValue, keepAllProxies, keepAllOriginals ) == 'aux'

end

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

local function hasInvalidOriginalError( docValue )

	return KSTableUtils.derefOpt( docValue, '_localOnly', 'localOriginalError' )

end

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

local function hasOriginalDownloadError( docValue )

	local originalHref = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href' )

	local permanentOriginalDownloadErrorForCurrentHref = KSTableUtils.derefOpt( docValue, '_localOnly', 'binaryDownloadErrors', '/rels/master', originalHref )

	return not not permanentOriginalDownloadErrorForCurrentHref

end

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

local function hasProxyDownloadError( docValue )

	local proxyRel
	if isVideoDocument( docValue ) then
		proxyRel = '/rels/rendition_type/720p'
	else
		proxyRel = '/rels/proxy_type/2560'
	end

	local proxyHref = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', proxyRel, 'href' )

	local permanentProxylDownloadErrorForCurrentHref = KSTableUtils.derefOpt( docValue, '_localOnly', 'binaryDownloadErrors', proxyRel, proxyHref )

	return not not permanentProxylDownloadErrorForCurrentHref
end

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

local function hasXmpDevelopDownloadError( docValue )

	local xmpDevelopHref = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'href' )

	local permanentXmpDevelopDownloadErrorForCurrentHref = KSTableUtils.derefOpt( docValue, '_localOnly', 'binaryDownloadErrors', '/rels/xmp/develop', xmpDevelopHref )

	return not not permanentXmpDevelopDownloadErrorForCurrentHref

end

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

local function getSyncingStatusIds( allSyncStatuses, isStatusForAlbums )

	-- Certain statuses (kAssetComplete, kAssetCompleteNoMaster, kAssetInvalidOriginal, etc.)
	-- would indicate complete asset. All other assets are considered 'syncing'.

	local nonSyncingStates = KSTableUtils.shallowcopy( kNonSyncingStates )
	local keepAllOriginals = NmbFileManagementSettings.keepAllOriginals()

	if isStatusForAlbums and not keepAllOriginals then
		table.insert( nonSyncingStates, kAssetNeedsOriginalDownloadForOffline )
		table.insert( nonSyncingStates, kAssetNeedsProxyDownloadForOffline )
	end

	local nonSyncingStateSet = KSTableUtils.mapArrayToSet( nonSyncingStates )

	-- Copy status ids into a table that can be unpacked.

	local syncingStatusIds = {}
	local combinedStatuses = 0

	for state, statusId in pairs( allSyncStatuses ) do

		if not nonSyncingStateSet[ state ] then

			table.insert( syncingStatusIds, statusId )
			combinedStatuses = combinedStatuses + statusId

		end

	end

	return syncingStatusIds, combinedStatuses

end

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

local function getErrorStatusIds( allSyncStatuses )

	local errorSyncStates = {
		kAssetInvalidOriginal,
		kAssetOriginalDownloadError,
		kAssetProxyDownloadError,
		kAssetDevelopXmpDownloadError,
		kAssetInvalidAux,
		kAssetInvalidXmpDevelop,
	}

	local errorStatusIds = {}

	for _, errorState in ipairs( errorSyncStates ) do

		if allSyncStatuses[ errorState ] then
			table.insert( errorStatusIds, allSyncStatuses[ errorState ] )
		end

	end

	return errorStatusIds

end

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

local function basicAlbumStatusStream( wfCatalog )

	return wfCatalog:indexStore():query {
			sql = [[SELECT albumId, syncStatus
						FROM albums ]],
			calm = 0.25
		}
		:map( function( albumStatuses )

			local statusForAlbum = {}

			for _, albumStatus in ipairs( albumStatuses ) do

				local albumId = albumStatus[ 1 ]
				local status = albumStatus[ 2 ]

				statusForAlbum[ albumId ] = kAlbumSyncStatusById[ status ]

			end

			return statusForAlbum

		end )
end

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

local function isAlbumCompleted( statusOrId )

	if type( statusOrId ) ~= 'string' then
		statusOrId = kAlbumSyncStatusById[ statusOrId ]
	end

	return statusOrId == kAlbumComplete
		or statusOrId == kAlbumCompleteForOfflineUse

end

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

local function localFileType( hasOriginal, isOriginalOnOfflineVolume, hasOriginalError, isMarkedForOffline, hasProxy, isProxyOnOfflineVolume )

	local assetLocalFileTypeId = kAssetHasNothing

	if hasOriginal and not isOriginalOnOfflineVolume and not hasOriginalError then

		if isMarkedForOffline then
			assetLocalFileTypeId = kAssetHasLocalOriginalForOffline
		else
			assetLocalFileTypeId = kAssetHasLocalOriginal
		end

	elseif hasProxy and not isProxyOnOfflineVolume then

		assetLocalFileTypeId = kAssetHasLocalProxy

	end

	return kAssetLocalFileType[ assetLocalFileTypeId ]

end

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

local function getLocalFileTypeForAsset( docValue, isOriginalOnOfflineVolume, isProxyOnOfflineVolume )

	return localFileType(
			WFCatAssetUtils.hasLocalOriginal( docValue ),
			isOriginalOnOfflineVolume,
			hasInvalidOriginalError( docValue ),
			WFCatFileSpec.getMarkedForOfflineUseFromDocValue( docValue ),
			WFCatAssetUtils.hasLocalProxy( docValue ),
			isProxyOnOfflineVolume )

end

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

local function getAssetStatus( docValue, keepAllProxies, keepAllOriginals )

	local prioritizedSyncStatus
	local syncStatusesCombinedInteger = 0

	local seenStatuses = {}

	local function includeStatus( statusStr )

		-- We must only count each status once, or else the bitwise logic
		-- we use on the combined sync status integer will break. This
		-- condition is a safeguard.

		if seenStatuses[ statusStr ] then
			logger:warnf( "Duplicate status (%s) detected, there may be a bug in getAssetStatus", statusStr )
			return
		end

		seenStatuses[ statusStr ] = true

		local statusNum = kAssetSyncStatus[ statusStr ]

		prioritizedSyncStatus = prioritizedSyncStatus or statusNum
		syncStatusesCombinedInteger = syncStatusesCombinedInteger + statusNum
			-- Using + here instead of bitwise or (|) because the latter is not
			-- available in Lua 5.1

	end

	if docValue._deleted then

		local links = KSTableUtils.derefOpt( docValue,
									 '_serverProvidedMetadata',
									 'links' )

		if not links then
			-- This gets executed if you delete an asset that hasn't been
			-- uploaded yet, it is called by the _map function in asset_status_table().
			return nil
		end

		includeStatus( kAssetNeedsMetadataUpload )

	elseif isStack( docValue ) then

		if not isLatestMetadataUploaded( docValue ) then
			includeStatus( kAssetNeedsMetadataUpload )
		else
			includeStatus( kAssetStackComplete )
		end

	else

		local needMetadataUpload = not isLatestMetadataUploaded( docValue )

		local needOriginalUpload = doesAssetNeedOriginalUploaded( docValue )

		-- For the purpose of sync status, this develop settings state considers
		-- externalized develop settings XMP, as well as any referenced aux objects.

		local needDevelopSettingsUpload =
				WFCatAssetUtils.doesAssetNeedDevelopSettingsUploaded( docValue )
					or WFCatAssetAux.hasUnsyncedDevelopAuxLocally( docValue )

		if needMetadataUpload
				or needOriginalUpload
				or needDevelopSettingsUpload then

			if WFCatOzErrors.hasStorageExceededError( docValue ) then
				includeStatus( kAssetStorageLimitExceeded )
			end

			if WFCatOzErrors.hasTrialOrSubscriptionExpiredError( docValue )
					or WFCatOzErrors.hasEntitlementExpiredError( docValue ) then
				includeStatus( kAssetTrialOrSubscriptionExpired )
			end

			if WFCatOzErrors.hasInsufficientPermissionError( docValue ) then
				includeStatus( kAssetInsufficientPermissionError )
			end

		end

		if hasInvalidOriginalError( docValue ) and not WFOzDocUtils.isValidOriginalAvailableOnOz( docValue ) then
			includeStatus( kAssetInvalidOriginal )
		end

		if needMetadataUpload then

			includeStatus( kAssetNeedsMetadataUpload )

		end

		if WFCatAssetAux.hasLocalAuxErrorForCurrentDevelopSettings( docValue ) then

			includeStatus( kAssetInvalidAux )

		end

		if WFCatAssetUtils.hasLocalXmpDevelopError( docValue ) then

			includeStatus( kAssetInvalidXmpDevelop )

		end

		if needOriginalUpload then

			includeStatus( kAssetNeedsOriginalUpload )

		end

		if needDevelopSettingsUpload then

			includeStatus( kAssetNeedsDevelopXmpUpload )

		end

		if doesAssetNeedOriginalDownloaded( docValue, keepAllProxies, keepAllOriginals ) then

			if isMarkedForOfflineUse( docValue ) or keepAllOriginals then
				includeStatus( kAssetNeedsOriginalDownloadForOffline )
			end

			includeStatus( kAssetNeedsOriginalDownload )

		end

		if doesAssetNeedProxyDownloaded( docValue, keepAllProxies, keepAllOriginals ) then

			if isMarkedForOfflineUse( docValue ) or keepAllProxies then
				includeStatus( kAssetNeedsProxyDownloadForOffline )
			end

			includeStatus( kAssetNeedsProxyDownload )

		end

		if doesAssetNeedDevelopXmpDownloaded( docValue, keepAllProxies, keepAllOriginals )
				or doesAssetNeedAuxDownloaded( docValue, keepAllProxies, keepAllOriginals ) then

			includeStatus( kAssetNeedsDevelopXmpDownload )

		end

		if hasOriginalDownloadError( docValue ) then

			includeStatus( kAssetOriginalDownloadError )

		end

		if hasProxyDownloadError( docValue ) then

			includeStatus( kAssetProxyDownloadError )

		end

		if hasXmpDevelopDownloadError( docValue ) then

			includeStatus( kAssetDevelopXmpDownloadError )

		end

		-- Completion statuses are mutually exclusive

		if not prioritizedSyncStatus then

			if WFCatFileSpec.getOriginalLinkFromDocValue( docValue )
				and ( isMarkedForOfflineUse( docValue ) or keepAllOriginals ) then

				includeStatus( kAssetCompleteForOffline )

			elseif WFCatFileSpec.getOriginalLinkFromDocValue( docValue ) then

				includeStatus( kAssetComplete )

			elseif WFCatFileSpec.getProxyLinkFromDocValue( docValue )
				and not ( docValue._spaceId and docValue.subtype == 'video' ) then

				-- The space and subtype check above was added to address bug:
				-- https://jira.corp.adobe.com/browse/WF-1611
				-- The kAssetCompleteNoMaster sync status maps to "Synced from Lr Classic"
				-- in client UI. This is not true for shared-with-you video assets.
				-- Shared-with-you video assets will not have an accessible original
				-- or a true proxy. Videos, in general, use the 720p as the 'proxy'. This
				-- 720p rendition is available for all shared-with-you album members.
				--
				-- In order to prevent shared-with-you videos from showing up as
				-- "Synced from Lr Classic", we do not treat the presence of the 720p
				-- rendition as if the asset has a proxy for shared videos.
				--
				-- Note: The bug only applies to shared-with-you viewers/contributors
				-- and not editors because editors have access to the original.

				includeStatus( kAssetCompleteNoMaster )

			else

				includeStatus( kAssetCompleteNoNegatives )

			end

		end

	end

	return prioritizedSyncStatus, syncStatusesCombinedInteger

end

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

-- @mark Index Store Tables / Indexes

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

local function asset_status_table()

	local mapFn = function( docValue, emit )

		if docValue._deleted and not WFCatTrashUtils_isDeleteTrash( docValue ) then

			-- Don't count deleted assets unless they are delete trash as Cloud
			-- Trash generates another asset for each deleted or restored asset.

			return

		end

		if not isAssetDocument( docValue ) then

			-- Only emit sync status for assets docs.

			return

		end

		if not assetHasThumbnail( docValue ) then

			-- Don't emit sync status for something that won't show in the UI.
			-- Note that we are using assetHasThumbnail for this check instead of
			-- isValidDisplayableAssetDocument, since isValidDisplayableAssetDocument
			-- rules out deleted assets; based on the WFCatTrashUtils_isDeleteTrash check
			-- above, certain deleted assets should be indexed.

			return

		end

		local keepNothingPrioritizedStatus, keepNothingStatuses
				= getAssetStatus( docValue, false, false )

		local keepAllProxiesPrioritizedStatus, keepAllProxiesStatuses
				= getAssetStatus( docValue, true, false )

		local keepAllOriginalsPrioritizedStatus, keepAllOriginalsStatuses
				= getAssetStatus( docValue, false, true )

		local keepEverythingPrioritizedStatus, keepEverythingStatuses
				= getAssetStatus( docValue, true, true )

		if not ( keepNothingPrioritizedStatus
				and keepAllProxiesPrioritizedStatus
				and keepAllOriginalsPrioritizedStatus
				and keepEverythingPrioritizedStatus ) then

		  -- If any of these statuses are missing, don't emit sync status.

			return

		end

		local originalVolumeGuid = WFCatFileSpec.getOriginalVolumeGuidFromDocValue( docValue )
		local proxyVolumeGuid = WFCatFileSpec.getProxyVolumeGuidFromDocValue( docValue )

		local assetId = docValue._id

		-- isSearchable is used in queries that drive the cloudy indicator UI, so that
		-- it doesn't show status related to trash/space assets.

		local isSearchableBool = not docValue._spaceId
				and not WFCatIndexUtils.isTrashAssetDocument( docValue )

		local isSearchable = isSearchableBool and 1 or 0

		local spaceId = docValue._spaceId

		emit( assetId, keepNothingPrioritizedStatus, keepNothingStatuses,
				kKeepNothing, isSearchable, originalVolumeGuid, proxyVolumeGuid, spaceId )

		emit( assetId, keepAllOriginalsPrioritizedStatus, keepAllOriginalsStatuses,
				kKeepOriginals, isSearchable, originalVolumeGuid, proxyVolumeGuid, spaceId )

		emit( assetId, keepAllProxiesPrioritizedStatus, keepAllProxiesStatuses,
				kKeepProxies, isSearchable, originalVolumeGuid, proxyVolumeGuid, spaceId )

		emit( assetId, keepEverythingPrioritizedStatus, keepEverythingStatuses,
				kKeepEverything, isSearchable, originalVolumeGuid, proxyVolumeGuid, spaceId )

	end

	return {

		sql = [[CREATE TABLE asset_sync_status_ui(
					assetId,
					prioritizedSyncStatus,
					syncStatuses,
					keepLocallyConfiguration,
					isSearchable,
					volumeGuidUsedByOriginal,
					volumeGuidUsedByProxy,
					spaceId )]],

		useLocalId = true,

		hasOneRowPerDocument = false,

		map = mapFn,

		relevantTypeSubtypeTuples = {
			{ 'asset', '*' }
		},

	}

end

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

local function asset_status_by_assetId_index()
	return [[CREATE INDEX asset_sync_status_ui_by_assetId_prioritizedSyncStatus ON asset_sync_status_ui( assetId, prioritizedSyncStatus )]]
end

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

local function asset_status_by_prioritizedSyncStatus_index()
	return [[CREATE INDEX asset_sync_status_ui_by_prioritizedSyncStatus ON asset_sync_status_ui( prioritizedSyncStatus, assetId )]]
end

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

local function asset_status_by_syncStatuses_index()
	return [[CREATE INDEX asset_sync_status_ui_by_syncStatuses ON asset_sync_status_ui( syncStatuses, assetId )]]
end

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

local function asset_status_by_keepLocallyConfiguration_index()
	return [[CREATE INDEX asset_sync_status_ui_by_keepLocallyConfiguration ON asset_sync_status_ui( keepLocallyConfiguration, assetId )]]
end

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

local function asset_status_by_isSearchable_index()
	return [[CREATE INDEX asset_sync_status_ui_by_isSearchable ON asset_sync_status_ui( isSearchable, assetId )]]
end

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

local function asset_local_file_type_table()

	return {

		sql = [[CREATE TABLE asset_sync_local_file_type_ui( assetId, localFileType INTEGER )]],

		useLocalId = true,

		hasOneRowPerDocument = true,

		relevantTypeSubtypeTuples = {
			{ 'asset', 'image' },
			{ 'asset', 'video' },
			{ 'asset', 'deleted_image' },
			{ 'asset', 'deleted_video' }
		},

		docValueFilter = isFileBasedAssetDocument,

		map = function( docValue, emit )

			if isValidDisplayableAssetDocument( docValue ) and
				isFileBasedAssetDocument( docValue ) then

				local localFileType = getLocalFileTypeForAsset( docValue )

				-- SPAM [jandrew 2018-09-05]: The assets table does not emit for assets
				-- that do not contain a capture date. In order to maintain consistency
				-- between the tables, I've added that here as well.

				local captureDate = derefOpt( docValue, 'captureDate' )

				if localFileType and captureDate then

					local assetId = docValue._id
					emit( assetId, localFileType )

				end

			end

		end,

	}

end

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

local function asset_local_file_type_by_assetId_index()
	return [[CREATE INDEX asset_sync_local_file_type_ui_by_assetId_status ON asset_sync_local_file_type_ui( assetId, localFileType )]]
end

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

local function asset_local_file_type_by_localFileType_index()
	return [[CREATE INDEX asset_sync_local_file_type_ui_by_status ON asset_sync_local_file_type_ui( localFileType )]]
end

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

local function unconfirmedDeletions_table()

	return {

		sql = [[CREATE TABLE unconfirmedDeletions( assetId )]],

		useLocalId = true,

		hasOneRowPerDocument = true,

		relevantTypeSubtypeTuples = {
			{ 'asset', '*' }
		},

		map = function( docValue, emit )

			-- only include deleted asset with unsynced metadata

			-- Deleted documents don't obey the relevant asset type filter
			-- see WFRxIndexStoreMapServer.lua 'if docValue.deleted'
			-- so make sure to check isAssetDocument (we end up processing
			-- error docs without this check).
			if docValue._deleted and isAssetDocument( docValue ) and not isLatestMetadataUploaded( docValue ) then

				-- The pusher does not send deletions to Oz when there's no
				-- corresponding document on Oz (see WFCatMetadataPusher.deltaForDeletDoc),
				-- so don't bother emitting deleted docs in this case.

				local hasDocEverSyncedToOz = not not docValue._serverProvidedMetadata
					-- SPM is only present for docs that have previously synced.

				if hasDocEverSyncedToOz then

					local assetId = docValue._id

					emit( assetId )

				end

			end

		end,

	}

end

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

-- @mark APIs

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

function WFCatSyncStatus.getVolumeInfoForSyncStatus( wfCatalog )

	local allVolumeAgents = wfCatalog:getAllVolumeAgents()

	local volumeAgentStatusStreams = {}

	local originalsPathInfo = wfCatalog:fileSpec():originalsPathInfo()
	local volumeGuidForOriginals = originalsPathInfo.volumeGuid

	local proxiesPathInfo = wfCatalog:fileSpec():proxiesPathInfo()
	local volumeGuidForProxies = proxiesPathInfo.volumeGuid

	-- Build up a table of volume agent status streams.

	for _, agent in pairs( allVolumeAgents ) do
		table.insert( volumeAgentStatusStreams, agent:getVolumeStatusStream() )
	end

	table.insert( volumeAgentStatusStreams, Rx.Observable.just( nil ) )
		-- Add a nil observable in case there are no volume streams. This can
		-- happen in unit tests.

	return Rx.Observable.combineLatest( unpack( volumeAgentStatusStreams ) )

		:map( function( ... )

			local knownVolumes = {}
			local onlineVolumes = {}
			local outOfSpaceVolumes = {}

			for _, volumeStatus in ipairs( arg ) do

				local volumeGuid = volumeStatus.volumeGuid
				local isOnline = volumeStatus.isOnline
				local outOfSpace = volumeStatus.isOutOfSpace

				knownVolumes[ volumeGuid ] = true

				onlineVolumes[ volumeGuid ] = isOnline

				outOfSpaceVolumes[ volumeGuid ] = outOfSpace

			end

			return volumeGuidForOriginals, volumeGuidForProxies,
					onlineVolumes, outOfSpaceVolumes, knownVolumes

		end )

end

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

--[[!
	\brief Fetch a table of Space IDs that are inaccessible. Here, inaccessible
		means that the owner of this space has an expired account, or that the
		space is under maintenance. 
	
	\param catalog (WFCatalog)

	\return <code>Observable</code> An observable that fires with a table of
		inaccessible space IDs.
--]]

function WFCatSyncStatus.getBlockedSpaceIdsMapStream( wfCatalog )

	return wfCatalog:indexStore():query {
			sql = [[ SELECT docId
						FROM space
						WHERE space.entitlementExpired = 1
							OR space.isUnderMaintenance = 1 ]],
			singleColumnQuery = true,
			calm = 2,
		}

		:map( function( spaceIds )

			local spaceIdsMap = {}

			for _, spaceId in ipairs( spaceIds ) do
				spaceIdsMap[ spaceId ] = true
			end

			return spaceIdsMap

		end )

end

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

function WFCatSyncStatus.getCombinedAssetStatusAsStr( wfCatalog, syncStatusAndVolumeInfos )

	--[[

		True asset sync status is made up 4 separate data points.

		1. The current state of the asset as generated by "getAssetStatus". This
			is the same status that is stored in sync status index store table.

		2. The state of all spaces that have been shared with the user. If someone
			has shared a space with a user, and that space owner has an expired account,
			nothing can be synced to or from that space.

		3. The current state of the volumes where the assets are stored. If an asset
		 	needs to be downloaded, but the volume we need to download it to is
			offline, that's a new unique state.

		4. The current global state of sync; is sync paused and/or offline.

	]]--

	local mappedVolumeStatuses = WFCatSyncStatus.getVolumeInfoForSyncStatus( wfCatalog )

	local volumeAgents = wfCatalog:getAllVolumeAgents()
	local volumeAgent = volumeAgents[ next( volumeAgents ) ]
	local syncPausedStream = volumeAgent and volumeAgent:isSyncPausedStream() or Rx.Observable.just( false )
	local internetConnectionStream = volumeAgent and volumeAgent:hasInternetConnectionStream() or Rx.Observable.just( true )
	local blockedSpaceIdsStream = WFCatSyncStatus.getBlockedSpaceIdsMapStream( wfCatalog )

	-- syncStatusAndVolumeInfos is an Observable that produces an array of rows containing
	-- 1: sync status constant mapping to getAssetStatus
	-- 2: originalVolumeGuid
	-- 3: proxyVolumeGuid
	-- 4: markedAsOffline

	return Rx.Observable.combineLatest(
			syncStatusAndVolumeInfos,
			blockedSpaceIdsStream,
			mappedVolumeStatuses )

		:map( function( syncStatusAndVolumeInfoRows, blockedSpaceIds, volumeGuidForOriginals,
				volumeGuidForProxies, onlineVolumes, outOfSpaceVolumes, knownVolumes )

			local combinedSyncStatus

			for _, syncStatusAndVolumeInfoRow in ipairs( syncStatusAndVolumeInfoRows ) do

				local syncStatus = syncStatusAndVolumeInfoRow[ 1 ]
				local volumeGuidUsedByOriginal = syncStatusAndVolumeInfoRow[ 2 ]
				local volumeGuidUsedByProxy = syncStatusAndVolumeInfoRow[ 3 ]
				local spaceId = syncStatusAndVolumeInfoRow[ 4 ]

				local isOriginalOnKnownVolume = volumeGuidUsedByOriginal and knownVolumes[ volumeGuidUsedByOriginal ]
				local isProxyOnKnownVolume = volumeGuidUsedByProxy and knownVolumes[ volumeGuidUsedByProxy ]

				local isOriginalOnOnlineVolume = volumeGuidUsedByOriginal and onlineVolumes[ volumeGuidUsedByOriginal ]
				local isProxyOnOnlineVolume = volumeGuidUsedByProxy and onlineVolumes[ volumeGuidUsedByProxy ]

				local isVolumeForOriginalsOnline = onlineVolumes[ volumeGuidForOriginals ]
				local isVolumeForProxiesOnline = onlineVolumes[ volumeGuidForProxies ]

				local isVolumeForOriginalsOutOfSpace = outOfSpaceVolumes[ volumeGuidForOriginals ]
				local isVolumeForProxiesOutOfSpace = outOfSpaceVolumes[ volumeGuidForProxies ]

				local isBlockedSpace = spaceId and blockedSpaceIds[ spaceId ]

				-- Calculate the sync status.

				local currentDocSyncStatus = WFCatSyncStatus.calculateSyncStatusWithVolumeStatus(
						syncStatus, volumeGuidUsedByOriginal, volumeGuidUsedByProxy,
						isOriginalOnKnownVolume, isProxyOnKnownVolume,
						isVolumeForOriginalsOnline, isVolumeForProxiesOnline,
						isVolumeForOriginalsOutOfSpace, isVolumeForProxiesOutOfSpace,
						isOriginalOnOnlineVolume, isProxyOnOnlineVolume, isBlockedSpace )

				if not combinedSyncStatus then
					combinedSyncStatus = currentDocSyncStatus
				elseif combinedSyncStatus ~= currentDocSyncStatus then
					combinedSyncStatus = nil
					break
				end

			end

			return combinedSyncStatus

		end )

		:flatMap( function( syncStatus )

			return Rx.Observable.combineLatest(
				Rx.Observable.just( syncStatus ),
				syncPausedStream,
				internetConnectionStream )

		end)

		:map( function( syncStatus, isSyncPaused, isOnline )

			return WFCatSyncStatus.calculateSyncStatusWithGlobalStatus( syncStatus, isSyncPaused, not isOnline )

		end )

		:map( WFCatSyncStatus.translateAssetSyncStatus )

end

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

function WFCatSyncStatus.getCombinedOzStatusFromIdsAsStr( wfCatalog, assetIds )

	return wfCatalog:indexStore():query {
			sql = [[SELECT ozHasOriginal, ozHasProxy FROM localStatusAndOzStatus JOIN CARRAY( ?, ? , 'int32' ) AS ai ON localStatusAndOzStatus.docId = ai.value ]],
			calm = 2,
			assetIds,
			#assetIds,
		}:map( function( ozHasRows )

			local combinedOzStatusStr

			local previousOzHasOriginal
			local previousOzHasProxy

			-- returns kOzHasOriginal if all assets have original on Oz, kOzHasProxy if all assets have only a proxy on Oz
			-- kOzHasNothing if all assets have nothing on Oz and nil if the assets are mixed
			for _, ozHas in ipairs( ozHasRows ) do

				local ozHasOriginal = ozHas[ 1 ] == 1
				local ozHasProxy = ozHas[ 2 ] == 1

				if not combinedOzStatusStr then

					if ozHasOriginal then

						combinedOzStatusStr = kOzHasOriginal

					elseif ozHasProxy then

						combinedOzStatusStr = kOzHasProxy

					else

						combinedOzStatusStr = kOzHasNothing

					end

				elseif previousOzHasOriginal ~= ozHasOriginal
					or previousOzHasProxy ~= ozHasProxy then

					combinedOzStatusStr = nil
					break

				end

				previousOzHasProxy = ozHasProxy

				previousOzHasOriginal = ozHasOriginal

			end

			return combinedOzStatusStr

		end )

end

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

function WFCatSyncStatus.getCombinedLocalFileTypeAsStr( wfCatalog, syncStatusAndVolumeInfos )
	-- syncStatusAndVolumeInfos is an Observable that produces an array of rows containing
	-- 1: sync status constant mapping to getAssetStatus
	-- 2: originalVolumeGuid
	-- 3: proxyVolumeGuid
	-- 4: markedAsOffline
	local mappedVolumeStatuses = WFCatSyncStatus.getVolumeInfoForSyncStatus( wfCatalog )

	return Rx.Observable.combineLatest(
		syncStatusAndVolumeInfos,
		mappedVolumeStatuses )

		:map( function( syncStatusAndVolumeInfoRows, volumeGuidForOriginals,
				volumeGuidForProxies, onlineVolumes )

			local combinedLocalFileType

			for _, syncStatusAndVolumeInfoRow in ipairs( syncStatusAndVolumeInfoRows ) do

				local volumeGuidUsedByOriginal = syncStatusAndVolumeInfoRow[ 2 ]
				local volumeGuidUsedByProxy = syncStatusAndVolumeInfoRow[ 3 ]
				local isMarkedForOffline = syncStatusAndVolumeInfoRow[ 4 ] == 1

				local isOriginalOnOfflineVolume = volumeGuidUsedByOriginal and not onlineVolumes[ volumeGuidUsedByOriginal ]
				local isProxyOnOfflineVolume = volumeGuidUsedByProxy and not onlineVolumes[ volumeGuidUsedByProxy ]

				local docLocalFileType = localFileType(
					not not volumeGuidUsedByOriginal,
					isOriginalOnOfflineVolume,
					nil, -- need to fix this has InvalidOriginalError shouldn't be empty
					isMarkedForOffline,
					not not volumeGuidUsedByProxy,
					isProxyOnOfflineVolume )

				if not combinedLocalFileType then

					combinedLocalFileType = docLocalFileType

				elseif combinedLocalFileType ~= docLocalFileType then

					combinedLocalFileType = nil
					break

				end

			end

			return combinedLocalFileType

		end )

		:map( WFCatSyncStatus.translateAssetSyncLocalFileType )

end

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

function WFCatSyncStatus.getCombinedMarkedForOfflineUse( wfCatalog, syncStatusAndVolumeInfos )
	-- syncStatusAndVolumeInfos is an Observable that produces an array of rows containing
	-- 1: sync status constant mapping to getAssetStatus
	-- 2: originalVolumeGuid
	-- 3: proxyVolumeGuid
	-- 4: markedAsOffline
	return syncStatusAndVolumeInfos
		:map( function( isMarkedForOfflineRows )

			-- returns true if all assets are marked for offline, false if no assets are marked for offline
			-- and nil if the assets are mixed
			local combinedMarkedForOfflineUse

			for _, row in ipairs( isMarkedForOfflineRows ) do

				local markedForOfflineUse = row[ 4 ] == 1

				if combinedMarkedForOfflineUse then

					combinedMarkedForOfflineUse = markedForOfflineUse

				elseif combinedMarkedForOfflineUse ~= markedForOfflineUse then

					combinedMarkedForOfflineUse = nil
					break

				end

			end

			return combinedMarkedForOfflineUse

		end )

end

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

function WFCatSyncStatus.getKeepLocallyConfiguration()

	local keepAllOriginals = NmbFileManagementSettings.keepAllOriginals()

	local keepAllProxies = NmbFileManagementSettings.keepAllProxies()

	local configuration

	if keepAllOriginals and keepAllProxies then
		configuration = kKeepEverything
	elseif keepAllOriginals then
		configuration = kKeepOriginals
	elseif keepAllProxies then
		configuration = kKeepProxies
	else
		configuration = kKeepNothing
	end

	return configuration

end

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

function WFCatSyncStatus.getAlbumSyncStatus( docValue )

	local syncStatus = kAlbumSyncStatus[ kAlbumComplete ]

	local isMarkedForOffline = WFCatFileSpec.getMarkedForOfflineUseFromDocValue( docValue )

	if isLatestMetadataUploaded( docValue ) then

		if isMarkedForOffline then

			syncStatus = kAlbumSyncStatus[ kAlbumCompleteForOfflineUse ]

		else

			syncStatus = kAlbumSyncStatus[ kAlbumComplete ]

		end

	else

		if isMarkedForOffline then

			syncStatus = kAlbumSyncStatus[ kAlbumSyncingForOfflineUse ]

		else

			syncStatus = kAlbumSyncStatus[ kAlbumSyncing ]

		end

	end

	return syncStatus

end

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

function WFCatSyncStatus.getSyncStatusForAlbums( wfCatalog, optIgnoreAlbumSyncStatus )

	local albumStatusesStream

	local keepAllOriginals = NmbFileManagementSettings.keepAllOriginals()

	if optIgnoreAlbumSyncStatus then

		-- Don't consider the sync status of the actual album document when determining
		-- the status to show for the album. Essentially this means that we only consider
		-- the sync status of the underlying asset documents since this also ignores the
		-- album_asset documents.
		albumStatusesStream = wfCatalog:indexStore():query {
			sql = [[SELECT albumId, isMarkedForOffline FROM albums ]],
			calm = 0.25
		}
		:map( function( albums )

			local statusForAlbum = {}

			for _, album in ipairs( albums ) do

				local albumId = album[ 1 ]
				local isMarkedForOfflineUse = album[ 2 ] and album[ 2 ] > 0

				statusForAlbum[ albumId ] = isMarkedForOfflineUse and kAlbumCompleteForOfflineUse or kAlbumComplete

			end

			return statusForAlbum

		end )

	else

		albumStatusesStream = basicAlbumStatusStream( wfCatalog )

	end

	local _, combinedStatusInteger = getSyncingStatusIds( kAssetSyncStatus, true )

	local keepLocallyConfiguration = WFCatSyncStatus.getKeepLocallyConfiguration()

	local query = [[SELECT DISTINCT albumId
					FROM album_asset_v2
					WHERE assetId IN
						(
							SELECT assetId
								FROM asset_sync_status_ui
								WHERE keepLocallyConfiguration = ?
									AND syncStatuses & ?
						) ]]

	local syncingAlbumsFromAssets = wfCatalog:indexStore():query {
			sql = query,
			calm = 3,
			keepLocallyConfiguration,
			combinedStatusInteger
		}

		:map( function( syncingAlbums )

			local statusForAlbum = {}

			for _, syncingAlbum in ipairs( syncingAlbums ) do
				statusForAlbum[ syncingAlbum[ 1 ] ] = kAlbumSyncing
			end

			return statusForAlbum

		end )

	local syncingAlbumsMarkedForOfflineUseFromAssets = Rx.Observable.just( {} )

	if not keepAllOriginals then

		combinedStatusInteger = WFCatSyncStatus.createCombinedStatusesInteger(
				kAssetSyncStatus[ kAssetNeedsOriginalDownloadForOffline ],
				kAssetSyncStatus[ kAssetNeedsProxyDownloadForOffline ] )

		syncingAlbumsMarkedForOfflineUseFromAssets = wfCatalog:indexStore():query {
				sql = [[ SELECT DISTINCT albumId
							FROM album_asset_v2
							INNER JOIN albums USING( albumId )
							WHERE albums.isMarkedForOffline = 1
							AND assetId IN
							(
								SELECT assetId
									FROM asset_sync_status_ui
									WHERE keepLocallyConfiguration == ?
										AND syncStatuses & ?
								UNION
								SELECT assetId
									FROM assets
									WHERE isMarkedForOffline = 0
							) ]],
				calm = 3,
				keepLocallyConfiguration,
				combinedStatusInteger
			}

			:map( function( syncingAlbums )

				local statusForAlbum = {}

				for _, syncingAlbum in ipairs( syncingAlbums ) do
					statusForAlbum[ syncingAlbum[ 1 ] ] = kAlbumSyncingForOfflineUse
				end

				return statusForAlbum

			end )

	end

	local sharedWithYouAlbumIdsStream = wfCatalog:indexStore():query {
				sql = [[ SELECT DISTINCT albumId
							FROM albums
							WHERE spaceId IS NOT NULL]],
				calm = 3,
			}

			:map( function( rows )

				local sharedWithYouAlbums = {}

				for _, row in ipairs( rows ) do
					sharedWithYouAlbums[ row[ 1 ] ] = true
				end

				return sharedWithYouAlbums

			end )

	local volumeAgents = wfCatalog:getAllVolumeAgents()
	local volumeAgent = volumeAgents[ next( volumeAgents ) ]
	local syncPausedStream = volumeAgent and volumeAgent:isSyncPausedStream() or Rx.Observable.just( false )
	local internetConnectionStream = volumeAgent and volumeAgent:hasInternetConnectionStream() or Rx.Observable.just( true )
	local mappedVolumeStatuses = WFCatSyncStatus.getVolumeInfoForSyncStatus( wfCatalog )

	return Rx.Observable.combineLatest( albumStatusesStream, syncingAlbumsFromAssets,
		syncingAlbumsMarkedForOfflineUseFromAssets, syncPausedStream,
		internetConnectionStream, sharedWithYouAlbumIdsStream, mappedVolumeStatuses )

		:calm( 0.5, WFRxScheduler.sharedScheduler() )

		:map( function( albumStatusMap, albumStatusFromAssetsMap, albumMarkedForOfflineStatusFromAssetsMap,
			isSyncPaused, isSyncOnline, sharedWithYouAlbumIds, volumeGuidForOriginals, _, onlineVolumes,
			outOfSpaceVolumes )

			local combinedAlbumStatuses = {}

			local isVolumeForOriginalsOnline = onlineVolumes[ volumeGuidForOriginals ]
			local isVolumeForOriginalsOutOfSpace = outOfSpaceVolumes[ volumeGuidForOriginals ]

			for albumId, albumStatus in pairs( albumStatusMap ) do

				local combinedStatus

				local albumStatusFromAssets = albumMarkedForOfflineStatusFromAssetsMap[ albumId ]
						or albumStatusFromAssetsMap[ albumId ]

				if not sharedWithYouAlbumIds[ albumId ]
					and ( albumStatus == kAlbumSyncingForOfflineUse
					or albumStatusFromAssets == kAlbumSyncingForOfflineUse
					or ( albumStatus == kAlbumCompleteForOfflineUse and albumStatusFromAssets == kAlbumSyncing )
					or ( keepAllOriginals and ( albumStatus == kAlbumSyncing or albumStatusFromAssets == kAlbumSyncing ) ) ) then

					if isSyncPaused then

						combinedStatus = kAlbumSyncPaused

					elseif not isSyncOnline then

						combinedStatus = kAlbumSyncNoConnection

					elseif not isVolumeForOriginalsOnline then

						combinedStatus = kAlbumSyncingForOfflineUseToOfflineVolume

					elseif isVolumeForOriginalsOutOfSpace then

						combinedStatus = kAlbumSyncingForOfflineUseToOutOfSpaceVolume

					else

						combinedStatus = kAlbumSyncingForOfflineUse

					end

				elseif albumStatus == kAlbumSyncing or albumStatusFromAssets == kAlbumSyncing then

					if isSyncPaused then

						combinedStatus = kAlbumSyncPaused

					elseif not isSyncOnline then

						combinedStatus = kAlbumSyncNoConnection

					else

						combinedStatus = kAlbumSyncing

					end

				elseif not sharedWithYouAlbumIds[ albumId ]
					and ( ( albumStatus == kAlbumCompleteForOfflineUse and albumStatusFromAssets == kAlbumComplete )
					or ( keepAllOriginals and ( albumStatus == kAlbumComplete or albumStatusFromAssets == kAlbumComplete ) ) ) then

					combinedStatus = kAlbumCompleteForOfflineUse

				elseif albumStatus == kAlbumComplete or albumStatusFromAssets == kAlbumComplete then

					combinedStatus = kAlbumComplete

				else

					combinedStatus = albumStatus

				end

				combinedAlbumStatuses[ albumId ] = combinedStatus

			end

			return combinedAlbumStatuses

		end )

end

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

function WFCatSyncStatus.getSyncStatusOfParentCollectionOfAlbums( wfCatalog, albumIds )

	return wfCatalog:indexStore():query {
			sql = [[SELECT albumsParents.albumId, albumsParents.syncStatus
					FROM albums as albumsParents
					INNER JOIN albums ON albums.parentId == albumsParents.albumId
					WHERE albums.docId IN CARRAY(?,?,'int32')
				]],

			calm = 2,
			albumIds,
			#albumIds,
		}

		:map( function( albumStatuses )

			local syncedAlbumsById = {}

			for _, albumStatus in ipairs( albumStatuses ) do

				local albumId = albumStatus[ 1 ]
				local statusId = albumStatus[ 2 ]

				syncedAlbumsById[ albumId ] = isAlbumCompleted( statusId )

			end

			return syncedAlbumsById

		end )

end

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

function WFCatSyncStatus.getSyncStatusOfAlbumsOfAlbumAssets( wfCatalog, albumAssetIds )

	return wfCatalog:indexStore():query {
			sql = [[SELECT albums.albumId, albums.syncStatus
					FROM albums
					INNER JOIN album_asset_v2 ON album_asset_v2.albumId == albums.albumId
					WHERE album_asset_v2.docId IN CARRAY(?, ?, 'int32') ]],

			calm = 2,
			albumAssetIds,
			#albumAssetIds
		}

		:map( function( albumStatuses )

			local syncedAlbumsById = {}

			for _, albumStatus in ipairs( albumStatuses ) do

				local albumId = albumStatus[ 1 ]
				local statusId = albumStatus[ 2 ]

				syncedAlbumsById[ albumId ] = isAlbumCompleted( statusId )

			end

			return syncedAlbumsById

		end )

end

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

-- [jandrew 2022-01-04]: WF-1644:

-- An ongoing task within `WFCatErrorHandler` constantly monitors asset-dependent
-- documents (Versions, Comments, Favorites, etc) that are blocked from upload due
-- to issues syncing the referenced Asset document. This task will clear the errors
-- from the asset-dependent documents if it determines that the referenced Asset
-- document has been successfully synced to Oz. It determines this by calling the
-- WFCatSyncStatus.getSyncStatusOfAssetsOf* family of functions. Unfortunately, the
-- old implementations of these functions determined the sync-status of the
-- referenced Asset document using WFCatSyncStatus.isAssetComplete which incorrectly
-- returns true for assets that are in error states. The new implementation relies
-- on kFullySyncedStates, which contains a reduced set of states and only considers
-- the true "complete" states.

local kFullySyncedStates = {
	kAssetComplete,
	kAssetCompleteNoMaster,
	kAssetCompleteNoNegatives,
	kAssetCompleteForOffline,
	kAssetStackComplete
}

local kFullySyncedStatesSet = KSTableUtils.mapArrayToSet( kFullySyncedStates )

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

local function reduceSyncStatuses( assetStatuses )

	local isDocumentSyncedByAssetId = {}

	for _, assetStatus in ipairs( assetStatuses ) do

		local assetId = assetStatus[ 1 ]
		local statusId = assetStatus[ 2 ]

		if type( statusId ) ~= 'string' then
			statusId = kAssetSyncStatusById[ statusId ]
		end

		isDocumentSyncedByAssetId[ assetId ] = not not kFullySyncedStatesSet[ statusId ]

	end

	return isDocumentSyncedByAssetId

end

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

--[[!

	\brief Given a list of album-asset IDs, this function returns a map of the asset
		IDs referenced by the album-asset documents, along with a boolean indicating
		the sync status of the associated assets.

	\param wfCatalog (WFCatalog)
	\param albumAssetIds (table) A table of album-asset IDs.

	\return (void) Returns a map whose keys are asset IDs, and whose values are
		booleans representing the whether or not the asset is considered fully synced.

--]]

function WFCatSyncStatus.getSyncStatusOfAssetsOfAlbumAssets( wfCatalog, albumAssetIds )

	local keepLocallyConfiguration = WFCatSyncStatus.getKeepLocallyConfiguration()

	return wfCatalog:indexStore():query {
			sql = [[ SELECT asset_sync_status_ui.assetId, asset_sync_status_ui.prioritizedSyncStatus
						FROM asset_sync_status_ui
						INNER JOIN album_asset_v2 ON album_asset_v2.assetId == asset_sync_status_ui.assetId
						INNER JOIN CARRAY( ?, ?, 'int32' ) AS aai ON aai.value = album_asset_v2.docId
						WHERE keepLocallyConfiguration == ? ]],
			calm = 2,
			albumAssetIds,
			#albumAssetIds,
			keepLocallyConfiguration,
		}

		:map( reduceSyncStatuses )

end

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

--[[!

	\brief Given a list of version IDs, this function returns a map of the asset
		IDs referenced by the version documents, along with a boolean indicating
		the sync status of the associated assets.

	\param wfCatalog (WFCatalog)
	\param versionIds (table) A table of version IDs.

	\return (void) Returns a map whose keys are asset IDs, and whose values are
		booleans representing the whether or not the asset is considered fully synced.

--]]

function WFCatSyncStatus.getSyncStatusOfAssetsOfVersions( wfCatalog, versionIds )

	local keepLocallyConfiguration = WFCatSyncStatus.getKeepLocallyConfiguration()

	return wfCatalog:indexStore():query {
			sql = [[ SELECT asset_sync_status_ui.assetId, asset_sync_status_ui.prioritizedSyncStatus
						FROM asset_sync_status_ui
						INNER JOIN versions ON versions.assetId == asset_sync_status_ui.assetId
						INNER JOIN CARRAY( ?, ?, 'int32' ) AS vi ON vi.value = versions.localId
						WHERE keepLocallyConfiguration == ? ]],
			calm = 2,
			versionIds,
			#versionIds,
			keepLocallyConfiguration
		}

		:map( reduceSyncStatuses )

end

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

--[[!

	\brief Given a list of favorite IDs, this function returns a map of the asset
		IDs referenced by the favorite documents, along with a boolean indicating
		the sync status of the associated assets.

	\param wfCatalog (WFCatalog)
	\param favoriteIds (table) A table of favorite IDs.

	\return (void) Returns a map whose keys are asset IDs, and whose values are
		booleans representing the whether or not the asset is considered fully synced.

--]]

function WFCatSyncStatus.getSyncStatusOfAssetsOfFavorites( wfCatalog, favoriteIds )

	local keepLocallyConfiguration = WFCatSyncStatus.getKeepLocallyConfiguration()

	return wfCatalog:indexStore():query {
			sql = [[ SELECT asset_sync_status_ui.assetId, asset_sync_status_ui.prioritizedSyncStatus
						FROM asset_sync_status_ui
						INNER JOIN spacefavorite ON spacefavorite.assetId == asset_sync_status_ui.assetId
						INNER JOIN CARRAY( ?, ?, 'char*' ) AS fi ON fi.value = spacefavorite.docId
						WHERE keepLocallyConfiguration == ? ]],
			calm = 2,
			favoriteIds,
			#favoriteIds,
			keepLocallyConfiguration
		}

		:map( reduceSyncStatuses )

end

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

--[[!

	\brief Given a list of comment IDs, this function returns a map of the asset
		IDs referenced by the comment documents, along with a boolean indicating
		the sync status of the associated assets.

	\param wfCatalog (WFCatalog)
	\param commentIds (table) A table of comment IDs.

	\return (void) Returns a map whose keys are asset IDs, and whose values are
		booleans representing the whether or not the asset is considered fully synced.

--]]

function WFCatSyncStatus.getSyncStatusOfAssetsOfComments( wfCatalog, commentIds )

	local keepLocallyConfiguration = WFCatSyncStatus.getKeepLocallyConfiguration()

	return wfCatalog:indexStore():query {
			sql = [[ SELECT asset_sync_status_ui.assetId, asset_sync_status_ui.prioritizedSyncStatus
						FROM asset_sync_status_ui
						INNER JOIN comment ON comment.assetId == asset_sync_status_ui.assetId
						INNER JOIN CARRAY( ?, ?, 'char*' ) AS ci ON ci.value = comment.docId
						WHERE keepLocallyConfiguration == ? ]],
			calm = 2,
			commentIds,
			#commentIds,
			keepLocallyConfiguration
		}

		:map( reduceSyncStatuses )

end

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

--[[!
	\brief This function is used by LrD to determine global sync status, as indicated
		by the cloudy icon.
	
	\param catalog (WFCatalog)

	\return <code>Observable</code> An observable that fires with a boolean indicating
		whether or not sync is pending for any asset. Note that assets associated with
		inaccessible spaces are excluded from this query.
--]]

function WFCatSyncStatus.syncPendingForAnyAsset( wfCatalog )

	local syncingStatusIds = getSyncingStatusIds( kAssetSyncStatus )

	local keepLocallyConfiguration = WFCatSyncStatus.getKeepLocallyConfiguration()

	local isSyncPendingSQL = string.format(
			[[ SELECT prioritizedSyncStatus
				FROM asset_sync_status_ui
				LEFT JOIN space ON space.docId = asset_sync_status_ui.spaceId
				WHERE ( space.docId IS NULL
							OR ( space.entitlementExpired = 0 AND space.isUnderMaintenance = 0 ))
					AND keepLocallyConfiguration = ?
					AND prioritizedSyncStatus IN ( %s )
				LIMIT 1 ]],
				WFSQLiteUtils.createVariableArgList( #syncingStatusIds ) )

	return wfCatalog:indexStore():query {
			sql = isSyncPendingSQL,
			singleColumnQuery = true,
			keepLocallyConfiguration,
			unpack( syncingStatusIds )
		}
		:map( function( results )
			return not not results[ 1 ]
		end )
		:distinctUntilChanged()

end

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

function WFCatSyncStatus.getLocalIdsWithSyncPendingOrError( wfCatalog )

	local syncingStatusIds = getSyncingStatusIds( kAssetSyncStatus )

	local errorStatusIds = getErrorStatusIds( kAssetSyncStatus )

	local keepLocallyConfiguration = WFCatSyncStatus.getKeepLocallyConfiguration()

	local syncingOrErrorStatusIds = KSTableUtils.appendArray( syncingStatusIds, errorStatusIds )

	local unpack = table.unpack or unpack
	local combinedStatusInteger = WFCatSyncStatus.createCombinedStatusesInteger(
			unpack( syncingOrErrorStatusIds ) )

	local assetsWithSyncPendingOrErrorSQL =
			[[SELECT docId
				FROM asset_sync_status_ui
				WHERE keepLocallyConfiguration == ?
					AND syncStatuses & ? ]]

	return wfCatalog:indexStore():query {
			sql = assetsWithSyncPendingOrErrorSQL,
			singleColumnQuery = true,
			keepLocallyConfiguration,
			combinedStatusInteger
		}

end

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

--[[!
	\brief Get a table containing all binary download error status IDs.
--]]

local function getBinaryDownloadErrorStatusIds()

	local allSyncStatuses = kAssetSyncStatus

	local binaryDownloadErrorSyncStates = {
		kAssetOriginalDownloadError,
		kAssetProxyDownloadError,
		kAssetDevelopXmpDownloadError,
	}

	local binaryDownloadErrorStatusIds = {}

	for _, errorState in ipairs( binaryDownloadErrorSyncStates ) do

		if allSyncStatuses[ errorState ] then

			binaryDownloadErrorStatusIds[ #binaryDownloadErrorStatusIds + 1 ] =
					allSyncStatuses[ errorState ]

		end

	end

	return binaryDownloadErrorStatusIds

end

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

--[[!
	\brief Given a Space ID, get the local IDs of all assets in that space that
		are marked binary download errors.
	
	\param catalog (WFCatalog)
	\param spaceId (string) The Space ID.

	\return <code>Observable</code> An observable that fires once with a table
		of local asset IDs.
--]]

function WFCatSyncStatus.getLocalIdsWithBinaryDownloadErrorsForSpace( wfCatalog, spaceId )

	local binaryDownloadErrorStatusIds = getBinaryDownloadErrorStatusIds()

	local keepLocallyConfiguration = WFCatSyncStatus.getKeepLocallyConfiguration()

	local unpack = table.unpack or unpack

	local combinedStatusInteger = WFCatSyncStatus.createCombinedStatusesInteger(
			unpack( binaryDownloadErrorStatusIds ) )

	local assetsWithSyncPendingOrErrorSQL =
			[[SELECT docId
				FROM asset_sync_status_ui
			  	WHERE spaceId == ?
					AND keepLocallyConfiguration == ?
					AND syncStatuses & ? ]]

	return wfCatalog:indexStore():queryOnce {
			sql = assetsWithSyncPendingOrErrorSQL,
			singleColumnQuery = true,
			spaceId,
			keepLocallyConfiguration,
			combinedStatusInteger
		}

end

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

-- Allows for identification and count of assets pending restoration, which is
-- needed for 'pancake' messaging purposes when a restore happens while the user
-- is out of Oz storage or has an expired subscription.

function WFCatSyncStatus.getNumAssetsWithRestorationPending( wfCatalog )

	local assetsWithRestorationPendingSQL =
		[[SELECT COUNT(1), MIN(purgeDate) FROM assetsPendingRestore]]

	return wfCatalog:indexStore():query {
			sql = assetsWithRestorationPendingSQL,
			calm = 2,
		}
		:map( function( rows )
			local row = rows[ 1 ]
			return { numAssets = row[ 1 ], firstPurgeDate = row[ 2 ] }
		end )
end

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

function WFCatSyncStatus.getDocIdsAndStatusWithSyncPendingOrError( wfCatalog )

	local syncingStatusIds = getSyncingStatusIds( kAssetSyncStatus )

	local errorStatusIds = getErrorStatusIds( kAssetSyncStatus )

	local keepLocallyConfiguration = WFCatSyncStatus.getKeepLocallyConfiguration()

	local syncingOrErrorStatusIds = KSTableUtils.appendArray( syncingStatusIds, errorStatusIds )

	-- This query gets all assets with syncing or error statuses, excluding
	-- any asset that is within in inaccessible space.

	local assetsWithSyncPendingOrErrorSQL = string.format(
		[[ SELECT assetId, prioritizedSyncStatus
			FROM asset_sync_status_ui
			LEFT JOIN space ON space.docId = asset_sync_status_ui.spaceId
				WHERE ( space.docId IS NULL
							OR ( space.entitlementExpired = 0 AND space.isUnderMaintenance = 0 ))
				AND keepLocallyConfiguration == ?
				AND prioritizedSyncStatus IN ( %s ) ]],
		WFSQLiteUtils.createVariableArgList( #syncingOrErrorStatusIds ) )

	return wfCatalog:indexStore():query {
			sql = assetsWithSyncPendingOrErrorSQL,
			keepLocallyConfiguration,
			unpack( syncingOrErrorStatusIds )
		}
		:map( function( rows )
			local results = {}
			for _, row in ipairs( rows ) do
				table.insert( results, { assetId = row[ 1 ], status = row[ 2 ], statusString = kAssetSyncStatusById[ row[ 2 ] ] } )
			end
			return results
		end )
end

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

function WFCatSyncStatus.persistentSyncErrorForAnyAsset( wfCatalog )

	local errorStatusIds = getErrorStatusIds( kAssetSyncStatus )

	-- Because search eliminates assets in spaces and does not include assets
	-- that are in the trash use assetGridStatus for this search so that the
	-- cloudy icon won't show an error state when errors exist in those
	-- areas.  The respect album / trash grid views will show the badges with
	-- error there so leave the existing status as is.

	-- Note that isSearchable is only used when searching for assets in error states, so
	-- it's safe to use the data for any configuration. (the revelant errors should be identical
	-- across kKeepNothing, kKeepEverything, etc).

	return wfCatalog:indexStore():query {
			sql = string.format(
				[[SELECT prioritizedSyncStatus
					FROM asset_sync_status_ui
					WHERE isSearchable == 1
						AND keepLocallyConfiguration == ?
						AND prioritizedSyncStatus IN ( %s )
					LIMIT 1]],
				WFSQLiteUtils.createVariableArgList( #errorStatusIds ) ),
			singleColumnQuery = true,
			kKeepNothing,
			unpack( errorStatusIds )
		}
		:map( function( results )
			return KSTableUtils.derefOpt( results, 1 ) ~= nil
		end )
		:distinctUntilChanged()

end

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

function WFCatSyncStatus.getSyncStatusForAssets( wfCatalog )

	local keepLocallyConfiguration = WFCatSyncStatus.getKeepLocallyConfiguration()

	-- For the query, isSearchable != 1 means it is in the trash or a shared with you
	-- album and we don't want to include it in the sync status counts.

	return wfCatalog:indexStore():query {
			sql = [[ SELECT prioritizedSyncStatus, count( prioritizedSyncStatus )
					FROM asset_sync_status_ui
					WHERE isSearchable == 1
						AND keepLocallyConfiguration == ?
					GROUP BY prioritizedSyncStatus ]],
			keepLocallyConfiguration
		}
		:map( function( rows )
			local statusCounts = {}

			for _, row in ipairs( rows ) do
				local statusId = row[ 1 ]
				local statusCount = row[ 2 ]

				if statusId and statusCount then
					local status = kAssetSyncStatusById[ statusId ]
					if status then
						statusCounts[ status ] = tonumber( statusCount ) or 0
					end
				end
			end

			for _, status in pairs( kAssetSyncStatusById ) do
				if not statusCounts[ status ] then
					statusCounts[ status ] = 0
				end
			end

			return statusCounts
		end )

end

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

function WFCatSyncStatus.isDoingInitialSync( wfCatalog )

	return wfCatalog:docStore():ongoingLocalDocValue( 'Oz-isDoingInitialSync' )

		:map( function( docValue )

			local isDoingInitialSync = true

			if docValue.isDoingInitialSync ~= nil then

				isDoingInitialSync = docValue.isDoingInitialSync

			end

			return isDoingInitialSync

		end )

end

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

function WFCatSyncStatus.translateAssetSyncStatus( status )

	return kAssetSyncStatusById[ status ]

end

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

function WFCatSyncStatus.translateAssetSyncLocalFileType( localFileType )

	return kAssetLocalFileTypeById[ localFileType ]

end

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

function WFCatSyncStatus.completeNoErrorSyncStatus()

	return require 'WFCatSearchBySyncStatus'.completeNoErrorSyncStatus()()()

end

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

function WFCatSyncStatus.calculateSyncStatusWithVolumeStatus(
		statusOrId, hasLocalOriginal, hasLocalProxy,
		isOriginalOnKnownVolume, isProxyOnKnownVolume,
		isVolumeForOriginalsOnline, isVolumeForProxiesOnline,
		isVolumeForOriginalsOutOfSpace, isVolumeForProxiesOutOfSpace,
		isOriginalOnOnlineVolume, isProxyOnOnlineVolume,
		isBlockedSpace )

	if type( statusOrId ) ~= 'string' then
		statusOrId = kAssetSyncStatusById[ statusOrId ]
	end

	if statusOrId == kAssetNeedsOriginalDownload
		or statusOrId == kAssetNeedsOriginalDownloadForOffline then

		if not isVolumeForOriginalsOnline then

			-- Original needs to be downloaded to an offline volume.
			statusOrId = kAssetNeedsOriginalDownloadToOfflineVolume

		elseif isVolumeForOriginalsOutOfSpace then

			-- Original needs to be downloaded to an out-of-space volume.
			statusOrId = kAssetNeedsOriginalDownloadToOutOfSpaceVolume

		end

	elseif statusOrId == kAssetNeedsProxyDownload then

		if not isVolumeForProxiesOnline then

			-- Proxy needs to be downloaded to an offline volume.
			statusOrId = kAssetNeedsProxyDownloadToOfflineVolume

		elseif isVolumeForProxiesOutOfSpace then

			-- Proxy needs to be downloaded to an out-of-space volume.
			statusOrId = kAssetNeedsProxyDownloadToOutOfSpaceVolume

		end

	elseif statusOrId == kAssetNeedsOriginalUpload then

		if hasLocalOriginal and not isOriginalOnKnownVolume then

			-- Original needs to be migrated to the original volume.
			statusOrId = kAssetNeedsOriginalMigration

		elseif hasLocalOriginal and not isOriginalOnOnlineVolume then

			-- Original needs to be uploaded, but is on an offline volume.
			statusOrId = kAssetNeedsOriginalUploadFromOfflineVolume

		end

	elseif kNonSyncingStateSet[ statusOrId ] then

		if hasLocalProxy and not isProxyOnKnownVolume then

			-- Proxy needs to be migrated to the proxy volume.
			statusOrId = kAssetNeedsProxyMigration

		elseif hasLocalProxy and not isProxyOnOnlineVolume then

			-- Proxy is downloaded, but on an offline volume.
			statusOrId = kAssetProxyIsOnOfflineVolume

		end

		if hasLocalOriginal and not isOriginalOnKnownVolume then

			-- Original needs to be migrated to the original volume.
			statusOrId = kAssetNeedsOriginalMigration

		elseif hasLocalOriginal and not isOriginalOnOnlineVolume then

			-- Original is downloaded, but on an offline volume.
			statusOrId = kAssetOriginalIsOnOfflineVolume

		end

	end

	if isBlockedSpace then

		-- Asset belongs to a space that is not accessible.
		statusOrId = kAssetSyncPaused

	end

	return kAssetSyncStatus[ statusOrId ]

end

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

function WFCatSyncStatus.calculateSyncStatusWithGlobalStatus(
		statusOrId, isSyncPaused, isOffline )

	if type( statusOrId ) ~= 'string' then
		statusOrId = kAssetSyncStatusById[ statusOrId ]
	end

	if isSyncPaused or isOffline then

		if statusOrId == kAssetNeedsMetadataUpload
			or statusOrId == kAssetNeedsOriginalUpload
			or statusOrId == kAssetNeedsDevelopXmpUpload
			or statusOrId == kAssetNeedsOriginalDownload
			or statusOrId == kAssetNeedsOriginalDownloadForOffline
			or statusOrId == kAssetNeedsProxyDownload
			or statusOrId == kAssetNeedsProxyDownloadForOffline
			or statusOrId == kAssetNeedsDevelopXmpDownload
			or statusOrId == kAssetNeedsOriginalDownloadToOfflineVolume
			or statusOrId == kAssetNeedsProxyDownloadToOfflineVolume
			or statusOrId == kAssetNeedsOriginalUploadFromOfflineVolume
			or statusOrId == kAssetNeedsOriginalDownloadToOutOfSpaceVolume
			or statusOrId == kAssetNeedsProxyDownloadToOutOfSpaceVolume then

			statusOrId = isOffline and kAssetSyncNoConnection or kAssetSyncPaused

		end

	end

	return kAssetSyncStatus[ statusOrId ]

end

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

function WFCatSyncStatus.offlineVolumesRequiringSyncing( wfCatalog )

	local keepLocallyConfiguration = WFCatSyncStatus.getKeepLocallyConfiguration()

	local syncStatusQuery = wfCatalog:indexStore():query {
		sql = [[ SELECT DISTINCT prioritizedSyncStatus, volumeGuidUsedByOriginal,
						volumeGuidUsedByProxy, spaceId
					FROM asset_sync_status_ui
					WHERE keepLocallyConfiguration == ?
						AND prioritizedSyncStatus IN ( ?, ?, ?, ?, ? ) ]],
		keepLocallyConfiguration,
		kAssetSyncStatus[ kAssetNeedsOriginalDownload ],
		kAssetSyncStatus[ kAssetNeedsOriginalDownloadForOffline ],
		kAssetSyncStatus[ kAssetNeedsProxyDownload ],
		kAssetSyncStatus[ kAssetNeedsProxyDownloadForOffline ],
		kAssetSyncStatus[ kAssetNeedsOriginalUpload ],
		calm = 1,
	}

	-- Combine the asset sync status query with the volume info status stream. We
	-- use this information to map the asset sync status to a new status based
	-- on the connection status of the volumes.

	local mappedVolumeStatuses = WFCatSyncStatus.getVolumeInfoForSyncStatus( wfCatalog )
	local blockedSpaceIdsStream = WFCatSyncStatus.getBlockedSpaceIdsMapStream( wfCatalog )

	local volumesRequiringSyncingStream
		= Rx.Observable.combineLatest( syncStatusQuery, blockedSpaceIdsStream, mappedVolumeStatuses )

			:map( function( rows, blockedSpaceIds, volumeGuidForOriginals,
					volumeGuidForProxies, onlineVolumes, outOfSpaceVolumes, knownVolumes )

				local volumesRequiringSync = {}

				local isVolumeForOriginalsOnline = onlineVolumes[ volumeGuidForOriginals ]
				local isVolumeForProxiesOnline = onlineVolumes[ volumeGuidForProxies ]

				local isVolumeForOriginalsOutOfSpace = outOfSpaceVolumes[ volumeGuidForOriginals ]
				local isVolumeForProxiesOutOfSpace = outOfSpaceVolumes[ volumeGuidForProxies ]

				for _, row in ipairs( rows ) do

					local syncStatusId = row[ 1 ]
					local volumeGuidUsedByOriginal = row[ 2 ]
					local volumeGuidUsedByProxy = row[ 3 ]
					local spaceId = row[ 4 ]

					local isOriginalOnKnownVolume = knownVolumes[ volumeGuidUsedByOriginal ]
					local isProxyOnKnownVolume = knownVolumes[ volumeGuidUsedByProxy ]

					local isOriginalOnOnlineVolume = onlineVolumes[ volumeGuidUsedByOriginal ]
					local isProxyOnOnlineVolume = onlineVolumes[ volumeGuidUsedByProxy ]

					local isBlockedSpace = spaceId and blockedSpaceIds[ spaceId ]

					-- Calculate the new sync status.

					syncStatusId = WFCatSyncStatus.calculateSyncStatusWithVolumeStatus(
							syncStatusId, volumeGuidUsedByOriginal, volumeGuidUsedByProxy,
							isOriginalOnKnownVolume, isProxyOnKnownVolume,
							isVolumeForOriginalsOnline, isVolumeForProxiesOnline,
							isVolumeForOriginalsOutOfSpace, isVolumeForProxiesOutOfSpace,
							isOriginalOnOnlineVolume, isProxyOnOnlineVolume,
							isBlockedSpace )

					local syncStatus = kAssetSyncStatusById[ syncStatusId ]

					if syncStatus == kAssetNeedsOriginalUploadFromOfflineVolume then

						volumesRequiringSync[ volumeGuidUsedByOriginal ] = true

					elseif syncStatus == kAssetNeedsOriginalDownloadToOfflineVolume then

						volumesRequiringSync[ volumeGuidForOriginals ] = true

					elseif syncStatus == kAssetNeedsProxyDownloadToOfflineVolume then

						volumesRequiringSync[ volumeGuidForProxies ] = true

					end

				end

				return volumesRequiringSync

			end )

	return volumesRequiringSyncingStream

end

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

function WFCatSyncStatus.getAssetSyncStatusList()

	return kAssetSyncStatus

end

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

function WFCatSyncStatus.whatToDownload( ... )

	return whatToDownload( ... )

end

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

function WFCatSyncStatus.convertStatusIntegerToStatusTable( combinedStatuses )

	local statusStrings = {}

	for k, v in pairs( kAssetSyncStatus ) do

		local result = bit.band( combinedStatuses, v )

		if result > 0 then

			statusStrings[ #statusStrings + 1 ] = k

		end

	end

	table.sort( statusStrings )

	return statusStrings

end

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

function WFCatSyncStatus.createCombinedStatusesInteger( ... )

	local combinedStatusInteger = 0

	for _, statusInteger in ipairs( { ... } ) do

		combinedStatusInteger = combinedStatusInteger + statusInteger
			-- Using + above instead of bitwise or (|) because it's not available
			-- in Lua 5.1

	end

	return combinedStatusInteger

end

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

function WFCatSyncStatus.isAssetCompleted( statusOrId )

	if type( statusOrId ) ~= 'string' then
		statusOrId = kAssetSyncStatusById[ statusOrId ]
	end

	return not not kNonSyncingStateSet[ statusOrId ]

end

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

function WFCatSyncStatus.generateIndexSchema( indexStoreSchema )

	indexStoreSchema:addTable( asset_status_table() )

	indexStoreSchema:addIndex( asset_status_by_assetId_index() )

	indexStoreSchema:addIndex( asset_status_by_prioritizedSyncStatus_index() )

	indexStoreSchema:addIndex( asset_status_by_syncStatuses_index() )

	indexStoreSchema:addIndex( asset_status_by_keepLocallyConfiguration_index() )

	indexStoreSchema:addIndex( asset_status_by_isSearchable_index() )

	indexStoreSchema:addTable( asset_local_file_type_table() )

	indexStoreSchema:addIndex( asset_local_file_type_by_assetId_index() )

	indexStoreSchema:addIndex( asset_local_file_type_by_localFileType_index() )

	indexStoreSchema:addTable( unconfirmedDeletions_table() )

end

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

function WFCatSyncStatus.generateIndexSchema_mobile( indexStoreSchema )

	check( 'WFRxIndexStoreSchema' )

	indexStoreSchema:addTable( unconfirmedDeletions_table() )

end

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

-- @mark Debug & Test

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

function WFCatSyncStatus._DEBUG_getAssetStatus( ... )

	return getAssetStatus( ... )

end

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

function WFCatSyncStatus._DEBUG_getLocalFileTypeForAsset( ... )

	return getLocalFileTypeForAsset( ... )

end

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

function WFCatSyncStatus._DEBUG_getNonSyncingAssetSyncStatusList()

	return kNonSyncingStates

end

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

return WFCatSyncStatus
