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

WFCatStyleBinaryFetcher.lua

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

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

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

local Rx = require 'Rx'
local Observable = Rx.Observable
local WFCatStyleBinaryDownloader = require 'WFCatStyleBinaryDownloader'
local WFLogger = require 'WFLogger'
local KSIdGenerator = require 'KSIdGenerator'
local KSTableUtils = require 'KSTableUtils'
local WFRxArrayDiffer = require 'WFRxArrayDiffer'
local WFRxContinuousTask = require 'WFRxContinuousTask'
local WFCatBinaryFetcherBase = require 'WFCatBinaryFetcherBase'
local NmbProfileDownloadHelper = require 'NmbProfileDownloadHelper'
local WFRxHttpClient = require 'WFRxHttpClient'
local WFFileUtils = require 'WFFileUtils'
local NmbAcr = require 'NmbAcr'

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

local logger = WFLogger 'WFCatStyleBinaryFetcher'
	--:setLevel( 'DEBUG' )

local STYLE_QUERY_CALM_INTERVAL = 1 --In secs. Arbitrary value.
local STYLE_MAX_NUM_OF_IN_FLIGHT_REQUESTS = 20
	-- If there are errors and there are no updates to
	-- the style table, then we need to trigger the
	-- download process to retry the error cases.
local DEFAULT_PRIORITY = WFRxHttpClient.requestPriorityLow

local WFAcrOperations = assert( NmbAcr.WFAcrOperations )

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

-- Creates a separate function for downloading of 'v2' presets and profiles.
-- Per consults with Brian K, it is probably not wise to try to channel download
-- of presets and profiles through the pixel path provider state machinery,
-- as there is a 'baked in assumption' that the downloadable assets have a
-- docValue.type of 'image'. It might be feasible (though probably overkill)
-- to implement a separate state machine for managing profile/preset sync
--
-- Another possible path, per Brian is:
--[[

Actually thinking further, I think preset sync is very similar to `RenditionPerAssetPathFetcher.lua`

`RenditionPerAssetPathFetcher.lua` ensures we have at one least one local rendition per each `assetId` in the catalog. That's pretty much the same as ensuring we have a local preset file for each `presetId` in the catalog.

Here are the `RenditionPerAssetPathFetcher.lua` implementation details:

1. it sets up a `"allLocalAssetIds"` submodel which has a `:value()` stream that emits

```{ allAssetIds, {} }
{ addToAllAssetIds, removeFromAllAssetIds }
{ addToAllAssetIds, removeFromAllAssetIds }
...
```

2. it sets up a `"idlePixelPaths"` submodel. The submodel `:sendEvent( "fetchPaths" )` call drives the current work. The submodel `:value()` stream is the ongoing results from the `:sendEvent( "fetchPaths" )` calls.

3. The `"allLocalAssetIds"` submodel `:value()` stream is fed into a "needs rendition path" queue. That queue is popped on an `repeat ... until false` `AgEventLoup` task. The inner loop of that task spends most of its time in a wait state unless no current rendition downloads are pending and the "needs rendition path" queue is non empty.

When the wait is lifted, the queue is popped and the result is passed to `:sendEvent( "fetchPaths" )` on `"idlePixelPaths"` submodel.

4. As results come back, we update the current rendition downloads are pending list as needed

So if you copy `RenditionPerAssetPathFetcher.lua` to say, `LocalPresetSyncer.lua` file, make these changes plus implementing the daemon code to fulfill them, you'll be be pretty far along with preset sync:

1. In `LocalPresetSyncer.lua`, replace the `"allLocalAssetIds"' submodel  with a new `"needsPresetSyncIds"` submodel which should emit a stream of the following

```{ needsPresetDownloadPresetIds, {} }
{ addToNeedsPresetDownloadPresetIds, removeFromNeedsPresetDownloadPresetIds }
{ addToNeedsPresetDownloadPresetIds, removeFromNeedsPresetDownloadPresetIds }
...
```

2. On the daemon, implement `"needsPresetSyncIds"` so that it first get the list of all `presetIds` from the doc store and filters out all of them we either can't download or don't need to download. Then as updates come in (when presets are added, remove, or updated), emit the updates as arrays of adds and deletes. See `WFCatAssets.allLocalAssetIdsStream` for something very similar to that.

3. In `LocalPresetSyncer.lua`, replace `"idlePixelPaths"` submodel with `"ensurePresetIsLocal"` submodel and call `:sendEvent( "fetchPreset" )` on it.

4. On the daemon, implement that `:sendEvent( "fetchPreset" )` to accept one `presetId` at a time. Each time the daemon gets a new `presetId`, cancel the current download (if there is one) and start downloading the new `presetId` after checking the local path. In any case, when done with the `presetId` return `{ presetId, isLocal, maybeErrorString }` on the `:value()` stream so that the `AgEventLoup` task will unblock its wait.

5. In `LocalPresetSyncer.lua`, make whatever additional changes needed so that `"needsPresetSyncIds"` submodel `:value()` updates the work queue and that the `"ensurePresetIsLocal"` submodel `:value()` output is handled correctly.

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

local function isDownloadSuccess( status )

	return not not status

end
--------------------------------------------------------------------------------

local function getPendingStyles( self, indexStore )

	local pendingStylesQuery = indexStore:query {
		sql = [[ SELECT docId FROM presetsAndProfiles
				WHERE waitingForBinary = 1 and ozHasMaster = 1 ]],
				quiesceUntilUpToDate = true,
				calm = STYLE_QUERY_CALM_INTERVAL,
				singleColumnQuery = true
	}

	return pendingStylesQuery
		
		:map( function( docIds )
			
			if docIds and #docIds > 0 then
				return docIds
			end
			
			return {}
			
		end )
		
		:call( WFRxArrayDiffer )
		
end

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

local function filterWithModel( models, t )
	
	local result = {}
	models = KSTableUtils.mapArrayToSet( models )
	
	for i, v in ipairs( t ) do
		local modelName = v and #v > 1 and v[ 2 ]
		if modelName and models[ modelName ] then
			result[ #result + 1 ] = v[ 1 ]
		end
	end
	
	return result
end

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

local function createRequestWithPriority( array, priority )
	
	local result = {}
	
	for i, v in ipairs( array ) do
		result[ v ] = {
			httpPriority = priority,
		}
	end
	
	return result
end


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

local function getPendingCameraProfiles( self, indexStore )

	local pendingStylesQuery = indexStore:query {
		sql = [[ SELECT docId, cameraModel FROM customCameraProfiles
				WHERE waitingForBinary = 1 and href NOT NULL ]],
				quiesceUntilUpToDate = true,
				calm = STYLE_QUERY_CALM_INTERVAL,
	}

	return pendingStylesQuery
		
		:combineLatest( self._catalog:getCatalogProfileCache():getCameraModelsInUseStream() )
		
		:map( function( queryResult, modelsInUse )
			
			local numItems = queryResult and #queryResult
			if numItems > 0 then
			
				return filterWithModel( modelsInUse, queryResult )
			end
			
			return {}
		end )
		
		:call( WFRxArrayDiffer )
		
		:tap( function( additions, removals )
			
			logger:debugf( "PendingCameraProfiles: additions : %s, removals : %s",
				#additions, #removals )
			
		end )
		
end

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

local function listenForPendingStyles( self, catalog )

	return getPendingStyles( self, catalog:indexStore() )
	
		:merge( getPendingCameraProfiles( self, catalog:indexStore() ) )
		
		:flatMapLatest( function( additions, removals )
			
			return self._continuousTaskForStyleChangeListening:throwIfNotRunning( additions, removals )
		
		end )
		
		:map( function( additions, removals )
			
			additions = KSTableUtils.mapArrayToSet( additions )
			removals = KSTableUtils.mapArrayToSet( removals )
			
			self:addAndRemoveRequests( additions, removals )
			
		end )
	
		:catch( function( errorMessage )
		
			return self._continuousTaskForStyleChangeListening:throwIfNotStopped( errorMessage )
		end )
end

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

local function makeAdjustablePriorityASAPRequestForCameraModelImpl( self, cameraModelName )

	local pendingStylesQuery = self._catalog:indexStore():queryOnce {
		sql = [[ SELECT docId FROM customCameraProfiles
				WHERE waitingForBinary = 1 AND href NOT NULL 
					AND cameraModel = ?]],
		quiesceUntilUpToDate = true,
		singleColumnQuery = true,
		string.lower( cameraModelName ),
	}

	return pendingStylesQuery
		
		:map( function( docIds )
			
			if docIds and #docIds > 0 then
			
				local additions = createRequestWithPriority( docIds, WFRxHttpClient.requestPriorityHigh )
				
				self:addAndRemoveRequests( additions, {} )
				
				return additions
				
			else
			
				return {}
				
			end
			
		end )
end

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

local function makeResetToDefaultPriorityFunc( self, additions )
	
	return function()

		return self:assignDefaultPriorityForRequests( additions )
		
	end
end

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

local WFCatStyleBinaryFetcher = {

	WFCatBinaryFetcherBase,
	
	_catalog = false,
	
	_binaryDownloader = false,
	
	_continuousTaskForStyleChangeListening= false,
}

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

function WFCatStyleBinaryFetcher:__init( catalog, optMaxFlightRequests )

	self._catalog = catalog
	self._binaryDownloader = WFCatStyleBinaryDownloader( catalog )
	
	local downloadTaskDelays = {
		progressiveBackoff = false,
		errorDelay = 10,
		maxDelay = 10,
	}

	-- For style notification
	local continuousTaskForStyleListening = function() return listenForPendingStyles( self, catalog ) end
	self._continuousTaskForStyleChangeListening = WFRxContinuousTask( "StyleDownloadNotificationTask", continuousTaskForStyleListening, downloadTaskDelays )
	
	-- for super class constructor
	return catalog, optMaxFlightRequests or STYLE_MAX_NUM_OF_IN_FLIGHT_REQUESTS
end

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

function WFCatStyleBinaryFetcher:getRefreshStream()
	
	return self:getResponseStream()
		:filter( isDownloadSuccess )
end

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

function WFCatStyleBinaryFetcher:start()
	
	self:WFCatBinaryFetcherBase_start()
	
	-- Set up the db listening subscription after
	-- setting up the listeners.
	self._continuousTaskForStyleChangeListening:startContinuousTask()
end

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

function WFCatStyleBinaryFetcher:stop()

	self._continuousTaskForStyleChangeListening:stopContinuousTask()
	
	self:WFCatBinaryFetcherBase_stop()
end

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

function WFCatStyleBinaryFetcher:close()
	
	self:stop()
end

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

function WFCatStyleBinaryFetcher:_doOneDownload( requestKey, requestValue, requestId )

	local httpPriority = requestValue.httpPriority or DEFAULT_PRIORITY
	return NmbProfileDownloadHelper.doOneCustomStyleDownload( self._binaryDownloader, requestKey, httpPriority )
end

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

function WFCatStyleBinaryFetcher:makeAdjustablePriorityASAPRequestForCameraModel( cameraModelName )
	
	if not self._isStarted then
		return Rx.Observable.just()
	end
	
	-- Process the camera model and add new request if needed.
	-- Return a function which will reduce the priority of
	-- the pending requests, if any.
	
	return makeAdjustablePriorityASAPRequestForCameraModelImpl( self, cameraModelName )
		:map( function( additions )
			return makeResetToDefaultPriorityFunc( self, additions )
		end )
end

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

function WFCatStyleBinaryFetcher:assignDefaultPriorityForRequests( requests )
	
	return self:maybeChangePriorityForRequests( requests, DEFAULT_PRIORITY )
end

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

function WFCatStyleBinaryFetcher:ensureCustomCameraProfileExists( href )

	return self._catalog:getCatalogProfileCache():getInfoForHref( href )
		:flatMap( function( info )
			
			if not info then
				logger:warnf( "We dont have info about the href yet. Href:%s", href )
				return Observable.just( false )
			end
			
			local path = self._catalog:fileSpec():pathForDCP( info.filename )
			if WFFileUtils.exists( path ) == "file" then
				logger:debugf( "Profile exists. filename: %s", info.filename )
				return Observable.just( true )
			end
			
			if not self._isStarted then
				logger:debugf( "Sync is paused. Cannot download: %s", info.filename )
				return Observable.just( false )
			end
			
			info.httpPriority = WFRxHttpClient.requestPriorityHigh
			local requestId = KSIdGenerator.uuidInLowercaseHex()
			
			return self:_doOneDownloadWithResponseStream( info.docId, info, requestId )
				
				:flatMap( function( requestKey, success, pathOrErr )
				
					logger:debugf( "ensureCustomProfileExists status: %s, pathOrErr : %s", success, pathOrErr )
					if not success then
						return Observable.just( false )
					end
				
					-- If we have downloaded the profile, then we need to make sure
					-- that the daemon will use it too.
					return WFAcrOperations.refreshACRDatabases()
						:defaultIfEmpty()
						:map( function()
							return true
						end )
				end )	
		end )
end

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

return Rx.Class 'WFCatStyleBinaryFetcher'( WFCatStyleBinaryFetcher )