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

WFCatExport.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2016 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remains 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 are protected by all applicable
intellectual property laws, including trade secret and copyright laws.
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.oz_client.managed_catalog
local WFCatFileSpec = require 'WFCatFileSpec'

	-- WF.core.utils
local WFFileUtils = require 'WFFileUtils'
local KSTableUtils = require 'KSTableUtils'

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

	-- Modules.export
local WFCatExportTask = require 'WFCatExportTask'

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

local derefOpt = KSTableUtils.derefOpt
local fileSystemAttributesFn = WFFileUtils.fileSystemAttributes
local isDownloadNeededAndSizeForAssetFn
local tryToClaimCacheSpaceFn

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

local function pack( ... )

	return { ... } 

end

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

isDownloadNeededAndSizeForAssetFn = function( wfCatalog )

	return function( assetId )

		return wfCatalog:docStore():docValue( assetId )
			:map( function( docValue )
				if docValue._empty or docValue.type ~= 'asset' then
					-- This case shouldn't happen and if it occurs
					-- then it will be treated as an error during
					-- actual export. For size calculation, maybe
					-- we can ignore this file.
					logger:warnf( 'Invalid asset. Ignoring for size calculation.' )
					return false, 0
				end

				local originalPath = WFCatFileSpec.buildOriginalPathFromDocValue( docValue )
				local originalFileSize = tonumber( derefOpt( docValue, "importSource", "fileSize") )
				if originalPath and WFFileUtils.exists( originalPath ) then
					-- Original exists.
					logger:tracef( 'Original file exists' )
					return false, originalFileSize
				end

				-- Original doesn't exist locally.
				logger:tracef( 'Need to download original file of size %s', tostring( originalFileSize ) )
				return true, originalFileSize
			end )
		end
end

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

local function pathIsInSameDiskAsCache( wfCatalog, path )

	local cacheRootPathInfo = wfCatalog:fileSpec():originalsPathInfo()
	
	local cacheRootPath = WFFileUtils.createFullPathFromRelativePathAndGuid(
			cacheRootPathInfo.relativePath, cacheRootPathInfo.volumeGuid )
	
	if not cacheRootPath then
	
		-- We are not able to create the path.

		logger:warnf( 'Creating cache root path failed ( with error %s)',
				tostring( cacheRootPath ) )

		return nil
	
	end
	
	local cachePathCallOk, cacheRootPathAttributes = pcall( fileSystemAttributesFn, cacheRootPath )
	local pathCallOk, pathAttributes = pcall( fileSystemAttributesFn, path )

	if not cachePathCallOk or not pathCallOk then
		-- We are not able to check the file system
		-- attributes. 
		-- We are suppressing the error here, maybe
		-- we shouldn't. This will be treated as if
		-- it is a different disk.
		if not cachePathCallOk then
			logger:warnf( 'Fetching diskspace attr failed for %s ( with error %s)', 
				cacheRootPath, tostring( cacheRootPathAttributes ) )
		else
			logger:warnf( 'Fetching diskspace attr failed for %s ( with error %s)', 
				path, tostring( pathAttributes ) )
		end
		return nil
	end
	
	local isInSameDisk = cacheRootPathAttributes.fileSystemNumber == pathAttributes.fileSystemNumber
	logger:tracef( 'Same disk check for Cache(%s) and destination(%s) result: %s', 
		cacheRootPath, path, tostring( isInSameDisk ) )
	return isInSameDisk

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

local function getAdditionalSpace( freeSpace, spaceNeeded )

	local additionalSpace = spaceNeeded - freeSpace
	return additionalSpace > 0 and additionalSpace or 0

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

tryToClaimCacheSpaceFn = function( wfCatalog, spaceNeeded )

	local fileSpec = wfCatalog:fileSpec()
	
	local mainVolumeGuid = WFFileUtils.getVolumeGuidForPath( fileSpec:catalogPath() )
	
	local originalsVolumeGuid = fileSpec:originalsPathInfo().volumeGuid

	local volumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( originalsVolumeGuid )

	if volumeAgent then
	
		return volumeAgent:getVolumeStatusStream()
			:take( 1 )
			:flatMapLatest( function( volumeStatus )
			
				-- If the volume where the originals are stored is offline, we
				-- download the assets needed for export to the main volume instead.
			
				if volumeStatus.isOnline then
				
					-- Attempt to claim space on the originals volumes.
				
					-- FIXME [jandrew 2017-02-08]: As a future enhancement, if we're
					-- unable to claim enough space on the originals volume, we could
					-- instead temporarily download the originals to the main volume.
				
					return volumeAgent:tryToClaimSpace( spaceNeeded )
					
				elseif mainVolumeGuid ~= originalsVolumeGuid then
					
					-- Originals volume is offline, so attempt to claim space on the main volume.
					
					volumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( mainVolumeGuid )
					return volumeAgent:tryToClaimSpace( spaceNeeded )
					
				end
			
				return Observable.just( 0 )
			
			end )
	
			:map( function( freeSpace, isOriginalVolumeOffline )
				-- Return the extra space needed in cache disk
				local additionalSpaceNeeded = getAdditionalSpace( freeSpace, spaceNeeded )
				logger:tracef( 'After purge, freeSpace: %s, spaceNeeded: %s, additionalSpaceNeeded: %s', 
					tostring( freeSpace ), tostring( spaceNeeded ), tostring( additionalSpaceNeeded ) )
				return additionalSpaceNeeded
			end )
			
	else
		logger:tracef( 'No volumeAgent. Returning 0.' )
		return Observable.just( 0 )
	end

end

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

local function additionalSpaceNeededOnDisk( wfCatalog, spaceNeeded, path )

	local ok, pathAttributes = pcall( fileSystemAttributesFn, path )
	if not ok then
		logger:warnf( 'Fetching diskspace attr failed for %s ( with error %s)', 
				path, tostring( pathAttributes ) )
		return Observable.just( 0 )
	end

	local additionalSpace = getAdditionalSpace( pathAttributes.fileSystemFreeSize, spaceNeeded )
	logger:tracef( "Additional space needed in '%s' is %s", path, tostring( additionalSpace ) )
	return Observable.just( additionalSpace )

end

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

local WFCatExport = {}

--------------------------------------------------------------------------------
--[[!
	\brief Exports a list of assets from catalog

	\param wfCatalog (WFCatalog) catalog
	\param requests (list) list of requests to be processed
		Each request should contain the asset id and the
		associated settings

	\return Observable reports progress and sends onCompleted when done
]]--

local function exportAssets( wfCatalog, pixelProviderProcessorStream, 
		styleSynchronizer, requests )

	local et = WFCatExportTask( wfCatalog, pixelProviderProcessorStream, styleSynchronizer )

	et:exportAssets( requests )
	
	return et
end

--------------------------------------------------------------------------------
--[[!
	\brief Export one or more assets from catalog

	\param wfCatalog (WFCatalog) catalog
	\param assetId list (string) List of asset ids to be exported
	\param commonExportSettings (table) of common export settings such as format, dimension, etc.
	\param extraSettingsByAssetId (table) of assetId to exportInfo
		watermarkParams: includes watermark settings if required
		devSettingsOverrides: overrides that are not committed to docStore
		outputFileName: desired output filename

	\return Observable reports progress and sends onCompleted when done
]]--

function WFCatExport.exportAssetsFromCatalog( wfCatalog, pixelProviderProcessorStream, 
		styleSynchronizer, assetIds, commonExportSettings, extraSettingsByAssetId )
	
	assert( require 'WFPlatformInfo'.hasACR, "ACR required for export" )
	
	local requests = {}
	for _, assetId in ipairs( assetIds ) do
		
		local extraSettings = extraSettingsByAssetId[ assetId ] or {}
		
		local exportSettings = KSTableUtils.deepCopy( commonExportSettings )
		
		-- Allow overriding watermark params for specific assets. This is not 
		-- used currently, but can be used in the future.
		if extraSettings.watermarkParams then
			exportSettings.watermarkParams = extraSettings.watermarkParams
		end
		
		requests[ #requests + 1 ] = {
			id = assetId,
			fileName = extraSettings.outputFileName,
			devSettingsOverride = extraSettings.devSettingsOverrides,
			settings = exportSettings,
		}
		
	end

	return exportAssets( wfCatalog, pixelProviderProcessorStream, 
		styleSynchronizer, requests )
end

--------------------------------------------------------------------------------
--[[!
	\brief Calculates the size required for export and tries to purge the cache
	inorder to make room for export. It takes into account the space for
	downloading the originals as well as the space required for writing
	the output file.

	\param wfCatalog (WFCatalog) catalog
	\param assetId list (string) List of asset ids to be exported
	\param settings (table) Export settings which includes info
		like format, destination folder etc.

	\return Observable that reports the additional size needed for
	export to work in the same disk as the cache folder and the same
	for destination folder. If both are on the same disk then, 
	the second value will be 0.
]]--

function WFCatExport.checkAndReclaimSpace( wfCatalog, assetIds, settings )

	local destinationFolder = settings.destinationFolder
	--local format = settings.format

	local isDestInSameDiskAsCache = pathIsInSameDiskAsCache( wfCatalog, destinationFolder )

	local assetSizeNeededObservable = Observable.from( assetIds )
										:flatMap( isDownloadNeededAndSizeForAssetFn( wfCatalog ) )
										:share()
	local totalDownloadSizeObservable = assetSizeNeededObservable
								:filter( function( isDownloadNeeded, size )
									return isDownloadNeeded
								end )
								:map( function( isDownloadNeeded, size )
									return size
								end )
								:sum()

	local currentLocalSizeObservable = assetSizeNeededObservable
								:filter( function( isDownloadNeeded, size )
									return not isDownloadNeeded
								end )
								:map( function( isDownloadNeeded, size )
									return size
								end )
								:sum()

	return Observable.zip( totalDownloadSizeObservable, currentLocalSizeObservable )
		:flatMapLatest( function( totalSizeToBeDownloaded, currentLocalSize )

			-- For jpegs, the file size will be less than that of 
			-- original, but here we are ignoring this.
			-- For originals, we are not considering the size
			-- of sidecar file.

			local totalSizeNeededInDest = totalSizeToBeDownloaded + currentLocalSize
			local totalSizeNeededInCacheDisk = totalSizeToBeDownloaded

			logger:tracef( 'Total size needed in dest:%s, cache:%s, isSameDisk:%s',
				tostring( totalSizeNeededInDest ), tostring( totalSizeNeededInCacheDisk ),
				tostring( isDestInSameDiskAsCache ) )
 
			if isDestInSameDiskAsCache then
				totalSizeNeededInCacheDisk = totalSizeNeededInCacheDisk + totalSizeNeededInDest
			end

			local additionalSpaceNeededInCacheDiskObservable = 
				tryToClaimCacheSpaceFn( wfCatalog, totalSizeNeededInCacheDisk )

			local additionalSpaceNeededInDestDiskObservable
			if isDestInSameDiskAsCache then
				-- Return 0, since dest is on the same disk and the first
				-- observable will account for it.
				additionalSpaceNeededInDestDiskObservable = Observable.just( 0 )
			else
				additionalSpaceNeededInDestDiskObservable =
					additionalSpaceNeededOnDisk( wfCatalog, totalSizeNeededInDest, destinationFolder )
			end

			return Observable.zip( 
					additionalSpaceNeededInCacheDiskObservable,
					additionalSpaceNeededInDestDiskObservable )

		end )
		:map( pack )

end

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

local TEST_fns = {}
	
TEST_fns.mock_isDownloadNeededAndSizeForAssetFn_results = {}
TEST_fns.mock_fileSystemAttributesFn_results = {}

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

TEST_fns.mock_isDownloadNeededAndSizeForAssetFn = function( wfCatalog )
	return function( assetId )
			return Observable.just( unpack( TEST_fns.mock_isDownloadNeededAndSizeForAssetFn_results[ assetId ] or {}) )
		end
end

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

TEST_fns.mock_fileSystemAttributesFn = function( path )
	return TEST_fns.mock_fileSystemAttributesFn_results[ path ]
end

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

TEST_fns.mock_tryToClaimCacheSpaceFn = function( wfCatalog, spaceNeeded )
	local attributes = fileSystemAttributesFn( wfCatalog:fileSpec():rootPath() )
	return Observable.just( getAdditionalSpace( attributes.fileSystemFreeSize, spaceNeeded ) )
end

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

function WFCatExport.setUpTestFunctions( downloadNeededSizeResultMap, fileSystemResultMap )
	TEST_fns.mock_isDownloadNeededAndSizeForAssetFn_results = downloadNeededSizeResultMap
	TEST_fns.mock_fileSystemAttributesFn_results = fileSystemResultMap
	
	fileSystemAttributesFn = TEST_fns.mock_fileSystemAttributesFn
	isDownloadNeededAndSizeForAssetFn = TEST_fns.mock_isDownloadNeededAndSizeForAssetFn
	tryToClaimCacheSpaceFn = TEST_fns.mock_tryToClaimCacheSpaceFn
end

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

return WFCatExport
