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

PixelProviderSchedulerWorker.lua

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

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

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

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

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

	-- WF.oz_client.managed_catalog
local WFCatUser = require 'WFCatUser'
local WFCatSpec = require 'WFCatSpec'
local WFCatalog = require 'WFCatalog'

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

	-- Rx
local Rx = require "Rx"

	-- WF.oz_client.managed_catalog.pixel_provider
local PixelPathProviderRequestTypes = require 'PixelPathProviderRequestTypes'
local GridRangeUtils = require 'GridRangeUtils'

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

local logger = require 'WFLogger'( "PixelProviderSchedulerWorker" )

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

local KSTableUtils_mapArrayToSet = assert( KSTableUtils.mapArrayToSet,
			"KSTableUtils.mapArrayToSet does not exist" )

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

local GridRangeUtils_getRangeDeltas = GridRangeUtils.getRangeDeltas

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

local GridRangeUtils_isInRange = GridRangeUtils.isInRange

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

local PixelProviderSchedulerWorker = {
	
	_wfCatalog = false,
	
	_scheduler = false,

	WFCloseSupport.addCloseSupportMethods,

}

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

function PixelProviderSchedulerWorker:__init( serverBaseUrl, userId,
											userInfoCachePath,
											managedCatalogIdOrDoc,
											rootCatalogDataDir, 
											catalogOptions,
											pixelProviderSettings)

	logger:info( "PixelProviderSchedulerWorker is created" )

	local wfCatUser = WFCatUser( serverBaseUrl, userId, userInfoCachePath )

	local wfCatSpec = WFCatSpec( wfCatUser, managedCatalogIdOrDoc )

	local wfCatalog = WFCatalog( wfCatSpec, rootCatalogDataDir,
									catalogOptions )

	self:setOwned {
		_wfCatalog = wfCatalog,
		_scheduler = require 'PixelPathProviderScheduler'( wfCatalog, pixelProviderSettings ),
	
	}

end

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

PixelProviderSchedulerWorker.close = function()

	logger:info( "PixelProviderSchedulerWorker is closing" )

end

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

function PixelProviderSchedulerWorker:dispatch( name, ... )

	WFArguments_check( 'PixelProviderSchedulerWorker', 'string' )

	local schedulerFunc = 
			assert( self._scheduler[ name ],
					string.format( "scheduler function %s missing", name ) )

	return schedulerFunc( self._scheduler, ... )

end

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

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

--------------------------------------------------------------------------------
								
local function notInSet( v, set )

	return not set[ v ] and v or nil

end

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

function PixelProviderSchedulerWorker:updateAssetIdList( pixStreamId,
														prevAssetIds,
														newAssetIds )
	
	local newAssetIdsSet = KSTableUtils_mapArrayToSet( newAssetIds )	

	local removedAssetIds = 
			WFFunctionUtils_filter( notInSet, prevAssetIds, newAssetIdsSet )
	
	self._scheduler:batchRemoveRequests( pixStreamId, removedAssetIds )

end

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

function PixelProviderSchedulerWorker:updateCellSizes( pixStreamId, assetIds, 
														cellSizeData )

	
	local assetIdIndexes = {}
	
	for i, assetId in ipairs( assetIds ) do

		assetIdIndexes[ assetId ] = assetIdIndexes[ assetId ] or i

	end

	self._scheduler:updateCellSizes( pixStreamId, assetIdIndexes, cellSizeData )

end

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

local function accumulateAllRanges( ranges, assetIds, initialValue, 
										accumFunc, 
											... )

	local currVal = initialValue

	for _, range in ipairs( ranges ) do

		local first, last = unpack( range )

		for i = first, last, first <= last and 1 or -1 do

			local assetId = assetIds[ i ]

			if assetId then

				currVal = accumFunc( currVal, assetId, i, ... )

			end

		end

	end

	return currVal

end

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

local function accumulateAddedAssetIdIndex( addedAssetIdIndexes, _, index,
												prevRange )

	local action = GridRangeUtils_isInRange( prevRange, index ) and
					"update" or "create"

	addedAssetIdIndexes[ #addedAssetIdIndexes + 1 ] = { action, index }

	return addedAssetIdIndexes

end

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

local function maybeAccumulateRemovedAssetId( removedAssetIds, assetId, index,
													bufferedRange )

	if not GridRangeUtils_isInRange( index, bufferedRange ) then

		removedAssetIds[ #removedAssetIds + 1 ] = assetId

	end

	return removedAssetIds

end

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

local function accumulateRemovedOnscreenAssetIds( accumulatedArrays, assetId,
													index, prevOnScreenRange,
													bufferedRange )

	if not GridRangeUtils_isInRange( index, bufferedRange ) then
		
		accumulatedArrays.removedAssetIds = 
			accumulatedArrays.removedAssetIds or {}

		-- went from onscreen to not even buffered
		accumulatedArrays.removedAssetIds =
			maybeAccumulateRemovedAssetId( accumulatedArrays.removedAssetIds,
												assetId, index, bufferedRange )

	else

		accumulatedArrays.newPrefetchAssetIdIndexes = 
			accumulatedArrays.newPrefetchAssetIdIndexes or {}

		-- dropped from visible to prefetch
		accumulatedArrays.newPrefetchAssetIdIndexes = 
			accumulateAddedAssetIdIndex(
				accumulatedArrays.newPrefetchAssetIdIndexes, assetId, index,
				prevOnScreenRange )

	end

	return accumulatedArrays

end

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

local function swapPair( pair )

	if type( pair ) ~= "table" or #pair ~= 2 then

		return pair

	end

	return { pair[ 2 ], pair[ 1 ] }

end

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

local idle = PixelPathProviderRequestTypes.names.idle

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

local grid_visible = PixelPathProviderRequestTypes.names.grid_visible

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

local grid_prefetch = assert( PixelPathProviderRequestTypes.names.grid_prefetch,
									"grid_prefetch not defined" )

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

local function stringifyRange( range )

	return string.format( "{ %s, %s }", unpack( range ) )

end

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

local function concatRange( rangeStr, range )

	local thisRangeStr = stringifyRange( range )

	return rangeStr and 
			string.format( "%s, %s", rangeStr, thisRangeStr ) or
			thisRangeStr

end

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

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

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

local function describeRanges( desc, ranges )

	return string.format( "%s: %s", desc, 
						WFFunctionUtils_reduce( nil, concatRange, ranges ) )

end

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

local function updateForScrollingImp( 
				pixStreamId, assetIds,
				prevOnScreenRange, onScreenRange,
				prevBufferedRange, bufferedRange,
				scrollDirection, 
				prevFetchAheadRange, fetchAheadRange,
				prevFetchBehindRange, fetchBehindRange )

	local addedOnScreenRanges, removedOnScreenRanges = 
			GridRangeUtils_getRangeDeltas( prevOnScreenRange, onScreenRange )

	if logger.will_info then

		local rangeStr = string.format( "%s: %s", pixStreamId, "onScreenRange, bufferedRange, fetchAheadRange, fetchBehindRange" )

		logger:info( describeRanges( rangeStr, { onScreenRange, bufferedRange,
													fetchAheadRange,
													fetchBehindRange } ) )

		logger:info( describeRanges( "addedOnScreenRanges", addedOnScreenRanges ) )

		logger:info( describeRanges( "removedOnScreenRanges", 
										removedOnScreenRanges ) )

	end

	local newOnScreenAssetIdsIndexes =
				accumulateAllRanges( 
					addedOnScreenRanges, assetIds, {},
							accumulateAddedAssetIdIndex,
								prevBufferedRange )

	local accumulatedArrays =
			accumulateAllRanges( 
						removedOnScreenRanges, assetIds, {},
							accumulateRemovedOnscreenAssetIds,
								prevOnScreenRange, bufferedRange )

	local removedAssetIds = accumulatedArrays.removedAssetIds or {}

	local newPrefetchAssetIdIndexes = 
			accumulatedArrays.newPrefetchAssetIdIndexes or {}
						
	-- For the fetch ahead and fetch behind ranges, we want to add the most
	-- important ranges to the scheduler first. The most important ranges
	-- depend on the scroll direction. For forward scrolled, fetch ahead
	-- is most important. Otherwise, the opposite is true.
	--
	local currAndPrevRangePairs = { { fetchAheadRange, prevFetchAheadRange },
									{ fetchBehindRange, prevFetchBehindRange } }
	
	if scrollDirection < 0 then

		currAndPrevRangePairs = swapPair( currAndPrevRangePairs )

	end

	for _, currAndPrevRangePair in ipairs( currAndPrevRangePairs ) do

		local fetchRange, prevFetchRange = unpack( currAndPrevRangePair ) 

		local addedFetchRanges, removedFetchRanges = 
				GridRangeUtils_getRangeDeltas( prevFetchRange, fetchRange )

		if fetchRange == fetchBehindRange then

			-- there may be up to two added fetch behind  ranges. In that case,
			-- the second range is "closer" to the onscreen area than the
			-- first, so we swap the order
			addedFetchRanges = swapPair( addedFetchRanges )

			-- furthermore, for each fetch behind range, the end of the range
			-- is closer to onscreen than the start of the range, so we should
			-- swap the range orders for fetch behind ranges, too
			
			addedFetchRanges[ 1 ] = swapPair( addedFetchRanges[ 1 ] )

			addedFetchRanges[ 2 ] = swapPair( addedFetchRanges[ 2 ] )

		end

		if logger.will_info then

			local addedDesc = fetchRange == fetchBehindRange and 
								"addedFetchBehindRanges" or
								"addedFetchAheadRanges"

			logger:info( describeRanges( addedDesc, addedFetchRanges ) )

			local removedDesc = fetchRange == fetchBehindRange and 
								"removedFetchBehindRanges" or
								"removedFetchAheadRanges"

			logger:info( describeRanges( removedDesc, removedFetchRanges ) )

		end

		newPrefetchAssetIdIndexes =
				accumulateAllRanges( 
					addedFetchRanges, assetIds, newPrefetchAssetIdIndexes,
							accumulateAddedAssetIdIndex,
								prevBufferedRange )

		removedAssetIds =
			accumulateAllRanges( 
					removedFetchRanges, assetIds, removedAssetIds,
						maybeAccumulateRemovedAssetId,
							bufferedRange )

	end
				
	return newOnScreenAssetIdsIndexes, newPrefetchAssetIdIndexes,
				removedAssetIds


end

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

function PixelProviderSchedulerWorker:updateForGridScrolling( 
										pixStreamId, assetIds, cellSizeData,
										prevOnScreenRange, onScreenRange,
										prevBufferedRange, bufferedRange,
										scrollDirection, 
										prevFetchAheadRange, fetchAheadRange,
										prevFetchBehindRange, fetchBehindRange )
				
	local newOnScreenAssetIdsIndexes, newPrefetchAssetIdIndexes,
			removedAssetIds = 
				updateForScrollingImp( 
					pixStreamId, assetIds,
					prevOnScreenRange, onScreenRange,
					prevBufferedRange, bufferedRange,
					scrollDirection, 
					prevFetchAheadRange, fetchAheadRange,
					prevFetchBehindRange, fetchBehindRange )

	self._scheduler
		:createThumbnailRenditionRequestOrUpdatePriority( 
			pixStreamId, newOnScreenAssetIdsIndexes, assetIds, cellSizeData,
			grid_visible )

	self._scheduler
		:createThumbnailRenditionRequestOrUpdatePriority( 
			pixStreamId, newPrefetchAssetIdIndexes, assetIds, cellSizeData,
			grid_prefetch )

	self._scheduler
		:updatePriorityOnDownloadingRequestsOrRemove(
			pixStreamId, removedAssetIds, idle )

end

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

function PixelProviderSchedulerWorker:updateForLoupeScrolling( 
										pixStreamId, assetIds, cellSizeData,
										prevOnScreenRange, onScreenRange,
										prevBufferedRange, bufferedRange,
										scrollDirection, 
										prevFetchAheadRange, fetchAheadRange,
										prevFetchBehindRange, fetchBehindRange )
				
	local newOnScreenAssetIdsIndexes, newPrefetchAssetIdIndexes,
			removedAssetIds = 
				updateForScrollingImp( 
					pixStreamId, assetIds,
					prevOnScreenRange, onScreenRange,
					prevBufferedRange, bufferedRange,
					scrollDirection, 
					prevFetchAheadRange, fetchAheadRange,
					prevFetchBehindRange, fetchBehindRange )

	self._scheduler
		:createLoupeRenditionRequestOrUpdatePriority(
			pixStreamId, newOnScreenAssetIdsIndexes, assetIds, cellSizeData,
			grid_visible )

	self._scheduler
		:createLoupeRenditionRequestOrUpdatePriority(
			pixStreamId, newPrefetchAssetIdIndexes, assetIds, cellSizeData,
			grid_prefetch )

	self._scheduler
		:batchRemoveRequests( pixStreamId, removedAssetIds )

end

--------------------------------------------------------------------------------
-- definedForwardedMethod defines a PixelProviderSchedulerWorker:apiName( ... )
-- method that in turn just calls PixelProviderScheduler:apiName( ... )
--------------------------------------------------------------------------------

local function defineForwardedMethod( apiName )

	PixelProviderSchedulerWorker[ apiName ] =
		function( self, ... )

			WFArguments_check( 'PixelProviderSchedulerWorker' )

			local scheduler = self._scheduler

			return scheduler[ apiName ]( scheduler, ... )

		end

end

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

defineForwardedMethod( 'pixStreamIdPixelPathStream' )

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

defineForwardedMethod( 'pixStreamIdsPixelPathStream' )

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

defineForwardedMethod( 'containsPixStreamIdSubstrPixelPathStream' )

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

defineForwardedMethod( 'bufferedPixelPathStream' )

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

defineForwardedMethod( '_DEBUG_requestStatsStream' )

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

defineForwardedMethod( '_DEBUG_localAssetIdStatesStream' )

--------------------------------------------------------------------------------
-- bindMethod just maps workerThread:request( apiName, ... ) to
-- PixelProviderScheduler:apiName( ... )
--------------------------------------------------------------------------------

local function bindMethod( methodName, pixelProviderSchedulerWorker )

	WFRequestDispatcher.onMessageDo {

		message = methodName,

		target = pixelProviderSchedulerWorker,

	}

end

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

PixelProviderSchedulerWorker.class = {}

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

function PixelProviderSchedulerWorker.class.initMessageProcessor( 
												Class, encodedPackedThreadArgs )

	local packedThreadArgs = KSJsonDecoder.decodeWebSafeJson(
								encodedPackedThreadArgs )

	local pixelProviderSchedulerWorker = Class( unpack( packedThreadArgs ) )

	WFCloseSupport.mustCloseAtThreadTermination( pixelProviderSchedulerWorker )

	bindMethod( 'dispatch', pixelProviderSchedulerWorker )

	bindMethod( 'updateAssetIdList', pixelProviderSchedulerWorker )

	bindMethod( 'updateCellSizes', pixelProviderSchedulerWorker )

	bindMethod( 'updateForGridScrolling', pixelProviderSchedulerWorker )

	bindMethod( 'updateForLoupeScrolling', pixelProviderSchedulerWorker )

	bindMethod( 'pixStreamIdPixelPathStream', pixelProviderSchedulerWorker )

	bindMethod( 'pixStreamIdsPixelPathStream', pixelProviderSchedulerWorker )

	bindMethod( 'containsPixStreamIdSubstrPixelPathStream', 
					pixelProviderSchedulerWorker )

	bindMethod( 'bufferedPixelPathStream', pixelProviderSchedulerWorker )

	bindMethod( '_DEBUG_requestStatsStream', pixelProviderSchedulerWorker )

	bindMethod( '_DEBUG_localAssetIdStatesStream', pixelProviderSchedulerWorker )

end


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

return Rx.Class( 'PixelProviderSchedulerWorker', PixelProviderSchedulerWorker )
