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

WFOzRenditionServer.lua

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

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

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

	-- RxLua
local Rx = require 'Rx'

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

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

	-- WF.core.rxlua
local WFRxPromiseBridge = require 'WFRxPromiseBridge'

	-- WF.core.lua.threading
local WFWorkerThread = require 'WFWorkerThread'

	-- WF.core.lua
local KSData = require 'KSData'
local WFObjectProxy = require 'WFObjectProxy'

	-- WF.core.notifications
local KSSystemNotifications = require 'KSSystemNotifications'

	-- WF.core.os
local KSIdGenerator = require 'KSIdGenerator'
local KSDate = require 'KSDate'
local WFPlatformInfo = require 'WFPlatformInfo'

	-- WF.core.reactive
local KSPromises = require 'KSPromises'
local KSReactive = require 'KSReactive'

	-- WF.core.utils
local KSMemoryCache = require 'KSMemoryCache'
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFCapabilities = require 'WFCapabilities'
local WFCloseSupport = require 'WFCloseSupport'
local WFCatUrlUtils = require 'WFCatUrlUtils'
local WFUnknown = require 'WFUnknown'

	-- WF.core.performance
local WFPerformanceReport = require 'WFPerformanceReport'

	-- WF.oz_client.catalog.rendition_server
local WFOzRenditionServerUtils = require 'WFOzRenditionServerUtils'

	-- WF.oz_client.catalog
local OzCatalogUrls = require 'OzCatalogUrls'
local OzCatHelperFns = require 'OzCatHelperFns'
local OzReferenceDimension = require 'OzReferenceDimension'

	-- WF.web.json
local KSJsonDecoder = require 'KSJsonDecoder'

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

local usingWFDocStore = require 'WFCapabilities'.getCapability 'OzDocStore.disable'

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

local USE_INDEX_STORE_FOR_MD5_LOOKUP = false
	-- We might as well try fetching the document value because eventually it
	-- gives us better update behavior.
	
local FALLBACK_TO_INDEX_STORE_FOR_MD5_LOOKUP = true
	-- However, if we get a useful value from the index server first, that's
	-- obviously desirable.

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

local DEBUG_DOWNLOAD_FETCH_CANCELING = false
local LOG_AND_COUNT_RENDITION_REQUESTS = false
local LOG_MD5_UPDATES = false
local LOG_THUMBNAIL_REQUESTS = false
local CAPTURE_REQUEST_TIMINGS = false

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

local assert = assert
local check = assert( WFArguments.check )
local KSTableUtils_makeFullyWeak = assert( KSTableUtils.makeFullyWeak )
local KSTableUtils_derefOpt = assert( KSTableUtils.derefOpt )
local OzCatHelperFns_DISABLE_MOST_FETCHES = OzCatHelperFns.DISABLE_MOST_FETCHES
local OzCatHelperFns_processHttpRequestResult = OzCatHelperFns.processHttpRequestResult
local OzReferenceDimension_getReferenceDimensions =
	assert( OzReferenceDimension.getReferenceDimensions )
local WFCloseSupport_fetchCached = assert( WFCloseSupport.fetchCached )
local WFCloseSupport_hasBeenOrWillBeClosed = assert( WFCloseSupport.hasBeenOrWillBeClosed )
local WFOzRenditionServerUtils_touchOzRendition =
	assert( WFOzRenditionServerUtils.touchRendition )
local WFOzRenditionServerUtils_shouldTouchOzRendition =
	assert( WFOzRenditionServerUtils.shouldTouchRendition )
local WFOzRenditionServerUtils_getFullMD5HashedKey = WFOzRenditionServerUtils.getFullMD5HashedKey


local ozLinkNameForOzRenditionName =
	assert( WFOzRenditionServerUtils.ozLinkNameForRenditionName )
local renditionKindForOzRenditionName =
	assert( WFOzRenditionServerUtils.renditionKindForRenditionName )

local now = KSDate.timeNowForAnimation

local asKSPromise = WFRxPromiseBridge

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

local renditionServerLog = WFLogger 'WFOzRenditionServer'
	-- :setLevel 'debug'

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

local jpegDecodeService = WFWorkerThread 'KSJpegDecodeServiceEPInit.lua'

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

local DOWNLOAD_THUMBNAIL2X_IF_PREVIEW = false
	-- Should we try to download the 2x thumbnail if we have a preview
	-- even if the 2x thumbnail isn't available?
	
local OBEY_DEFER_TRIGGERS = WFPlatformInfo.isiOS or WFPlatformInfo.isAndroid
	-- Should we obey defer triggers?
	-- We obey them on mobile platforms and ignore them on "desktop" class
	-- platforms.

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

-- Device renditions include the small preview version of the preview rendition

local deviceRenditionNameArg = 'WFOzRenditionServer.DeviceRenditionName'

local validRenditionNames = {
	"thumbnail",
	"thumbnail2x",
	"smallPreview",
	"preview",
	"master",
}

WFArguments.registerChecker( deviceRenditionNameArg, KSTableUtils.mapArrayToSet( validRenditionNames ) )

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

-- Oz renditions do not include the small preview

local ozRenditionNameArg = 'WFOzRenditionServer.OzRenditionName'

WFArguments.registerChecker( ozRenditionNameArg, {
	thumbnail = true,
	preview = true,
	master = true,
	thumbnail2x = true,
} )

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

local function readRenditionLink( docValue, ... )

	local pathLen = select( '#', ... )
	if pathLen < 2 then
		return KSTableUtils_derefOpt( docValue, ... )
	end
	
	local packed = { ... }
	local link, part
	link = packed[ pathLen ]
	local path = { unpack( packed, 1, pathLen - 1 ) }
	if link == 'href' or link == 'invalid' then
		part = link
		link = packed[ pathLen - 1 ]
		path = { unpack( packed, 1, pathLen - 2 ) }
	end
	
	local result
	
	local function try( what )
		if what then
			link = what
		end
		local t = KSTableUtils.shallowcopy( path )
		t[ #t + 1 ] = link
		if part then
			t[ #t + 1 ] = part
		end
		result = KSTableUtils_derefOpt( docValue, unpack( t, 1, pathLen ) )
	end
	
	try()
	
	if not result then
		if link == '/rels/rendition_type/144h' then
			try '/rels/rendition_type/thumbnail2x'
		end
	end
	
	if not result then
		if link == '/rels/rendition_type/1024' then
			try '/rels/rendition_type/2048'
		end
	end

	return result

end

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

local ozRenditionNameForDeviceRenditionName = {
	thumbnail = 'thumbnail',
	thumbnail2x = 'thumbnail2x',
	preview = 'preview',
	smallPreview = 'preview',
	master = 'master',
}

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

local decodeOptionForDeviceRenditionName = {
	smallPreview = { 720, 720 }, -- size limit
}

-- On the iPhone, we want the small previews to be 320 points wide with
-- unlimited height.

if WFPlatformInfo.isiPhone then
	if WFPlatformInfo.isHighDPIDevice then
		decodeOptionForDeviceRenditionName.smallPreview = { 640, 0 }
	else
		decodeOptionForDeviceRenditionName.smallPreview = { 320, 0 }
	end
end

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

local cascadedBehaviorCacheKeyToDeviceRenditionNames = {
	_previewBCache2 = WFCapabilities.callFilters(
		"WFOzRenditionServer.previewCacheSequence",
		{ 'preview', 'thumbnail2x', 'thumbnail', sequential = false }
	),
	_smallPreviewBCache2 = WFCapabilities.callFilters(
		"WFOzRenditionServer.smallPreviewCacheSequence",
		{ 'smallPreview', 'thumbnail2x', 'thumbnail', sequential = true }
	),
	_thumbnailBCache2 = WFCapabilities.callFilters(
		"WFOzRenditionServer.thumbnailCacheSequence",
		{ 'thumbnail2x', 'thumbnail', sequential = true }
	)
}

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

local thumbnailCacheSize, minimalThumbnailCacheSize, previewCacheSize

if WFPlatformInfo.isMac then
	thumbnailCacheSize = WFCapabilities.callFilters( "WFOzRenditionServer.thumbnailCacheSize", 4000 )
	minimalThumbnailCacheSize = 4000
	previewCacheSize = 10
elseif WFPlatformInfo.isiOS then
	thumbnailCacheSize = WFCapabilities.callFilters( "WFOzRenditionServer.thumbnailCacheSize", 100 )
	minimalThumbnailCacheSize = 20
	previewCacheSize = 3
elseif WFPlatformInfo.isAndroid then
	thumbnailCacheSize = WFCapabilities.callFilters( "WFOzRenditionServer.thumbnailCacheSize", 100 )
	minimalThumbnailCacheSize = 20
	previewCacheSize = 3
else
	-- GRUNGE [arahn 2012-08-28]: What are sensible defaults for other platforms?
	thumbnailCacheSize = 100
	minimalThumbnailCacheSize = 20
	previewCacheSize = 3
end

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

-- Cache info for renditions. We maintain a KSMemoryCache for images and a fully-weak
-- cache for behaviors.

-- imageCacheName: Name of the cache for decoded images. Indexed by MD5 hash.
-- behaviorCacheName: Name of the cache for fetch behaviors. Indexed by photo id.
-- minimalImageCacheSize: Number of entries to allow in the cache when we minimize
-- standardImageCacheSize: Number of entries to allow in the cache when not minimized

-- We map the rendition names down to a smaller set of rendition kinds so that we can employ
-- common processing where appropriate.

local cacheInfoForDeviceRenditionName = {

	thumbnail = {
		imageCacheName = '_thumbnailCache',
		behaviorCacheName = '_thumbnailBCache',
		minimalImageCacheSize = minimalThumbnailCacheSize,
		standardImageCacheSize = thumbnailCacheSize,
	},

	thumbnail2x = {
		imageCacheName = '_thumbnailCache',
			-- [mhamburg 2012-03-22]
			-- Shared thumbnail cache with the small thumbnails. This is safe
			-- because the cache is indexed by MD5 hash just like the data cache.
			-- Note that this means that we will perform some operations on this cache
			-- twice such as creating it and minimizing it but that's harmless and beats
			-- managing two cahes or having extra logic to decide which cache we are
			-- talking to.
		behaviorCacheName = '_thumbnail2xBCache',
		minimalImageCacheSize = minimalThumbnailCacheSize,
		standardImageCacheSize = thumbnailCacheSize,
	},

	preview = {
		imageCacheName = '_previewCache',
		behaviorCacheName = '_previewCacheB',
		minimalImageCacheSize = previewCacheSize,
		standardImageCacheSize = previewCacheSize,
	},

	smallPreview = {
		imageCacheName = '_previewCache',
		behaviorCacheName = '_smallPreviewCacheB',
		minimalImageCacheSize = previewCacheSize,
		standardImageCacheSize = previewCacheSize,
	},

}

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

local kDownsampledOzRenditions = {
	preview = true,
	thumbnail = true,
	thumbnail2x = true
}

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

local kDiagnosticTimepoints = { "start", "md5QueryResult", "md5", "startDownload", "finishDownload", "deliver" }

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

local function _fetchFields( t, fields, idx, n )
	if n > idx then
		return t[ fields[ idx ] ], _fetchFields( t, fields, idx + 1, n )
	else
		return t[ fields[ idx ] ]
	end
end
local function fetchFields( t, fields )
	return _fetchFields( t, fields, 1, #fields )
end

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

local function recordDiagnosticTiming( self, photoId, renditionName, stage, optTime )
	local requestTiming = CAPTURE_REQUEST_TIMINGS and self._requestTimings[ photoId ]
	if requestTiming then
		local timingsForRendition = requestTiming[ renditionName ]
		if timingsForRendition == 'delivered' then
			return
		end
		if not timingsForRendition then
			timingsForRendition = {}
			requestTiming[ renditionName ] = timingsForRendition
			self._velocityBuffer[ renditionName ]:add( function()
				return
					fetchFields( timingsForRendition, kDiagnosticTimepoints )
			end )
		end
		if stage then
			if not timingsForRendition[ stage ] then
				timingsForRendition[ stage ] = ( optTime or now() ) - requestTiming.start
			end
		end
		if stage == 'deliver' or stage == nil then
			requestTiming[ renditionName ] = 'delivered'
			local allDelivered = true
			for _, v in pairs( requestTiming ) do
				if v ~= 'delivered' then
					allDelivered = false
					break
				end
			end
			if allDelivered then
				self._requestTimings[ photoId ] = nil
			end
		end
	end
end

local velocityStages = {
	"start",
	unpack( validRenditionNames ),
}

local function visualizeVelocities( velocityBuffers )
	local result = {}
	
	for index, renditionName in ipairs( velocityStages ) do
		result[ string.format( "%d_%s", index, renditionName ) ] = { velocityBuffers[ renditionName ]:visualize( 'boxAndWhisker' ) }
	end
	
	return result
end

local function buildVelocityBufferForRenditions()
	local result = {}
	
	for index, renditionName in ipairs( velocityStages ) do
		result[ renditionName ] = WFDiagnosticLog.makeRingBuffer( kDiagnosticTimepoints )
	end
	
	return result
end

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

-- Check to see whether the proxy reports itself as closed.

local function checkProxy( obj )

	if not obj then
		return true
	end
	
	if type( obj ) ~= 'userdata' then
		return true
	end
	
	if obj:proxyHasBeenOrWillBeClosed() then
		return false
	end
	
	return true

end

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

local function fetchCachedBehavior( cache, key, checkObjectProxy )

	return KSReactive.fetchCachedBehavior(
					cache, key, checkObjectProxy and checkProxy )

end

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

local function getBehaviorCache( self, key )

	local cache = self[ key ]
	
	if not cache then
		cache = KSTableUtils_makeFullyWeak()
		self[ key ] = cache
	end
	
	return cache

end

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

local function flushCaches( self )
	
	for _, info in pairs( cacheInfoForDeviceRenditionName ) do
		self[ info.imageCacheName ]:flush()
		self[ info.behaviorCacheName ] = false
	end
	
	for cacheName in pairs( cascadedBehaviorCacheKeyToDeviceRenditionNames ) do
		self[ cacheName ] = false
	end
	
end


--============================================================================--
--[[!
	\class WFOzRenditionServer

	\brief This class provides renditions (such as thumbnails and previews)
		to Oz clients. It manages the local cache of renditions and triggers
		downloads from the Oz server when necessary.
--]]

-- @mark WFOzRenditionServer class

local renditionServerMethods = {

	_catalogId = false,
		-- (string) catalog ID (currently used only for diagnostic log)

	_docStore = false,
		-- (OzDocStore)
		
	_dataCache = false,
		-- (KSDataCache)
	
	_indexStore = false,
		-- (WFIndexStore)
	
	_ozUser = false,
		-- (OzUser)
	
	_cacheEpochB = false,
		-- (B< number >) cache epoch

	_processHttpRequestResultFn = false,
		-- (function) call to process HTTP request results and update
		-- access status as appropriate
	
	_recentPreviews = false,
		-- (table) most recent preview behaviors

	_velocityBuffer = false,
		-- diagnostic ring buffer for watching how long thumbnail requests
		-- take to fulfill
	
	_requestTimings = false,
		-- weigh-points for requests as they move throught the pipe
		
	-- _activeThumbnailDataCacheFetchesBlocker = false,
		-- Blocker. Disallow other data cache fetches while we have thumbnail
		-- fetches active.
		
	_md5sQueryCache = false,
		-- Fully weak table mapping photoId to MD5 queryB
		
	_md5ForRenditionCache = false,
		-- Oz rendition name --> fully weak: photoId --> behavior
		
	_decodeImageCache = false,
		-- Fully weak mapping from MD5s to promises for decoded images
		
	_wfCatalog = false,
		-- (WFCatalog) instance
		
	WFCloseSupport.addCloseSupportMethods,
	
}

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

for _, info in pairs( cacheInfoForDeviceRenditionName ) do

	-- Can't assert here -- thumbnail and thumbnail2x share the same
	-- imageCacheName. (Intentional.)

	-- assert( renditionServerMethods[ info.imageCacheName ] == nil, "image cache item already defined" )
	assert( renditionServerMethods[ info.behaviorCacheName ] == nil, "behavior cache item already defined" )
	
	renditionServerMethods[ info.imageCacheName ] = false -- KSMemoryCache of images
	renditionServerMethods[ info.behaviorCacheName ] = false -- fully weak cache of behaviors

end

--------------------------------------------------------------------------------
--[[!
	\brief Creates a new rendition server for a catalog.
	
	\param catalog (OzCatalogService) The catalog.
		
--]]

function renditionServerMethods:__init( catalog, ozUser, catalogId, dataCache, wfCatalog )

	self._catalogId = catalogId
	self._docStore = assert( catalog:docStore() )
	self._indexStore = assert( catalog:indexStore() )
	self._ozUser = assert( ozUser )
	self._wfCatalog = assert( wfCatalog )
	
	self:setOwned {
		_dataCache = dataCache
	}
	
	self._recentPreviews = {}
	
	-- self._activeThumbnailDataCacheFetchesBlocker = Blocker()
	
--	catalog:addCloseTarget( self )
	-- callers should assign using setOwned instead of us assuming ownership by that param
	
	self:setOwned {
		_cacheEpochB = WFOzRenditionServerUtils.cacheEpochB( self._docStore, self._indexStore )
	}

	self._processHttpRequestResultFn = function( ... )
		return OzCatHelperFns_processHttpRequestResult( catalog, ... )
	end

	for _, info in pairs( cacheInfoForDeviceRenditionName ) do
		self[ info.imageCacheName ] = KSMemoryCache( info.standardImageCacheSize )
	end
	
	for cacheName in pairs( cascadedBehaviorCacheKeyToDeviceRenditionNames ) do
		rawset( self, cacheName, false )
	end

	-- Purge caches when going into background and when we receive
	-- memory warnings.

	KSSystemNotifications.ownedOnNotificationDo(
		'didEnterBackground',
		self,
		function() flushCaches( self ) end )
		
	KSSystemNotifications.ownedOnNotificationDo(
		'didReceiveMemoryWarning',
		self,
		function() flushCaches( self ) end )

	WFDiagnosticLog.addLogProvider( self )

	self._velocityBuffer = buildVelocityBufferForRenditions()
	
	if CAPTURE_REQUEST_TIMINGS then
		self._requestTimings = {}
	end
	
	self._md5sQueryCache = KSTableUtils_makeFullyWeak()
	
	self._md5ForRenditionCache = { }
	
	for ozRenditionName in pairs( ozLinkNameForOzRenditionName ) do
		self._md5ForRenditionCache[ ozRenditionName ] = KSTableUtils_makeFullyWeak()
	end
	
	self._decodeImageCache = { }
	
	for deviceRenditionName in pairs( cacheInfoForDeviceRenditionName ) do
		self._decodeImageCache[ deviceRenditionName ] = KSTableUtils_makeFullyWeak()
	end
	
end

--------------------------------------------------------------------------------
--[[!
	\brief Flushes all the 'preview' type caches, and drives garbage collection.
]]--

function renditionServerMethods:flushPreviewCache()

	for deviceRenditionName, info in pairs( cacheInfoForDeviceRenditionName ) do
	
		local ozRenditionName = ozRenditionNameForDeviceRenditionName[ deviceRenditionName ]

		if renditionKindForOzRenditionName[ ozRenditionName ] == 'preview' then
			self[ info.imageCacheName ]:flush()
		end

	end

	-- We've just dumped a few strong references to sizeable image data.
	-- Drive the GC.

	collectgarbage()
	collectgarbage()

end

--------------------------------------------------------------------------------
--[[!
	\brief Sets whether to use a minimal thumbnail cache.
	
	When transitioning to a minimal thumbnail cache, the call triggers garbage collection
	in order to reclaim any image data that has been made eligible for collection.
	
	\param minimal (boolean) The new value for the preference, \c true to use minimal caching.
]]--

function renditionServerMethods:setUseMinimalThumbnailCache( --[[boolean]] minimal )

	for deviceRenditionName, info in pairs( cacheInfoForDeviceRenditionName ) do
	
		local ozRenditionName = ozRenditionNameForDeviceRenditionName[ deviceRenditionName ]

		if renditionKindForOzRenditionName[ ozRenditionName ] == 'thumbnail' then
			self[ info.imageCacheName ]:adjustMaximumEntries(
				minimal and info.minimalImageCacheSize or info.standardImageCacheSize )
		end

	end

	if minimal then

		-- We've just dumped a bunch of strong references to image data.
		-- Drive the GC.

		collectgarbage()
		collectgarbage()

	end

end
local kIndexForRendition = {
	thumbnail = 1,
	thumbnail2x = 2,
	preview = 3,
	master = 5,
}

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

local function getPhotoMD5sB( self, photoId )

	local cachedB = WFCloseSupport_fetchCached( self._md5sQueryCache, photoId )
	
	if cachedB then
		return cachedB:autoCloseRelayB()
	end
	
	local cacheKey
	
	if type( photoId ) == 'string' then
		cacheKey = 'md5:' .. tostring( photoId )
	end
	
	local queryB = self._indexStore:queryB {
		cacheKey = cacheKey,
		enqueue = 'fast',
		sql = [[SELECT
			thumbnail, thumbnail2x, preview, master
		FROM
			coreMD5
		WHERE
			docId = ?
		]],
		photoId
	}
	
	local resultB = queryB
	
	if CAPTURE_REQUEST_TIMINGS then
		resultB = resultB:closingMapB( function( ... )
			recordDiagnosticTiming( self, photoId, 'thumbnail', 'md5QueryResult' )
			recordDiagnosticTiming( self, photoId, 'thumbnail2x', 'md5QueryResult' )
			recordDiagnosticTiming( self, photoId, 'preview', 'md5QueryResult' )
			recordDiagnosticTiming( self, photoId, 'master', 'md5QueryResult' )
			return ...
		end )
	end
	
	if LOG_MD5_UPDATES then
		local text = string.format( "md5s updated: photoId = %s", photoId )
		resultB = resultB:closingMapB( function( ... )
			renditionServerLog:debugf( text )
			return ...
		end )
	end
	
	resultB = resultB:closingMapB( function( rows )
		return KSTableUtils_derefOpt( rows, 1 )
	end )
		
	self._md5sQueryCache[ photoId ] = resultB
	
	return resultB:autoCloseRelayB()

end

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

local function unhintedMD5ForOzRenditionViaIndexStoreB(
					self, photoId, ozRenditionName )
					
	local md5sB = getPhotoMD5sB( self, photoId )

	local md5B = md5sB:closingMapB( function( md5s )
		local md5 = md5s and md5s[ kIndexForRendition[ ozRenditionName ] ]
		return md5
	end )
	
	return md5B
					
end

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

local function unhintedMD5ForOzRenditionB(
					self, photoId, ozRenditionName, useIndexStore, optDiagnosticLogEntry )
					
	local md5B
					
	if USE_INDEX_STORE_FOR_MD5_LOOKUP and useIndexStore then
	
		md5B = unhintedMD5ForOzRenditionViaIndexStoreB( self, photoId, ozRenditionName )
		
	else
	
		local docValueB = self._docStore:docValueCheckTypeB( 'asset', photoId )
		
		md5B = docValueB:closingMapB( function( docValue )
		
			if usingWFDocStore then
				
				return KSTableUtils_derefOpt(
				docValue, '_localOnly', 'renditions', ozRenditionName, 'md5' )
				
			else
			
				return KSTableUtils_derefOpt(
					docValue, '_localOnly', ozRenditionName, 'md5' )
			end
		end )
		
		if FALLBACK_TO_INDEX_STORE_FOR_MD5_LOOKUP and useIndexStore then
		
			md5B = md5B:closingMapSwitchB( function( md5 )
				if md5 then
					return KSReactive.constantB( md5 )
				else
					return unhintedMD5ForOzRenditionViaIndexStoreB( self, photoId, ozRenditionName )
				end
			end )
			
		end
	
	end
	
	-- Log it if appropriate
	
	if optDiagnosticLogEntry then
	
		md5B = md5B:closingMapB( function( ... )
		
			local md5 = ( ... )
		
			if md5 then
				optDiagnosticLogEntry.md5 = md5
				optDiagnosticLogEntry.time_md5 = KSDate.timeInIso8601Format( nil, true )
			end
		
			return ...
		
		end )
	
	end
	
	-- Touch the rendition if appropriate
	
	if WFOzRenditionServerUtils_shouldTouchOzRendition( ozRenditionName ) then
		
		md5B = md5B:closingMapB( function( ... )
		
			local md5 = ( ... )
		
			if md5 then
			
				WFOzRenditionServerUtils_touchOzRendition( self._docStore,
														   self._cacheEpochB,
														   photoId,
														   ozRenditionName )
		
			end
			
			return ...

		end )
		
	end

	return md5B

end

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

-- We compute the current MD5 hash by or-ing together the MD5 hashes from the
-- database lookup (a) and the hint or pass through (b).

local function orMD5( a, b )

	if a then
		return a
	end
	
	return b

end

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

local function md5ForOzRenditionB( self, photoId, ozRenditionName, md5hint,
								   optDiagnosticLogEntry )
								
	local behaviorCache = self._md5ForRenditionCache[ ozRenditionName ]
								
	local cachedB = WFCloseSupport_fetchCached( behaviorCache, photoId )
							
	if cachedB then
		return cachedB:autoCloseRelayB()
	end
	
	local md5B
	
	if md5hint then
	
		-- If we have an MD5 hint, then construct a behavior that starts as the
		-- hint and then becomes the fetch.
	
		-- Timer
		--		> md5ForRendition behavior (initial value and subsequent values)
		--		> filter nil
		--		start with the hint value
		
		md5B = KSReactive.falseThenTrueB( 0.5 )
			:closingMapSwitchB( function( doQuery )

				if not doQuery or WFCloseSupport_hasBeenOrWillBeClosed( self ) then
					return KSReactive.constantB( nil )
				end
				
				-- Given that we had an MD5 hint, it presumably came from the index store
				-- so we aren't interested in looking for more data from the index store.

				return unhintedMD5ForOzRenditionB(
							self, photoId, ozRenditionName, false, optDiagnosticLogEntry )

			end )
	
	else
	
		md5hint = nil	-- Avoid having false also in the system
	
		-- Otherwise, just construct the fetch.
		
		md5B = unhintedMD5ForOzRenditionB(
					self, photoId, ozRenditionName, true, optDiagnosticLogEntry )
					
	end
	
	local variableB = KSReactive.variableB( md5hint )
	
	local resultB = KSReactive.closingDataCallB( orMD5, md5B, variableB )
	
	rawset( resultB, '_WFOzRenditionServer_md5Variable', variableB )
	
	behaviorCache[ photoId ] = resultB
	
	return resultB:autoCloseRelayB()

end

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

-- Bypass the index store for doing an MD5 update. We also do this when the
-- data cache read fails.

local function updateMD5ForOzRendition(
					self, photoId, ozRenditionName, ... )

	local behaviorCache = self._md5ForRenditionCache[ ozRenditionName ]
								
	local cachedB = WFCloseSupport_fetchCached( behaviorCache, photoId )
	
	if not cachedB then
		return
	end
	
	local variableB = rawget( cachedB, '_WFOzRenditionServer_md5Variable' )
	
	if variableB and not WFCloseSupport_hasBeenOrWillBeClosed( variableB ) then
		variableB:set( ... )
	end

end

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

local function isAssetMetadataUploadedToOz( docValue )

	local spm = docValue._serverProvidedMetadata
	
	if spm then
	
		local revId = docValue._revId
		
		local revIds = spm.revision_ids
		
		if revIds then
		
			local cnt = #revIds
			
			for i = cnt, 1, -1 do
				
				local ozRevId = revIds[ i ]
				if revId == ozRevId then
					return true
				end
				
			end
			
		end
		
	end
	
	return false
	
end

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

local function shouldDownloadOzRendition( docValue, photoId, ozRenditionName )

	renditionServerLog:debugf( "shouldDownloadOzRendition sniffing docValue %s", docValue )

	-- If we're still waiting for docValue, punt for now.

	if not docValue then
		renditionServerLog:infof( "shouldDownloadOzRendition %s %s = false: no docValue",
								   photoId, ozRenditionName )
		return false
	end

	-- Is local version current? If so, nothing else matters.
	
	local localDescription = KSTableUtils_derefOpt( docValue, '_localOnly', ozRenditionName  )

	local localRenditionIsForRev = localDescription and localDescription.revId
	
	-- Extract the revision ID from the server link for this rendition.
	
	local ozLinkName = ozLinkNameForOzRenditionName[ ozRenditionName ]

	local serverRevisionId = readRenditionLink( docValue,
													'_serverProvidedMetadata',
													'links',
													ozLinkName,
													'href' )

	local serverRevisionInvalid = readRenditionLink( docValue,
													'_serverProvidedMetadata',
													'links',
													ozLinkName,
													'invalid' )
			
	local serverRenditionId
	
	if serverRevisionId then
		-- TODO [arahn 2013-05-29]: WHY are we splitting apart the URL like this?
		-- we could just use the renditionUrl in its entirety to decide
		-- if the local rendition matches.  see StopTheRenditionInsanity elsewhere
		-- in this file.
		serverRevisionId, serverRenditionId = serverRevisionId:match( '([0-9a-f]+)%/renditions%/([0-9a-f]+)$' )
	end
	
	if localDescription
		and localDescription.revId
		and localDescription.md5
	then

		
		if docValue
			and isAssetMetadataUploadedToOz( docValue )
			and ( localRenditionIsForRev ~= serverRevisionId )
			and ( serverRevisionId ~= nil )
			and ( not serverRevisionInvalid )
		then
			return true
		end

		renditionServerLog:debugf( "shouldDownloadOzRendition %s %s = false: local rev is valid",
								   photoId, ozRenditionName )
		return false

	else

		renditionServerLog:debugf( "shouldDownloadOzRendition %s %s rejected local rev based on"
								   .. " serverRevisionId = %s and localRenditionIsForRev = %s"
								   .. " serverRenditionId = %s ",
								   photoId, ozRenditionName, serverRevisionId,
								   localRenditionIsForRev, serverRenditionId )

	end

	-- Local isn't current: Download version from Oz unless Oz thinks it's invalid.
	
	local serverDescription = readRenditionLink( docValue, '_serverProvidedMetadata', 'links', ozLinkName )
	
	-- If we lack a server description for the 2x thumbnails, we will still try
	-- to download them if the server has a valid preview. Note that an invalid 2x
	-- thumbnail on the server will not result in a download.
	
	if DOWNLOAD_THUMBNAIL2X_IF_PREVIEW then

		if not serverDescription and ozRenditionName == 'thumbnail2x' then
			local previewLinkName = ozLinkNameForOzRenditionName[ 'preview' ]
			serverDescription = readRenditionLink( docValue, '_serverProvidedMetadata', 'links', previewLinkName )
		end

	end

	local result = ( serverDescription and not serverDescription.invalid ) or not localDescription

	renditionServerLog:debugf( "shouldDownloadOzRendition %s %s = %s: based on SD = %s",
							   photoId, ozRenditionName, result, serverDescription )

	return result

end

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

local function getCachedImage( imageCache, deviceRenditionName, key )

	if not imageCache or not key or not deviceRenditionName then
		return nil
	end
	
	key = string.format( "%s:%s", deviceRenditionName, key )

	local cachedImage = imageCache:getData( key )
	
	if not cachedImage then
		return nil
	end
	
	if WFCloseSupport_hasBeenOrWillBeClosed( cachedImage ) then
		return nil
	end
	
	if cachedImage.isClosed and cachedImage:isClosed() then
		return nil
	end
	
	if cachedImage.proxyHasBeenOrWillBeClosed and cachedImage:proxyHasBeenOrWillBeClosed() then
		return nil
	end
	
	return cachedImage

end

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

local function storeCachedImage( imageCache, deviceRenditionName, key, value )

	if not imageCache or not key or not value or not deviceRenditionName then
		return
	end
	
	key = string.format( "%s:%s", deviceRenditionName, key )
	
	imageCache:add( key, value )

end

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

-- Get the value for a URL retrying once if we get a Bad Gateway error

local function getWithRetryP( self, ozUser, url, highPriority, ozRenditionName, docValue )

	-- Do a fetch

	local requestPriority = WFRxHttpClient.requestPriorityMedium
	
	if highPriority then
		requestPriority = WFRxHttpClient.requestPriorityHigh
	end
	
	local options = {
		requestPriority = requestPriority,
	}
			
	local wfCatSpec = self._wfCatalog:ozCatalogSpec()
	
	local getRequest = wfCatSpec:wfOzUser():getBinary( url, options )
	local p = WFPerformanceReport.wrapObservable( 'WFOzRenditionServer: get ' .. ozRenditionName, getRequest, {
				url = url,
				renditionName = ozRenditionName,
			}, 
			WFRxHttpClient.extractHttpTimingForPerformanceReport )
		:call( asKSPromise )

	-- But we want to retry for a Bad Gateway error (502)

	p = p:mapDataP( function( status, data, headers )

	    -- Retry

	    if headers and headers.status_code == '502' then
	        
			local getRequest = wfCatSpec:wfOzUser():getBinary( url, options )
			
			return WFPerformanceReport.wrapObservable( 'WFOzRenditionServer: get ' .. ozRenditionName, getRequest, {
				url = url,
				renditionName = ozRenditionName,
			}, WFRxHttpClient.extractHttpTimingForPerformanceReport )
			:call( asKSPromise )
		end

		if headers and headers.status_code == '404' then
			
			local renditionGenerateUrl = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links',
											 '/rels/latest_revision', 'href' ) or ''
					
			renditionGenerateUrl = string.format( '%s%s', renditionGenerateUrl, "/renditions" )
			
			renditionGenerateUrl = OzCatHelperFns.checkForRelativeUrlAndAppendBase( renditionGenerateUrl, OzCatalogUrls.baseServerUrl )
			
			local ozRendition = ozRenditionName
			
			local ozLinkName = ozLinkNameForOzRenditionName[ozRendition]
			local renditionToGenerate = ozLinkName:match( '/rels/rendition_type/([0-9a-zA-Z]+)$' )
			
			local httpOptions = {
				requestMethod = 'POST',
				
				requestHeaders = {
					{ header = 'X-Generate-Renditions', value = renditionToGenerate },
				}
			}
			
			if not renditionGenerateUrl then
				return KSPromises.SAME
			end
			
			return wfCatSpec:wfOzUser():put( renditionGenerateUrl, httpOptions )
			
			:call( asKSPromise )
			:mapDataP( function( status, data, headers )
			
				-- we error out here as we cannot do more than waiting for Oz to re-generate the rendition
				-- once the rendition is re-created we'll get notified via the updates feed
				return 'error', "generated new rendition via Oz"
			end )
			
		end

	    -- Otherwise, just stay as we were

	    return KSPromises.SAME

	end )

	return p

end

function renditionServerMethods:generateFullResOnOz(photoId, isSpaceAsset)


    local wfCatSpec = self._wfCatalog:ozCatalogSpec()

    local docValueP = self._docStore:docValueOnceP( 'asset', photoId )

	local linkField = "/rels/rendition_create"

	if isSpaceAsset then

		linkField = "/rels/rendition_generate/fullsize"

	end

    return docValueP:mapP( function( _, docValue )

        local renditionGenerateUrl = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links',
            linkField, 'href' ) or ''

        local revisionIds = docValue._serverProvidedMetadata.revision_ids

        local lastRevisionId = revisionIds and revisionIds[#revisionIds]

        renditionGenerateUrl = string.gsub(renditionGenerateUrl, '{revision_id}', lastRevisionId)

		local renditionId = ''

		if isSpaceAsset then
			renditionId = KSIdGenerator.uuidInLowercaseHex()
		end

        renditionGenerateUrl = string.gsub(renditionGenerateUrl, '{rendition_id}', renditionId)

        renditionGenerateUrl = OzCatHelperFns.checkForRelativeUrlAndAppendBase( renditionGenerateUrl, OzCatalogUrls.baseServerUrl )

        local ozLinkName = '/rels/rendition_type/fullsize'
        local renditionToGenerate = ozLinkName:match( '/rels/rendition_type/([0-9a-zA-Z]+)$' )

        local httpOptions = {
            requestMethod = 'POST',

            requestHeaders = {
                { header = 'X-Generate-Renditions', value = renditionToGenerate },
            }
        }

        if not renditionGenerateUrl then
            return KSPromises.SAME
        end

		renditionServerLog:debugf( "generation url is = %s", renditionGenerateUrl )

        return wfCatSpec:wfOzUser():put( renditionGenerateUrl,'', httpOptions)
        :call( asKSPromise )
		:mapP(function(status, data, headers)

			if data == nil and isSpaceAsset then

				data = {
					url = renditionGenerateUrl
				}

			end

			return status, data, headers

		end)
    end)

end

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

local downloadPromiseCache = KSTableUtils_makeFullyWeak()
	-- A mapping from rendition URL to promise for its download
	-- We cache here rather than in the HTTP client because the HTTP client
	-- moves to aggressively close the data as it returns it which makes it
	-- difficult to return the same value from multiple requests.

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

local function downloadOzRenditionForUrlP( self, renditionUrl, highPriority, ozRenditionName, docValue )

	local cachedP = downloadPromiseCache[ renditionUrl ]
	
	if cachedP and not cachedP:isCanceled() then
		return cachedP
	end

	local p = getWithRetryP( self, self._ozUser, renditionUrl, highPriority, ozRenditionName, docValue )
	
	downloadPromiseCache[ renditionUrl ] = p
	
	return p

end

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

-- Get the URL for the Oz rendition

local function getPartialRenditionUrl(
					docValue, ozRenditionName, optDiagnosticLogEntry )

	local ozLinkName = ozLinkNameForOzRenditionName[ ozRenditionName ]

	local partialRenditionUrl = readRenditionLink( docValue, '_serverProvidedMetadata',
													   'links',
													   ozLinkName,
													   'href' )
													
	if optDiagnosticLogEntry then
		optDiagnosticLogEntry.ozLinkName = ozLinkName
		optDiagnosticLogEntry.time_ozLinkName = KSDate.timeInIso8601Format( nil, true )
		optDiagnosticLogEntry.partialRenditionUrl = partialRenditionUrl
	end

	return partialRenditionUrl

end

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

local function getDevSettingsLastChanged( docValue, optDiagnosticLogEntry )

	local devSettingsLastChanged = KSTableUtils_derefOpt( docValue,
												    '_localOnly',
													'devSettingsLastSetAtRev' )

	if optDiagnosticLogEntry then
		optDiagnosticLogEntry.devSettingsLastChanged = devSettingsLastChanged
	end
	
	return devSettingsLastChanged

end

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

local function logDownloadP( downloadP, optDiagnosticLogEntry )

	if optDiagnosticLogEntry then
	
		downloadP = downloadP:observeP( function( status, data, headers )

			optDiagnosticLogEntry.renditionUrlStatus = status
			optDiagnosticLogEntry.renditionUrlHeaders = headers
			optDiagnosticLogEntry.time_renditionUrlFetch = KSDate.timeInIso8601Format( nil, true )
			
			if status == 'error' then
				local result, message = pcall( KSJsonDecoder.decodeWebSafeJson, data )
				if result and message then
					optDiagnosticLogEntry.renditionUrlMessage = message
				end
			end
		
		end )
	
	end
	
	return downloadP

end

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

local function downloadOzRenditionForRenditionUrlP(
						self, renditionUrl, highPriority, ozRenditionName, docValue, optDiagnosticLogEntry )

	if optDiagnosticLogEntry then
		optDiagnosticLogEntry.renditionUrl = renditionUrl
	end

	renditionServerLog:infof( "renditionUrl = %s", renditionUrl )

	local resultP = downloadOzRenditionForUrlP( self, renditionUrl, highPriority, ozRenditionName, docValue )
	
	resultP = logDownloadP( resultP, optDiagnosticLogEntry )
	
	-- Process the results
	
	resultP = resultP:observeP( function( status, data, headers )
	
		self:_processHttpRequestResultFn( status, data, headers )
		
	end )
	
	return resultP

end

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

local function storeOzRenditionToDataCacheP(
					self, photoId, ozRenditionName,
					renditionMD5, data, previousMD5,
					optDiagnosticLogEntry )


	local storeP

	if previousMD5 then
	
		storeP = self._dataCache:removeP( previousMD5 )
			:mapP( function( status )

				return self._dataCache:putP( renditionMD5, data )

			end )

	else

		renditionServerLog:infof( string.format( '%s stored %s %s', photoId, ozRenditionName, renditionMD5 ) )
	
		storeP = self._dataCache:putP( renditionMD5, data )
		
	end
	
	
	return storeP:observeP( function( status, message )
	
		if status ~= 'data' then
	
			if optDiagnosticLogEntry then
				optDiagnosticLogEntry.cacheWriteFailedWithMessage = message
			end

			renditionServerLog:errorf( "failed to overwritee %s data to cache for photo %s: %s (previousMD5: %s)",
									   ozRenditionName,
									   photoId,
									   message,
									   previousMD5 )

		end
		
	end )


end

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

local function addLocalLinkForOzRenditionP(
			self, photoId, ozRenditionName, renditionUrl,
			renditionMD5, data,
			devSettingsLastChanged,
			optDiagnosticLogEntry )

	local referenceWidth, referenceHeight = OzReferenceDimension_getReferenceDimensions( data )
			
	local revId, renditionId
	
	if renditionUrl then
		-- TODO [arahn 2013-05-29]: WHY are we splitting apart the URL like this?
		-- we could just use the renditionUrl in its entirety to decide
		-- if the local rendition matches.  see StopTheRenditionInsanity elsewhere
		-- in this file.
		revId, renditionId = renditionUrl:match( '([0-9a-f]+)%/renditions%/([0-9a-f]+)$' )
	end

	local renditionOverlay = {
		md5 = renditionMD5,
		revId = revId,
		renditionId = renditionId,
		epoch = self._cacheEpochB:valueNow(),
		size = KSData.length( data ),
		referenceWidth = referenceWidth,
		referenceHeight = referenceHeight
	}
	
	local localOverlay
	if usingWFDocStore then
	
		localOverlay = {
			renditions = {
				[ ozRenditionName ] = renditionOverlay,
			},
		}
	
	else
	
		localOverlay = {
			[ ozRenditionName ] = renditionOverlay,
		}
		
	end

	-- GRUNGE [scouten 2011-08-04]: Ideally we'd be getting this notion
	-- of when dev settings were last changed from Oz, but it isn't there
	-- now.

	if devSettingsLastChanged == 'reverted' then
		localOverlay.devSettingsLastSetAtRev = revId
	end

	if optDiagnosticLogEntry then
		optDiagnosticLogEntry.cacheWriteOverlay = localOverlay
		optDiagnosticLogEntry.time_cacheWrite = KSDate.timeInIso8601Format( nil, true )
	end

	renditionServerLog:debugf( "data for URL %s written to cache under MD5 key %s, "
							   .. "now applying overlay %s to photo %s",
							   renditionUrl, renditionMD5, localOverlay, photoId )

	local resultP = self._docStore:applyOverlayP {
		_slot = 'main',
		_id = photoId,
		type = 'asset',
		_localOnly = localOverlay,
	}
	
	if optDiagnosticLogEntry then

		resultP = resultP:observeP( function()
			optDiagnosticLogEntry.time_updateDocValue = KSDate.timeInIso8601Format( nil, true )
		end )
		
	end
	
	return resultP

end

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

local function downloadAndStoreRenditionP(
			self, photoId, deviceRenditionName,
			renditionUrl,
			highPriority,
			devSettingsLastChanged, docValue,
			optDiagnosticLogEntry )
			
	if optDiagnosticLogEntry then
		optDiagnosticLogEntry.shouldDownload = not not renditionUrl
	end

	if not renditionUrl then
		return KSPromises.resolvedPromiseP( 'data', nil )
	end
			
	-- If the url from the server is partial, only then append the base. ASSET documents don't
	-- contain the base server url, it would be nice to have that.
	
	local ozRenditionName = ozRenditionNameForDeviceRenditionName[ deviceRenditionName ]

	local resultP = downloadOzRenditionForRenditionUrlP(
		self, renditionUrl, highPriority, ozRenditionName, docValue, optDiagnosticLogEntry )
		
	recordDiagnosticTiming( self, photoId, deviceRenditionName, "startDownload" )

	renditionServerLog:debugf( "Starting download rendition task for %s %s", photoId, deviceRenditionName )
		
	resultP = resultP:observeP( function( status, data )
		if status == 'data' and data and optDiagnosticLogEntry then
			local dataLength = KSData.length( data )
			optDiagnosticLogEntry.renditionReceivedSize = dataLength
			renditionServerLog:debugf( "got data for URL %s %d bytes :: writing to cache",
									   renditionUrl, dataLength )
		end
	end )
	
	resultP = resultP:observeP( function( status )
			recordDiagnosticTiming( self, photoId, deviceRenditionName, "finishDownload" )
			if optDiagnosticLogEntry then
				optDiagnosticLogEntry.downloadRenditionStatus = status
				optDiagnosticLogEntry.time_downloadRendition = KSDate.timeInIso8601Format( nil, true )
			end
			renditionServerLog:infof( "Download rendition task for %s %s resolved with status %s", 
										photoId, deviceRenditionName, status )
		end )


	-- Write to the cache if appropriate
	
	if kDownsampledOzRenditions[ ozRenditionName ] then

		local previousMD5 = KSTableUtils_derefOpt( docValue, '_localOnly', ozRenditionName, 'md5' )
	
		-- Store to the data cache..
		
		resultP = resultP:waitForP( function( status, data )
	
			if status ~= 'data' or not data then
				return
			end
		
			local renditionMD5 = data:digest( 'MD5' )

			-- Note that the data cache put is treated as a runUntilDone operation
			-- since once we have the data, we really don't want to fetch it again.
				
			renditionMD5 = WFOzRenditionServerUtils_getFullMD5HashedKey (renditionMD5, photoId)

			local storeP = storeOzRenditionToDataCacheP(
									self, photoId, ozRenditionName,
									renditionMD5, data, previousMD5,
									optDiagnosticLogEntry )
									
			-- Remove the store operation from limbo. This also eliminates our
			-- ability to cancel, but there really isn't much sense in canceling
			-- storing the data once we've read it.
									
			storeP:runUntilDone()
			
			-- It's now in the local cache, so go update anyone waiting on the MD5 hash
			-- rather than waiting for the various pieces of logic to catch up.
			
			updateMD5ForOzRendition( self, photoId, ozRenditionName, renditionMD5 )
			
			local updateDocStoreP = storeP:mapDataP( function()
				return addLocalLinkForOzRenditionP(
					self, photoId, ozRenditionName, renditionUrl,
					renditionMD5, data,
					devSettingsLastChanged,
					optDiagnosticLogEntry )
			end )
			
			-- Similarly, if we've got the data and we've stored, then we want to update
			-- the doc store.
			
			updateDocStoreP:runUntilDone()
			
			-- And then we tell any code that is still interested that it should wait for
			-- updateDocStoreP to complete. Note that we don't need storeP to complete since
			-- we have the data in the local cache.
			
			return updateDocStoreP

		end )
		
		-- But now we want to avoid having this get collected though we are fine
		-- with having it get canceled. And we want to allow the above map to run
		-- even if we aren't prepared to drive the promises forward outside of the
		-- internal runUntilDone logic. So we set up a dummy action at very low
		-- priority.
		
		resultP
			:whenResolvedDo()
			:setPriority( 'very_low' )
			:runUntilDone()
			
	end
	
	return resultP
			
end

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

local function getDownloadRenditionUrlAndDevSettingsLastChanged(
									docValue, photoId, ozRenditionName,
									optDiagnosticLogEntry )
									
	if not docValue
			or WFUnknown.isUnknown( docValue )
			or docValue.empty
			or not shouldDownloadOzRendition( docValue, photoId, ozRenditionName )
	then

		if optDiagnosticLogEntry then
			optDiagnosticLogEntry.shouldDownload = false
		end

		renditionServerLog:debugf( "shouldDownloadOzRendition for %s %s is false",
								   photoId, ozRenditionName )
								
		return nil, nil

	end
	
	renditionServerLog:debugf( "shouldDownloadOzRendition for %s %s is true",
							   photoId, ozRenditionName )
								
	local partialRenditionUrl =
		getPartialRenditionUrl( docValue, ozRenditionName, optDiagnosticLogEntry )
		
	if not partialRenditionUrl and docValue then
		renditionServerLog:infof( "no image URL in doc data RN = %s\n\ndocValue = %s",
								  ozRenditionName, docValue )
	end
	
	local baseUrl = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'base' )
	local renditionUrl = partialRenditionUrl
	
	if baseUrl and partialRenditionUrl then
		renditionUrl = WFCatUrlUtils.resolveToAbsoluteUrl( baseUrl, partialRenditionUrl )
	end
	
	local devSettingsLastChanged = getDevSettingsLastChanged( docValue, optDiagnosticLogEntry )
	
	if optDiagnosticLogEntry then
		optDiagnosticLogEntry.shouldDownload = not not renditionUrl
	end

	return renditionUrl, devSettingsLastChanged

end

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

local function makeRenditionDownloaderB(
		self, photoId, deviceRenditionName, highPriority, optDiagnosticLogEntry )
		
	check( 'WFOzRenditionServer', 'string', deviceRenditionNameArg )
	
	local docValueB = self._docStore:docValueCheckTypeB( 'asset', photoId )

	-- Get the information for the download
	
	local renditionUrlAndDevSettingsLastChangedB = docValueB:closingMapB( function( docValue )

		renditionServerLog:debugf( "OzRenditionDownloaderB %s %s got docValue %s",
								  photoId, deviceRenditionName, tostring( docValue ) )
		
		local ozRenditionName = ozRenditionNameForDeviceRenditionName[ deviceRenditionName ]
								
		local renditionUrl, devSettingsLastChanged =
			 getDownloadRenditionUrlAndDevSettingsLastChanged(
					docValue, photoId, ozRenditionName,
					optDiagnosticLogEntry )
		
		
		return renditionUrl, devSettingsLastChanged, docValue

	end )
	
	-- Do the download when appropriate
	
	local downloadB = renditionUrlAndDevSettingsLastChangedB
		:closingMapSwitchB( function( renditionUrl, devSettingsLastChanged, docValue )
		
			local p = downloadAndStoreRenditionP(
						self, photoId, deviceRenditionName,
						renditionUrl,
						highPriority,
						devSettingsLastChanged, docValue,
						optDiagnosticLogEntry )
						
			return p:untilResolvedB( 'data', false )
			
		end )
	
	return downloadB
		
end

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

local function suppressNilB( sourceB )

	local resultB = sourceB
		:closingFilterE( function( v ) return v ~= nil end )
		:closingHoldValueNowB( sourceB )
	
	return resultB

end

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

local kShouldLogRendition = {
	preview = true,
	smallPreview = true
}

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

-- Set up diagnostic log reporting, but only for previews.

local function logRenditionFetch( self, photoId, md5hint, deviceRenditionName )

	local optDiagnosticLogEntry
		
	if kShouldLogRendition[ deviceRenditionName ] then
		
		optDiagnosticLogEntry = {
			photoId = photoId,
			md5hint = md5hint,
			renditionName = deviceRenditionName,
			time_00 = KSDate.timeInIso8601Format( nil, true ),
		}
		
		local recentPreviews = self._recentPreviews
		recentPreviews[ #recentPreviews + 1 ] = optDiagnosticLogEntry
		if #recentPreviews > 10 then
			table.remove( recentPreviews, 1 )
		end
		
	end

	return optDiagnosticLogEntry

end

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

local function removeLocalLinkForOzRendition( docStore, photoId, ozRenditionName )

	docStore:applyOverlayToDocumentP {
		docType = 'asset',
		docId = photoId,
		overlay = {
			_localOnly = {
				[ ozRenditionName ] = {
					_delete = true,
				},
			},
		},
	}:runUntilDone()

end

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

local function rawDataCacheGetB( self, md5, debugRetainCount, highPriority )

	return self._dataCache:getB( md5, debugRetainCount, highPriority )

end

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

-- Fetch the rendition from the data cache.

local function getCachedOzRenditionDataForMD5B(
						self, ozRenditionName, md5, highPriority,
						debugRetainCount )
						
	-- Check to see whether we already have a promise for the data. Right now,
	-- this is just working with resolved promises because those don't have
	-- issues around cancelation.
	
	-- Construct the fetch promise.
	
	local b = rawDataCacheGetB( self, md5, debugRetainCount, highPriority )
	
	b = b:closingMapB( function( value, reason )
		if value == nil then
			return reason
		else
			return value
		end
	end )
		
	return b

end

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

-- Given the desired device rendition and the MD5 hash, we can now fetch the
-- data and decode the image.

local function fetchAndDecodeImageForMD5B(
			self, deviceRenditionName, md5, highPriority, logPhotoId )

	if not md5 or md5 == '' then
		return KSReactive.constantB( nil )
	end
	
	-- Check the image cache
	
	local cacheInfo = cacheInfoForDeviceRenditionName[ deviceRenditionName ]

	local imageCache = self[ cacheInfo.imageCacheName ]
	
	local cachedImage = getCachedImage( imageCache, deviceRenditionName, md5 )
	
	if cachedImage then
		return KSReactive.constantB( cachedImage )
	end
	
	-- Check the behavior cache
	
	local decodeImageCache = self._decodeImageCache[ deviceRenditionName ]
	
	local cachedB = fetchCachedBehavior( decodeImageCache, md5, true )
	
	if cachedB then
		return cachedB:autoCloseRelayB()
	end
	
	-- No go. We need to fetch and decode the data.
	
	local ozRenditionName = ozRenditionNameForDeviceRenditionName[ deviceRenditionName ]
	
	local dataB = getCachedOzRenditionDataForMD5B( self, ozRenditionName, md5, highPriority )
	
	--[[
	
	dataB = dataB:closingMapB( function( ... )
		local data = ( ... )
		local debugData = ( type( data ) == 'userdata' ) and 'userdata' or data
		renditionServerLog:debugf( string.format( "Received data for %s, %s: %s",
			tostring( logPhotoId ), tostring( ozRenditionName ), tostring( debugData ) ) )
		return ...
	end )
	
	]]
	
	local decodedB = dataB:closingMapSwitchB( function( ksData )
		
		if not ksData then
			return KSReactive.constantB( nil )
		end
		
		if type( ksData ) == 'string' then
			return KSReactive.constantB( ksData )
		end
		
		-- We got data. Decode it.

		renditionServerLog:debugf( "rawGetDeviceImageB: cachedDataB for photo %s got data of size = %d",
								  logPhotoId, KSData.length( ksData ) )
								
		local options = decodeOptionForDeviceRenditionName[ deviceRenditionName ] or { }
		
		-- When we decode data, we enqueue high-priority requests in LIFO order. We really want to go
		-- in decreasing order of when the request was created but that takes more information
		-- than we have.
		
		local selector = highPriority and '+decode' or 'decode'
		
		local decodedP = jpegDecodeService:requestP(
			selector, ksData, unpack( options ) )
			
		decodedP = decodedP:observeP( function( status, value )

			if status == 'data' and value then
			
				renditionServerLog:debugf( string.format( "Decoded %s, %s", tostring( logPhotoId ), tostring( deviceRenditionName ) ) )

				debug.getfenv( value )._renditionName = deviceRenditionName
				
				-- Cache the data on success

				if imageCache then
					storeCachedImage( imageCache, deviceRenditionName, md5, value )
				end

			elseif status == 'error' then
			
				renditionServerLog:debugf( string.format( "rawGetDeviceImageB: decode jpeg data failed with message %s", tostring( value ) ) )
				
			elseif not status then
			
				-- renditionServerLog:debugf( "Decode was canceled for image" )

			end
			
		end )
		
		return decodedP:untilResolvedB( 'data', 'initial_value' )

	end )
	
	--[[
	
	decodedB = decodedB:closingMapB( function( ... )
		local value = ( ... )
		local debugValue = ( type( value ) == 'userdata' ) and 'userdata' or value
		renditionServerLog:debugf( string.format( "fetchAndDecodeForMD5 %s, %s > %s",
			tostring( logPhotoId ), tostring( deviceRenditionName ), tostring( debugValue ) ) )
		return ...
	end )
	
	]]

	decodeImageCache[ md5 ] = decodedB
	
	return decodedB:autoCloseRelayB()

end

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

-- Given the MD5 hash for the rendition of the photo, we do the work of fetching
-- the rendition and decoding it. This routine mostly exists to clean up the
-- cases like cache misses where we need to remove the link in the doc store.
-- The real work happens in fetchAndDecodeImageForMD5B.

local function fetchAndDecodeImageForPhotoWithMD5B(
				self, photoId, deviceRenditionName, md5, highPriority, optDiagnosticLogEntry )
				
	local b = fetchAndDecodeImageForMD5B( self, deviceRenditionName, md5, highPriority, photoId )

	b = b:closingMapB( function( value )

		if value == 'cache miss' or (not value and md5) then

			-- On a cache miss, we want to remove the link in the document
			-- so that we don't fault this way again. (That said, we probably
			-- hope to get this back again.) Note that we have a risk here in
			-- that we have to blindly clear the local entry rather than just
			-- if it's for this particular MD5.

			local ozRenditionName = ozRenditionNameForDeviceRenditionName[ deviceRenditionName ]
	
			renditionServerLog:warnf( "rawGetDeviceImageB: cachedDataB for photo %s rendition %s MD5 %s was nil (cache miss)",
									  photoId, ozRenditionName, md5 )
									
			removeLocalLinkForOzRendition( self._docStore, photoId, ozRenditionName )
			
		end
		
		if optDiagnosticLogEntry and value and value ~= 'reading' then
		
			optDiagnosticLogEntry.imageFoundInLocalCache = value ~= 'cache miss'
			optDiagnosticLogEntry.time_cacheRead = KSDate.timeInIso8601Format( nil, true )
				-- FIXME: Actually, we are now post decompression so this isn't the time for the cache read
		
		end
		
		-- Map away status values
		
		if type( value ) == 'string' then
			value = nil
		end
		
		return value
		
	end )
	
	--[[
	
	b = b:closingMapB( function( ... )
		local value = ( ... )
		local debugValue = ( type( value ) == 'userdata' ) and 'userdata' or value
		renditionServerLog:debugf( string.format( "fetchAndDecodeForPhotoWithMD5 %s, %s > %s",
			tostring( photoId ), tostring( deviceRenditionName ), tostring( debugValue ) ) )
		return ...
	end )
	
	]]

	return b

end

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

local function fetchAndDecodeImageForPhotoB(
					self, photoId, deviceRenditionName, md5Hint, highPriority, optDiagnosticLogEntry )
	
	local ozRenditionName = ozRenditionNameForDeviceRenditionName[ deviceRenditionName ]
	
	local md5B = md5ForOzRenditionB( self, photoId, ozRenditionName, md5Hint,
								  	 optDiagnosticLogEntry )
								
	-- Log the timings for fetching the MD5 hash
								
	md5B = md5B:closingMapB( function( ... )
	
		local md5 = ( ... )
	
		recordDiagnosticTiming( self, photoId, deviceRenditionName, 'md5' )
		renditionServerLog:debugf( "rawGetDeviceImageB: photoId = %s, md5 = %s", photoId, md5 )

		if optDiagnosticLogEntry then
			optDiagnosticLogEntry.md5_resolved = md5
			optDiagnosticLogEntry.time_md5_resolved = KSDate.timeInIso8601Format( nil, true )
		end
		
		return ...

	end )
	
	-- Fetch and decode the image based on the MD5 value
	
	local decodedB = md5B:closingMapSwitchB( function( md5 )
		
		if WFUnknown.isUnknown( md5 ) then
			md5 = nil
		end
		return fetchAndDecodeImageForPhotoWithMD5B(
			self, photoId, deviceRenditionName, md5, highPriority, optDiagnosticLogEntry )
	end )
	
	--[[
	
	decodedB = decodedB:closingMapB( function( ... )
		local value = ( ... )
		local debugValue = ( type( value ) == 'userdata' ) and true or value
		renditionServerLog:debugf( string.format( "fetchAndDecodeForPhoto %s, %s > %s",
			tostring( photoId ), tostring( deviceRenditionName ), tostring( debugValue ) ) )
		return ...
	end )
	
	]]

	return decodedB
								
end

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

local function first( x )
	return x
end

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

local function rawGetDeviceImageB( self, photoId, deviceRenditionName, md5hint, highPriority )

	renditionServerLog:debugf( "rawGetDeviceImageB: photoId = %s", photoId )

	local optDiagnosticLogEntry =
		logRenditionFetch( self, photoId, md5hint, deviceRenditionName )
		
	local imageB = fetchAndDecodeImageForPhotoB(
		self, photoId, deviceRenditionName, md5hint, highPriority, optDiagnosticLogEntry )
		
	local downloadB = makeRenditionDownloaderB( self, photoId, deviceRenditionName, highPriority )
					
	-- We want to keep the download logic alive as long as the image is alive. So
	-- we tie it together with the image underneath a call that will just return
	-- the image. (A little expensive but it uses the mechanisms available.)
	
	imageB = KSReactive.closingDataCallB(
					first,
					imageB,
					downloadB:closingRawMapB( function() return 'data', true end  ) )
	
	-- Don't let nil values while we're going to the next item slip through.
	-- We can start out nil, but we won't slip back to being nil.
	
	imageB = suppressNilB( imageB )

	if renditionServerLog:will_debug() then

		imageB = imageB:closingMapB( function( ... )
		
			renditionServerLog:debugf( "imageB for %s %s returning new data",
									   photoId, deviceRenditionName )
									
			return ...
		
		end )
	
	end
	
	--[[
	
	imageB = imageB:closingMapB( function( ... )
		renditionServerLog:debugf( string.format( "rawGetDeviceImageB %s, %s > %s",
			tostring( photoId ), tostring( deviceRenditionName ), tostring( not not ( ... ) ) ) )
		return ...
	end )
	
	]]
	
	return imageB
		
end

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

local outstandingRequests = 0

-- FIXME: The image cache is no longer getting used!
-- FIXME [scouten 2012-10-04]: Who wrote this? Is it still true?

local function getDeviceImageB( self, photoId, deviceRenditionName, md5hint, highPriority, deferTriggerB )
			
	check( 'WFOzRenditionServer', 'string', deviceRenditionNameArg, '?string', '_' )
			
	local cacheInfo = cacheInfoForDeviceRenditionName[ deviceRenditionName ]
	
	if not cacheInfo then
		return KSReactive.constantB( nil )
	end

	local behaviorCache = getBehaviorCache( self, cacheInfo.behaviorCacheName )
	
	local cachedImageB = fetchCachedBehavior( behaviorCache, photoId, true )
	
	if cachedImageB then
		return cachedImageB:autoCloseRelayB()
	end
	
	-- If we don't already have a behavior in place for this, we might want to defer
	-- creation of it for a bit.
	
	if deferTriggerB then
	
		-- We don't cache when we're based off of a defer trigger because someone else might request
		-- it without the defer trigger.
		
		-- PERF [mhamburg 2011-03-18]: It might be nice to skip the defer if we ended up with a
		-- real value more rapidly.
		
		-- PERF [mhamburg 2011-08-14]: If someone requested the value and it actually fired, we
		-- would really prefer not to be sitting behind a defer trigger.
		
		local imageCache = self[ cacheInfo.imageCacheName ]
		
		local existingImage = getCachedImage( imageCache, deviceRenditionName, md5hint )
	
		local resultB = deferTriggerB:closingMapSwitchB( function( isLater )

			if isLater then
			
				-- Try again without the defer trigger

				return getDeviceImageB( self, photoId, deviceRenditionName, md5hint, highPriority, nil )

			elseif existingImage then

				local existingImageB = KSReactive.constantB( existingImage )
				existingImage = nil
				return existingImageB

			else

				return KSReactive.constantB( nil )

			end

		end )

		resultB = suppressNilB( resultB )
		
		return resultB
	
	end

	-- Nothing in cache, create it.

	local imageB = rawGetDeviceImageB( self, photoId, deviceRenditionName, md5hint, highPriority )
	
	if DEBUG_DOWNLOAD_FETCH_CANCELING then
	
		imageB:addCloseAction( function()

			local cacheSize = 0
			
			for _, beh in pairs( behaviorCache ) do
			
				if not WFCloseSupport_hasBeenOrWillBeClosed( beh ) then
					cacheSize = cacheSize + 1
				end
			
			end
		
			renditionServerLog:debugf( "closing imageB for photo ID ", photoId,
						 "   remaining cache size = ", cacheSize )
			
		end )
	
	end
	
	if LOG_AND_COUNT_RENDITION_REQUESTS then
	
		outstandingRequests = outstandingRequests + 1
		
		renditionServerLog:debugf( string.format( "Creating preview request: %s : %s, total = %d",
			tostring( photoId ), tostring( deviceRenditionName ), outstandingRequests ) )
		
		imageB:addCloseAction( function()
			outstandingRequests = outstandingRequests - 1
			renditionServerLog:debugf( string.format( "Closing preview request: %s : %s, total = %d",
				tostring( photoId ), tostring( deviceRenditionName ), outstandingRequests ) )
		end )
		
	end
	
	behaviorCache[ photoId ] = imageB
	
	return imageB:autoCloseRelayB()

end

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

local errors = setmetatable( { }, { __mode = 'kv' } )

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

local function fnEncodeError( status, ... )

	if status == 'error' then
		local err = { n = select( '#', ... ), ... }
		errors[ err ] = true
		return 'data', err
	else
		return status, ...
	end

end

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

-- GRUNGE [mhamburg 2012-03-22]: This probably deserves a unit test to make
-- sure it really does the right thing with dataCallB since so much of the
-- reactive system hides errors.

local function fnOrWithEncodedErrorFilter( a, b )

	-- We suppress error values for b.

	if errors[ b ] then
		b = nil
	end

	-- Error values for a are returned as a second result.
	
	if errors[ a ] then
		return b, unpack( a, 1, a.n )
	else
		return a or b
	end

end

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

local function closingOrB( aB, bB )

	if not aB then
		return bB
	end
	
	if not bB then
		return aB
	end
	
	local resultB = KSReactive.closingDataCallB(
			fnOrWithEncodedErrorFilter,
			aB:closingRawMapB( fnEncodeError ),
			bB:closingRawMapB( fnEncodeError ) )
	
	return resultB

end

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

local function nonFalseRawBehaviorValue( status, value )

	if status ~= 'data' then
		return 'data', true
	end
	
	return 'data', not not value

end

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

local function renditionImageTriggerB( renditionImageB )

	return renditionImageB:rawMapB( nonFalseRawBehaviorValue )
				
end

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

-- Triggers transition from false to true. They could probably become promises
-- in some future organization, but that would take more unification between
-- behaviors and promises.

local function andTriggersB( trigger1B, trigger2B )

	if trigger1B then
	
		if trigger2B then
		
			return KSReactive.dataCallB( function( a, b ) return a and b end,
				trigger1B, trigger2B )
		
		else
		
			return trigger1B
		
		end
	
	elseif trigger2B then
	
		return trigger2B
	
	else
		
		return nil
	
	end

end

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

-- This takes a sequence of rendition names to fetch and builds a compound
-- behavior to do the fetch preferring the first renditions in the list to
-- the later renditions. Values of false in the list result in the entry
-- being skipped. It returns a behavior for the image. The behaviors is
-- relayed (i.e., safe to close). The md5hint is for the lowest resolution version
-- being fetched.

-- GRUNGE [mhamburg 2012-03-22]: It would be nice to stop listening to the
-- lower-resolution behaviors once the higher resolution behavior has a non-nil
-- value.

local function getCascadedImageB( self, photoId, md5hint, deferTriggerB,
								  cascadedBehaviorCacheKey, deferSecondTriggerB )
								
	-- MD5 hints keep us from waiting since we know where to go get the data and
	-- we expect to have it local.
								
	if md5hint then
		deferTriggerB = nil
	end
								
	if not OBEY_DEFER_TRIGGERS then
		deferTriggerB = nil
		-- deferSecondTriggerB = nil
	end
								
	local deviceRenditionNames =
		cascadedBehaviorCacheKeyToDeviceRenditionNames[ cascadedBehaviorCacheKey ]
	
	assert( deviceRenditionNames, "No device renditions names for cascaded behavior" )
	
	local processSequentially = deviceRenditionNames.sequential
			
	if OzCatHelperFns_DISABLE_MOST_FETCHES then
		return KSReactive.constantB( nil )
	end

	if not photoId then
		return KSReactive.constantB( nil )
	end
	
	local behaviorCache = getBehaviorCache( self, cascadedBehaviorCacheKey )
	
	local cachedB = fetchCachedBehavior( behaviorCache, photoId, true )
		
	if cachedB then
		return cachedB:autoCloseRelayB()
	end
	
	local n = #deviceRenditionNames
	
	local imageB
	
	-- Note that the evolving trigger may refer to the individual rendition image
	-- behaviors, but those are all managed via the orB chain that gets built via
	-- imageB.
	
	local triggerB = deferTriggerB
	
	for i = n, 1, -1 do
	
		local renditionName = deviceRenditionNames[ i ]
		
		-- GRUNGE: Why not? The 1x thumbnails will load faster
		
		-- On Android, we do not want the 1x thumbs...
		if renditionName and not ( WFPlatformInfo.isAndroid and renditionName == 'thumbnail' ) then
		
			local highPriority = imageB == nil
				-- High priority if nothing to fall back on
		
			local renditionImageB =
				getDeviceImageB( self, photoId, renditionName, md5hint, highPriority, triggerB )
				
			-- We only care about errors on the primary request.
				
			if i ~= 1 then
				renditionImageB = renditionImageB and renditionImageB:closingSkipErrorsB( nil )
			end
			
			if renditionImageB then
				imageB = closingOrB( renditionImageB, imageB )
			end
			
			-- If we are processing sequentially, then we need a new trigger.

			if processSequentially and 1 < i and renditionImageB then
				triggerB = andTriggersB( triggerB, renditionImageTriggerB( renditionImageB ) )
			end
			
			if deferSecondTriggerB then
				triggerB = andTriggersB( triggerB, deferSecondTriggerB )
				deferSecondTriggerB = false -- use only once
			end

		end
		
		md5hint = nil
			-- Hint is only for first fetch.
	
	end
	
	if not imageB then
		return KSReactive.constantB( nil )
	end
	
	-- If we aren't deferred, then we can cache.
	
	if not deferTriggerB then
		behaviorCache[ photoId ] = imageB
	end
	
	local resultB = imageB:autoCloseRelayB()
	
	if CAPTURE_REQUEST_TIMINGS then
		resultB = resultB:closingMapB( function( image )
			if self._requestTimings[ photoId ] and image then
				-- we've delivered our first result
				local renditionName = debug.getfenv( image )._renditionName
				if renditionName then
					recordDiagnosticTiming( self, photoId, renditionName, "deliver" )
				else
					renditionServerLog:debugf( "Got image, but dont know rendition" )
				end
			end
			return image
		end )
	end
	
	return resultB
			
end

local logNewThumbnailRequest = function() end

if LOG_THUMBNAIL_REQUESTS then

	local thumbnailCount = 0

	local countE = KSReactive.sourceE()
	
	local calmCountE = countE:calmE( 1 )
	
	local logAndResetE = calmCountE:mapE( function( count )
		renditionServerLog:debugf( string.format( "%d thumbnail requests", thumbnailCount) )
		thumbnailCount = 0
	end )
	
	logAndResetE:protectFromAutoCloseE()

	logNewThumbnailRequest = function()
		thumbnailCount = thumbnailCount + 1
		if logAndResetE then	-- Generate a reference to logAndResetE
			countE:post( thumbnailCount )
		end
	end

end

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves (as a Behavior) a thumbnail rendition of a photo from the Oz server.

	 \param photoId (string) The photo's unique ID.
	 \param wantBigThumbnails (boolean) \c true to get big thumbnail. ((only big? small and big?))
	 \param md5hint (string) ((what?))
	 \param deferTriggerB (boolean) \c true to defer ((what?))

	 <code>B< jpegData | nil ></code> A Behavior whose value is the rendition. ((how? multiple?))

]]--

function renditionServerMethods:thumbnailB( --[[string]] photoId,
						--[[boolean]] wantBigThumbnails,
					    	--[[string]] md5hint,
					    	--[[boolean]] deferTriggerB,
							deferSecondTriggerB )
	
	if CAPTURE_REQUEST_TIMINGS then
		if not self._requestTimings[ photoId ] then
			self._requestTimings[ photoId ] = { start = now() }
		end
		recordDiagnosticTiming( self, photoId, 'thumbnail', "start" )
	end

	logNewThumbnailRequest( photoId )
	
	if OzCatHelperFns_DISABLE_MOST_FETCHES then
		return KSReactive.constantB( nil )
	end

	if not photoId then
		return KSReactive.constantB( nil )
	end
	
	if wantBigThumbnails then

		return getCascadedImageB( self, photoId, md5hint, deferTriggerB,
								  '_thumbnailBCache2', deferSecondTriggerB )

	else
	
		-- If we have an MD5 hint, then don't wait to start the fetch process.
	
		if md5hint then
			deferTriggerB = false
		end
	
		if not OBEY_DEFER_TRIGGERS then
			deferTriggerB = nil
			-- deferSecondTriggerB = nil
		end
		
		local highPriority = true -- nothing to fall back on

		local resultB = getDeviceImageB( self, photoId, 'thumbnail', md5hint, highPriority, deferTriggerB )
		
		if CAPTURE_REQUEST_TIMINGS then
			resultB = resultB:closingMapB( function( img )
				if self._requestTimings[ photoId ] and img then
					local renditionName = debug.getfenv( img )._renditionName
					if renditionName then
						recordDiagnosticTiming( self, photoId, renditionName, "deliver" )
					else
						renditionServerLog:debugf( "Got image, but dont know rendition" )
					end
				end
				return img
			end )
		end
		
		return resultB
		
	end
	
end

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves (as a Behavior) a preview rendition of a photo from the Oz server.

	 \param photoId (string) The photo's unique ID.
	 \param desiredSize ('small' or anything) string values will fetch specially decompressed and managed previews.
	 \param skipThumbnail (boolean) \c true to fetch only preview, \c false to fetch both thumbnail and preview.
	 \param deferTriggerB (behavior) We will wait for this behavior to go true before fetching

	<code>B< image | nil ></code> A Behavior whose value is the rendition.

]]--

function renditionServerMethods:previewB( --[[string]] photoId,
					--[[boolean | 'small']] desiredSize,
					--[[boolean]] skipThumbnail,
					--[[behavior]] deferTriggerB )
					
	local cascadedBehaviorCacheKey
	
	if desiredSize == 'small' then
		cascadedBehaviorCacheKey = '_smallPreviewBCache2'
	else
		cascadedBehaviorCacheKey = '_previewBCache2'
	end

	if skipThumbnail then
	
		local deviceRenditionName =
			cascadedBehaviorCacheKeyToDeviceRenditionNames[ cascadedBehaviorCacheKey ][ 1 ]
			
		local highPriority = true -- no fallback

		return getDeviceImageB( self, photoId, deviceRenditionName, nil, highPriority, deferTriggerB )

	else

		return getCascadedImageB( self, photoId, nil, -- no MD5 hint
								  deferTriggerB, cascadedBehaviorCacheKey )

	end
	
end

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

--[[!
	\brief Downloads the rendition in case we haven't yet done this.
	
	\param photoId (optional, string) The photo to fetch; if not provided, the
		promise resolves to \c nil)
	\param renditionName (string) The name of the rendition to be downloaded
	\return <code>P< bool ></code> A Promise that resolves to true when the rendition
		is locally available
]]--

function renditionServerMethods:downloadRenditionP( --[[string]] photoId,
													--[[string]] renditionName )

	check( 'WFOzRenditionServer', 'string', deviceRenditionNameArg )

	if not photoId then
		return KSPromises.resolvedPromiseP( 'error', "invalid photo id" )
	end
	
	local docValueP = self._docStore:docValueOnceP( 'asset', photoId )
	
	-- Get the information for the download
	
	local optDiagnosticLogEntry = nil
	local highPriority = false
	
	local downloadP = docValueP:mapDataP( function( _, docValue )

		renditionServerLog:debugf( "downloadRenditionP %s %s got docValue %s",
								 photoId, renditionName, tostring( docValue ) )
								
		local ozRenditionName = ozRenditionNameForDeviceRenditionName[ renditionName ]
			
		if not shouldDownloadOzRendition( docValue, photoId, ozRenditionName ) then
			return KSPromises.resolvedPromiseP( 'data', true )
		end
				
		local renditionUrl, devSettingsLastChanged =
			 getDownloadRenditionUrlAndDevSettingsLastChanged(
					docValue, photoId, ozRenditionName,
					optDiagnosticLogEntry )
		
		return downloadAndStoreRenditionP(
					self, photoId, renditionName,
					renditionUrl,
					highPriority,
					devSettingsLastChanged, docValue,
					optDiagnosticLogEntry )
		:mapDataP( function( status, data )
		
			return KSPromises.resolvedPromiseP( 'data', true )
		end )

	end )
	
	return downloadP
end


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

local nilP = KSPromises.resolvedPromiseP( 'data', nil )

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves (as a Promise) a known-current encoded rendition for a photo.
	
 	\warning The fetch functions such as <code>:previewB()</code>
	 	retrieve any pixels they can, even if they are stale or incorrectly sized.
	       	This function, however, fails if it cannot find a correct rendition.
	
	\param photoId (optional, string) The photo to fetch; if not provided, the
		promise resolves to \c nil)
	\param renditionType (string) The rendition type, such as \c 'thumbnail' or
		\c 'preview'. ((are there other types?))
	\param fetchNow (optional, boolean)When \c true, waits for the rendition to become
		 available (downloading if necessary) and then returns that data.
		 When \c false or \c nil, the returned Promise resolves to \c nil data
		if the preview is not already available locally
	\param debugRetainCount (optional, boolean) When \c true, wraps the resultant data
		object in a special class that enables retain/release debugging.
	
	\return <code>P< jpegData | nil ></code> A Promise that resolves to the rendition.
]]--

function renditionServerMethods:encodedRenditionP( --[[string]] photoId,
						  --[[string]] ozRenditionName,
						--[[boolean]] fetchNow,
						--[[boolean]] debugRetainCount )

	check( 'WFOzRenditionServer', '?string', 'string', '?boolean', '?boolean' )
	
	local optDiagnosticLogEntry = nil

	if not photoId then
		return nilP
	end
	
	-- Grab data from cache, but only if we know it's current;
	-- otherwise trigger a download from Oz now.

	local docValueP = self._docStore:docValueOnceP( 'asset', photoId )
	
	-- FIXME [mhamburg 2013-05-10]: I'm not sure this routine is doing what the documentation
	-- above regarding fetchNow says it should be doing.

	local imageDataP = docValueP
		:mapDataP( function( _, docValue )

			if not docValue then
				return 'error', "encodedRenditionP: no document data available"
			end
			
			local renditionUrl, devSettingsLastChanged =
				 getDownloadRenditionUrlAndDevSettingsLastChanged(
						docValue, photoId, ozRenditionName,
						optDiagnosticLogEntry )

			if renditionUrl then
			
				local highPriority = false
			
				local resultP = downloadAndStoreRenditionP(
						self, photoId, ozRenditionName,
						renditionUrl,
						highPriority,
						devSettingsLastChanged, docValue,
						optDiagnosticLogEntry )

				-- FIXME [ngautam 2012-02-17]: Remove this logic when downloadOzRenditionP
				-- starts returning the downloaded data.

				if fetchNow then

					resultP = resultP:mapDataP( function( _, _ )

						local resultPromiseP = self:encodedRenditionP( photoId,
																	   ozRenditionName,
																	   nil,
																	   debugRetainCount )

						return resultPromiseP

					end )

				end
					
				return resultP

			else
			
				local md5 = KSTableUtils_derefOpt( docValue, '_localOnly', ozRenditionName, 'md5' )

				if not md5 then
					return 'error', "encodedRendition: no rendition available either locally or on server"
				end
				
				renditionServerLog:debugf( "encodedRenditionP: fetching data for MD5 = %s", md5 )
				
				return self._dataCache:getP( md5, debugRetainCount )
				
			end
		
		end )

	return imageDataP
	
end

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves a known-current encoded preview for a photo.
	
 	\warning The fetch functions such as <code>:previewB()</code>
	 	retrieve any pixels they can, even if they are stale or incorrectly sized.
	       	This function, however, fails if it cannot find a correct rendition.
	
	\param photoId (optional, string) The photo to fetch; if not provided, the
		promise resolves to \c nil)
	\param fetchNow (optional, boolean)When \c true, waits for the rendition to become
		 available (downloading if necessary) and then returns that data.
		 When \c false or \c nil, the returned Promise resolves to \c nil data
		if the preview is not already available locally
	\param debugRetainCount (optional, boolean) When \c true, wraps the resultant data
		object in a special class that enables retain/release debugging.
	
	\return <code>P< jpegData | nil ></code> A Promise that resolves to the rendition.
]]--

function renditionServerMethods:encodedPreviewP( --[[string]] photoId,
						 --[[boolean]] fetchNow,
						 --[[boolean]] debugRetainCount )

	check( 'WFOzRenditionServer', '?string', '?boolean', '?boolean' )

	return self:encodedRenditionP( photoId, 'preview', fetchNow, debugRetainCount )

end

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

-- it is lame that a queryP on a closed index store never resolves, so make one
-- that resolves to an error message to include in lieu of actual results
-- otherwise, the Rendition Server section of the log will just be quietly absent
local function diagnosticLogQueryP( indexStore, query )
	if WFCloseSupport_hasBeenOrWillBeClosed( indexStore ) then
		return KSPromises.resolvedPromiseP( 'data', "ERROR: closed index store!" )
	else
		return indexStore:queryP( query ):runUntilDoneP()
	end
end

--------------------------------------------------------------------------------
--[[!
     \brief Enables logging of rendition operations.
]]--

function renditionServerMethods:sendDiagnosticLogData()

	WFDiagnosticLog.addSectionToLog {

		name = "Rendition Server",
		id = 'WFOzRenditionServer' .. self._catalogId,
		weight = 3000,

		body = {
		
			catalogId = self._catalogId,

			cacheEpochB = self._cacheEpochB,
			
			cacheSize = diagnosticLogQueryP( self._indexStore,
				[[ SELECT sum( size ) FROM cacheReferences ]] ),
			
			recentPreviews = self._recentPreviews,
			
			velocity = visualizeVelocities( self._velocityBuffer ),
		}
	
	}
			
end

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

function renditionServerMethods:shouldDownloadRendition( docValue, assetId, ozRenditionName )

	return shouldDownloadOzRendition( docValue, assetId, ozRenditionName )
	
end

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

local renditionServer_class = Rx.Class 'WFOzRenditionServer'( renditionServerMethods )

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

return WFObjectProxy( 'WFOzRenditionServer', renditionServer_class, nil, true )
