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

WFCatStyleBinaryDownloader.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 WFCloseSupport = require 'WFCloseSupport'
local WFBinaryRequester = require 'WFBinaryRequester'
local WFCatBinaryDownloader = require 'WFCatBinaryDownloader'
local WFRxHttpClient = require 'WFRxHttpClient'
local WFAsyncFileUtils = require 'WFAsyncFileUtils'
local WFFileUtils = require 'WFFileUtils'
local KSTableUtils = require 'KSTableUtils'
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local WFCatFileSpec = require 'WFCatFileSpec'
local WFLogger = require 'WFLogger'

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

local derefOpt = KSTableUtils.derefOpt

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

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

local function saveBinaryToFile( self, newPath, blob, docValue )
	
	-- [ashj 2018-05-03]: For style files, filename has to
	-- be same as assetId. Hence we are using saveToFile api.
	-- And hence we need not delete the old path, since it
	-- will be overwritten anyways.
	
	-- [ashj 2018-05-03]: For camera profiles, filename
	-- has to be same as the original file name. If a file
	-- already exists with the same name, then we need to
	-- save the new profile with a new name. Hence we
	-- need to delete the old file if it was existing.
	
	--[[
	1.	Save to a unique tmp file (named anything that ACR won't see)
	2a. If there is an existing filename for the asset in the docValue 
		then copy the saved file to the existing filename 
		(overwriting the file contents that are there)
	2b. if there is no existing filename for the asset in the 
		docValue then copy the saved file to the assetId filename
		(for preset/profile) overwriting anything that's there or copy to unique filename (for dcp)
	3.	If successful, update the asset's file annotation to the new filename (if it's new).
	4.	Always delete the saved tmp file error or not.
	
	]]
	--  [ashj 2018-05-04]: Steps 1 and 4 are not needed since we have
	-- the blob with us anyways.
	
	local saveFileObservable
	
	local oldPath = WFCatFileSpec.buildOriginalPathFromDocValue( docValue )
	
	
	if oldPath ~= newPath then
		-- [ashj 2018-05-03]: Either the file doesn't exists
		-- locally yet, or the old filename was different.
		-- This will mean that there is no file, which we
		-- should overwrite. This will hold good for
		-- camera profiles as well as for presets/profiles.
		
		-- [ashj 2018-05-04]: The newPath from fileSpec
		-- doesn't account for the files already present.
		-- If a camera profile has a new master which was
		-- alredy locally available with a -2 name, the
		-- oldPath and newPath will be different in that
		-- case. We will save the file as -3 and then
		-- remove the -2 file.
		
		saveFileObservable = WFAsyncFileUtils.saveToUniquelyNamedFile( blob, newPath )
		
	else
		-- [ashj 2018-05-03]: Both the file names are same.
		-- Overwrite the existing file.
		saveFileObservable = WFAsyncFileUtils.saveToFile( blob, newPath )
			:map( function()
				return newPath
			end )
	end
	
	return saveFileObservable
		:map( function( savedPath )
		
			-- [ashj 2018-04-02]: In 1.3 version, the filename
			-- used to be same as the styleName. It is possible that
			-- the style which was downloaded is actually saved with
			-- the filename now. In that case, we will need to delete
			-- the old file and save the new file with asset-id as
			-- filename.
	
			if oldPath and oldPath ~= newPath then
				pcall( WFFileUtils.deleteFile, oldPath )
			end
			
			return savedPath
		end )
end

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

local function saveToFile( self, catalog, assetId, docValue, blob )

	local subtype = docValue.subtype
	
	local path
	if subtype == "camera_profile" then
		
		path = catalog:fileSpec():pathForDCP( docValue.fileName )
	else
		path = catalog:fileSpec():pathForStyle( assetId )
	end
	
	
	return saveBinaryToFile( self, path, blob, docValue )
end

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

local function updateDocStoreOriginalInfo( docStore, assetId, filePath )

	return docStore:docValue( assetId )
		:flatMapLatest( function( docValue )
			
			if docValue._empty then
				local errStr = string.format( "AssetId %s has been deleted", assetId )
				return Rx.Observable.throw( errStr )
			end
			
			local sha256 = derefOpt( docValue, 'importSource', 'sha256' )
			local fileSize = derefOpt( docValue, 'importSource', 'fileSize' )
			
			local volumeGuid = filePath and WFFileUtils.getVolumeGuidForPath( filePath )
			local relativeFilePath = filePath and WFFileUtils.removeVolumeFromPath( filePath )

			if volumeGuid 
				and type( volumeGuid ) == 'string'
				and relativeFilePath
				and type( relativeFilePath ) == 'string' then
			
				local localOnly = {
					files = {
						[ "original" ] = {
							[ "master" ] = {
								relativePath = relativeFilePath,
								volumeGuid = volumeGuid,
								size = fileSize,
								sha256 = sha256
							}
						}
					}
				}

				local overlay = { 
					_id = assetId,
					_localOnly = localOnly,
				}
							
				return docStore:applyOverlay( overlay ) 
					:take( 1 )
					
			else
			
				return Rx.Observable.throw( "Unable to create doc store annotation for file " .. filePath )
			
			end
		end )

end

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

local function getBinaryResponseHandler( self, catalog )

	return function ( okay, maybeKSDataOrError, revId, 
		assetId, binaryType, size, additionalInfo )
		
		if okay then
		
			local blob = maybeKSDataOrError
		
			return catalog:docStore():docValue( assetId )
				:flatMapLatest( function( docValue )
					
					return saveToFile( self, catalog, assetId, docValue, blob )
			
				end )
				
				:flatMapLatest( function( path )
					
					return updateDocStoreOriginalInfo( catalog:docStore(), assetId, path )
						:map( function()
							return path
						end )
				
				end )
				
				:tap( function( path )
					
					-- finally function will be called only after
					-- disposing the subscription associated with
					-- this observable. We want the request to be
					-- marked as complete before that. Hence we
					-- are removing the request from inflight list
					-- here as well as on error.
					
					self._inFlightRequests[ assetId ] = nil
					self._binaryDownloadResponseStream:onNext( assetId, true, path )
				end )
				
				:tapOnError( function( err )
					
					self._inFlightRequests[ assetId ] = nil
					
					logger:errorf( "Error while fetching master for %s: %s", assetId, err )
					self._binaryDownloadResponseStream:onNext( assetId, false, err )
				end )
				
				:finally( function()
					if blob then
						blob:close()
					end
				end )
				
				:call( WFRxRunUntilDone )
				
		else
			
			local err = maybeKSDataOrError
			logger:errorf( "Error while downloading master for %s: %s", assetId, err )
			self._inFlightRequests[ assetId ] = nil
			self._binaryDownloadResponseStream:onNext( assetId, false, err )
			
		end
	end
end 


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

local WFCatStyleBinaryDownloader = {
	
	_catalog = false,
	
	_binaryRequester = false,
	
	_binaryRequesterSubscription = false,
	
	_binaryDownloadResponseStream = false,
	
	_inFlightRequests = false,
	
	WFCloseSupport.addCloseSupportMethods,
}

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

function WFCatStyleBinaryDownloader:__init( catalog )

	self._inFlightRequests = {}
	self._binaryRequester = WFBinaryRequester( WFCatBinaryDownloader( catalog ) )
	self._binaryRequesterSubscription = self._binaryRequester:getBinaryRequestStream()
		:subscribe( getBinaryResponseHandler( self, catalog ) )
	self._binaryDownloadResponseStream = Rx.Subject()
end

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

function WFCatStyleBinaryDownloader:requestStyleBinary( docId, httpPriority )
	
	local maybeInFlightRequest = self._inFlightRequests[ docId ]
	if maybeInFlightRequest then
		-- New request received for the assetid, which is
		-- already in progress. If its with a higher priority,
		-- then remove the old request and add new one.
		logger:warnf( "Multiple requests for the same style docId: %s, priority:%s", 
			docId, httpPriority )
		
		if httpPriority and httpPriority > maybeInFlightRequest.request.httpPriority then
			if maybeInFlightRequest.subscription then
				maybeInFlightRequest.subscription:dispose()
			end
			self._inFlightRequests[ docId ] = nil
		else
			-- Priority of the new request is less than or
			-- equal to the old one. Maybe the idle download
			-- got triggered when the high priority one is
			-- going on. Let the old request continue.
			return
		end
		
	end
	
	httpPriority = httpPriority or WFRxHttpClient.requestPriorityLow
	local ozBinaryType = "original"
	local ozBinarySubtype = "master"

	local cancelDownloadFunc =
		self._binaryRequester:requestBinary( docId, ozBinaryType,
										ozBinarySubtype, httpPriority )

	assert( type( cancelDownloadFunc ) == "function", "bad cancelDownloadFunc" )

	self._inFlightRequests[ docId ] = cancelDownloadFunc
end

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

function WFCatStyleBinaryDownloader:cancelStyleBinaryRequest( docId )
	
	local cancelDownloadFunc = self._inFlightRequests[ docId ]
	if cancelDownloadFunc then
		cancelDownloadFunc()
		self._inFlightRequests[ docId ] = nil
	end
end

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

function WFCatStyleBinaryDownloader:getResponseStream()
	
	return self._binaryDownloadResponseStream
end

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

function WFCatStyleBinaryDownloader:close()

end

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

return Rx.Class 'WFCatStyleBinaryDownloader'( WFCatStyleBinaryDownloader )