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

WFCatBinaryDownloader.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.

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

	-- RxLua
local Rx = require 'Rx'
local Observable = Rx.Observable

	-- WF.core.logger
local WFLogger = require 'WFLogger'

	-- WF.core.os
local KSDate = require 'KSDate'
local KSData = require 'KSData'

	-- WF.core.performance
local WFPerformanceReport = require 'WFPerformanceReport'

	-- WF.core.utils
local WFCloseSupport = require 'WFCloseSupport'
local KSPathUtils = require 'KSPathUtils'
local KSTableUtils = require 'KSTableUtils'
local WFAnalytics = require 'WFAnalytics'
local WFArguments = require 'WFArguments'
local WFFileUtils = require 'WFFileUtils'

	-- WF.oz_client.managed_catalog
local WFCatFileSpec = require 'WFCatFileSpec'

	-- WF.web.http_client
local WFRxHttpClient = require 'WFRxHttpClient'

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

local check = assert( WFArguments.check )
local derefOpt = assert( KSTableUtils.derefOpt )
local WFCatFileSpec_getUrlForLinkKey = assert( WFCatFileSpec.getUrlForLinkKey )

local logger = WFLogger 'WFCatBinaryDownloader'
--	:setLevel 'debug'


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

local WFCatBinaryDownloader = {

	_catalog = false,
		-- (WFCatalog)

	_catalogSpec = false,
		-- (WFCatSpec)

	_docStore = false,
		-- (WFRxDocStore)
		
	WFCloseSupport.addCloseSupportMethods,

}

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

function WFCatBinaryDownloader:__init( catalog )

	check( 'WFCatBinaryDownloader', 'WFCatalog' )

	self._catalog = catalog

	self._catalogSpec = catalog:ozCatalogSpec()

	self._docStore = catalog:docStore()

end

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

function WFCatBinaryDownloader:close()

end

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

local function downloadOneBinary( self, docId, linksKey, maybeUrl, maybeRevId, downloadPriority, downloadCost, useDirectDownload, maybeDestPath )

	logger:debugf( "downloadOneBinary assetId = %s links key = %s",
				   docId, linksKey )

	local startTime = KSDate.timeNowForAnimation()
	local savedUrl
	local revId, binaryUrl
	
	-- DownloadOneBinary not only called by getMaster or getProxy, but currently we only 
	-- enable master/proxies binary downloaded using direct-download
	useDirectDownload = useDirectDownload or false

	local urlObservable
	if maybeUrl and linksKey ~= '/rels/master' then
		urlObservable = Observable.just( maybeUrl, maybeRevId )
	else

		urlObservable = self._docStore:docValue( docId )
			:flatMapLatest( function( docValue )

				-- Check destination is writable before downloading to avoid excessive downloads
				-- of originals as they can be stored in a place where user has more control over
				-- file permissions. See NMB-23202.

				if linksKey == '/rels/master' then

					-- Attempting to create the directories is one source of failure, see if we can
					-- create them now by calling pathForOriginal before we start downloading.
					local fileSpecObj = self._catalog:fileSpec()
					local captureDate = docValue.captureDate or '0000-00-00T00:00:00.000'
					local fileName = derefOpt( docValue, 'importSource', 'fileName' ) or 'temp'
					local success, finalPath = pcall( fileSpecObj.pathForOriginal, fileSpecObj, fileName, captureDate, true )
					if not success then
						return Observable.throw( string.format(
							"Pre-download check failed: pathForOriginal failed for fileName %s with captureDate %s", fileName, captureDate ) )
					end

					local finalPathParent = KSPathUtils.parent( finalPath )
					local finalPathVolumeGuid = finalPath and WFFileUtils.getVolumeGuidForPath( finalPath )
					local tempFolder = fileSpecObj:tempFolderPathForVolume( finalPathVolumeGuid )

					-- User may have changed permissions on the temp folder within the originals directory.
					if WFFileUtils.isDirectoryReadOnly( tempFolder ) then
						return Observable.throw( string.format(
							"Pre-download check failed: readOnly temp directory %s", tempFolder ) )
					end

					-- User may have changed permissions on one of the destination folders in the originals directory.
					if WFFileUtils.isDirectoryReadOnly( finalPathParent ) then
						return Observable.throw( string.format(
							"Pre-download check failed: readOnly final directory %s", finalPathParent ) )
					end

				end

				return Observable.just( docValue )

			end )
			:map( function( value )
				return WFCatFileSpec_getUrlForLinkKey( value, linksKey ),
						derefOpt( value, '_revId' )
			end )
	end
	
	return urlObservable
		:flatMapLatest( function( fullUrl, revisionId )
			revId = revisionId
			binaryUrl = fullUrl
			if fullUrl  then

				savedUrl = fullUrl

				logger:infof( "downloadOneBinary assetId = %s links key = %s downloading from URL %s",
							  docId, linksKey, fullUrl )

				local options = {
					requestPriority = downloadPriority or WFRxHttpClient.requestPriorityMedium,
					requestCost = downloadCost or WFRxHttpClient.requestCostDefault,
				}
				
				if useDirectDownload then
				
					options['requestHeaders'] = {
						{ header = 'Prefer', value = 'download-direct' }
					}
					
					options['followRedirects'] = true
				end

				if maybeDestPath then

					options.filePath = maybeDestPath

				end

				return self._catalogSpec:getBinary( fullUrl, options )

			else

				return Observable.throw(
					string.format( "No URL provided by Oz for asset ID %s link %s",
								   docId, linksKey ) )

			end

		end )

		:flatMapLatest( function( result, headers )

			if maybeDestPath then

				return Observable.just( result, revId, headers )

			end

			-- check that the blob has the right length
			local headerContentLength = tonumber( derefOpt( headers, "Content-Length" ) )
			local headerContentEncoding = tostring( derefOpt( headers, "Content-Encoding" ) ) 
			local dataLength = KSData.length( result )
			
			if headerContentLength and dataLength ~= headerContentLength and headerContentEncoding ~= 'gzip' then
				logger:errorf( "downloadOneBinary assetId = %s data length (%d) doesn't match headers.Content-Length (%d)",
							  docId, dataLength, headerContentLength )
				return Observable.throw( "Error downloading file, dataLength doesn't match headers.Content-Length", headers )
			end

			return Observable.just( result, revId, headers )

		end )

		:tap(
			function()

				logger:infof( "downloadOneBinary assetId = %s links key = %s downloaded from URL %s in %.2f seconds",
							  docId, linksKey, savedUrl,
							  KSDate.timeNowForAnimation() - startTime )

			end,
			function( message, headers )

				-- High failure rate observed in splunk related to 404, a separate metric is used below via trackEventDownloadFailure.
				logger:warnf( "downloadOneBinary assetId = %s links key = %s downloading from URL %s\n\n"
							   .. "FAILED after %.2f seconds with message %s\n\nheaders %s\n\n",
							   docId, linksKey, savedUrl,
							   KSDate.timeNowForAnimation() - startTime,
							   message, headers )

			end )
		:catch( function( message, headers )

			-- The above may throw a different error if no url
			if binaryUrl then

				local failureInfo = {
					docId = docId,
					url = binaryUrl,
					responseData = message,
					headers = headers,
					isDirectDownload = useDirectDownload,
				}

				WFAnalytics.trackEventDownloadFailure( failureInfo )

			end

			return Observable.throw( message, headers, linksKey, binaryUrl, revId )
		
		end ) 

end

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

local function createDownloadStats( result )
	local stats = {}
	if KSData.isData( result ) then
		stats.fileSize = KSData.length( result )
	end
	return stats
end
	
--------------------------------------------------------------------------------

function WFCatBinaryDownloader:getRendition( docId, renditionType, downloadPriority )

	check( 'WFCatBinaryDownloader', '+string', '+string', '?number' )
	
	local downloadCost = WFRxHttpClient.requestCostDownloadRendition
	
	return WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: rendition_' .. renditionType, 
					downloadOneBinary( self, docId, '/rels/rendition_type/' .. renditionType, nil, nil, downloadPriority, downloadCost ),
					{ onNext = createDownloadStats } )

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

function WFCatBinaryDownloader:downloadRenditionToLocalFileStream( docId, renditionType, downloadPriority, destPath )

	check( 'WFCatBinaryDownloader', '+string', '+string', '?number', '+string' )
	
	local downloadCost = WFRxHttpClient.requestCostDownloadRendition
	
	return WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: renditionToFile_' .. renditionType, 
					downloadOneBinary( 
						self, 
						docId, 
						'/rels/rendition_type/' .. renditionType,
						 nil, --url
						 nil, --revId
						 downloadPriority,
						 downloadCost,
						 nil, --useDirectDownload
						 destPath ),
					{ onNext = createDownloadStats } )

end

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

function WFCatBinaryDownloader:getRenditionByUrl( docId, renditionType, url, revId, downloadPriority )
	
	check( 'WFCatBinaryDownloader', '+string', '+string', '+string', '+string', '?number' )	
	
	local downloadCost = WFRxHttpClient.requestCostDownloadRendition
	
	return WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: rendition_' .. renditionType, 
					downloadOneBinary( self, docId, '/rels/rendition_type/' .. renditionType, url, revId, downloadPriority, downloadCost ),
					{ onNext = createDownloadStats } )
end
	
--------------------------------------------------------------------------------

function WFCatBinaryDownloader:downloadRenditionByURLToLocalFileStream( docId, renditionType, url, revId, downloadPriority, destPath )

	check( 'WFCatBinaryDownloader', '+string', '+string', '+string', '+string', '?number', '+string' )	
	
	local downloadCost = WFRxHttpClient.requestCostDownloadRendition
	
	return WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: renditionToFile_' .. renditionType, 
					downloadOneBinary( 
						self,
						docId,
						'/rels/rendition_type/' .. renditionType,
						url,
						revId,
						downloadPriority,
						downloadCost,
						nil, --useDirectDownload
						destPath ),
					{ onNext = createDownloadStats } )
	
end

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

function WFCatBinaryDownloader:getProxy( docId, proxyType, downloadPriority, useDirectDownload )

	check( 'WFCatBinaryDownloader', '+string', '+string', '?number', '?boolean' )

	local downloadCost = WFRxHttpClient.requestCostDownloadNegative

	if WFRxHttpClient.requestPriorityHighest == downloadPriority then
		downloadCost = WFRxHttpClient.requestCostMaximizeBandwidth
	end
	
	if useDirectDownload == nil then
		useDirectDownload = true
	end

	return  WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: proxy_' .. proxyType, 
					downloadOneBinary( self, docId, '/rels/proxy_type/' .. proxyType, nil, nil, downloadPriority, downloadCost, useDirectDownload ),
					{ onNext = createDownloadStats } )

end

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

function WFCatBinaryDownloader:downloadProxyToLocalFileStream( 
								docId, proxyType, downloadPriority,
								destPath )

	check( 'WFCatBinaryDownloader', '+string', '+string', '?number', 'string' )

	local downloadCost = 
			downloadPriority == WFRxHttpClient.requestPriorityHighest and
				WFRxHttpClient.requestCostMaximizeBandwidth or
				WFRxHttpClient.requestCostDownloadNegative 

	local useDirectDownload = true

	return  WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: proxyToFile_' .. proxyType, 
					downloadOneBinary( self, docId, '/rels/proxy_type/' .. proxyType, nil, nil, downloadPriority, downloadCost, useDirectDownload, destPath ),
					{ onNext = createDownloadStats } )

end

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

function WFCatBinaryDownloader:getProxyByUrl( docId, proxyType, url, revId, downloadPriority, useDirectDownload )

	check( 'WFCatBinaryDownloader', '+string', '+string', '+string', '+string', '?number', '?boolean' )

	local downloadCost = WFRxHttpClient.requestCostDownloadNegative

	if WFRxHttpClient.requestPriorityHighest == downloadPriority then
		downloadCost = WFRxHttpClient.requestCostMaximizeBandwidth
	end
	
	if useDirectDownload == nil then
		useDirectDownload = true
	end

	return  WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: proxy_' .. proxyType, 
					downloadOneBinary( self, docId, '/rels/proxy_type/' .. proxyType, url, revId, downloadPriority, downloadCost, useDirectDownload ),
					{ onNext = createDownloadStats } )

end

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

function WFCatBinaryDownloader:downloadProxyByUrlToLocalFileStream( docId, proxyType, url, revId, downloadPriority, destPath )

	check( 'WFCatBinaryDownloader', '+string', '+string', '+string', '+string', '?number', 'string' )

	local downloadCost = 
			downloadPriority == WFRxHttpClient.requestPriorityHighest and
				WFRxHttpClient.requestCostMaximizeBandwidth or
				WFRxHttpClient.requestCostDownloadNegative 

	local useDirectDownload = true

	return  WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: proxyToFile_' .. proxyType, 
					downloadOneBinary( self, docId, '/rels/proxy_type/' .. proxyType, url, revId, downloadPriority, downloadCost, useDirectDownload, destPath ),
					{ onNext = createDownloadStats } )


end

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

function WFCatBinaryDownloader:getMaster( docId, downloadPriority, useDirectDownload )

	check( 'WFCatBinaryDownloader', '+string', '?number', '?boolean' )

	local downloadCost = WFRxHttpClient.requestCostDownloadNegative

	if WFRxHttpClient.requestPriorityHighest == downloadPriority then
		downloadCost = WFRxHttpClient.requestCostMaximizeBandwidth
	end
	
	if useDirectDownload == nil then
		useDirectDownload = true
	end

	return WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: master', 
					downloadOneBinary( self, docId, '/rels/master', nil, nil, downloadPriority, downloadCost, useDirectDownload ),
					{ onNext = createDownloadStats } )

end

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

function WFCatBinaryDownloader:downloadOriginalToFileStream( docId, downloadPriority, destPath )

	check( 'WFCatBinaryDownloader', '+string', '?number', 'string' )

	local downloadCost = 
			downloadPriority == WFRxHttpClient.requestPriorityHighest and
				WFRxHttpClient.requestCostMaximizeBandwidth or
				WFRxHttpClient.requestCostDownloadNegative 

	local useDirectDownload = true

	return WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: masterToFile', 
					downloadOneBinary( self, docId, '/rels/master', nil, nil, downloadPriority, downloadCost, useDirectDownload, destPath ),
					{ onNext = createDownloadStats } )

end

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

function WFCatBinaryDownloader:getMasterByUrl( docId, url, revId, downloadPriority, useDirectDownload )

	check( 'WFCatBinaryDownloader', '+string', '+string', '+string', '?number', '?boolean' )

	local downloadCost = WFRxHttpClient.requestCostDownloadNegative

	if WFRxHttpClient.requestPriorityHighest == downloadPriority then
		downloadCost = WFRxHttpClient.requestCostMaximizeBandwidth
	end
	
	if useDirectDownload == nil then
		useDirectDownload = true
	end

	return WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: master', 
					downloadOneBinary( self, docId, '/rels/master', url, revId, downloadPriority, downloadCost, useDirectDownload ),
					{ onNext = createDownloadStats } )

end

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

function WFCatBinaryDownloader:downloadOriginalByUrlToFileStream( docId, url, revId, downloadPriority, destPath )

	check( 'WFCatBinaryDownloader', '+string', '+string', '+string', '?number', 'string' )

	local downloadCost = 
			downloadPriority == WFRxHttpClient.requestPriorityHighest and
				WFRxHttpClient.requestCostMaximizeBandwidth or
				WFRxHttpClient.requestCostDownloadNegative 

	local useDirectDownload = true

	return WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: masterToFile', 
					downloadOneBinary( self, docId, '/rels/master', url, revId, downloadPriority, downloadCost, useDirectDownload, destPath ),
					{ onNext = createDownloadStats } )

end

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

function WFCatBinaryDownloader:getXmp( docId, downloadPriority )

	check( 'WFCatBinaryDownloader', '+string', '?number' )

	local downloadCost = WFRxHttpClient.requestCostDownloadXmp

	return WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: xmp', 
					downloadOneBinary( self, docId, '/rels/xmp', nil, nil, downloadPriority, downloadCost ),
					{ onNext = createDownloadStats } )

end

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

function WFCatBinaryDownloader:getXmpByUrl( docId, url, revId, downloadPriority )

	check( 'WFCatBinaryDownloader', '+string', '+string', '+string', '?number' )

	local downloadCost = WFRxHttpClient.requestCostDownloadXmp

	return WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: xmp', 
					downloadOneBinary( self, docId, '/rels/xmp', url, revId, downloadPriority, downloadCost ),
					{ onNext = createDownloadStats } )

end

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

function WFCatBinaryDownloader:getXmpDevelop( docId, downloadPriority )
	
	check( 'WFCatBinaryDownloader', '+string', '?number' )
	
	local downloadCost = WFRxHttpClient.requestCostDownloadXmp
	
	return WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: xmp_develop',
					downloadOneBinary( self, docId, '/rels/xmp/develop', nil, nil, downloadPriority, downloadCost ),
					{ onNext = createDownloadStats } )
					
end


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

function WFCatBinaryDownloader:getXmpDevelopByUrl( docId, url, revId, downloadPriority )
	
	check( 'WFCatBinaryDownloader', '+string', '+string', '+string', '?number' )
	
	local downloadCost = WFRxHttpClient.requestCostDownloadXmp
	
	return WFPerformanceReport.wrapObservable( 
					'WFCatBinaryDownloader: xmp_develop',
					downloadOneBinary( self, docId, '/rels/xmp/develop', url, revId, downloadPriority, downloadCost ),
					{ onNext = createDownloadStats } )
					
end

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

return Rx.Class 'WFCatBinaryDownloader'( WFCatBinaryDownloader )
