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

PixelProviderPendingDownloadInfoCache.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2024 Adobe
All Rights Reserved.

NOTICE: All information contained herein is, and remains
the property of Adobe and its suppliers, if any. The intellectual
and technical concepts contained herein are proprietary to Adobe
and its suppliers and are protected by all applicable intellectual
property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material
is strictly forbidden unless prior written permission is obtained
from Adobe.
------------------------------------------------------------------------------]]

	-- Rx
local Rx = require "Rx"

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

	-- WF.core
local KSTableUtils = require 'KSTableUtils'
local WFFunctionUtils = require 'WFFunctionUtils'

	-- pixel_provider
local PixelProviderSharedTaskUtils = require 'PixelProviderSharedTaskUtils'
local PixelProviderSharedBlobCache = require 'PixelProviderSharedBlobCache'
local PixelProviderDownloadToFileUtils = require 'PixelProviderDownloadToFileUtils'
local PixelPathProviderRequestTypes = require 'PixelPathProviderRequestTypes'
local PixelPathProviderSchedulerPrivateMethods = require 'PixelPathProviderSchedulerPrivateMethods'

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

local KSTableUtils_derefOpt = assert( KSTableUtils.derefOpt, 
										"KSTableUtils.derefOpt undefined" )

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

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

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

local function logAndRaiseError( str, ... )

	local errStr = select( '#', ... ) > 0 and string.format( str, ... ) or str

	logger:error( errStr )

	error( errStr )

end
--------------------------------------------------------------------------------
-- PixelProviderPendingDownloadInfoCache
--------------------------------------------------------------------------------

local PixelPendingDownloadInfoCachePrototype = {

	_ozAssetIdInfoMap = false,

	_activePixStreamRequestInfo = false,

	_maxDownloadCount = false,

	_downloadToFileDestPathErrorStream = false,

}

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

function PixelPendingDownloadInfoCachePrototype:__init( maxDownloadCount )

	self._ozAssetIdInfoMap = {}

	self._activePixStreamRequestInfo = {}

	self._maxDownloadCount = maxDownloadCount

	self._downloadToFileDestPathErrorStream = Rx.Subject()

end

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

function PixelPendingDownloadInfoCachePrototype:setMaxDownloadCount( maxDownloadCount )

	self._maxDownloadCount = maxDownloadCount

end

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

function PixelPendingDownloadInfoCachePrototype:reset()

	if not next( self._ozAssetIdInfoMap ) then

		return

	end

	self._ozAssetIdInfoMap = {}

	self._activePixStreamRequestInfo = {}

end

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

local function makeBinaryBucketId( ozBinaryType, ozBinarySizeName )

	return string.format( "%s:%s", ozBinaryType, 
									ozBinarySizeName or 'any_size' )

end

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

local function maybeGetEntry( self, ozAssetId, binaryType, binarySize ) 

	local ozAssetIdInfoMap = 
			self._ozAssetIdInfoMap[ ozAssetId ] 

	if not ozAssetIdInfoMap then

		return

	end

	local binaryBucketId = makeBinaryBucketId( binaryType, binarySize ) 

	-- 1) pop the active downloadInfoForBinaryBucketId

	local downloadInfoForBinaryBucketId = 
			KSTableUtils_derefOpt( ozAssetIdInfoMap,
									'binaryBucketIdDownloadInfoMap',
									binaryBucketId )

	return ozAssetIdInfoMap, binaryBucketId, downloadInfoForBinaryBucketId

end

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

function PixelPendingDownloadInfoCachePrototype:cancelAll()

	for ozAssetId, ozAssetIdInfoMap in pairs( self._ozAssetIdInfoMap ) do

		for binaryBucketId, downloadInfoForBinaryBucketId in pairs( ozAssetIdInfoMap.binaryBucketIdDownloadInfoMap or {} ) do

			local cancelFunc = downloadInfoForBinaryBucketId.cancelDownloadFunc

			if type( cancelFunc ) == "function" then

				logger:infof( "cancelAll is canceling download of %s for %s", 
								binaryBucketId, ozAssetId )

				cancelFunc()

			end

		end

	end

end

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

function PixelPendingDownloadInfoCachePrototype:hasEntry( ozAssetId, binaryType,
																binarySize ) 

	local ozAssetIdInfoMap, binaryBucketId, downloadInfoForBinaryBucketId = 
			maybeGetEntry( self, ozAssetId, binaryType, binarySize ) 

	return ozAssetIdInfoMap ~= nil and 
			binaryBucketId ~= nil and 
			downloadInfoForBinaryBucketId ~= nil

end

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

function PixelPendingDownloadInfoCachePrototype:allActiveDownloadRequests( )

	local activeDownloadRequests = {}
	
	for pixStreamId, pixStreamRequests in pairs( self._activePixStreamRequestInfo ) do
		
		for localDocId, downloadRequest in pairs( pixStreamRequests ) do
			
			table.insert( activeDownloadRequests, { pixStreamId, localDocId, unpack( downloadRequest ) } )
		
		end
	
	end
	
	return activeDownloadRequests

end

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

function PixelPendingDownloadInfoCachePrototype:getHttpPriorityIfDownloading( 
												ozAssetId, binaryType,
												binarySize ) 

	local _, _, downloadInfoForBinaryBucketId = 
			maybeGetEntry( self, ozAssetId, binaryType, binarySize ) 

	if type( downloadInfoForBinaryBucketId ) ~= "table" then

		return

	end

	return downloadInfoForBinaryBucketId.httpPriority 

end

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

function PixelPendingDownloadInfoCachePrototype:getPixelProviderPriorityIfDownloading( 
												ozAssetId, binaryType,
												binarySize ) 

	local _, _, downloadInfoForBinaryBucketId = 
			maybeGetEntry( self, ozAssetId, binaryType, binarySize ) 

	if type( downloadInfoForBinaryBucketId ) ~= "table" then

		return

	end

	return downloadInfoForBinaryBucketId.pixelProviderPriority 

end


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

function PixelPendingDownloadInfoCachePrototype:removePendingDownloadInfo( 
					ozAssetId, binaryType, binarySize ) 

	local ozAssetIdInfoMap, binaryBucketId, downloadInfoForBinaryBucketId = 
			maybeGetEntry( self, ozAssetId, binaryType, binarySize ) 

	if not ozAssetIdInfoMap then

		return

	end

	-- 1) pop the active downloadInfoForBinaryBucketId

	ozAssetIdInfoMap.binaryBucketIdDownloadInfoMap[ binaryBucketId ] = nil

	if not downloadInfoForBinaryBucketId then

		return

	end

	-- 2) maybe clear out containers that held the popped data

	-- a) clear out map if no other sized downloads for ozAssetId
	if not next( ozAssetIdInfoMap.binaryBucketIdDownloadInfoMap ) then

		self._ozAssetIdInfoMap[ ozAssetId ] = nil

	end

	-- b) Clear out the associated self._activePixStreamRequestInfo[ pixStreamId ][ localAssetId ] for this download
	local thisDownloadPixProviderRequests = downloadInfoForBinaryBucketId.pixProviderRequests

	for pixStreamId, localAssetIds in pairs( thisDownloadPixProviderRequests  ) do

		for localAssetId in pairs( localAssetIds ) do

			local activePixStreamRequestInfo =
					KSTableUtils_derefOpt( self._activePixStreamRequestInfo, 
											pixStreamId, localAssetId )

			if activePixStreamRequestInfo then

				self._activePixStreamRequestInfo[ pixStreamId ][ localAssetId ] = nil

			end

		end

	end

	-- 3) send the popped data

	local localAssetId = ozAssetIdInfoMap.localAssetId

	return localAssetId, downloadInfoForBinaryBucketId

end

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

local function getActiveDownloadCount( self )

	local activeDownloadCount = 0

	for _, ozAssetIdInfoMap in pairs( self._ozAssetIdInfoMap ) do

		for _ in pairs( ozAssetIdInfoMap.binaryBucketIdDownloadInfoMap ) do

			activeDownloadCount = activeDownloadCount + 1

		end

	end

	return activeDownloadCount

end

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

function PixelPendingDownloadInfoCachePrototype:canDownload()

	return getActiveDownloadCount( self ) < self._maxDownloadCount

end

--------------------------------------------------------------------------------
-- This cache keeps track of the revId <-> digest maps for each in flight
-- download request identified by the ( ozAssetId, binaryType, binarySize )
--
-- When the download completes, we can look up the digest via the revId
--------------------------------------------------------------------------------

function PixelPendingDownloadInfoCachePrototype:addNewPendingDownload( 
			ozAssetId, binaryType, binarySize, localAssetId,
			cancelDownloadFunc, httpPriority, pixelProviderPriority, maybeRenderSize, maybeRevId,
			maybeDigest, maybeOriginalBlobSha256 )

	if not self:canDownload() then

		logAndRaiseError( "Can't start a new download. There are %s active and only %s max allowed",
							getActiveDownloadCount( self ), self._maxDownloadCount )

	end
	
	local ozAssetIdInfoMap = self._ozAssetIdInfoMap[ ozAssetId ] or
									{ localAssetId = localAssetId, 
										binaryBucketIdDownloadInfoMap = {} }

	self._ozAssetIdInfoMap[ ozAssetId ] = ozAssetIdInfoMap

	local binaryBucketId = makeBinaryBucketId( binaryType, binarySize )

	if ozAssetIdInfoMap.binaryBucketIdDownloadInfoMap[ binaryBucketId ] ~= nil then

		logAndRaiseError( "There is already a download of id '%s' for assetId %s",
								binaryBucketId, localAssetId )

	end

	local revIdDigestMap = {}

	local revId = maybeRevId or binaryBucketId

	local digest = maybeDigest or binaryBucketId

	revIdDigestMap[ revId ] = digest

	ozAssetIdInfoMap.binaryBucketIdDownloadInfoMap[ binaryBucketId ] = 
			{ cancelDownloadFunc = cancelDownloadFunc,
				httpPriority = httpPriority,
				pixelProviderPriority = pixelProviderPriority,
				renderSize = maybeRenderSize,
				maybeOriginalBlobSha256 = maybeOriginalBlobSha256,
				revIdDigestMap = revIdDigestMap, 
				pixProviderRequests = {} }

end

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

function PixelPendingDownloadInfoCachePrototype:joinPendingDownload( 
			ozAssetId, binaryType, binarySize, pixStreamId, localAssetId )

	local _, _, downloadInfoForBinaryBucketId =
			maybeGetEntry( self, ozAssetId, binaryType, binarySize ) 

	if not downloadInfoForBinaryBucketId then

		logAndRaiseError( "Request %s:%s Can't join download of %s because there is no active download",
								pixStreamId, localAssetId, binaryType )
		
	end

	-- keep track of the pixstream requests on the download info, this is used
	-- to count the number of pixtream requests per download

	downloadInfoForBinaryBucketId.pixProviderRequests[ pixStreamId ] =
		downloadInfoForBinaryBucketId.pixProviderRequests[ pixStreamId ] or {}

	downloadInfoForBinaryBucketId.pixProviderRequests[ pixStreamId ][ localAssetId ] = true

	-- keep track of the pixProviderRequests by pixStream. This is used to map
	-- a pix stream request back to the actual download
	self._activePixStreamRequestInfo[ pixStreamId ] =
		self._activePixStreamRequestInfo[ pixStreamId ] or {}

	self._activePixStreamRequestInfo[ pixStreamId ][ localAssetId ] = 
		{ ozAssetId, binaryType, binarySize }
	
end

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

function PixelPendingDownloadInfoCachePrototype:cancelAndRemoveDownload( 
											reason, ozAssetId, ozBinaryType,
											ozBinarySizeName )

	logger:infof( "Canceling download of %s( %s ) for %s because %s", 
					ozBinaryType, ozBinarySizeName, ozAssetId, reason )

	local localAssetId, downloadInfoForBinaryBucketId =
			self:removePendingDownloadInfo( ozAssetId, ozBinaryType,
												ozBinarySizeName )

	if not localAssetId then

		return {}

	end

	if type( downloadInfoForBinaryBucketId ) ~= "table" then

		logger:warnf( "No download info for localAssetId %s of %s( %s ) for %s", 
						localAssetId, ozBinaryType, 
						ozBinarySizeName, ozAssetId )

		return {}

	end

	if type( downloadInfoForBinaryBucketId.cancelDownloadFunc ) ~= "function" or
			not downloadInfoForBinaryBucketId.cancelDownloadFunc() then

		logger:warnf( "Failed canceling download of localAssetId %s %s( %s ) for %s", 
						localAssetId, ozBinaryType, ozBinarySizeName, 
						ozAssetId )

	end

	return type( downloadInfoForBinaryBucketId.pixProviderRequests ) == "table" and
			downloadInfoForBinaryBucketId.pixProviderRequests or {}

end

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

function PixelPendingDownloadInfoCachePrototype:maybeGetActiveDownloadInfoForPixStreamRequest( 
											pixStreamId, localAssetId )

	local activePixStreamRequestInfo =
			KSTableUtils_derefOpt( self._activePixStreamRequestInfo, 
									pixStreamId, localAssetId )

	local ozAssetId, binaryType, binarySize = 
			unpack( activePixStreamRequestInfo or {} )

	if not ozAssetId then

		return

	end

	local ozAssetIdInfoMap, binaryBucketId, downloadInfoForBinaryBucketId = 
			maybeGetEntry( self, ozAssetId, binaryType, binarySize ) 

	return ozAssetId, binaryType, binarySize, ozAssetIdInfoMap, binaryBucketId,
			downloadInfoForBinaryBucketId

end

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

function PixelPendingDownloadInfoCachePrototype:removePixStreamRequest( 
											pixStreamId, localAssetId, state )

	local ozAssetId, binaryType, binarySize, _, _,
			downloadInfoForBinaryBucketId =
				self:maybeGetActiveDownloadInfoForPixStreamRequest( 
						pixStreamId, localAssetId )

	local didWarn

	if not ozAssetId then

		didWarn = true

		logger:warnf( "No activePixStreamRequestInfo for %s:%s with state %s (can happen if we delete pixel request and download completes before we successfully cancel download)", 
							pixStreamId, localAssetId, state )

		local activePixStreamRequestInfo =
				KSTableUtils_derefOpt( self._activePixStreamRequestInfo, 
										pixStreamId, localAssetId )

		if type( activePixStreamRequestInfo ) == "table" then

			self._activePixStreamRequestInfo[ pixStreamId ][ localAssetId ] = nil

		end

	end

	local pixProviderRequestsForPixStreamId =
			KSTableUtils_derefOpt( downloadInfoForBinaryBucketId,
									"pixProviderRequests", pixStreamId )

	if not pixProviderRequestsForPixStreamId then

		if didWarn then

			return

		end

		logger:warnf( "No pixProviderRequestsForPixStreamId for %s:%s with state %s (can happen if we delete pixel request and download completes before we successfully cancel download)",
						pixStreamId, localAssetId, state )

		return

	end

	pixProviderRequestsForPixStreamId[ localAssetId ] = nil

	local otherLocalAssetId = next( pixProviderRequestsForPixStreamId )

	if otherLocalAssetId then

		return

	end

	downloadInfoForBinaryBucketId.pixProviderRequests[ pixStreamId ] = nil

	local otherPixStreamId = next( downloadInfoForBinaryBucketId.pixProviderRequests ) 

	if otherPixStreamId then
		
		return

	end

	-- There are no active pix provider requests for this download, remove it and cancel it.
	self:cancelAndRemoveDownload( 
			"removed only pix provider download request", ozAssetId, binaryType, 
			binarySize ) 

end

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

local _needsDownloadStates

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

local function getNeedsDownloadStates( scheduler )

	if not _needsDownloadStates then

		-- add the download to blob states
		_needsDownloadStates =
			PixelProviderSharedBlobCache.appendT(
				PixelProviderSharedBlobCache.needsDownloadStates,
				PixelProviderDownloadToFileUtils.getNeedsDownloadToFileStates( 
					scheduler ) )

	end

	return _needsDownloadStates
			
end

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

local function getNextDownloadRequest( scheduler, wantDownloadToFileState )
	
	-- pop the highest priority download to file request or download to blob
	-- request, but only return the request if it matches
	-- wantDownloadToFileState. 
	--
	-- So, for example, if there are both a download to file request and a
	-- download to blob request and the blob request is higher priority, we
	-- won't return the download to file request until the download to blob
	-- request undergoes a state transition. 

	local request =
		{ PixelPathProviderSchedulerPrivateMethods.getNextRequestWithState( 
					scheduler,  
					unpack( getNeedsDownloadStates( scheduler ) ) ) }

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

	if not pixStreamId then

		return

	end

	local isNeedsDownloadToFileState = 
			PixelProviderDownloadToFileUtils.isNeedsDownloadToFileState( 
				scheduler, requestState )

	if wantDownloadToFileState ~= isNeedsDownloadToFileState then

		return

	end

	return request

end

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

PixelPendingDownloadInfoCachePrototype.getNextDownloadToBlobRequest =
function( _, scheduler )

	return getNextDownloadRequest( scheduler, false )

end

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

PixelPendingDownloadInfoCachePrototype.getNextDownloadToFileRequest =
function( _, scheduler )

	return getNextDownloadRequest( scheduler, true )

end

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

function PixelPendingDownloadInfoCachePrototype:cancelCurrentDownload( 
				reason, scheduler, ozAssetId, ozBinaryType, ozBinarySizeName )

	local pixProviderRequestsToRestart =
			self:cancelAndRemoveDownload( reason, ozAssetId, ozBinaryType,
											ozBinarySizeName )

	for pixStreamId, localAssetIds in pairs( pixProviderRequestsToRestart ) do

		for localAssetId in pairs( localAssetIds ) do
		
			scheduler:_maybeSetNextState( pixStreamId, localAssetId, 'reschedule_download' )

		end

	end

end

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

local PixelPathProviderRequestTypes_getName =
		assert( PixelPathProviderRequestTypes.getName,
				"PixelPathProviderRequestTypes.getName does not exist" )

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

local PixelPathProviderRequestTypes_names =
		assert( PixelPathProviderRequestTypes.names,
				"PixelPathProviderRequestTypes.names does not exist" )

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

local function isDownloadBlocked( scheduler, ozBinaryType, maybeDownloadPriority )

	local statusTable = scheduler:getWFDownloadStatus()
	
	local catalogStatus = statusTable.catalogStatus
	local wfDownloadStatus = statusTable.syncStatus
	local originalVolumeStatus = statusTable.originalVolumeStatus
	local proxyVolumeStatus = statusTable.proxyVolumeStatus
	local mainVolumeStatus = statusTable.mainVolumeStatus

	local maybePriorityName = maybeDownloadPriority 
			and PixelPathProviderRequestTypes_getName( maybeDownloadPriority )

	local downloadOriginalToMainDrive = false
	
	if catalogStatus == 'does_not_match' then
		
		return 'wf', catalogStatus
	
	end
	
	if wfDownloadStatus == 'offline' then
	
		-- We're unable to download anything when offline.
	
		return 'wf', wfDownloadStatus

	end
		
	if wfDownloadStatus == 'sync_paused' and
			( ozBinaryType == 'original' or ozBinaryType == 'proxy' ) then

		-- When sync is paused, don't download any negatives.

		return 'wf', wfDownloadStatus
		
	end
	
	if ozBinaryType == 'original' and 
			( originalVolumeStatus == 'out_of_disk_space' or 
				originalVolumeStatus == 'volume_offline' ) then

		-- Download the original to the main drive when original volume is out of space or offline
		-- if export(ensure_local_original_exists) or loupe(highest) requests it.

		if PixelPathProviderRequestTypes_names.ensure_local_original_exists == maybePriorityName or
			PixelPathProviderRequestTypes_names.highest == maybePriorityName then

			downloadOriginalToMainDrive = true
			
		else
	
			-- Unable to download original because of an offline volume, or space constraints.
			return 'volume', originalVolumeStatus
		
		end

	end
	
	if ozBinaryType == 'proxy' and 
			( proxyVolumeStatus == 'out_of_disk_space' or 
				proxyVolumeStatus == 'volume_offline' ) then
		
		-- Unable to download proxy because of an offline volume, or space constraints.
	
		return 'volume', proxyVolumeStatus
	
	end
	
	if ( ozBinaryType == 'rendition' or 
				ozBinaryType == 'xmp_develop' or 
				downloadOriginalToMainDrive ) and 
			mainVolumeStatus == 'out_of_disk_space' then

		-- Unable to download renditions/xmp because of space constraints.
	
		return 'volume', mainVolumeStatus

	end
	
	return false

end

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

PixelPendingDownloadInfoCachePrototype.isDownloadBlocked =
function( _, scheduler, ozBinaryType, maybeDownloadPriority )

	return isDownloadBlocked( scheduler, ozBinaryType, maybeDownloadPriority )

end

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

function PixelPendingDownloadInfoCachePrototype:maybeCancelExistingDownloads( 
			scheduler, cancelDownloadStatusReason )
	
	local currentDownloadRequests = self:allActiveDownloadRequests()

	cancelDownloadStatusReason =
		string.format( "download task status: %s", cancelDownloadStatusReason )

	for _, downloadRequest in ipairs( currentDownloadRequests ) do
		
		local pixStreamId, localAssetId, ozAssetId, ozBinaryType, 
				ozBinarySize = unpack( downloadRequest )
		
		local pixelProviderPriority = 
				self:getPixelProviderPriorityIfDownloading( 
						ozAssetId, ozBinaryType, ozBinarySize )
		
		local maybeBlockSource, maybeReason = 
				isDownloadBlocked( scheduler, ozBinaryType, pixelProviderPriority )
		
		if maybeBlockSource then
			
			logger:warnf( "Canceling download for %s:%s at priority %s because %s reported: %s",
							pixStreamId, localAssetId, pixelProviderPriority,  maybeBlockSource, maybeReason )

			self:cancelCurrentDownload(	
				cancelDownloadStatusReason, scheduler, ozAssetId, ozBinaryType,
				ozBinarySize )
			
		end
			
	end
	
end

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

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

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

local PixelPathProviderRequestTypes_comparePriorities =
		assert( PixelPathProviderRequestTypes.comparePriorities,
				"PixelPathProviderRequestTypes.comparePriorities does not exist" )

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

local function getCurrPixProviderPriorityForActiveDownloadRequests( 
					pendingDownloadPixProviderPriorities, pixProviderRequest,
					pendingDownloadInfoCache )

	local pixStreamId, localAssetId, _, currPixelProviderPriority =
			unpack( pixProviderRequest )

	local ozAssetId, binaryType, binarySize, _, binaryBucketId =
				pendingDownloadInfoCache:maybeGetActiveDownloadInfoForPixStreamRequest( 
						pixStreamId, localAssetId )

	if not binaryBucketId then

		return pendingDownloadPixProviderPriorities
		
	end

	local downloadInfoKey = string.format( "%s:%s", ozAssetId, binaryBucketId )

	local activeDownloadRequestInfo = 
			pendingDownloadPixProviderPriorities[ downloadInfoKey ] or
			{ currPixelProviderPriority, ozAssetId, binaryType, binarySize }

	-- set the pixel provider priority accordingly

	activeDownloadRequestInfo[ 1 ] =
		PixelPathProviderRequestTypes_comparePriorities(
			activeDownloadRequestInfo[ 1 ], 
			currPixelProviderPriority ) >= 0 and
		activeDownloadRequestInfo[ 1 ] or currPixelProviderPriority

	pendingDownloadPixProviderPriorities[ downloadInfoKey ] = 
		activeDownloadRequestInfo 

	return pendingDownloadPixProviderPriorities

end

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

local function getLowestPixProviderPriorityDownloadInfo( 
					lowestPriorityPreemptibleDownloadInfo, 
					currDownloadInfo,
					preemptingRequestPixProviderPriority )

	if PixelPathProviderRequestTypes_comparePriorities(
			currDownloadInfo[ 1 ],
			preemptingRequestPixProviderPriority ) >= 0 then

			-- this request can't be preempted, it has higher or equal priority
			-- than the preempting request.
			
		return lowestPriorityPreemptibleDownloadInfo

	end

	if not lowestPriorityPreemptibleDownloadInfo then

		-- this is the first preemptible download info we've seen
		return currDownloadInfo

	end

	if PixelPathProviderRequestTypes_comparePriorities(
			currDownloadInfo[ 1 ], 
			lowestPriorityPreemptibleDownloadInfo[ 1 ] ) < 0 then

		-- this request is lower priority
		return currDownloadInfo

	end

	return lowestPriorityPreemptibleDownloadInfo

end

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

local _isDownloadingStates

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

function PixelPendingDownloadInfoCachePrototype:maybePreemptDownload( 
			scheduler, preemptingRequestPixProviderPriority )

	_isDownloadingStates = 
		_isDownloadingStates or
		PixelProviderSharedBlobCache.appendT(
			PixelProviderSharedBlobCache.downloadingBlobStates,
			PixelProviderDownloadToFileUtils.getIsDownloadingToFileStates( 
				scheduler ) )

	local downloadingRequests =
			scheduler
				:_getRequestsWithStatesInPriorityOrder(
					unpack( _isDownloadingStates ) )

	local downloadInfoWithCurrPixProviderPriorities =
			WFFunctionUtils_reduce( {},
				getCurrPixProviderPriorityForActiveDownloadRequests, 
					downloadingRequests,
						self )

	local preemptibleDownloadInfo =
			WFFunctionUtils_reduce( nil, 
				getLowestPixProviderPriorityDownloadInfo,
					KSTableUtils.extractValues( downloadInfoWithCurrPixProviderPriorities ),
					preemptingRequestPixProviderPriority )

	if not preemptibleDownloadInfo then

		return false

	end

	local preemptedPriority, ozAssetId, ozBinaryType, ozBinarySizeName = 
			unpack( preemptibleDownloadInfo )

	local cancelReason = 
			string.format( "request at prioirty %s preempted by higher priority",
							preemptedPriority )

	self:cancelCurrentDownload( cancelReason, scheduler, ozAssetId,
									ozBinaryType, ozBinarySizeName )

	return true

end

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

local function mapPixelProviderPriorityToHttpPriority( currPixelProviderPriority )

	local priorityName = PixelPathProviderRequestTypes_getName(
								currPixelProviderPriority )

	if priorityName == PixelPathProviderRequestTypes_names.highest then
	
		return WFRxHttpClient.requestPriorityHighest
	
	end
	
	if priorityName == PixelPathProviderRequestTypes_names.grid_visible then

		return WFRxHttpClient.requestPriorityHigh

	end

	if priorityName == PixelPathProviderRequestTypes_names.lowest then
		
		return WFRxHttpClient.requestPriorityLow

	end

	return WFRxHttpClient.requestPriorityMedium

end

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

local function isHighHTTPPriority( httpPriority )

	return httpPriority == WFRxHttpClient.requestPriorityHigh or
			httpPriority == WFRxHttpClient.requestPriorityHighest

end

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

local function pixelProviderPriorityShouldPreemptHttpPriority( pixelProviderPriority, httpPriority )

	local pixelProviderHttpPriority = 
		mapPixelProviderPriorityToHttpPriority( pixelProviderPriority )

	return isHighHTTPPriority( pixelProviderHttpPriority ) and 
			WFRxHttpClient.isHigherPriority( 
				pixelProviderHttpPriority, httpPriority )

end

--------------------------------------------------------------------------------
	
PixelPendingDownloadInfoCachePrototype.pixelProviderPriorityShouldPreemptHttpPriority =
function( _, pixelProviderPriority, httpPriority )

	return pixelProviderPriorityShouldPreemptHttpPriority( 
			pixelProviderPriority, httpPriority )
end

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

function PixelPendingDownloadInfoCachePrototype:canStartOrJoinDownload( 
			scheduler, currPixelProviderPriority, ozAssetId, ozBinaryType,
			ozBinarySize ) 

	local maybeCurrHttpPriority =
			self:getHttpPriorityIfDownloading( 
					ozAssetId, ozBinaryType, ozBinarySize ) 

	if maybeCurrHttpPriority then

		local currHttpPriority = maybeCurrHttpPriority

		-- we are already downloading, see if we need to boost priority and
		-- warn that we won't if so.

		local newHttpPriority = 
			mapPixelProviderPriorityToHttpPriority( currPixelProviderPriority )

		if pixelProviderPriorityShouldPreemptHttpPriority( 
				currPixelProviderPriority, currHttpPriority ) then

			logger:warnf( "We have a '%s' pixel provider priority request for an existing download of %s %s for ozAssetId %s that was started at http priority %s. We need the highest http priority in order to make sure this request now takes precedent over all other http requests, so we are going to restart the current http request.",
				PixelPathProviderRequestTypes_getName( currPixelProviderPriority ),
				ozBinaryType, ozBinarySize, ozAssetId, currHttpPriority )

			self:cancelCurrentDownload( 
				"restart at higher priority", scheduler, ozAssetId,
				ozBinaryType, ozBinarySize )

		elseif currHttpPriority ~= newHttpPriority and
			( currHttpPriority == WFRxHttpClient.requestPriorityLow or
				( currHttpPriority == WFRxHttpClient.requestPriorityMedium and
					( newHttpPriority == WFRxHttpClient.requestPriorityHigh or 
						newHttpPriority == WFRxHttpClient.requestPriorityHighest ) ) ) then

			logger:warnf( "We have a '%s' pixel provider priority request for an existing download of %s %s for ozAssetId %s which was started at http priority %s. The new http priority should be %s, which is higher, but we don't support updating the inflight http requests.",
				PixelPathProviderRequestTypes_getName( currPixelProviderPriority ),
				ozBinaryType, ozBinarySize, ozAssetId, currHttpPriority,
				newHttpPriority )

		end

		return true

	end


	return self:canDownload() or
			self:maybePreemptDownload( scheduler, currPixelProviderPriority )

end

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

function PixelPendingDownloadInfoCachePrototype:maybeCancelExistingDownloadsIfBadStatus( scheduler )

	local statusTable = scheduler:getWFDownloadStatus()

	for downloadStatusType, status in pairs( statusTable ) do
	
		if status ~= 'okay' then

			local cancelReason =  
					string.format( "download status type %s has status %s",
										downloadStatusType, status )
		
			self:maybeCancelExistingDownloads( scheduler, cancelReason )
			
		end
		
	end

end

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

local PixelProviderSharedTaskUtils_maybeGetHrefForValidRenditionWithSizeFromRenderInfo =
		PixelProviderSharedTaskUtils.maybeGetHrefForValidRenditionWithSizeFromRenderInfo

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

local PixelProviderSharedTaskUtils_maybeGetHrefForInvalidRenditionWithSizeFromRenderInfo =
		PixelProviderSharedTaskUtils.maybeGetHrefForInvalidRenditionWithSizeFromRenderInfo

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

local PixelProviderSharedTaskUtils_maybeGetHrefForProxyTypeFromRenderInfo =
		PixelProviderSharedTaskUtils.maybeGetHrefForProxyTypeFromRenderInfo

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

local PixelProviderSharedTaskUtils_getOriginalDownloadInfoFromRenderInfo =
		PixelProviderSharedTaskUtils.getOriginalDownloadInfoFromRenderInfo
--------------------------------------------------------------------------------

local function getBinaryUrlAndRevIdFromRenderInfo( maybeBaseRenderInfo, ozBinaryType, ozBinarySubtype )
	
	if type( maybeBaseRenderInfo ) ~= 'table' then

		return nil

	end

	local baseRenderInfo = maybeBaseRenderInfo
	
	local href

	local originalSha256

	if ozBinaryType == 'rendition' and ozBinarySubtype == "fullsize" then

		href = baseRenderInfo.downloadFullRenditionURI

	elseif ozBinaryType == 'rendition' then

		local renditionSize = ozBinarySubtype

		href = PixelProviderSharedTaskUtils_maybeGetHrefForValidRenditionWithSizeFromRenderInfo( 
				baseRenderInfo, renditionSize )

		if not href then

			href = PixelProviderSharedTaskUtils_maybeGetHrefForInvalidRenditionWithSizeFromRenderInfo( 
					baseRenderInfo, renditionSize )

		end

	elseif ozBinaryType == 'proxy' then

		local proxySubtype = ozBinarySubtype

		href = PixelProviderSharedTaskUtils_maybeGetHrefForProxyTypeFromRenderInfo( 
				baseRenderInfo, proxySubtype )

	elseif ozBinaryType == 'original' then

		href, originalSha256  = 
			PixelProviderSharedTaskUtils_getOriginalDownloadInfoFromRenderInfo( 
				baseRenderInfo )

	elseif ozBinaryType == 'xmp_develop' then

		href = baseRenderInfo.ozXmpDevelopLink

	elseif ozBinaryType == 'xmp_sidecar' then

		href = baseRenderInfo.ozXmpLink

	end
	
	if not href then

		logger:warnf( "Unexpected ozBinaryType %s and ozBinarySubtype %s, no url hint provided to binary downloader.", 
				ozBinaryType, ozBinarySubtype )
		
		return

	end
	
	return href, baseRenderInfo.revId, originalSha256

end

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

function PixelPendingDownloadInfoCachePrototype:startNewDownload( 
									binaryRequester, baseRenderInfoCache,
									ozAssetId, ozBinaryType, ozBinarySubtype,
									localAssetId, currPixelProviderPriority,
									renderSize, revId, digest )

	logger:infof( "About to start download %s %s for ozAssetId %s (localAssetId %s priority %s) at size: %s",
					ozBinaryType, ozBinarySubtype, ozAssetId,
					localAssetId, 
					PixelPathProviderRequestTypes_getName( currPixelProviderPriority ),
					renderSize )

	local httpPriority = 
			mapPixelProviderPriorityToHttpPriority( currPixelProviderPriority )
	
	local maybeBaseRenderInfo = baseRenderInfoCache:peekCacheItem( localAssetId )

	local binaryUrl, maybeRevIdFromBaseRenderInfo, maybeOriginalBlobSha256 = 
			getBinaryUrlAndRevIdFromRenderInfo( 
				maybeBaseRenderInfo, ozBinaryType, ozBinarySubtype )

	if not binaryUrl then

		return false, 
				string.format( "missing expected url for download of type: %s subtype: %s",
									ozBinaryType, ozBinarySubtype )

	end
	
	local cancelBinaryRequesterRequestFunc =
			binaryRequester
				:maybeRequestBinaryAtUrl( 
					ozAssetId, ozBinaryType, ozBinarySubtype, binaryUrl, 
					revId or maybeRevIdFromBaseRenderInfo, httpPriority )

	if type( cancelBinaryRequesterRequestFunc ) ~= "function" then

		return false, "binary requester didn't start a request"

	end

	self:addNewPendingDownload( 
			ozAssetId, ozBinaryType, ozBinarySubtype, localAssetId,
			cancelBinaryRequesterRequestFunc, httpPriority, currPixelProviderPriority,
			renderSize, revId, digest, maybeOriginalBlobSha256 )

	return true

end

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

function PixelPendingDownloadInfoCachePrototype:downloadToFileDestPathErrorStream()
	
	return self._downloadToFileDestPathErrorStream

end

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

function PixelPendingDownloadInfoCachePrototype:startNewDownloadToFile( 
			downloadToFileFunc, canonicalLocalPathStream, baseRenderInfoCache,
			localAssetId, ozAssetId, ozBinaryType, ozBinarySubtype,
			currPixelProviderPriority )
	
	local baseRenderInfo = baseRenderInfoCache:peekCacheItem( localAssetId )

	if not baseRenderInfo then

		return false, string.format( "missing base render info for assetId %s", 
										localAssetId )

	end

	local binaryUrl = 
			getBinaryUrlAndRevIdFromRenderInfo( 
				baseRenderInfo, ozBinaryType, ozBinarySubtype )

	if not binaryUrl then

		return false, 
				string.format( "missing expected url for download of type: %s subtype: %s",
									ozBinaryType, ozBinarySubtype )

	end

	local httpPriority = 
			mapPixelProviderPriorityToHttpPriority( currPixelProviderPriority )
	
	local cancelBinaryRequesterRequestFunc 

	local downloadToFileSubscription =
		canonicalLocalPathStream
			:map( 
				function( destPath )

					cancelBinaryRequesterRequestFunc =
						downloadToFileFunc( 
							destPath, ozAssetId, ozBinaryType, ozBinarySubtype,
							binaryUrl, httpPriority )
			
				end )
			:catch(
				function( canonicalLocalPathErrStr )

					self._downloadToFileDestPathErrorStream
						:onNext( 
							false,
							canonicalLocalPathErrStr,
							ozAssetId,
							ozBinaryType,
							ozBinarySubtype )

					return Rx.Observable.empty()

				end )
			:subscribeOnCompleted( 
				function()

					logger:infof( "started maybeRequestBinaryAtUrlToFile for assetId %s %s %s",
									localAssetId, ozBinaryType, ozBinarySubtype )

				end )

	local function cancelDownloadToFileFunc()

		-- stop the above stream
		downloadToFileSubscription:dispose()

		if type( cancelBinaryRequesterRequestFunc ) ~= "function" then

			-- we haven't started a binary request yet, so we consider this
			-- a successful cancel
			return true

		end

		-- otherwise we relay the result of canceling the binary request
		return cancelBinaryRequesterRequestFunc()
		
	end

	self:addNewPendingDownload( 
			ozAssetId, ozBinaryType, ozBinarySubtype, localAssetId,
			cancelDownloadToFileFunc, httpPriority, currPixelProviderPriority )

	return true

end

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

return Rx.Class( "PixelProviderPendingDownloadInfoCache", 
					PixelPendingDownloadInfoCachePrototype )

