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

PixelProviderSharedBlobCache.lua

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

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

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

	-- wf.core
local KSData = require 'KSData'
local WFArguments = require 'WFArguments'
local WFArguments_check = assert( WFArguments.check,
										"missing WFArguments.check" )
local WFFiniteStateMachine = require 'WFFiniteStateMachine'
local WFFunctionUtils = require 'WFFunctionUtils'
local KSTableUtils = require 'KSTableUtils'

	-- Rx
local Rx = require "Rx"

	-- WF.oz_client.managed_catalog.pixel_provider
local PixelPathProviderStateMachine = require 'PixelPathProviderStateMachine'
local PixelProviderSharedTaskUtils = require 'PixelProviderSharedTaskUtils'
local WFCatRenderSize = require 'WFCatRenderSize'

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

local logger = require 'WFLogger'( 'PixelProviderSharedBlobCache' )

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

local WFFunctionUtils_reduce = assert( WFFunctionUtils.reduce,
			"WFFunctionUtils.reduce does not exist" )

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

local WFFunctionUtils_filter = assert( WFFunctionUtils.filter,
			"WFFunctionUtils.filter does not exist" )

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

local function isNot( f )

	return function( ... )

		return not f( ... )

	end

end

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

local function appendT( ... )

	local appendedT = {}

	local ts = {}

	for i = 1, select( '#', ... ) do

		ts[ i ] = select( i, ... )

		assert( type( ts[ i ] ) == "table",
					string.format( "arg %s is not a table!", i ) )

	end

	local j = 0

	for _, t in ipairs( ts ) do

		for _, v in ipairs( t ) do
		
			j = j + 1

			appendedT[ j ] = v

		end

	end

	return appendedT

end

--------------------------------------------------------------------------------
-- Creates a pixStreamId-like id given the binary type and size
--------------------------------------------------------------------------------

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

local PixelProviderSharedTaskUtils_ozRenditionNameToRenderSize =
		assert( PixelProviderSharedTaskUtils.ozRenditionNameToRenderSize,
				"PixelProviderSharedTaskUtils.ozRenditionNameToRenderSize is missing" )

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

local function makeBinaryBucketId( ozBinaryType, ozBinarySizeName, maybeDigest )

	if ozBinaryType == 'original' then

		return 'original_negative'

	end

	if ozBinaryType == 'proxy' then

		return 'proxy_negative'

	end
	
	if ozBinaryType == 'xmp_develop' then
		
		return 'xmp_develop'
	
	end

	if ozBinaryType == 'xmp_sidecar' then

		return 'xmp_sidecar'

	end

	local blobSize = ozBinarySizeName

	if ozBinaryType == "rendition" then

		blobSize = 
			PixelProviderSharedTaskUtils_ozRenditionNameToRenderSize( ozBinarySizeName )

	end

	return string.format( "%s:%s:%s", ozBinaryType, blobSize, 
							maybeDigest )

end

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

local WFCatRenderSize_getBoundedRenderSize =
		assert( WFCatRenderSize.getBoundedRenderSize,
				"WFCatRenderSize.getBoundedRenderSize not defined" )

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

local function maybeGetNormalizedRenderSize( 
				baseRenderInfo, requestedRenderSize ) 

	local croppedLongSide = tonumber( baseRenderInfo.croppedLongSide )

	if not croppedLongSide then

		return

	end

	return WFCatRenderSize_getBoundedRenderSize( 
			croppedLongSide, requestedRenderSize )

end

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

local DownloadStates = PixelPathProviderStateMachine.DownloadStates

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

local function getDownloadableRenditionSizeNameMatchingPredicate(
					availableOzRenditionInfos, predicate, ... )

	local haveMatchedPixelSize

	availableOzRenditionInfos =
		type( availableOzRenditionInfos ) == "table" and 
			availableOzRenditionInfos or {}
	
	for _, currRenditionInfo in ipairs( availableOzRenditionInfos ) do
		
		if type( currRenditionInfo ) == "table" then

			if predicate( haveMatchedPixelSize, currRenditionInfo.size, ... ) then

				haveMatchedPixelSize = currRenditionInfo.size

			end

		end

	end

	return haveMatchedPixelSize

end

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

local function matchesRenderSize( haveMatchedPixelSize, currRenditionSizeName, 
									wantedRenderSize )

	if haveMatchedPixelSize then

		return false

	end

	local currRenderSize = 
			PixelProviderSharedTaskUtils_ozRenditionNameToRenderSize( 
				currRenditionSizeName )
			
	if currRenderSize == wantedRenderSize then

		return true

	end

	return false

end

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

local function isLargestStandardSizeRendition( currMaxRenditionSizeName, 
										currRenditionSizeName )

	if not currMaxRenditionSizeName and
			currRenditionSizeName == "fullsize" then

		return false

	end

	local currSizeInPixels =
			tonumber( PixelProviderSharedTaskUtils_ozRenditionNameToRenderSize( 
						currRenditionSizeName ) ) or 0

	local currMaxSizeInPixels =
			tonumber( PixelProviderSharedTaskUtils_ozRenditionNameToRenderSize( 
						currMaxRenditionSizeName ) ) or 0

	return currSizeInPixels > currMaxSizeInPixels

end

--------------------------------------------------------------------------------
-- isStandardSizeRenditionBestFitForCroppedLongSide 
-- 
-- this function returns true if currRenditionSizeName is the best, otherwise
-- returns false

local function isStandardSizeRenditionBestFitForCroppedLongSide(
				bestRenditionSizeName, currRenditionSizeName,
					croppedLongSide )

	if not bestRenditionSizeName and currRenditionSizeName ~= "full" then

		-- we have no best, so curr must be best
		return true

	end

	if currRenditionSizeName == "thumbnail2x" and croppedLongSide < 320 then

		-- we have an asset smaller than 320, so curr is best
		return true

	end

	if croppedLongSide < 320 and bestRenditionSizeName == "thumbnail2x" then

		-- we have an asset smaller than 320 and have already seen 320, so curr
		-- is not best
		return false

	end

	local bestMatchSize =
			tonumber( PixelProviderSharedTaskUtils_ozRenditionNameToRenderSize( 
						bestRenditionSizeName ) ) or 0 

	local currSize =
			tonumber( PixelProviderSharedTaskUtils_ozRenditionNameToRenderSize( 
						currRenditionSizeName ) ) or 0

	if bestMatchSize < croppedLongSide then

		-- our best match size isn't bigger than croppedLongSide
		
		if currSize >= croppedLongSide then

			-- our currSize is bigger than croppedLongSide, use current
			return true

		end

		-- neither the best nor current are bigger than long side, return true if
		-- current is biggest
		return bestMatchSize < currSize

	end

	-- best match size is guaranteed to be >= croppedSize
	
	if currSize < croppedLongSide then

		-- our currSize is not bigger than croppedLongSide, don't use it
		return false

	end

	-- bot the best and current are bigger than long side, return true if
	-- current is smaller
	return currSize < bestMatchSize 

end

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

local function maybeGetStandardSizedDownloadableRenditionNameImp( 
				baseRenderInfo, localAssetId, requestedRenderSize ) 

	local boundedNormalizedRenderSize = 
			maybeGetNormalizedRenderSize( baseRenderInfo, requestedRenderSize ) 

	if not boundedNormalizedRenderSize then 

		logger:infof( "We requested render size %s for localAssetId %s, but there is no downloadable Oz pixel size that works",  
						requestedRenderSize, localAssetId )
		
		return

	end

	if boundedNormalizedRenderSize == "full" then

		return getDownloadableRenditionSizeNameMatchingPredicate(
				baseRenderInfo.renditions, 
				isStandardSizeRenditionBestFitForCroppedLongSide, 
					baseRenderInfo.croppedLongSide or 2048 )

	end

	return getDownloadableRenditionSizeNameMatchingPredicate(
			baseRenderInfo.renditions, 
			matchesRenderSize, 
				boundedNormalizedRenderSize )

end

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

local function maybeGetStandardSizedDownloadableRenditionName( 
				baseRenderInfoCache, localAssetId, requestedRenderSize ) 

	local baseRenderInfo = baseRenderInfoCache:peekCacheItem( localAssetId ) or 
							{}

	return maybeGetStandardSizedDownloadableRenditionNameImp( 
				baseRenderInfo, localAssetId, requestedRenderSize ) 

end

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

local function isBestFitForRenderSize( 
				bestRenderSize, currRenderSizeName, wantedRenderSize )

	-- return true to select currRenderSize, return false to select
	-- bestRenderSize

	if not bestRenderSize then

		-- no bestRenderSize yet, which means currRenderSize is best
		return true

	end

	-- make sure everything is a number

	bestRenderSize = tonumber( bestRenderSize ) or 0

	local currRenderSize =
			tonumber( PixelProviderSharedTaskUtils_ozRenditionNameToRenderSize( 
						currRenderSizeName ) ) or 0

	wantedRenderSize = tonumber( wantedRenderSize ) or 0

	if bestRenderSize == wantedRenderSize then

		-- we already have the size we want, reject currRenderSize
		return false

	end

	if currRenderSize == wantedRenderSize then

		-- we found our perfect match
		return true

	end

	if bestRenderSize < wantedRenderSize then

		-- our bestRenderSize is too small, choose currRenderSize if it's bigger
		return bestRenderSize < currRenderSize

	end

	-- at this point, we know bestRenderSize > wantedRenderSize

	if currRenderSize < wantedRenderSize then
	
		-- reject currRenderSize, it's too small
		return false

	end
	
	-- both bestRenderSize and currRenderSize are bigger than wantedRenderSize,
	-- pick the smaller of the two
	
	return currRenderSize < bestRenderSize

end

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

local function getBestMatchRenditionImp( 
				baseRenderInfo, renditions, localAssetId, requestedRenderSize ) 

	local boundedNormalizedRenderSize = 
			maybeGetNormalizedRenderSize( baseRenderInfo, requestedRenderSize ) 

	if not boundedNormalizedRenderSize then 

		logger:infof( "We requested render size %s for localAssetId %s, but there is no downloadable Oz pixel size that works",  
						requestedRenderSize, localAssetId )
		
		return

	end

	return getDownloadableRenditionSizeNameMatchingPredicate(
			renditions, 
			isBestFitForRenderSize, 
				boundedNormalizedRenderSize )

end

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

local function getBestMatchRenditionForRenderInfo( 
				renderInfo, localAssetId, requestedRenderSize ) 

	return getBestMatchRenditionImp( 
			renderInfo, renderInfo.renditions, localAssetId,
			requestedRenderSize ) 

end

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

local function getBestMatchInvalidRenditionForRenderInfo( 
				renderInfo, localAssetId, requestedRenderSize ) 

	return getBestMatchRenditionImp( 
			renderInfo, renderInfo.invalid_renditions, localAssetId,
			requestedRenderSize ) 

end

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

local function isFullSizeRenditionDownloadForExportState( downloadState )

	return downloadState == DownloadStates.TryExistingFullSizeRenditionDownload or
			downloadState == DownloadStates.TryJustCreatedFullSizeRenditionDownload or
			downloadState == DownloadStates.TryUpdatedFullSizeRenditionDownload 

end

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

local function maybeGetDownloadableRenditionSizeNameForDownloadState( 
				downloadState, baseRenderInfoCache, localAssetId,
				requestedRenderSize ) 

	local baseRenderInfo = baseRenderInfoCache:peekCacheItem( localAssetId ) or 
							{}

	if isFullSizeRenditionDownloadForExportState( downloadState ) then

		return baseRenderInfo.downloadFullRenditionURI and "fullsize"

	end

	if downloadState == DownloadStates.NeedLoupeRenditionDownload or
		downloadState == DownloadStates.TryBiggestPossibleRenditionDownload then

		return getDownloadableRenditionSizeNameMatchingPredicate(
				baseRenderInfo.renditions, isLargestStandardSizeRendition )

	end

	if downloadState == DownloadStates.NeedInvalidRenderDownload then

		return getBestMatchInvalidRenditionForRenderInfo(
				baseRenderInfo, localAssetId, requestedRenderSize ) 

	end

	return maybeGetStandardSizedDownloadableRenditionNameImp( 
				baseRenderInfo, localAssetId, requestedRenderSize ) 

end

--------------------------------------------------------------------------------
-- makeGetSatisfiedByOzBinaryRequests
--------------------------------------------------------------------------------

local SaveDownloadStates = PixelPathProviderStateMachine.SaveDownloadStates

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

local needToDownloadProxyStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.DownloadRetryCount, 
					DownloadStates.NeedProxyDownload,
					DownloadStates.NeedProxyDownloadForRender,
					DownloadStates.NeedProxyDownloadForBestPossibleLocalNegativePath ) }

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

local downloadingProxyStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.DownloadRetryCount, 
					DownloadStates.DownloadingProxy,
					DownloadStates.DownloadingProxyForRender,
					DownloadStates.DownloadingProxyForBestPossibleLocalNegativePath ) }

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

local needToSaveProxyStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.SaveDownloadRetryCount, 
				SaveDownloadStates.NeedToSaveProxy,
				SaveDownloadStates.NeedToSaveProxyForRender,
				SaveDownloadStates.NeedToSaveProxyForBestPossibleLocalNegativePath ) }

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

local savingProxyStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.SaveDownloadRetryCount, 
				SaveDownloadStates.SavingProxy,
				SaveDownloadStates.SavingProxyForRender,
				SaveDownloadStates.SavingProxyForBestPossibleLocalNegativePath ) }

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

local downloadingOrSavingProxyStates = 
		KSTableUtils.invert( appendT( needToDownloadProxyStates, downloadingProxyStates,
					needToSaveProxyStates, savingProxyStates ) )

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

local function isProxyDownloadState( state )

	return downloadingOrSavingProxyStates[ state ] ~= nil

end

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

local needToDownloadOriginalStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.DownloadRetryCount, 
					DownloadStates.NeedOriginalDownload,
					DownloadStates.NeedOriginalDownloadForRender,
					DownloadStates.NeedOriginalDownloadForBestPossibleLocalNegativePath ) }

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

local downloadingOriginalStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.DownloadRetryCount, 
					DownloadStates.DownloadingOriginal,
					DownloadStates.DownloadingOriginalForRender,
					DownloadStates.DownloadingOriginalForBestPossibleLocalNegativePath ) }

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

local needToSaveOriginalStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.SaveDownloadRetryCount,
				SaveDownloadStates.NeedToSaveOriginal,
				SaveDownloadStates.NeedToSaveOriginalForRender,
				SaveDownloadStates.NeedToSaveOriginalForBestPossibleLocalNegativePath ) }

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

local savingOriginalStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.SaveDownloadRetryCount,
				SaveDownloadStates.SavingOriginal,
				SaveDownloadStates.SavingOriginalForRender,
				SaveDownloadStates.SavingOriginalForBestPossibleLocalNegativePath ) }

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

local downloadingOrSavingOriginalStates = 
		KSTableUtils.invert( appendT( needToDownloadOriginalStates, downloadingOriginalStates,
					needToSaveOriginalStates, savingOriginalStates ) )

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

local function isOriginalDownloadState( state )

	return downloadingOrSavingOriginalStates[ state ] ~= nil

end

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

local needToDownloadXmpDevelopStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.DownloadRetryCount,
					DownloadStates.NeedXmpDevelopDownload,
					DownloadStates.NeedXmpDevelopDownloadForRender ) }

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

local downloadingXmpDevelopStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.DownloadRetryCount,
					DownloadStates.DownloadingXmpDevelop,
					DownloadStates.DownloadingXmpDevelopForRender ) }

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

local needToSaveXmpDevelopStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.SaveDownloadRetryCount,
				SaveDownloadStates.NeedToSaveXmpDevelop,
				SaveDownloadStates.NeedToSaveXmpDevelopForRender ) }

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

local savingXmpDevelopStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.SaveDownloadRetryCount,
				SaveDownloadStates.SavingXmpDevelop,
				SaveDownloadStates.SavingXmpDevelopForRender ) }
				
--------------------------------------------------------------------------------

local downloadingOrSavingXmpDevelopStates = 
		KSTableUtils.invert( appendT( needToDownloadXmpDevelopStates, downloadingXmpDevelopStates,
					needToSaveXmpDevelopStates, savingXmpDevelopStates ) )

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

local function isXmpDevelopDownloadState( state )

	return downloadingOrSavingXmpDevelopStates[ state ] ~= nil

end

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

local needToDownloadXmpSidecarStates =
	{ WFFiniteStateMachine.makeRetryableStates(
			PixelPathProviderStateMachine.DownloadRetryCount,
					DownloadStates.NeedXmpSidecarDownload ) }

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

local downloadingXmpSidecarStates =
	{ WFFiniteStateMachine.makeRetryableStates(
			PixelPathProviderStateMachine.DownloadRetryCount,
					DownloadStates.DownloadingXmpSidecar ) }

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

local needToSaveXmpSidecarStates =
	{ WFFiniteStateMachine.makeRetryableStates(
			PixelPathProviderStateMachine.SaveDownloadRetryCount,
				SaveDownloadStates.NeedToSaveXmpSidecar ) }

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

local savingXmpSidecarStates =
	{ WFFiniteStateMachine.makeRetryableStates(
			PixelPathProviderStateMachine.SaveDownloadRetryCount,
				SaveDownloadStates.SavingXmpSidecar ) }

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

local downloadingOrSavingXmpSidecarStates =
		KSTableUtils.invert( appendT( needToDownloadXmpSidecarStates, downloadingXmpSidecarStates,
					needToSaveXmpSidecarStates, savingXmpSidecarStates ) )

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

local function isXmpSidecarDownloadState( state )

	return downloadingOrSavingXmpSidecarStates[ state ] ~= nil

end

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

local function stateMatchesBinaryType( state, ozBinaryType )

	if ozBinaryType == 'proxy' then

		return isProxyDownloadState( state )

	elseif ozBinaryType == 'original' then

		return isOriginalDownloadState( state )

	elseif ozBinaryType == 'xmp_develop' then
	
		return isXmpDevelopDownloadState( state )
		
	elseif ozBinaryType == 'xmp_sidecar' then

		return isXmpSidecarDownloadState( state )

	end

	return false

end

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

local function requestHasSameBinaryType( request, ozBinaryType )

	local _, _, _, _, requestState = unpack( request )

	return stateMatchesBinaryType( requestState, ozBinaryType ) and
			request or nil

end

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

local function makeGetSatisfiedByOzBinaryRequests( equivalentStateArray )

	WFArguments_check( 'table' )

	return function ( scheduler, localAssetId, ozBinaryType )

		local requestWithSameAssetAndState =
				scheduler:_getLocalAssetIdRequestsWithState( 
							localAssetId, unpack( equivalentStateArray ) )
							
		return WFFunctionUtils_filter( requestHasSameBinaryType,
										requestWithSameAssetAndState,
										ozBinaryType )

	end

end

--------------------------------------------------------------------------------
-- getEquivalentRenditionRequests
--------------------------------------------------------------------------------

local function requestWantsSameRendition( request, activeRenditionInfoCache,
											wantedRenderSize, wantedDigest )

	local pixStreamId, localAssetId = unpack( request )

	local activeRenditionInfo = activeRenditionInfoCache:peekCacheItem(
								pixStreamId, localAssetId ) 

	if not activeRenditionInfo then

		return

	end

	if activeRenditionInfo.digest ~= wantedDigest then

		return

	end

	if activeRenditionInfo.renderSize ~= wantedRenderSize then

		return

	end

	return request

end

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

local function getEquivalentRenditionRequests( scheduler,
													activeRenditionInfoCache,
													localAssetId, renderSize,
													digest, ... )

	local requestsWithSameAssetAndStates =
			scheduler:_getLocalAssetIdRequestsWithState( 
						localAssetId, ... )

	return WFFunctionUtils_filter( requestWantsSameRendition, 
									requestsWithSameAssetAndStates,
									activeRenditionInfoCache,
									renderSize, digest )

end

--------------------------------------------------------------------------------
-- makePendingSaveBlobCache 
--------------------------------------------------------------------------------

local DownloadedBlobPendingSaveCachePrototype = {

	_downloadedAssetBlobsInfo = false,

}

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

function DownloadedBlobPendingSaveCachePrototype:__init()

	self._downloadedAssetBlobsInfo = {}

end

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

function DownloadedBlobPendingSaveCachePrototype:pushBlob( 
											localAssetId, binaryBlob,
											binaryType, ozBinarySizeName,
											maybeDigest )

	WFArguments_check( 'DownloadedBlobPendingSaveCache', 'number', '?KSData',
							'string', '? string | number', '? string' )

	local assetBlobsInfo = self._downloadedAssetBlobsInfo[ localAssetId ] or {}

	local binaryBucketId = makeBinaryBucketId( binaryType, ozBinarySizeName,
												maybeDigest )
	
	local maybeOldBlob = unpack( assetBlobsInfo[ binaryBucketId ] or {} )

	if binaryBlob and maybeOldBlob then

		local newBlobSize = KSData.length( binaryBlob )

		binaryBlob:close()

		local blobSubtype = ozBinarySizeName and 
								tostring( ozBinarySizeName ) or ""

		local errStr = string.format( "cannot clobber existing %s:%s blob size %s with new blob size %s",
										binaryType, blobSubtype,
										KSData.length( maybeOldBlob ), newBlobSize )

		logger:error( errStr )

		error( errStr )

	end

	assetBlobsInfo[ binaryBucketId ] = binaryBlob and 
										{ binaryBlob, maybeDigest } or nil

	self._downloadedAssetBlobsInfo[ localAssetId ] = next( assetBlobsInfo ) and
														assetBlobsInfo or nil

end

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

function DownloadedBlobPendingSaveCachePrototype:peekBlob( localAssetId, 
															binaryType, 
															ozBinarySizeName,
															digest )

	local assetBlobsInfo = self._downloadedAssetBlobsInfo[ localAssetId ] or {}

	if not assetBlobsInfo then

		return

	end

	local binaryBucketId = makeBinaryBucketId( binaryType, ozBinarySizeName,
												digest )

	local maybeBlobData = assetBlobsInfo[ binaryBucketId ] 

	if type( maybeBlobData ) ~= "table" then

		return

	end

	return unpack( maybeBlobData )

end

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

function DownloadedBlobPendingSaveCachePrototype:popBlob( localAssetId, 
															binaryType, 
															ozBinarySizeName, 
															digest )

	WFArguments_check( 'DownloadedBlobPendingSaveCache', 'number', 'string', 
							'? string | number', '? string' )
	
	local blob, pixStreamIds = self:peekBlob( localAssetId, binaryType, 
												ozBinarySizeName, digest )

	if not blob then

		return

	end

	self:pushBlob( localAssetId, nil, binaryType, ozBinarySizeName, digest )

	return blob, pixStreamIds

end

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

function DownloadedBlobPendingSaveCachePrototype:_destroyAllBlobs()

	for assetId, assetBlobsInfo in pairs( self._downloadedAssetBlobsInfo ) do

		for bucketId, assetBlobInfo in pairs( assetBlobsInfo ) do

			local blob = assetBlobInfo[ 1 ]

			blob:close()

			assetBlobsInfo[ bucketId ] = nil
				
		end

		if not next( assetBlobsInfo ) then

			-- no blobs left for this assetId

			self._downloadedAssetBlobsInfo[ assetId ] = nil

		end

	end

end

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

function DownloadedBlobPendingSaveCachePrototype:_destroyBlobsByAssetId( assetId, maybeBucketPredicate )

	local assetBlobsInfo = self._downloadedAssetBlobsInfo[ assetId ]

	maybeBucketPredicate = maybeBucketPredicate or function() return true end

	if assetBlobsInfo then
	
		for bucketId, assetBlobInfo in pairs( assetBlobsInfo ) do

			if maybeBucketPredicate( bucketId ) then
			
				local blob = assetBlobInfo[ 1 ]

				blob:close()

				assetBlobsInfo[ bucketId ] = nil
				
			end
				
		end
	
		if not next( assetBlobsInfo ) then

			-- no blobs left for this assetId

			self._downloadedAssetBlobsInfo[ assetId ] = nil

		end
	
	end

end

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

function DownloadedBlobPendingSaveCachePrototype:destroyAll()

	self:_destroyAllBlobs()

end

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

function DownloadedBlobPendingSaveCachePrototype:destroyAllForLocalAssetId( 
													localAssetId )

	self:_destroyBlobsByAssetId( localAssetId )

end

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

local function matchesBucketType( bucketPredicate )

	return function( binaryBucketId )

		return bucketPredicate( binaryBucketId )

	end

end

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

local function isNegativeBucketId( binaryBucketId )

	return binaryBucketId == 'original_negative' or
			binaryBucketId == 'proxy_negative'
			
end

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

local function isRenditionBucketId( binaryBucketId )

	return isNot( isNegativeBucketId )( binaryBucketId ) and 
			binaryBucketId ~= 'xmp_develop' and
			binaryBucketId ~= 'xmp_sidecar'

end

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

function DownloadedBlobPendingSaveCachePrototype:destroyAllLocalAssetIdNegatives( 
													localAssetId )

	self:_destroyBlobsByAssetId( localAssetId, matchesBucketType( isNegativeBucketId ) )

end

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

function DownloadedBlobPendingSaveCachePrototype:destroyAllLocalAssetIdRenditions( 
													localAssetId )

	self:_destroyBlobsByAssetId( localAssetId, matchesBucketType( isRenditionBucketId ) )

end

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

function DownloadedBlobPendingSaveCachePrototype:localAssetIdsWithBlob()

	local localAssetIds = {}

	for localAssetId in pairs( self._downloadedAssetBlobsInfo ) do
	
		localAssetIds[ #localAssetIds + 1 ] = localAssetId

	end

	return localAssetIds

end

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

local DownloadedBlobPendingSaveCache =
		Rx.Class( "DownloadedBlobPendingSaveCache", 
					DownloadedBlobPendingSaveCachePrototype )

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

local function makePendingSaveBlobCache()

	return DownloadedBlobPendingSaveCache()	

end

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

local needRenderDownloadStates = 
	appendT(
		{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.DownloadRetryCount, 
			DownloadStates.NeedRenderDownload ) },
		{ DownloadStates.NeedLoupeRenditionDownload,
		  DownloadStates.TryExistingFullSizeRenditionDownload,
		  DownloadStates.TryJustCreatedFullSizeRenditionDownload,
		  DownloadStates.TryUpdatedFullSizeRenditionDownload,
		  DownloadStates.TryBiggestPossibleRenditionDownload,
		  DownloadStates.NeedInvalidRenderDownload } )

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

local needNegativeDownloadStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.DownloadRetryCount, 
			DownloadStates.NeedProxyDownload,
			DownloadStates.NeedProxyDownloadForRender,
			DownloadStates.NeedOriginalDownload,
			DownloadStates.NeedOriginalDownloadForRender,
			DownloadStates.NeedOriginalDownloadForBestPossibleLocalNegativePath,
			DownloadStates.NeedProxyDownloadForBestPossibleLocalNegativePath ) }
			
--------------------------------------------------------------------------------

local needXmpDevelopDownloadStates = 
	{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.DownloadRetryCount,
			DownloadStates.NeedXmpDevelopDownload,
			DownloadStates.NeedXmpDevelopDownloadForRender ) }

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

local needXmpSidecarDownloadStates =
	{ WFFiniteStateMachine.makeRetryableStates(
			PixelPathProviderStateMachine.DownloadRetryCount,
			DownloadStates.NeedXmpSidecarDownload ) }

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

local function addToSet( set, k )

	set[ k ] = true 

	return set

end

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

local function makeSet( array )
	
	return WFFunctionUtils_reduce( {}, addToSet, array )

end

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

local _needRenderDownloadStateSet = makeSet( needRenderDownloadStates )

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

local function isNeedsRenditionDownloadState( state )

	return not not _needRenderDownloadStateSet[ state ]

end

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

local downloadingRenditionStates = 
	appendT(
		{ WFFiniteStateMachine.makeRetryableStates( 
			PixelPathProviderStateMachine.DownloadRetryCount, 
					DownloadStates.DownloadingRender ) },
		{ DownloadStates.DownloadingLoupeRendition,
		  DownloadStates.DownloadingExistingFullRenditionLinkForExport,
		  DownloadStates.DownloadingJustCreatedFullRenditionLinkForExport,
		  DownloadStates.DownloadingUpdatedFullRenditionLinkForExport,
		  DownloadStates.DownloadingBiggestPossibleRenditionLinkForExport,
		  DownloadStates.DownloadingInvalidRender } )

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

local downloadingNegativeStates = appendT( downloadingOriginalStates, 
											downloadingProxyStates )

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

local needToSaveNegativeStates = appendT( needToSaveOriginalStates,
											needToSaveProxyStates )

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

local savingNegativeStates = appendT( savingOriginalStates, savingProxyStates )

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

local saveNegativeStates = appendT( needToSaveNegativeStates,
										savingNegativeStates )
										
--------------------------------------------------------------------------------

local saveXmpDevelopStates = appendT( needToSaveXmpDevelopStates,
										savingXmpDevelopStates )
										
--------------------------------------------------------------------------------

local saveXmpSidecarStates = appendT( needToSaveXmpSidecarStates,
										savingXmpSidecarStates )

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

local fulfilledBySuccessfulNegativeSaveStates = appendT( saveNegativeStates,
													needNegativeDownloadStates )
													
													
--------------------------------------------------------------------------------

local fulfilledBySuccessfulXmpDevelopSaveStates = appendT( saveXmpDevelopStates,
															needXmpDevelopDownloadStates )

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

local fulfilledBySuccessfulXmpSidecarSaveStates = appendT( saveXmpSidecarStates,
															needXmpSidecarDownloadStates )

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

local canJoinNegativeBlobSaveStates = appendT( needToSaveNegativeStates, 
												needNegativeDownloadStates )
												
--------------------------------------------------------------------------------

local canJoinXmpDevelopBlobSaveStates = appendT( needToSaveXmpDevelopStates,
													needXmpDevelopDownloadStates )

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

local canJoinXmpSidecarBlobSaveStates = appendT( needToSaveXmpSidecarStates,
													needXmpSidecarDownloadStates )

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

return {

	-- exported functions

	appendT = appendT,

	maybeGetStandardSizedDownloadableRenditionName = 
		maybeGetStandardSizedDownloadableRenditionName,

	maybeGetDownloadableRenditionSizeNameForDownloadState = 
		maybeGetDownloadableRenditionSizeNameForDownloadState,

	getBestMatchRenditionForRenderInfo = getBestMatchRenditionForRenderInfo,

	getBestMatchInvalidRenditionForRenderInfo =
		getBestMatchInvalidRenditionForRenderInfo,

	isProxyDownloadState = isProxyDownloadState,

	isOriginalDownloadState = isOriginalDownloadState,
	
	isXmpDevelopDownloadState = isXmpDevelopDownloadState,

	isXmpSidecarDownloadState = isXmpSidecarDownloadState,

	getActiveNegativeDownloadRequests = 
		makeGetSatisfiedByOzBinaryRequests( downloadingNegativeStates ),

	getNeedNegativeDownloadRequests = 
		makeGetSatisfiedByOzBinaryRequests( needNegativeDownloadStates ),
			
	getFulfilledBySuccessfulNegativeSaveRequests =
		makeGetSatisfiedByOzBinaryRequests( appendT( fulfilledBySuccessfulNegativeSaveStates,
														fulfilledBySuccessfulXmpDevelopSaveStates,
														fulfilledBySuccessfulXmpSidecarSaveStates ) ),

	getWaitingForNegativeSaveRequests =
		makeGetSatisfiedByOzBinaryRequests( appendT( saveNegativeStates,
														saveXmpDevelopStates,
														saveXmpSidecarStates ) ),

	getCanJoinNegativeSaveRequests =
		makeGetSatisfiedByOzBinaryRequests( appendT( canJoinNegativeBlobSaveStates,
														canJoinXmpDevelopBlobSaveStates,
														canJoinXmpSidecarBlobSaveStates ) ),

	getNeedNegativeSaveRequests =
		makeGetSatisfiedByOzBinaryRequests( appendT( needToSaveNegativeStates, 
												needToSaveXmpDevelopStates,
												needToSaveXmpSidecarStates ) ),

	getActiveNegativeSaveRequests =
		makeGetSatisfiedByOzBinaryRequests( appendT( savingNegativeStates,
												savingXmpDevelopStates,
												savingXmpSidecarStates ) ),
		
	getNeedXmpDevelopDownloadRequests = 
		makeGetSatisfiedByOzBinaryRequests( needXmpDevelopDownloadStates ),
	
	getActiveXmpDevelopDownloadRequests = 
		makeGetSatisfiedByOzBinaryRequests( downloadingXmpDevelopStates ),

	getNeedXmpSidecarDownloadRequests =
		makeGetSatisfiedByOzBinaryRequests( needXmpSidecarDownloadStates ),

	getActiveXmpSidecarDownloadRequests =
		makeGetSatisfiedByOzBinaryRequests( downloadingXmpSidecarStates ),

	getEquivalentRenditionRequests = getEquivalentRenditionRequests,

	makePendingSaveBlobCache = makePendingSaveBlobCache,

	isNeedsRenditionDownloadState = isNeedsRenditionDownloadState,

	-- exported constants

	needRenderDownloadStates = needRenderDownloadStates,

	needsDownloadStates = appendT( needRenderDownloadStates, 
									needNegativeDownloadStates, 
									needXmpDevelopDownloadStates,
									needXmpSidecarDownloadStates ),

	downloadingRenditionStates = downloadingRenditionStates,

	needToSaveOriginalStates = needToSaveOriginalStates,

	needToSaveProxyStates = needToSaveProxyStates,

	needToSaveRenderBinaryStates = appendT( needToSaveNegativeStates,
											needToSaveXmpDevelopStates,
											needToSaveXmpSidecarStates ),

	savingRenderBinaryStates = appendT( savingNegativeStates,
										savingXmpDevelopStates,
										savingXmpSidecarStates ),

	saveRenderBinaryStates = appendT( saveNegativeStates,
										saveXmpDevelopStates,
										saveXmpSidecarStates ),

	downloadingBlobStates = appendT( downloadingRenditionStates,
										downloadingNegativeStates,
										downloadingXmpDevelopStates,
										downloadingXmpSidecarStates ),

	fulfilledBySuccessfulNegativeSaveStates = 
		fulfilledBySuccessfulNegativeSaveStates,

}
