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

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, baseRenderInfo.subtype )

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

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

-- ordered table to prioritize video sizes.
local _videoSizeOrder = KSTableUtils.invert( {
	"360p",
	"720p",
	"1080p709",
	"1080p" 
	} )

local function isBestFitForVideoRenderSize( bestRenderSize, currRenderSizeName, wantedRenderSize )

	-- our wanted render size is a video request

	local isBestRenderSizeVideoType = not not ( bestRenderSize and _videoSizeOrder[ bestRenderSize ] )
	local isCurrRenderSizeNameSizeVideoType = not not ( currRenderSizeName and _videoSizeOrder[ currRenderSizeName ] )
	
	if not isBestRenderSizeVideoType then
		-- if the bestRenderSize currently is not a video type,
		
		if isCurrRenderSizeNameSizeVideoType then
			-- make best render size the current render size if it is a video type
			return true
		end
		
		return false
	end
	
	-- here we know the bestRenderSize is a video type
	
	if not isCurrRenderSizeNameSizeVideoType then
		-- currentRenderSize is not a video type so reject it
		return false
		
	end
	
	-- here we know both bestRenderSize and currRenderSize are video types
	
	if bestRenderSize == wantedRenderSize then
		-- already have the size we want
		return false
	end
	
	if currRenderSizeName == wantedRenderSize then
		-- found our match
		return true
	end
	
	local currRenderSizeRank = _videoSizeOrder[ currRenderSizeName ]
	local bestRenderSizeRank = _videoSizeOrder[ bestRenderSize ]
	local wantedRenderSizeRank = _videoSizeOrder[ wantedRenderSize ]
	
	if bestRenderSizeRank < wantedRenderSizeRank then

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

	end

	-- at this point, we know bestRenderSize > wantedRenderSize

	if currRenderSizeRank < wantedRenderSizeRank 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 currRenderSizeRank < bestRenderSizeRank

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
	
	if not tonumber( wantedRenderSize ) and _videoSizeOrder[ wantedRenderSize ] then
		-- if our wanted render size is a video request
		return isBestFitForVideoRenderSize( bestRenderSize, currRenderSizeName, wantedRenderSize )
	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 = 
	{ SaveDownloadStates.NeedToSaveProxy,
		SaveDownloadStates.NeedToSaveProxyForRender,
		SaveDownloadStates.NeedToSaveProxyForBestPossibleLocalNegativePath }

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

local savingProxyStates = 
	{ 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 = 
	{ SaveDownloadStates.NeedToSaveOriginal,
		SaveDownloadStates.NeedToSaveOriginalForRender,
		SaveDownloadStates.NeedToSaveOriginalForBestPossibleLocalNegativePath }

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

local savingOriginalStates = 
	{ 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.RenderNeedXmpDevelopDownload ) }

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

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

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

local needToSaveXmpDevelopStates = 
	{ SaveDownloadStates.NeedToSaveXmpDevelop,
		SaveDownloadStates.RenderNeedToSaveXmpDevelop }

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

local savingXmpDevelopStates = 
	{ SaveDownloadStates.SavingXmpDevelop,
		SaveDownloadStates.RenderSavingXmpDevelop }
				
--------------------------------------------------------------------------------

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 =
	{ SaveDownloadStates.NeedToSaveXmpSidecar }

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

local savingXmpSidecarStates =
	{ 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

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

local function pushBlobImp( self, 
											localAssetId, binaryBlob,
											ozBinaryType, maybeOzBinarySizeName,
											maybeDigest, ... )

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

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

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

	if binaryBlob and maybeOldBlob then

		local newBlobSize = KSData.length( binaryBlob )

		binaryBlob:close()

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

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

		logger:error( errStr )

		error( errStr )

	end

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

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

end

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

function DownloadedBlobPendingSaveCachePrototype:pushBlob( 
											localAssetId, binaryBlob,
											ozBinaryType, ozBinarySizeName,
											maybeDigest, ... )

	if ozBinaryType == "original" or ozBinaryType == "xmp_develop" then

		local errStr = string.format( ":pushBlob() unsupported for ozBinaryType %s",
										ozBinaryType )

		logger:error( errStr )

		error( errStr )

	end

	return pushBlobImp( self, localAssetId, binaryBlob, ozBinaryType,
						ozBinarySizeName, maybeDigest, ... )

end

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

function DownloadedBlobPendingSaveCachePrototype:pushXmpDevelopBlob( 
											localAssetId, binaryBlob )

	WFArguments_check( 'DownloadedBlobPendingSaveCache', 'number', 'KSData' )

	return pushBlobImp( self, localAssetId, binaryBlob, 'xmp_develop' )

end

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

local function pushOriginalBlobImp( self, localAssetId, binaryBlob, sha256 )

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

	return pushBlobImp( self, localAssetId, binaryBlob, 'original', nil, nil,
							sha256 )

end

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

function DownloadedBlobPendingSaveCachePrototype:pushOriginalBlobWithSha256( 
											localAssetId, binaryBlob, sha256 )

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

	return pushOriginalBlobImp( self, localAssetId, binaryBlob, sha256 )

end

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

local _missingSha256 = {}

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

function DownloadedBlobPendingSaveCachePrototype:pushOriginalBlobWithoutSha256( 
											localAssetId, binaryBlob )

	WFArguments_check( 'DownloadedBlobPendingSaveCache', 'number', 'KSData' )


	return pushOriginalBlobImp( self, localAssetId, binaryBlob, _missingSha256 )

end

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

local function peekBlobImp( self, 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:peekBlob( localAssetId, 
															binaryType, 
															ozBinarySizeName,
															digest )

	if binaryType == "xmp_develop" then

		local errStr = ":peekBlob() unsupported for ozBinaryType xmp_develop"

		logger:error( errStr )

		error( errStr )

	end

	return peekBlobImp( self, localAssetId, binaryType, ozBinarySizeName,
						digest )

end

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

function DownloadedBlobPendingSaveCachePrototype:peekXmpDevelopBlob( localAssetId )

	WFArguments_check( 'DownloadedBlobPendingSaveCache', 'number' )

	return peekBlobImp( self, localAssetId, "xmp_develop" )

end

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

local function clearBlob( self, localAssetId, binaryType, ozBinarySizeName,
							maybeDigest )

	pushBlobImp( self, localAssetId, nil, binaryType, ozBinarySizeName,
					maybeDigest )

end

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

local function popBlobImp( self, localAssetId, binaryType, ozBinarySizeName,
							maybeDigest )

	WFArguments_check( 'DownloadedBlobPendingSaveCache', 'number', 'string', 
							'? string | number', '? string' )
	
	local blobAndData = { peekBlobImp( self, localAssetId, binaryType, 
											ozBinarySizeName, maybeDigest ) }

	local blob = blobAndData[ 1 ]

	if not blob then

		return

	end

	clearBlob( self, localAssetId, binaryType, ozBinarySizeName, maybeDigest )

	return blob, unpack( blobAndData, 2 )

end

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

function DownloadedBlobPendingSaveCachePrototype:popBlob( localAssetId, 
															ozBinaryType, 
															ozBinarySizeName, 
															maybeDigest )

	if ozBinaryType == "xmp_develop" or ozBinaryType == "original" then

		local errStr = string.format( ":popBlob() unsupported for ozBinaryType %s",
										ozBinaryType )

		logger:error( errStr )

		error( errStr )

	end

	return popBlobImp( self, localAssetId, ozBinaryType, ozBinarySizeName,
							maybeDigest )

end

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

function DownloadedBlobPendingSaveCachePrototype:popXmpDevelopBlob( localAssetId )

	WFArguments_check( 'DownloadedBlobPendingSaveCache', 'number' )

	return popBlobImp( self, localAssetId, 'xmp_develop' )

end

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

function DownloadedBlobPendingSaveCachePrototype:popOriginalBlob( localAssetId )

	WFArguments_check( 'DownloadedBlobPendingSaveCache', 'number' )

	local blob, sha256 = popBlobImp( self, localAssetId, 'original' )

	if not blob then

		return

	end

	sha256 = sha256 ~= _missingSha256 and sha256 or nil

	return blob, sha256

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 = 
	{ DownloadStates.NeedXmpDevelopDownload,
		DownloadStates.RenderNeedXmpDevelopDownload }

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

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,

}
