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

PixelProviderLocalBinaryPathCheckTask.lua

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

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

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

	-- Rx
local Rx = require "Rx"
local Observable = Rx.Observable

	-- WF.core.utils
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local KSDate = require 'KSDate'
local KSTableUtils = require 'KSTableUtils'
local WFFileUtils = require 'WFFileUtils'
local WFCrypto = require 'WFCrypto'

	-- WF.oz_client.managed_catalog.pixel_provider
local PixelPathProviderStateMachine = require 'PixelPathProviderStateMachine'
local WFCatPixelFileCacheCheck = require 'WFCatPixelFileCacheCheck'
local NmbPathRestorer = require 'NmbPathRestorer'
local PixelPathProviderSchedulerPrivateMethods =
		require "PixelPathProviderSchedulerPrivateMethods"

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

local logger = require 'WFLogger'( 'PixelProviderLocalBinaryPathCheckTask' )

local runUntilDone = WFRxRunUntilDone

local KSTableUtils_derefOpt = KSTableUtils.derefOpt

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

local LocalBinaryPathCheckStates = 
		PixelPathProviderStateMachine.LocalBinaryPathCheckStates

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

local function deleteLocalFileAnnotation( docStore, localId, binaryType )

	-- SPAM [jandrew 8/18/2016]:
	-- The doc store annotation was innaccurate, so we should
	-- remove the annotation. This allows us to redownload
	-- the asset as needed.
	
	-- TODO: Move this annotation deletion to a new task. Something like:
	-- PixelProviderPixelClearLocalBinaryPathInDocStoreTask
	
	local deleteFilesArray = {}
	deleteFilesArray[ binaryType ] = { _delete = true }

	return docStore:applyOverlay(
			{	
				_id = localId,
				_localOnly = {  
					files = deleteFilesArray
				}
			} )
								
end

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

local function errorLocalFileAnnotation( docStore, localId )

	return docStore:applyOverlay(
			{	
				_id = localId,
				_localOnly = {  
					localOriginalError = 'InvalidLocalOriginal'
				}
			} )
								
end

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

local function createNeedsDownloadHandler( scheduler, downloadUri, binaryType, 
											binarySize, pixStreamId, localAssetId )

	return function()
		
		if downloadUri then
		
			logger:infof( "Negative not found locally for %s|%s, but there is a download URI", binaryType, binarySize )

			scheduler:_maybeSetNextState( pixStreamId, localAssetId, 'need_download_binary' )

			return

		end
			
		logger:infof( "Negative not found locally, nor do we have a download link for %s|%s", 
				binaryType, binarySize )

		scheduler:_maybeSetNextState( pixStreamId, localAssetId, 'no_downloadable_uri' )

	end

end

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

local function createErrorHandler( scheduler, pixStreamId, localAssetId )

	return function( errStr )

		logger:errorf( "error in checkForSpecificValidLocalPathFn: %s", errStr )
			
		scheduler:_maybeSetNextState( pixStreamId, localAssetId, 'no_downloadable_uri' )
		
	end
		
end

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

local function checkForBinaryAndMaybeDeleteAnnotation( docStore, cacheChecker, maybeBinaryPath, 
										localAssetId, binaryType, binarySize, needSpecificBinary )

	if not maybeBinaryPath then
		return Observable.just( false )
	end

	return cacheChecker:checkPath( maybeBinaryPath )
							
		:flatMapLatest( function( localPathExists, localPathIsOnOfflineVolume )
		
			if not localPathExists then
			
				if not localPathIsOnOfflineVolume then		
												
					-- If the local path doesn't exist, the asset may be on an offline volume.
					-- If that's the case, don't delete the annotation.
					
					return docStore:docValue( localAssetId )
						
						:flatMapLatest( function( docValue )
						
							-- If the asset is not synced yet, then we should produce an error but keep the
							-- annotation visible
							
							local availableOnOz = require "WFOzDocUtils".isOriginalAvailableOnOz( docValue )
							
							-- If asset is from LrClassic then we don't have the original locally or on Oz.
							local willNotUploadMaster = KSTableUtils.derefOpt( docValue, 'importSource', 'willNotUploadMaster' )	
							
							if not availableOnOz and not willNotUploadMaster then
								return errorLocalFileAnnotation( docStore, localAssetId, binaryType )
									:map( function()
										return false
									end )
							end

							return deleteLocalFileAnnotation( docStore, localAssetId, binaryType )
								:map( function()
									return false
								end )
						end)
						
				elseif needSpecificBinary then
		
					-- If the local path is on an offline volume and we need to access this
					-- specific path, archive the binary path so it can be redownloaded.
		
					return NmbPathRestorer.archiveLocalFilePath( docStore, localAssetId, binaryType, binarySize )
						:map( function()
							return false
						end )
						
				else
				
					return Observable.just( false )
				
				end
		
			end
			
			return Observable.just( maybeBinaryPath )
			
		end )

end

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

local function checkExpectedPathForOriginal( docStore, cacheChecker, fileSpecObj, localAssetId )

	-- The following checks to see if the original already exists in the expected
	-- location but is untracked by our system. This can happen if the user
	-- manually copies the originals folder to a new machine.

	return docStore:docValue( localAssetId )
	
		:flatMapLatest( function( docValue )

			local captureDate = docValue.captureDate
			
			captureDate = type( captureDate ) == 'string' and 
							KSDate.isKSDate( captureDate ) and
							captureDate or 
							'0000-00-00T00:00:00.000' -- Unknown date
			
			local fileName = KSTableUtils_derefOpt( docValue, 'importSource', 'fileName' )
			
			if type( fileName ) ~= "string" then
				return Observable.just( false )
			end
			
			local sha256 = KSTableUtils_derefOpt( docValue, 'importSource', 'sha256' )
			
			local expectedPath = fileSpecObj:pathForOriginal( fileName, captureDate, true, true )

			if not expectedPath
				or not sha256 then
				
				return Observable.just( false )
				
			end

			logger:infof( "Checking for untracked negative for asset %d at path: %s", localAssetId, expectedPath )

			return cacheChecker:checkPath( expectedPath )
				:map( function( localPathExists )
					return expectedPath, localPathExists, sha256
				end )
				
		end )
		
		:flatMapLatest( function( pathToFile, localPathExists, sha256 )
		
			if not pathToFile 
				or not localPathExists 
				or not sha256 then
				
				return Observable.just( false )
				
			end
		
			logger:infof( "An untracked negative exists for asset %d at path: %s", localAssetId, pathToFile )
		
			return WFCrypto.asyncDigestFile( pathToFile, 'SHA256' )
				:map( function( digest )
					return pathToFile, digest == sha256 and sha256
				end )
		
		end )
		
		:flatMapLatest( function( pathToFile, maybeSha256 )
		
			if not pathToFile 
				or not maybeSha256 then
			
				logger:infof( "The untracked negative does not match for asset %d", localAssetId )
				return Observable.just( false )
				
			end
		
			logger:infof( "The untracked negative is a match for asset %d", localAssetId )
		
			local volumeGuid = WFFileUtils.getVolumeGuidForPath( pathToFile )
			
			local relativeFilePath = WFFileUtils.removeVolumeFromPath( pathToFile )
		
			if volumeGuid
				and type( volumeGuid ) == 'string'
				and relativeFilePath
				and type( relativeFilePath ) == 'string' then
		
				local binarySizes = {}
			
				binarySizes[ 'master' ] = {
				
					relativePath = relativeFilePath,
					volumeGuid = volumeGuid,
					sha256 = maybeSha256
					
				}
				
				local files = {}
				files[ 'original' ] = binarySizes
				
				return docStore:applyOverlay(
						{	
							_id = localAssetId,
							_localOnly = {  
								files = files
							}
						} )
						
					:map( function()
					
						return pathToFile
						
					end )
		
			end

			return Observable.just( false )

		end )
		
end

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

local function makeNoLocalNegativeHandler( scheduler, baseRenderInfoCache,
											pixStreamId, localAssetId )
	
	return function()

		local baseRenderInfo = baseRenderInfoCache:peekCacheItem( localAssetId )
		
		if type( baseRenderInfo ) ~= "table" then

			logger:warnf( "Can't check for downloadable local negative path %s.%s: no baseRenderInfo",
							pixStreamId, localAssetId )

			scheduler:_maybeSetNextState( pixStreamId, localAssetId, 'need_docstore_info' )

			return

		end
		
		local noLocalBinaryTransition =
				( baseRenderInfo.ozProxyLink ~= nil and 'need_download_proxy' ) or
				( baseRenderInfo.ozOriginalLink ~= nil and 'need_download_original' ) or
				'no_downloadable_uri'

		scheduler:_maybeSetNextState( pixStreamId, localAssetId, noLocalBinaryTransition )

	end

end

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

local function isTrue( v )

	return v == true

end

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

local function checkForAnyValidLocalNegativePath( scheduler, docStore, fileChecker,
													fileSpecObj,
													localBinaryPathCache,
													baseRenderInfoCache,
													request )

	local pixStreamId, localAssetId = unpack( request )

	scheduler:_applyTransitionToRequest( request, 'start_non_rendition_check' )
	
	local baseRenderInfo = baseRenderInfoCache:peekCacheItem( localAssetId )
	
	if type( baseRenderInfo ) ~= "table" then

		logger:warnf( "Can't check for local negative path %s.%s: no baseRenderInfo",
						pixStreamId, localAssetId )

		return scheduler:_applyTransitionToRequest( request, 'need_docstore_info' )

	end
	
	local onNoLocalNegativeHandler = makeNoLocalNegativeHandler(
										scheduler, baseRenderInfoCache,
										pixStreamId, localAssetId )

	local onErrorHandler = createErrorHandler( scheduler, pixStreamId, localAssetId )
	
	local localProxyPathCheckResultStream = 
			checkForBinaryAndMaybeDeleteAnnotation( 
				docStore, fileChecker, baseRenderInfo.proxyPath, localAssetId,
				'proxy' )

	local localOriginalPathCheckResultStream = 
			checkForBinaryAndMaybeDeleteAnnotation( 
				docStore, fileChecker, baseRenderInfo.originalPath,
				localAssetId, 'original' )
	
	local checkExpectedPathForOriginalStream = checkExpectedPathForOriginal( docStore, fileChecker, fileSpecObj, localAssetId )
	
	-- Check if there's a local proxy.
	return localProxyPathCheckResultStream
			:flatMapLatest( 

				function( maybeLocalProxyPath )
					
					if not maybeLocalProxyPath then
						
						return localOriginalPathCheckResultStream
								:map( 
									function( maybeLocalOriginalPath )

										return nil, maybeLocalOriginalPath

									end )
					
					end

					return Observable.just( maybeLocalProxyPath )
					
				end )
				
			:flatMapLatest( function( maybeLocalProxyPath, maybeLocalOriginalPath )
			
				if not maybeLocalProxyPath and not maybeLocalOriginalPath then
				
					return checkExpectedPathForOriginalStream
						:map( function( maybeExpectedLocalOriginalPath )
							return maybeLocalProxyPath, maybeExpectedLocalOriginalPath
						end )
				
				end
			
				return Observable.just( maybeLocalProxyPath, maybeLocalOriginalPath )
			
			end )
				
			:map( 

				function( maybeLocalProxyPath, maybeLocalOriginalPath )

					if not scheduler:_hasPixstreamRequestForLocalAssetId(
											pixStreamId, localAssetId ) then

						return false

					end

					local localNegativePath = 
							maybeLocalProxyPath or maybeLocalOriginalPath 

					if not localNegativePath then

						return true

					end

					local localNegativeType = 
							maybeLocalProxyPath and 'proxy' or 'original'

					localBinaryPathCache:pushCacheItem( 
						pixStreamId, localAssetId, 
						{ localNegativePath, localNegativeType } )
		
					logger:infof( "Negative already exists for %d at path: %s", localAssetId, localNegativePath )

					-- must call _maybeSetNextState instead of _applyTransitionToRequest because
					-- this is a thread callback and the request data may have changed
					scheduler:_maybeSetNextState( pixStreamId, localAssetId, 'has_local' )

					return false
				
				end )
			:filter( isTrue )
			:tap( onNoLocalNegativeHandler, onErrorHandler ) 
			:call( runUntilDone ) 
		
end

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

local function checkForSpecificValidLocalPath( 
				scheduler, state, docStore, fileChecker, fileSpecObj,
				localBinaryPathCache, baseRenderInfoCache, request, binarySize )

	local binaryType =
			( ( state == LocalBinaryPathCheckStates.NeedCheckForLocalOriginal or
					state == LocalBinaryPathCheckStates.NeedCheckOriginalForBestPossibleLocalNegativePath ) and 
						'original' ) or
			( ( state == LocalBinaryPathCheckStates.NeedCheckForLocalProxy or
					state == LocalBinaryPathCheckStates.NeedCheckLocalProxyForRender or
					state == LocalBinaryPathCheckStates.NeedCheckProxyForBestPossibleLocalNegativePath ) and 
						'proxy' ) or
			( ( state == LocalBinaryPathCheckStates.NeedCheckForLocalXmpDevelop or
					state == LocalBinaryPathCheckStates.NeedCheckForLocalXmpDevelopForRender ) and
						'xmp_develop' ) or
			( ( state == LocalBinaryPathCheckStates.NeedCheckForLocalXmpSidecar ) and
						'xmp_sidecar' ) or
			nil
	
	if not binaryType then
	
		logger:errorf( "needsCheckForLocal state '%s' not recognized, will download original",
						state )
		
		binaryType = 'original'
	
	end

	local pixStreamId, localAssetId = unpack( request )
		
	scheduler:_applyTransitionToRequest( request, 'start_non_rendition_check' )
		
	local baseRenderInfo = baseRenderInfoCache:peekCacheItem( localAssetId )
	
	if type( baseRenderInfo ) ~= "table" then

		logger:warnf( "Can't check for local negative path %s.%s: no baseRenderInfo",
						pixStreamId, localAssetId )

		return scheduler:_applyTransitionToRequest( request, 'need_docstore_info' )

	end
		
	local maybeLocalPath 	
	local downloadUri 
	
	if binaryType == 'xmp_develop' then
		local localPathSha256 = baseRenderInfo.xmpDevelopPathSha256
		local xmpCameraRawSha256 = type( baseRenderInfo.xmpCameraRaw ) == 'table' and baseRenderInfo.xmpCameraRaw.sha256
		
		if xmpCameraRawSha256 == localPathSha256 then 
			maybeLocalPath = baseRenderInfo.xmpDevelopPath
		end
		downloadUri = baseRenderInfo.ozXmpDevelopLink

	elseif binaryType == 'xmp_sidecar' then

		-- Note: baseRenderInfo.xmpSidecarExists == nil means we don't know if there is a sidecar file or not yet

		maybeLocalPath = baseRenderInfo.xmpSidecarPath
		downloadUri = ( baseRenderInfo.xmpSidecarExists or baseRenderInfo.xmpSidecarExists == nil ) and baseRenderInfo.ozXmpLink

	elseif binaryType == 'original' then
		maybeLocalPath = baseRenderInfo.originalPath
		downloadUri = baseRenderInfo.ozOriginalLink
	else
		maybeLocalPath = baseRenderInfo.proxyPath
		downloadUri = baseRenderInfo.ozProxyLink
	end

	local onNeedsDownloadHandler = createNeedsDownloadHandler( scheduler, downloadUri, binaryType, 
																binarySize, pixStreamId, localAssetId )
	
	local onErrorHandler = createErrorHandler( scheduler, pixStreamId, localAssetId )
	
	local checkExpectedPathForOriginalStream = checkExpectedPathForOriginal( docStore, fileChecker, fileSpecObj, localAssetId )
	
	local maybeLocalBinaryPathStream = 
			checkForBinaryAndMaybeDeleteAnnotation( docStore, fileChecker, maybeLocalPath, localAssetId, binaryType, binarySize, true )

	return maybeLocalBinaryPathStream

		:flatMapLatest( function( maybeLocalBinaryPath )
			
			if not maybeLocalBinaryPath and binaryType == 'original' then
			
				return checkExpectedPathForOriginalStream
			
			end
		
			return Observable.just( maybeLocalBinaryPath )
		
		end )

		:flatMapLatest( function( maybeLocalBinaryPath )
		
			if maybeLocalBinaryPath then

				if not scheduler:_hasPixstreamRequestForLocalAssetId(
										pixStreamId, localAssetId ) then

					return Observable.empty()

				end

				localBinaryPathCache:pushCacheItem( 
					pixStreamId, localAssetId, 
					{ maybeLocalBinaryPath, binaryType } )

				logger:infof( "Local negative already exists for %s|%s at path: %s", binaryType, binarySize, maybeLocalBinaryPath )

				-- must call _maybeSetNextState instead of _applyTransitionToRequest because
				-- this is a thread callback and the request data may have changed
				scheduler:_maybeSetNextState( pixStreamId, localAssetId, 'has_local' )

				return Observable.empty()

			end
	
			return Observable.just( true )
	
		end )

		:filter( function( maybeNeedsDownload )
			return maybeNeedsDownload
		end )
		
		:tap( onNeedsDownloadHandler, onErrorHandler )
			
		:call( runUntilDone )

end

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

local _needLocalBinaryCheckStates = {
		LocalBinaryPathCheckStates.NeedCheckForLocalOriginal,
		LocalBinaryPathCheckStates.NeedCheckForLocalProxy,
		LocalBinaryPathCheckStates.NeedCheckForAnyLocalNegative,
		LocalBinaryPathCheckStates.NeedCheckAnyLocalNegativeForRender,
		LocalBinaryPathCheckStates.NeedCheckLocalProxyForRender,
		LocalBinaryPathCheckStates.NeedCheckOriginalForBestPossibleLocalNegativePath,
		LocalBinaryPathCheckStates.NeedCheckProxyForBestPossibleLocalNegativePath,
		LocalBinaryPathCheckStates.NeedCheckForLocalXmpDevelop,
		LocalBinaryPathCheckStates.NeedCheckForLocalXmpDevelopForRender,
		LocalBinaryPathCheckStates.NeedCheckForLocalXmpSidecar }

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

local function processRequests( scheduler, taskState, localBinaryPathCache, baseRenderInfoCache )
	
	repeat
		
		local request =
			{ PixelPathProviderSchedulerPrivateMethods.getNextRequestWithState( 
					scheduler,  unpack( _needLocalBinaryCheckStates ) ) }
		
		local pixStreamId, _, binarySize, _, state = unpack( request )

		if not pixStreamId then

			break

		end
		
		
		if state == LocalBinaryPathCheckStates.NeedCheckForAnyLocalNegative or
				state == LocalBinaryPathCheckStates.NeedCheckAnyLocalNegativeForRender then

			checkForAnyValidLocalNegativePath( 
				scheduler, taskState.docStore, taskState.fileChecker,
				taskState.fileSpecObj, localBinaryPathCache, baseRenderInfoCache, 
				request )

		else

			checkForSpecificValidLocalPath( 
				scheduler, state, taskState.docStore, taskState.fileChecker,
				taskState.fileSpecObj, localBinaryPathCache, baseRenderInfoCache, 
				request, binarySize )

		end
		
	until false
	
end

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

local function setupTask( docStore, pixelsPath, fileSpecObj )

	return {
		
		docStore = docStore,
		
		fileChecker = WFCatPixelFileCacheCheck( pixelsPath ),
		
		fileSpecObj = fileSpecObj,
		
	}

end

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

return {

	processRequests = processRequests,

	setupTask = setupTask,
	
	checkExpectedPathForOriginal = checkExpectedPathForOriginal -- Exposed for tests.

}
