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

NmbStdProfileBinaryFetcher.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 WFLogger = require 'WFLogger'
local WFRxContinuousTask = require 'WFRxContinuousTask'
local WFFileUtils = require 'WFFileUtils'
local WFCatBinaryFetcherBase = require 'WFCatBinaryFetcherBase'
local NmbAcr = require 'NmbAcr'
local NmbProfileBinaryDownloader = require 'NmbProfileBinaryDownloader'
local NmbProfileDownloadHelper = require 'NmbProfileDownloadHelper'
local WFRxArrayDiffer = require 'WFRxArrayDiffer'
local WFRxHttpClient = require 'WFRxHttpClient'
local KSIdGenerator = require 'KSIdGenerator'

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

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

local MAX_NUM_OF_IN_FLIGHT_REQUESTS = 20
local DEFAULT_PRIORITY = WFRxHttpClient.requestPriorityMedium

local WFAcrOperations = assert( NmbAcr.WFAcrOperations )

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

local function wrapWithSelf( self, fn )

	return function( ... )

		return fn( self, ... )

	end

end

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

local function getKey( _, _, key )

	return key

end

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

local function isDownloadSuccess( status )

	return not not status

end

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

local function getProfilesForModel( self, cameraModel )

	-- SPAM [ashj 2018-07-20]: The model name that we have
	-- is in upper case. But the one which is with ACR is
	-- not. Hence we are caching the map here with the key
	-- in upper case.
	
	return self._profileCatalog:getProfileCache():getProfilesForModel( cameraModel )
end

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

local function getPendingCameraModels( self )

	return self._catalog:getCatalogProfileCache():getCameraModelsInUseStream()
		
		:call( WFRxArrayDiffer )
		
end

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

local function needsDownload( self, profileInfo )
					
	if not profileInfo.fileName or not profileInfo.relativePath then
		return false
	end
	
	local profilePath = 
		self._catalog:fileSpec():pathForStandardProfile( 
			profileInfo.relativePath )
	
	if WFFileUtils.exists( profilePath ) then
		-- Already exists.
		return false
	end
	
	return true
end

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

local function processNewCameraModel( self, cameraModel, httpPriority )

	httpPriority = httpPriority or DEFAULT_PRIORITY
	self._processedCameraModels[ cameraModel ] = true
	
	return getProfilesForModel( self, cameraModel )
		:flatMap( function( profileInfos )
		
			logger:debugf( "Received profileInfos for model: %s profileInfo size:%s", 
				cameraModel, profileInfos and #profileInfos or "nil" )
				
			local additions = {}
			
			return Observable.from( profileInfos or {} )
				
				:filter( wrapWithSelf( self, needsDownload ) )
				
				:tap( function( profileInfo )
				
					logger:debugf( "Will try downloading profile: %s", profileInfo.fileName )
					profileInfo.httpPriority = httpPriority
					additions[ profileInfo.fileName ] = profileInfo
					
				end )
				:toArray()
				:map( function()
					self:addAndRemoveRequests( additions )
					return additions
				end )
		end )
		
end

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

local function yetToProcessModel( self, cameraModel )

	return not self._processedCameraModels[ cameraModel ]
		
end

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

local function processCameraModels( self )

	logger:debugf( "Starting processCameraModels" )
	
	-- TODO [ashj 2018-08-24]: This is a continuous running
	-- observable. Need to make sure that we respect the
	-- start and stop signals.
	
	return getPendingCameraModels( self )
	
		:flatMapLatest( function( docsAndUrls )

			return self._continuousTaskForChangeListening:throwIfNotRunning( docsAndUrls )
				-- Need to throw as the observable doesn't ever complete.
		end )
		
		:flatMap( function( additions, removals )
			
			-- Not removing any profile when images
			-- with a camera model is removed.
			
			return Observable.from( additions )
				:filter( wrapWithSelf( self, yetToProcessModel ) )
				:flatMapLatest( function( docsAndUrls )

					return self._continuousTaskForChangeListening:throwIfNotRunning( docsAndUrls )
						-- Need to throw as the observable doesn't ever complete.
				end )
				
				:flatMap( wrapWithSelf( self, processNewCameraModel ) ) 
				
		end )
		
		:toArray()
		
		:defaultIfEmpty( {} )
		
		:tap( function( processedEntries )
			
			self._continuousTaskForChangeListening:taskDidWork( not not next( processedEntries )  )
			
		end )
		
		:catch( function( errorMessage )
		
			logger:errorf( "Error in processCameraModels: %s", errorMessage )
			return self._continuousTaskForChangeListening:throwIfNotStopped( errorMessage )
		end )
		
end

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

local function handleNewStdProfileStream( self )
	
	return self:getRefreshStream()
		:map( getKey )
		:bufferWithTimeOrCount( 20, 1000, require 'WFRxScheduler'.sharedScheduler() )
		
		:flatMap( function( profilesAdded )
		
			if #profilesAdded > 0 then
				
				return self._cameraProfileLocalStatus:noteNewStandardProfiles( profilesAdded )
				
			end
			
			return Observable.empty()
		end )
end

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

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

		return self:assignDefaultPriorityForRequests( additions )
		
	end
end

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

local function getGenericLogFunctionsForSubscribe( streamName )

	return {
		function()
			-- noop onNext fn
		end,
		
		function( err )
			logger:errorf( "Stream: %s ended with error: %s",
									streamName, err )
		end
	}
	
end

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

local NmbStdProfileBinaryFetcher = {

	WFCatBinaryFetcherBase,
	
	_isStarted = false,
	
	_catalog = false,
	
	_profileCatalog = false,
	
	_cameraProfileLocalStatus = false,
	
	_processedCameraModels = false,
	
	_cameraModelProfilesMap = false,
	
	_binaryDownloader = false,
	
	_refreshSubscription = false,
	
	_continuousTaskForChangeListening= false,

}

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

function NmbStdProfileBinaryFetcher:__init( catalog, profileCatalog, 
		cameraProfileLocalStatus, optMaxFlightRequests )

	self._catalog = catalog
	self._profileCatalog = profileCatalog
	self._cameraProfileLocalStatus = cameraProfileLocalStatus
	self._processedCameraModels = {}
	self._binaryDownloader = NmbProfileBinaryDownloader( catalog, profileCatalog )
	
	local downloadTaskDelays = {
		progressiveBackoff = true,
		errorDelay = 10,
		maxDelay = 300,
	}

	-- For style notification
	local continuousTaskForListening = function() return processCameraModels( self ) end
	self._continuousTaskForChangeListening = WFRxContinuousTask( "StdProfileDownloadNotificationTask", continuousTaskForListening, downloadTaskDelays )
	
	-- for super class constructor
	return catalog, optMaxFlightRequests or MAX_NUM_OF_IN_FLIGHT_REQUESTS
end
--------------------------------------------------------------------------------

function NmbStdProfileBinaryFetcher:__inited( )
	
	-- Set up Refresh subscription.
	self._refreshSubscription = handleNewStdProfileStream( self )
		:subscribe( unpack( getGenericLogFunctionsForSubscribe( "handleNewStdProfileStream" ) ) )
	
end
--------------------------------------------------------------------------------

function NmbStdProfileBinaryFetcher:start()
	
	self._isStarted = true
	self._processedCameraModels = {}
	
	self:WFCatBinaryFetcherBase_start()
	
	-- Set up the db listening subscription after
	-- setting up the listeners.
	self._continuousTaskForChangeListening:startContinuousTask()
end

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

function NmbStdProfileBinaryFetcher:stop()

	self._isStarted = false
	self._continuousTaskForChangeListening:stopContinuousTask()
	
	self:WFCatBinaryFetcherBase_stop()
end

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

function NmbStdProfileBinaryFetcher:close()
	
	self:stop()
	
	if self._refreshSubscription then
		self._refreshSubscription:dispose()
		self._refreshSubscription = nil
	end
end

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

function NmbStdProfileBinaryFetcher:_doOneDownload( requestKey, requestValue, requestId )
	
	return NmbProfileDownloadHelper.doOneStdProfileDownload( 
		self._binaryDownloader, requestKey, requestValue, requestValue.httpPriority or DEFAULT_PRIORITY )
end

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

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


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

function NmbStdProfileBinaryFetcher:makeAdjustablePriorityASAPRequestForCameraModel( cameraModel )
	
	if not self._isStarted then
		return 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 processNewCameraModel( self, string.lower( cameraModel ), WFRxHttpClient.requestPriorityHigh )
		:map( function( additions )
			return makeResetToDefaultPriorityFunc( self, additions )
		end )
end

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

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

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

function NmbStdProfileBinaryFetcher:ensureStandardCameraProfileExists( href )

	return self._profileCatalog:getProfileCache():getStandardProfileInfoForHref( 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():pathForStandardProfile( info.relativePath )
			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.fileName, info, requestId )
				
				:flatMap( function( requestKey, success, pathOrErr )
				
					logger:debugf( "ensureStandardProfileExists 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 async acr operations in wf models will use it too.
					return WFAcrOperations.refreshACRDatabases()
						:defaultIfEmpty()
						:map( function()
							return true
						end )
				end )	
		end )
end
--------------------------------------------------------------------------------

return Rx.Class 'NmbStdProfileBinaryFetcher'( NmbStdProfileBinaryFetcher )