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

WFCatExportGenericProvider.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.core.logging
local WFLogger = require 'WFLogger'

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

	-- WF.core.os
local KSPathUtils = require 'KSPathUtils'

	-- WF.core.lua
local KSData = require 'KSData'

	-- WF.oz_client.managedCatalog.pixelProvider
local WFCatRenderSize = require 'WFCatRenderSize'
local WFCatFileSpec = require 'WFCatFileSpec'

	-- WF.oz_client.managedCatalog.metadata
local WFCatExportHelper = require 'WFCatExportHelper'

	-- lightroomcc
local NmbAcr = require 'NmbAcr'

local DISABLE_WATERMARK_NEW_SIZE_SPEC = false

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

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

local derefOpt = assert( KSTableUtils.derefOpt )

local WFCrContext = assert( NmbAcr.WFCrContext )

local tiffFormatProvider = {
	fileExtension = '.tif',
	getFormatParams = function ( settings )
		
		local formatParams = {
			formatType = "TIFF",
			compression = settings.compression or "compressionMethod_None",
			includeLightroomImageResource = settings.includeLightroomImageResource or false,
		}
		
		return formatParams
		
	end
}

local jpegFormatProvider = {
	fileExtension = '.jpg',
	getFormatParams = function ( settings )
		local formatParams = {
			formatType = "JPEG",
			quality = settings.quality or 0.8,
			includeMetadata = settings.includeMetadata ~= false,
			includeColorProfile = settings.includeColorProfile ~= false,
		}
		
		return formatParams
		
	end
}

local dngFormatProvider = {
	fileExtension = '.dng',
	getFormatParams = function ( settings )
		local formatParams = {
			formatType = "DNG",
			lossyCompression = settings.lossyCompression,
			embedFastCache = settings.embedFastCache,
			embedOriginal = settings.embedOriginal,
		}
		
		return formatParams
		
	end
}

local formatProvider = {}

formatProvider [ 'tiff' ] = tiffFormatProvider
formatProvider [ 'jpeg' ] = jpegFormatProvider
formatProvider [ 'dng' ] = dngFormatProvider

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

-- TOFIX: Might want to use the max proxy size here.
-- This will be used, only if the client is not
-- providing the size. This should ideally happen
-- only from the test cases.
local kRenderSize = 2560

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

local function prependWith( val )

	return function( ... )

		return val, ...

	end
end

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

local function getTiffOrientation( docValue )

	local orientation = derefOpt( docValue, 'develop', 'userOrientation' ) or 1
	return orientation
end

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

local function getAssetWidthHeight( docValue )

	local croppedWidth = tonumber( derefOpt( docValue,
										'develop', 'croppedWidth' ) )

	local croppedHeight = tonumber( derefOpt( docValue,
										'develop', 'croppedHeight' ) )

	local width = tonumber( derefOpt( docValue,
										'importSource', 'originalWidth' ) )
	local height = tonumber( derefOpt( docValue,
										'importSource', 'originalHeight' ) )

	return ( croppedWidth and croppedHeight )
			and { croppedWidth, croppedHeight }
			or { width, height }

end

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

local function getCroppedLongSide( docValue )

	local width, height = unpack( getAssetWidthHeight( docValue ) )
	return math.max( width, height )

end

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

local function getResizeOptions( settings, croppedLongSide, assetWidth, assetHeight )

	local resizeType, width, height

	if settings.dimensionType == 'full' then

		resizeType = 'longEdge'
		width = croppedLongSide
		height = croppedLongSide

	elseif settings.dimensionType == 'custom' then

		resizeType = settings.resizeType
		width = settings.customMaxSize
		height = settings.customMaxSize

	else

		resizeType = 'longEdge'
		width = tonumber( settings.dimensionType ) or kRenderSize
		height = width

	end

	return resizeType, width, height
end

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

local function getAssetInfo( docValue )
	if docValue._empty or docValue.type ~= 'asset' then
		return Observable.throw( 'invalid_asset_id' )
	end
	
-- [anmurark 2017-10-11]: Photos uploaded from Photoshop don't have the 'develop' table in the docValue.
-- Export should still work even if the develop table is missing. Commenting out the below code to 
-- remove the export failure due to missing develop table.

--	local develop = derefOpt( docValue, "develop" )
--	
--	if type( develop ) ~= 'table' then
--		logger:errorf( "For asset id :%s docValue.develop is of type:%s",
--			docValue._localId, type( develop ) )
--		return Observable.throw( 'invalid_asset_document' )
--	end
	
	local croppedLongSide = getCroppedLongSide( docValue )
	local fileName = derefOpt( docValue, "importSource", "fileName" )
	local orientation = getTiffOrientation( docValue )
	
	return Observable.just( {
		docValue = docValue,
		croppedLongSide = croppedLongSide, 
		fileName = fileName,
		orientation = orientation,
	} )
	
end

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

local function getNegativePath( exportPixelProviderTask, assetId, tryWithProxy, fallbackToProxySize, renderSize )

	local originalStream = exportPixelProviderTask:getOriginal( assetId )

	return originalStream
			:take( 1 )
			:flatMapLatest( function( success, originalFilePath )

				if success and originalFilePath then
					return Observable.just( originalFilePath )
				end

				-- We couldn't fetch original. Lets try to fetch proxy
				-- if it is possible to render using that.
				if tryWithProxy and WFCatRenderSize.canRenderWithProxy( renderSize ) then
					
					return WFCatExportHelper.getProxyPath( exportPixelProviderTask, assetId )
				end
				
				-- We can't render the requested size using proxy.
				-- Render a lower size if request allows to do so.
				if fallbackToProxySize then
					
					return WFCatExportHelper.getProxyPath( exportPixelProviderTask, assetId )
						:map( function( negativePath )
							return negativePath, true -- Did fallback.
						end )
				end

				-- It is not possible to render with proxy too. Send error.
				local errMsg = originalFilePath or 'PixelProvider returned nil'
				logger:warnf( "Failed to fetch original for assetId '%s' because: %s. Cant render with proxy.", 
					assetId, errMsg  )

				return Observable.throw( 'original_unavailable' )
			end)

end

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

local function getRenderParams( assetInfo, settings, optDevSettingsOverride, didFallback  )
	
	local orientation = optDevSettingsOverride and optDevSettingsOverride.orientation or assetInfo.orientation
		
	local longSide = optDevSettingsOverride 
					and math.max( optDevSettingsOverride.croppedWidth, optDevSettingsOverride.croppedHeight )
					or assetInfo.croppedLongSide

	local assetWidth, assetHeight = unpack( getAssetWidthHeight( assetInfo.docValue ) )
	
	local resizeType, width, height = getResizeOptions( settings, longSide, assetWidth, assetHeight )
	
	if didFallback then
		-- We wont be able to generate the render with requested size.
		-- Use proxy size instead
		local proxySize = WFCatFileSpec.getProxySizeFromDocValue( assetInfo.docValue )
		
		resizeType = 'longEdge'
		width = proxySize
		height = proxySize
	end
	
	local useNewSizeSpec = true
	if DISABLE_WATERMARK_NEW_SIZE_SPEC then
		useNewSizeSpec = false
	end
	
	local outputParams = {
		width = width,
		height = height,
		resizeType = resizeType,
		orientation = orientation,
		allowUpsampling = settings.allowUpsampling or false,
		resolution = settings.resolution or 240,
		resolutionUnit = settings.resolutionUnit or "inch",
		bitDepth = settings.bitDepth or 8,
		stripCameraRawSettings = settings.stripCameraRawSettings or false,
		embeddedMetadataOption = settings.embeddedMetadataOption or "copyrightOnly",
		removeLocationMetadata = settings.removeLocationMetadata or false,
		colorSpace = settings.colorSpace or "sRGB",
		includePreview = settings.includePreview or false,
		outputSharpenParams = settings.outputSharpeningOn and settings.outputSharpenParams,
		preserveTransparency = settings.preserveTransparency or false,
		watermarkParams = settings.includeWatermark and settings.watermarkParams,
		useNewSizeSpec = useNewSizeSpec,
		
	}
	
	local formatParams = formatProvider[ settings.format ].getFormatParams( settings )

	return outputParams, formatParams

end

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

local function getXmpAndSettingsForRender( exportPixelProviderTask, negativePath, didFallback, assetInfo, userId, settings, optDevSettingsOverride )

	if settings.applyXmp == false then
		
		local emptyXmp = require 'WFXmp'.WFXmp()
		local emptyXmpString = tostring( emptyXmp:serialize() )
	
	
		local outputParams, formatParams = getRenderParams( assetInfo, settings, optDevSettingsOverride, didFallback )
		
		-- Don't consider the asset's orientation for shared-with-you images. The downloaded full-size rendition will
		-- already take it into account
		outputParams.orientation = 1
		
		return Observable.just( negativePath, assetInfo.fileName, emptyXmpString, outputParams, formatParams, didFallback )
	end
	
	return WFCatExportHelper.getXmpForExport( exportPixelProviderTask, negativePath, assetInfo.docValue, userId, optDevSettingsOverride, true )

		:map( function( metadata )
			
			local xmp = tostring( metadata:serialize() )		
			
			local outputParams, formatParams = getRenderParams( assetInfo, settings, optDevSettingsOverride, didFallback )
	
			return negativePath, assetInfo.fileName, xmp, outputParams, formatParams, didFallback
			
		end )
end

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

local function ensureCameraProfileExistsIfNeeded( docValue, maybeProfileDownloaderTask )

	if not maybeProfileDownloaderTask then
		-- We dont have the downloader access.
		-- Ignore the task
		return Observable.just( true )
	end
	
	local cameraProfileSection = derefOpt( docValue, "develop", "profiles", "camera" )
	if not cameraProfileSection then
		-- No link. Return.
		return Observable.just( true )
	end
	
	return maybeProfileDownloaderTask:ensureCameraProfileExists( 
		cameraProfileSection.filename, cameraProfileSection.href )
end

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

local function renderPixels( inputFile, outputFile, xmp, outputParams, formatParams, didFallback )

	if WFFileUtils.exists( inputFile ) ~= 'file' then
		return Observable.throw( 'invalid_original' )
	end
		
	local ok, renderContext = pcall( WFCrContext, inputFile )
		
	if not ok then
		return Observable.throw( 'invalid_original' )
	end
	
	--ashj: TODO -- Add error handling for all params
	
	return renderContext
		:renderFinalImageAndEncode( 
			xmp, 
			outputParams, 
			formatParams )
		:catch( function()
			return Observable.throw( 'unsupported_file' )
		end)
		:map( prependWith( didFallback ) )
		:map( prependWith( outputFile ) )
		
end

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

-- @mark WFCatExportGenericProvider
--------------------------------------------------------------------------------

local WFCatExportGenericProvider = {}

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

function WFCatExportGenericProvider:exportAsset( request, wfCatalog, 
		exportPixelProviderTask, maybeProfileDownloaderTask )
	
	--Basic input validation
	local settings = request.settings
	local assetId = request.id 
	local devSettingsOverride = request.devSettingsOverride
	local userId = wfCatalog:ozUserId()
	
	if not assetId then
		logger:warnf( 'Export failed as asset id supplied is nil' )
		return Observable.throw( 'invalid_asset_id' )
	end
	
	
	local destinationFolder = request.settings and request.settings.destinationFolder
                                                               
	if not destinationFolder or WFFileUtils.exists( destinationFolder ) ~= 'directory' then
		logger:warnf( "Destination folder '%s' seems to be invalid", destinationFolder )
		return Observable.throw( 'invalid_destination' )
	end
	
	if not formatProvider[ request.settings.format ] then
		logger:warnf( 'Export failed as export format is not supported' )
		return Observable.throw( 'invalid_format' )
	end
	
	local warnings = {}
	
	if request.warnings then
		warnings = KSTableUtils.shallowcopy( request.warnings )
	end

	return wfCatalog:docStore():docValue( assetId )
	
		:flatMapLatest( getAssetInfo )
		
		:flatMapLatest( function( assetInfo )
			
			local assetWidth, assetHeight = unpack( getAssetWidthHeight( assetInfo.docValue ) )
			local _, width, height = getResizeOptions( settings, assetInfo.croppedLongSide, assetWidth, assetHeight )
			local tryWithProxy = settings.tryWithProxy ~= false
			local renderSize = math.max( width, height )
			local fallbackToProxySize = settings.fallbackToProxySize or false
			
			if request.sourceImagePath then
				return Observable.just( request.sourceImagePath, false, assetInfo )
			end
			
			return getNegativePath( exportPixelProviderTask, assetId, tryWithProxy, fallbackToProxySize, renderSize )
					:flatMap( function( negativePath, didFallback )
					
						if didFallback then
							warnings[ #warnings + 1 ] = 'exported_with_fallback'
						end
						
						return ensureCameraProfileExistsIfNeeded( assetInfo.docValue, maybeProfileDownloaderTask )
							:map( function( isSuccess )
								if not isSuccess then
									warnings[ #warnings + 1 ] = 'missing_profile'
								end
								return negativePath, didFallback, assetInfo
							end )
					end )
					

		end)
		
		:map( function( negativePath, didFallback, assetInfo )
			return exportPixelProviderTask, negativePath, didFallback, assetInfo, userId, settings, devSettingsOverride 
		end )
		
		:flatMapLatest( getXmpAndSettingsForRender )
		
		:flatMapLatest( renderPixels )
		
		:map( function( fileName, didFallback, imageBlob )
			
			fileName = request.fileName or fileName
			
			-- Change the file extension
			fileName = fileName:match( "(.+)%..*$" )
			-- Replace any invalid char with _. Note that the name can still be
			-- invalid since windows doesn't support filenames with some
			-- reserved keywords as well. But this will reduce the chances
			-- by a huge margin.
			fileName = string.gsub( fileName, "[<>:\"/\\|\?\*]", "_" )
			
			local extension = formatProvider[ request.settings.format ].fileExtension
			fileName = fileName..extension
			
			--ashj: TODO: Need to check name conflict resolution policy
			--ashj: TODO Write metadata as needed. Might need WFCrContext here.
			local destPath = KSPathUtils.child( destinationFolder, fileName )
			local ok, path = pcall( KSData.saveToUniquelyNamedFile, imageBlob, destPath )
			
			if not ok or not path then
				local errMsg = path or 'KSData.saveToUniquelyNamedFile return nil'
 				logger:warnf( "Failed to save blob to path '%s' because: %s", destPath, errMsg  )

				error( 'write_failed' )
			end
			
			-- TODO [ashj 2018-07-30]: Decide how to
			-- handle multiple warnings.
			-- Taking first warning for now.
			
			--Return the exported path since it can be different
			-- from the <destination>/<fileName> in case of
			-- conflicts.
			return {
				path = path,
				warning = #warnings > 0 and warnings,
			}
			
		end)
	
end


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

return WFCatExportGenericProvider
