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

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,

	_stopped = 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:stop()

	self._stopped = true

	if WFCloseSupport.hasBeenOrWillBeClosed( self._scheduler ) then

		-- it's fine if the scheduler is already closed
		return true

	end

	-- otherwise we try to shutdown the scheduler
	local ok, stopSucceededOrErrStr, maybeStopFailureReason =
			pcall( self._scheduler.stopMainLoop, self._scheduler )

	if not ok then

		local errStr = stopSucceededOrErrStr

		return false, errStr

	end

	local stopOk = stopSucceededOrErrStr

	if not stopOk then

		local stopFailReason = 
				maybeStopFailureReason or 
				"stop failed but didn't give reason"

		return false, stopFailReason

	end

	return true

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( index, prevRange ) 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

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

function PixelProviderSchedulerWorker:updateForLoupeWalk( 
										pixStreamId, assetIds, cellSizeData,
										prevAssetIdIndices, assetIdIndices )

	local currIndicesInUse = KSTableUtils.mapArrayToSet( assetIdIndices )
	local prevUsedIndices = {}
	local newOnScreenAssetIdsIndexes = {}
	local newPrefetchAssetIdIndexes = {}
	local removedAssetIds = {}
	
	for i = 1, #prevAssetIdIndices do
		
		local index = prevAssetIdIndices[ i ]
		
		prevUsedIndices[ index ] = true
		
		if not currIndicesInUse[ index ] then
			local id = assetIds[ index ]
			removedAssetIds[ #removedAssetIds + 1 ] = id
		end
	end
	
	local prevLoupeAssetId = #prevAssetIdIndices > 0 and assetIds[ prevAssetIdIndices[ 1 ] ]
	 
	-- for assetIdIndices, the first item is always the active loupe asset, the rest are apron prefetch
	for i = 1, #assetIdIndices do
		
		local index = assetIdIndices[ i ]
		
		local isPrevUsed = prevUsedIndices[ index ] and true or false
		
		if i == 1 then
			
			if not isPrevUsed or prevLoupeAssetId ~= assetIds[ index ] then
				
				local action = isPrevUsed and "update" or "create"
				
				newOnScreenAssetIdsIndexes[ #newOnScreenAssetIdsIndexes + 1 ] = { action, index }
			end
			
		else
		
			if not isPrevUsed or prevLoupeAssetId == assetIds[ index ] then
			
				local action = isPrevUsed and "update" or "create"
				
				newPrefetchAssetIdIndexes[ #newPrefetchAssetIdIndexes + 1 ] = { action, index }
			end
		end
	end

	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 )

	assert( type( methodName ) == "string", "methodName must be a string" )

	local func = pixelProviderSchedulerWorker[ methodName ]

	if type( func ) ~= "function" then

		error( string.format( "PixelProviderSchedulerWorker.%s is not a function",
								methodName ) )

	end

	WFRequestDispatcher.onMessage(
		methodName,
		function( ... )

			if pixelProviderSchedulerWorker._stopped then

				return Rx.Observable.throw( 
					string.format( "Can't run %s, pixel provider worker is stopped", 
										methodName ) )

			end

			return func( pixelProviderSchedulerWorker, ... )

		end )

end

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

PixelProviderSchedulerWorker.class = {}

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

function PixelProviderSchedulerWorker.class.initMessageProcessor( 
												Class, encodedPackedThreadArgs )

	local packedThreadArgs = KSJsonDecoder.decodeWebSafeJson(
								encodedPackedThreadArgs )

	local pixelProviderSchedulerWorker = Class( unpack( packedThreadArgs ) )

	WFCloseSupport.mustCloseAtThreadTermination( pixelProviderSchedulerWorker )

	bindMethod( 'stop', pixelProviderSchedulerWorker )

	bindMethod( 'dispatch', pixelProviderSchedulerWorker )

	bindMethod( 'updateAssetIdList', pixelProviderSchedulerWorker )

	bindMethod( 'updateCellSizes', pixelProviderSchedulerWorker )

	bindMethod( 'updateForGridScrolling', pixelProviderSchedulerWorker )

	bindMethod( 'updateForLoupeScrolling', pixelProviderSchedulerWorker )
	
	bindMethod( 'updateForLoupeWalk', pixelProviderSchedulerWorker )

	bindMethod( 'pixStreamIdPixelPathStream', pixelProviderSchedulerWorker )

	bindMethod( 'pixStreamIdsPixelPathStream', pixelProviderSchedulerWorker )

	bindMethod( 'containsPixStreamIdSubstrPixelPathStream', 
					pixelProviderSchedulerWorker )

	bindMethod( 'bufferedPixelPathStream', pixelProviderSchedulerWorker )

	bindMethod( '_DEBUG_requestStatsStream', pixelProviderSchedulerWorker )

	bindMethod( '_DEBUG_localAssetIdStatesStream', pixelProviderSchedulerWorker )

	WFRequestDispatcher.enableDiagnosticLogForRequestStats()

end


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

return Rx.Class( 'PixelProviderSchedulerWorker', PixelProviderSchedulerWorker )
