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

WFCatFileSpec.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'

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

	-- WF.core.utils
local KSPathUtils = require 'KSPathUtils'
local KSTableUtils = require 'KSTableUtils'
local WFFileUtils = require 'WFFileUtils'
local WFArguments = require 'WFArguments'
local _ = require 'WFArguments_uuid'
local WFCloseSupport = require 'WFCloseSupport'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFTableMerge = require 'WFTableMerge'

	-- WF.oz_client.catalog.index
local WFOzContentTypes = require 'WFOzContentTypes'

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

	-- WF.oz_client.managed_catalog.pixel_provider
local WFCatPixelPathGenerator = require 'WFCatPixelPathGenerator'

local NmbFileManagementSettings = require 'NmbFileManagementSettings'

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

local check = assert( WFArguments.check )

local derefOpt = assert( KSTableUtils.derefOpt )

local WFCatTrashUtils_getOriginalAssetMasterLink = assert( WFCatTrashUtils.getOriginalAssetMasterLink )
local WFCatTrashUtils_getOriginalAssetLinkForKey = assert( WFCatTrashUtils.getOriginalAssetLinkForKey )
local WFCatTrashUtils_getOriginalAssetXmpDevelopLink = assert( WFCatTrashUtils.getOriginalAssetXmpDevelopLink )
local WFCatTrashUtils_getOriginalAssetXmpLink = assert( WFCatTrashUtils.getOriginalAssetXmpLink )
local WFCatTrashUtils_getOriginalAssetUrlForLinkKey = assert( WFCatTrashUtils.getOriginalAssetUrlForLinkKey )
local WFCatTrashUtils_getOriginalAssetLinks = assert( WFCatTrashUtils.getOriginalAssetLinks )

local logger = WFLogger 'WFCatFileSpec'

local kRemoveMe = "remove_me"

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

local function useTrashLink()

	return true

	-- Let's try always returning true as we probably want to use this if we
	-- are paused as well. The downfall may be that_localOnly.binaryDownloadErrors
	-- may get set if we try to download from an original asset that is no longer
	-- there.

	-- If the above approach is identified as problematic use this:
	--	return WFCatOzErrors.hasStorageExceededError( docValue )
	--		or WFCatOzErrors.hasTrialOrSubscriptionExpiredError

end

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

local function buildAssetPath( docValue, ... )

	local segments = { ... }
	local nSegments = select( '#', ... )

	-- SPAM [jandrew 2017-04-27]: This is a little bit hacky, but it
	-- works around a parameter passing issue. buildAssetPath is called
	-- below by a helper function, and the last param was sometimes nil.
	-- In the event the last param was nil, the segment count was 1
	-- greater than the actual number of segments, and so the following
	-- derefOpt would always return nil. To avoid this issue, we now pass
	-- in kRemoveMe as the last param, instead of nil, and remove it 
	-- from the segment list prior to calling derefOpt.
	
	if segments[ nSegments ] == kRemoveMe then
		table.remove( segments )
	end
	
	local maybeAsset = derefOpt( docValue, unpack( segments ) ) 
	local path = maybeAsset and maybeAsset.path

	if maybeAsset and not path then
	
		local relativePath = maybeAsset.relativePath
		
		local volumeGuid = maybeAsset.volumeGuid
		
		if relativePath and volumeGuid then
		
			local success, assetPath = pcall( 
					WFFileUtils.createFullPathFromRelativePathAndGuid,
					relativePath, volumeGuid )
					
			if success and assetPath then
			
				return assetPath
			
			end
			
		end
	
	end

	return path

end

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

local function buildOriginalPathFromDocValue( _, docValue )

	return buildAssetPath( docValue, '_localOnly', 'files', 'original', 'master' )

end

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

-- The following function fetches the absolute path of the proxy. We no longer
-- use this annotation, since we instead store assets via relative paths. However,
-- we use this function to see which assets need to be updated to use the new
-- relative path semantics.

local function getAbsoluteOriginalPathFromDocValue( _, docValue )

	return derefOpt( docValue, '_localOnly', 'files', 'original', 'master', 'path' )

end

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

local function getOriginalVolumeGuidFromDocValue( _, docValue )

	return derefOpt( docValue, '_localOnly', 'files', 'original', 'master', 'volumeGuid' )

end

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

local function getOriginalRelativePathFromDocValue( _, docValue )

	return derefOpt( docValue, '_localOnly', 'files', 'original', 'master', 'relativePath' )

end

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

local function getLocalOriginalSha256( _, docValue )

	local sha256 = derefOpt( docValue, '_localOnly', 'files', 'original', 'master', 'sha256' )

	return sha256

end

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

local function getLocalOriginalSha256WithFallback( _, docValue )

	local sha256 = getLocalOriginalSha256( _, docValue )

	if not sha256 then

		-- SPAM [jandrew 2017-06-08]: if there's no local SHA256, it means the asset was
		-- downloaded or imported prior to this date. In this case, we assume that the
		-- local asset SHA256 matches the asset payload SHA256.

		sha256 = derefOpt( docValue, 'importSource', 'sha256' )

	end

	return sha256

end

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

local function isVideo( subtype )
	return subtype == 'video' or subtype == 'deleted_video'
end

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

local function extractProxyStorageParamFromDocValue( _, extractionFunc, docValue, param, optProxyType )

	if optProxyType then

		return extractionFunc( docValue, '_localOnly', 'files', 'proxy', optProxyType, param ), optProxyType

	end
	
	if isVideo( docValue.subtype ) then
		
		if WFPlatformInfo.isWindows then
			-- 1080p709 proxies are specifically created in REC709 colorspace. These files are meant to
			-- handle HDR video playback on Windows where the original colorspace (bt2020) is not fully
			-- supported. This is ONLY meant for Windows to uses as a workaround and macOS clients 
			-- should never use these proxies. The check here comes before regular 1080p because
			-- we know that only bt2020 files would request 1080p709 proxies and these proxies are not
			-- created by default. In the case of both 1080p and 1080p709 proxies existing locally do to
			-- the asynchronous request or residual files on disk, we prioritize using 1080p709.
			local maybe1080p709 = extractionFunc( docValue, '_localOnly', 'files', 'proxy', '1080p709', param )

			if maybe1080p709 then
			
				return maybe1080p709, '1080p709'
			
			end
			
		end

		local maybe1080p = extractionFunc( docValue, '_localOnly', 'files', 'proxy', '1080p', param )

		if maybe1080p then
		
			return maybe1080p, '1080p'
		
		end

		local maybe360p = extractionFunc( docValue, '_localOnly', 'files', 'proxy', '360p', param )

		if maybe360p then
		
			return maybe360p, '360p'
		
		end

		return extractionFunc( docValue, '_localOnly', 'files', 'proxy', '720p', param ), '720p'

	end

	local maybe2560 = extractionFunc( docValue, '_localOnly', 'files', 'proxy', '2560', param ) 
	
	if maybe2560 then
		
		return maybe2560, '2560'
	
	end
	
	local maybePano

	if derefOpt( docValue, '_localOnly', 'files', 'proxy', 'panorama' ) then
		maybePano = extractionFunc( docValue, '_localOnly', 'files', 'proxy', 'panorama', param )
	else -- check old location (pano is deprecated)
		maybePano = extractionFunc( docValue, '_localOnly', 'files', 'proxy', 'pano', param )
	end

	if maybePano then
	
		return maybePano, 'panorama'
	
	end
	
	local maybeFullsize = extractionFunc( docValue, '_localOnly', 'files', 'proxy', 'fullsize', param )

	if maybeFullsize then
	
		return maybeFullsize, 'fullsize'
	
	end
	
	return nil
	
end

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

local function buildProxyPathFromDocValue( self, docValue, optProxyType )

	return extractProxyStorageParamFromDocValue( self, buildAssetPath, docValue, kRemoveMe, optProxyType )
	
end

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

-- The following function fetches the absolute path of the proxy. We no longer
-- use this annotation, since we instead store assets via relative paths. However,
-- we use this function to see which assets need to be updated to use the new
-- relative path semantics.

local function getAbsoluteProxyPathFromDocValue( self, docValue, optProxyType )

	return extractProxyStorageParamFromDocValue( self, derefOpt, docValue, 'path', optProxyType )
	
end

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

local function getProxyVolumeGuidFromDocValue( self, docValue, optProxyType )

	return extractProxyStorageParamFromDocValue( self, derefOpt, docValue, 'volumeGuid', optProxyType )
	
end

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

local function getProxyRelativePathFromDocValue( self, docValue, optProxyType )

	return extractProxyStorageParamFromDocValue( self, derefOpt, docValue, 'relativePath', optProxyType )
	
end

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

local function getLocalProxySha256( self, docValue, optProxyType )

	local sha256 = extractProxyStorageParamFromDocValue( self, derefOpt, docValue, 'sha256', optProxyType )
	
	if not sha256 then

		-- SPAM [jandrew 2017-06-08]: if there's no local SHA256, it means the asset was
		-- downloaded or imported prior to this date. In this case, we assume that the
		-- local asset SHA256 matches the asset payload SHA256.

		sha256 = derefOpt( docValue, 'importSource', 'proxySHA256' ) or derefOpt( docValue, 'importSource', 'sha256' )

	end

	return sha256
	
end

--------------------------------------------------------------------------------
-- getOriginalLinkFromDocValue returns
--
-- 	maybeAnyOriginalLink, isLinkValid, maybeInvalidOriginalLink
--
-- 20230823 
--
-- Note: historically, this function returned just
--
-- 		maybeAnyOriginalLink, isLinkValid
--
-- i.e. the first return value would be non nil for some invalid links and
-- caller was expected to check both the maybeAnyOriginalLink and isLinkValid
-- return values to ensure the link is valid. 
--
-- This behavior is preserved.
--
-- The maybeInvalidOriginalLink return value is a new addition to this function
-- that will always be non-nil if the link is invalid
--

local function getOriginalLinkFromDocValue( _, docValue )

	local original = derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master' )
		or useTrashLink( docValue ) and WFCatTrashUtils_getOriginalAssetMasterLink( docValue )

	local rawOriginalLink = derefOpt( original, 'href' )

	-- Now check if the link is deny listed or invalid and adjust return values
	-- accordingly

	-- File on Oz might not exist even though it presents a link to it (NMB-3111) and 
	-- multiple download attempts have failed.  Pretend like it doesn't exist so 
	-- we don't try to download something we know doesn't exist	
	local permanentOriginalDownloadErrorForCurrentLink = 
			derefOpt( docValue, '_localOnly', 'binaryDownloadErrors', '/rels/master', rawOriginalLink )
	
	-- Upload may be corrupt, but still the link exists on Oz and we've removed
	-- the sha256 error from the main payload See NMB-4862
	local permanentOriginalUploadErrorForCurrentLink = 
			derefOpt( docValue, '_localOnly', 'binaryUploadErrors', '/rels/master', rawOriginalLink )

	if permanentOriginalDownloadErrorForCurrentLink or
			permanentOriginalUploadErrorForCurrentLink then

		-- if there are permanent upload or download errors, we don't want to
		-- use the links 
		return nil

	end

	-- From this point, we will be returning a link, possibly invalid

	local sha256Error = derefOpt( docValue,
								  'importSource',
								  'status',
								  'error',
								  'all',
								  'reason',
								  'sha256' )

	if sha256Error then

		-- File on oz has bad sha256.
		--
		-- We may want to use it as last resort, but not as a default. We
		-- return the link as optional third parameter for downstream clients.
		--
		-- Scenarios that cause this state: 
		--
		--  * User imports a file and it is a valid file but the uploaded bits
		--    get corrupted during transfer
		--  * local file is modified before it is synced up. in that case, Oz
		--    has valid file, but sha256 is mismatched
		--
		return nil, true, rawOriginalLink

	end

	-- 20230823 bkruse
	--
	-- if it isn't a known bad sha256 per above, the link still might be invalid.
	--
	-- Historically, we returned the link and the invalid state. 
	--
	-- We preserve that behavior here and add the additional third return value
	-- for the invalid link. This preserves the behavior for existing callers and
	-- guarantees new callers can always access the invalid link.
	local isInvalid = derefOpt( original, 'invalid' )

	local maybeInvalidOriginalLink = isInvalid and rawOriginalLink or nil
	
	return rawOriginalLink, isInvalid, maybeInvalidOriginalLink

end

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

local function getProxyHref( docValue, proxySize )

	local linksKey = '/rels/proxy_type/' .. proxySize
	
	local links = derefOpt( docValue, '_serverProvidedMetadata', 'links', linksKey )
			or useTrashLink( docValue ) and WFCatTrashUtils_getOriginalAssetLinkForKey( docValue, linksKey )

	local href = derefOpt( links, 'href' )
	local exists = derefOpt( links, 'exists' )
	local isInvalid = derefOpt( links, 'invalid' )
	local permanentDownloadError = derefOpt( docValue, '_localOnly', 'binaryDownloadErrors', linksKey, href )
	
	if ( href or exists ) and not permanentDownloadError then
		return href, exists, isInvalid
	end

end

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

local function get720pProxyHref( docValue )

	-- The 720p proxy is actually a rendition, hence the special handling.
	
	local linksKey = '/rels/rendition_type/720p'

	local links = derefOpt( docValue, '_serverProvidedMetadata', 'links', linksKey )
		or useTrashLink( docValue ) and WFCatTrashUtils_getOriginalAssetLinkForKey( docValue, linksKey )

	local href = derefOpt( links, 'href' )
	local isInvalid = derefOpt( links, 'invalid' )
	local permanentDownloadError = derefOpt( docValue, '_localOnly', 'binaryDownloadErrors', linksKey, href )
	
	if href and not permanentDownloadError then
		return href, isInvalid
	end

end

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

local function getProxyLinkFromDocValue( _, docValue, optProxyType )
	
	local link, proxySize, isInvalid

	if optProxyType then
		
		-- if the proxy size was specified, check for that specific link	
		
		proxySize = optProxyType

		if optProxyType == '720p' then
		
			-- The 720p proxy is actually a rendition, hence the special handling.
			
			link, isInvalid = get720pProxyHref( docValue )
		
		else
		
			link, _, isInvalid = getProxyHref( docValue, proxySize )
		
		end
		
	elseif isVideo( docValue.subtype ) then
	
		proxySize = '1080p' -- default if no links exist
	
		-- If no proxy size is specified, the default order is 1080p, 360p, then 720p.
		-- The 1080p and 360p proxies are true proxies, while the 720p is a rendition
		-- (which is why we consider it last).
		
		-- 1080p709
		
		local proxy1080p709Link, proxy1080p709Exists, proxy1080p709IsInvalid
				= getProxyHref( docValue, '1080p709' )
		
		-- 1080p
		
		local proxy1080pLink, proxy1080pExists, proxy1080pIsInvalid
				= getProxyHref( docValue, '1080p' )
		
		-- 360p
		
		local proxy360pLink, proxy360pExists, proxy360pIsInvalid
				= getProxyHref( docValue, '360p' )
		
		-- 720p
		
		local proxy720pLink, proxy720pIsInvalid = get720pProxyHref( docValue )
		
		if WFPlatformInfo.isWindows and ( proxy1080p709Link or proxy1080p709Exists ) then
			link, proxySize, isInvalid = proxy1080p709Link, '1080p709', proxy1080p709IsInvalid
		elseif proxy1080pLink or proxy1080pExists then
			link, proxySize, isInvalid = proxy1080pLink, '1080p', proxy1080pIsInvalid
		elseif proxy360pLink or proxy360pExists then
			link, proxySize, isInvalid = proxy360pLink, '360p', proxy360pIsInvalid
		elseif proxy720pLink then
			link, proxySize, isInvalid = proxy720pLink, '720p', proxy720pIsInvalid
		end
		
	else
		
		proxySize = '2560' -- default if no links exist
		
		-- Panorama
		
		local proxyPanoramaLink, proxyPanoramaExists, proxyPanoramaIsInvalid
				= getProxyHref( docValue, 'panorama' )
		
		-- 2560
		
		local proxy2560Link, proxy2560Exists, proxy2560IsInvalid
				= getProxyHref( docValue, '2560' )
		
		-- Fullsize
		
		local proxyFullsizeLink, proxyFullsizeExists, proxyFullsizeIsInvalid
				= getProxyHref( docValue, 'fullsize' )
		
		if proxyPanoramaLink or proxyPanoramaExists then
			link, proxySize, isInvalid = proxyPanoramaLink, 'panorama', proxyPanoramaIsInvalid
		elseif proxy2560Link or proxy2560Exists then
			link, proxySize, isInvalid = proxy2560Link, '2560', proxy2560IsInvalid
		elseif proxyFullsizeLink or proxyFullsizeExists then
			link, proxySize, isInvalid = proxyFullsizeLink, 'fullsize', proxyFullsizeIsInvalid
		end
	
	end

	return link, proxySize, isInvalid

end

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

local function getProxySizeFromDocValue( self, docValue, optProxyType )

	return extractProxyStorageParamFromDocValue( self, derefOpt, docValue, 'size', optProxyType )
	
end

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

local function getOriginalSizeFromDocValue( _, docValue )

	return derefOpt( docValue, '_localOnly', 'files', 'original', 'master', 'size' )
		or derefOpt( docValue, 'importSource', 'fileSize' )
		
end

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

local function getRenditionLinksFromDocValue( _, docValue )

	local renditionLinks = {}
	
	local links = derefOpt( docValue, '_serverProvidedMetadata', 'links' )
	
	renditionLinks[ 'thumbnail' ] = derefOpt( links, '/rels/rendition_type/144h', 'href' )
	renditionLinks[ 'thumbnail2x' ]= derefOpt( links, '/rels/rendition_type/thumbnail2x', 'href' )
	renditionLinks[ '640' ] = derefOpt( links, '/rels/rendition_type/640', 'href' )
	renditionLinks[ '1280' ] = derefOpt( links, '/rels/rendition_type/1280', 'href' )
	renditionLinks[ '2048' ] = derefOpt( links, '/rels/rendition_type/2048', 'href' )
	renditionLinks[ 'fullsize' ] = derefOpt( links, '/rels/rendition_type/fullsize', 'href' )
	renditionLinks[ 'panorama' ] = derefOpt( links, '/rels/rendition_type/panorama', 'href' )
	
	if isVideo( docValue.subtype )  then
		renditionLinks[ '360p' ] = derefOpt( links, '/rels/rendition_type/360p', 'href' )
		renditionLinks[ '720p' ] = derefOpt( links, '/rels/rendition_type/720p', 'href' )
		renditionLinks[ '1080p' ] = derefOpt( links, '/rels/rendition_type/1080p', 'href' )
		renditionLinks[ '1080p709' ] = derefOpt( links, '/rels/rendition_type/1080p709', 'href' )
		renditionLinks[ 'hls' ] = derefOpt( links, '/rels/rendition_type/hls', 'href' )
	end
	
	return renditionLinks
	
end

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

local function checkOzForNonThumbnailRendition( self, docValue )

	local renditionLinks = getRenditionLinksFromDocValue( self, docValue )
	local sufficientRenditionFoundOnOz = false
	
	local assetIsAVideo = isVideo( docValue.subtype )
	
	for renditionSize, link in pairs( renditionLinks ) do
	
		-- For images, Oz must contain at least a 640 rendition that we can download.
		-- For videos, any rendition will do. ( though the name of this function is 
		-- bit of a misnomer ).
	
		if assetIsAVideo or not string.find( renditionSize, 'thumbnail' ) then
	
			if link then 
				sufficientRenditionFoundOnOz = true
				break 
			end
	
		end
	
	end
	
	return sufficientRenditionFoundOnOz
	
end

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

local function getXmpDevelopLinkAndSha256FromDocValue( _, docValue )

	local maybeDevelop = derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop' )
		or useTrashLink( docValue ) and WFCatTrashUtils_getOriginalAssetXmpDevelopLink( docValue )
	local xmpDevelopLink = derefOpt( maybeDevelop, 'href' )
	local linkSha256 = derefOpt( maybeDevelop, 'sha256' )

	-- will not use link if it is marked invalid
	local isXmpDevelopInvalid = derefOpt( maybeDevelop, 'invalid' )
	if isXmpDevelopInvalid then
		return nil, nil
	end

	-- will not use link if it has been annotated with an error
	local xmpDevelopDownloadError = derefOpt( docValue, '_localOnly', 'binaryDownloadErrors', '/rels/xmp/develop', xmpDevelopLink )
	if xmpDevelopDownloadError then
		return nil, nil
	end

	-- will not use link if it does not match the sha256 in develop section or if
	-- there is no xmpCameraRaw.sha256 in the develop section
	local developSha256 = derefOpt( docValue, 'develop', 'xmpCameraRaw', 'sha256' )
	if linkSha256 and linkSha256 ~= developSha256 then
		return nil, nil
	end

	return xmpDevelopLink, linkSha256

end

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

local function getSelfLinkFromDocValue( _, docValue )

	local selfHref = derefOpt( docValue, '_serverProvidedMetadata', 'links', 'self', 'href' )

	return selfHref
	
end

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

local function getTableOfAuxLinksFromDocValue( _, docValue )

	-- Return only the links for auxiliaries that are associated with masks,
	-- filters in the develop section, as well as any c2pa aux files. The
	-- resulting table is keyed by the digest of the auxiliary object.

	local auxLinks = {}

	local WFCatAssetAux = require 'WFCatAssetAux' -- Require here to avoid looper.
	
	local developAuxDigestList = WFCatAssetAux.extractDevelopAuxDigestsFromDocValue( docValue )
	local c2paDigestList = WFCatAssetAux.extractC2paAuxDigestFromDocValue( docValue )
	local currentAuxDigestList = WFTableMerge.applyOverlay( developAuxDigestList, c2paDigestList )
	
	for auxId, auxDigest in pairs( currentAuxDigestList ) do

		local digest = auxDigest
		
		if digest then

			local link = WFCatAssetAux.getDownloadLink( docValue, digest )

			auxLinks[ digest ] = link

		end

	end

	return next( auxLinks ) and auxLinks or nil

end

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

local function getXmpLinkFromDocValue( _, docValue )
	
	local maybeXmp = derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp' )
	local xmpLink = derefOpt( maybeXmp, 'href' )

	if xmpLink == nil and useTrashLink( docValue ) then
		maybeXmp = WFCatTrashUtils_getOriginalAssetXmpLink( docValue )
		xmpLink = derefOpt( maybeXmp, 'href' )
	end

	local isXmpInvalid = derefOpt( maybeXmp, 'invalid' )

	local xmpDownloadError = derefOpt( docValue, '_localOnly', 'binaryDownloadErrors', '/rels/xmp', xmpLink )
	
	if isXmpInvalid or xmpDownloadError then
		return nil
	end
	
	return xmpLink
	
end

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

local function getUrlForLinkKey( _, docValue, linksKey )

	return derefOpt( docValue, '_serverProvidedMetadata', 'links', linksKey, 'href' )
		or useTrashLink( docValue ) and WFCatTrashUtils_getOriginalAssetUrlForLinkKey( docValue, linksKey )

end

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

local function getLinksFromDocValue( _, docValue )

	return derefOpt( docValue, '_serverProvidedMetadata', 'links' )
		or useTrashLink( docValue ) and WFCatTrashUtils_getOriginalAssetLinks( docValue )

end

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

local function getWatermarkContentType( docValue )
	
	local fileName = derefOpt( docValue, 'importSource', 'fileName' )
	local extension = fileName:match( "%.([^%.]+)$" ):lower()
	-- All the allowed extensions validated already.
	return extension == "png" and 'image/png' or 'image/jpeg'
	
end

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

local function getContentTypeFromDocValue( _, docValue )

	if( docValue.type == "watermark" ) then
		return getWatermarkContentType( docValue )
	end
	
	local contentType = derefOpt( docValue, 'importSource', 'contentType' )

	if not contentType then
		return WFOzContentTypes.contentTypeForSubtype( derefOpt( docValue, 'subtype' ) )
	end

	return contentType

end

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

local function getMarkedForOfflineUseFromDocValue( _, docValue )

	return derefOpt( docValue, '_localOnly', 'isMarkedForOfflineUse' )

end

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

local function getXmpDevelopInfoFromDocValue( _, docValue )
	
	return derefOpt( docValue, '_localOnly', 'files', 'xmp_develop' )
	
end

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

local function getXmpSidecarPathFromDocValue( _, docValue )
	return buildAssetPath( docValue, '_localOnly', 'files', 'original', 'xmp' )
end

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

local function getXmpSidecarFileSizeFromDocValue( _, docValue )
	return derefOpt( docValue, '_localOnly', 'files', 'original', 'xmp', 'fileSize' )
end

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

local function getXmpSidecarExistsFromDocValue( _, docValue )
	-- returns true, false, or nil for uknown
	return derefOpt( docValue, '_localOnly', 'xmpSidecarExists' )
end

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

local function getCameraProfileLinkFromDocValue( _, docValue )

	return derefOpt( docValue, 'develop', 'profiles', 'camera', 'href' )

end

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

local function getCameraProfileFilenameFromDocValue( _, docValue )

	return derefOpt( docValue, 'develop', 'profiles', 'camera', 'filename' )
	
end

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

local function getOriginalCannotBeRendered( _, docValue )

	local codec = derefOpt( docValue, 'video', 'videoCodec' )
	
	if codec == 'HEVC' then

		return true

	end

	local fileName = derefOpt( docValue, "importSource", "fileName" )

	if type( fileName ) ~= "string" then

		-- can't check extension, but be optimistic and assume we *can* render
		-- original file
		return false

	end

	if string.match( string.lower( fileName ), '.avi$' ) then

		-- 2018.08.27 bkruse
		-- Our video decoder (DLMS v12) only supports AVI files on Windows
		return not WFPlatformInfo.isWindows

	end

	return false
	
end

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

local function maybeAddParentFoldersToPath( maybePathInfo, defaultPathInfo, ozCatalogId )

	if maybePathInfo and
		maybePathInfo.relativePath and
		maybePathInfo.volumeGuid then

		-- If the user specified destination is the same as the base catalog dir,
		-- there's no need to make the path more distinct.
		
		if maybePathInfo.relativePath == defaultPathInfo.relativePath then
			return maybePathInfo
		end

		local appDir = KSPathUtils.child( maybePathInfo.relativePath, 'Lightroom CC' )

		maybePathInfo.relativePath = KSPathUtils.child( appDir, ozCatalogId )

		return maybePathInfo

	end 
	
	return defaultPathInfo

end

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

local _docStoreFilename = "Managed Catalog.mcat"

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

local _endsWithDocStoreFileNamePattern = 
		string.format( "%s$", _docStoreFilename )

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

local function isDocStoreFilePath( _, path )
	
	return string.find( path, _endsWithDocStoreFileNamePattern )

end

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

local function getDefaultDocstorePathForFolder( baseCatalogDir )

	return KSPathUtils.child( baseCatalogDir, _docStoreFilename )

end

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

local function getDefaultDocstorePath( _, baseFolder )
	
	return getDefaultDocstorePathForFolder( baseFolder )

end

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

-- @mark WFCatFileSpec class

--------------------------------------------------------------------------------
--[[!
	\class WFCatFileSpec

	\brief A \c WFCatFileSpec gathers together information about where data associated with a
		catalog should live in the local file system
]]--

local WFCatFileSpec = {

	_rootDir = false,
	
	_catalogPath = false,
	
	_docStorePath = false,
	
	_pixelsPath = false,
	
	_catalogPathInfo = false,
	
	_originalsPathInfo = false,
	
	_proxiesPathInfo = false,
	
	_deletionBacklogPath = false,
	
	_crUserPathInfo = false,
	
	class = {
		buildOriginalPathFromDocValue = buildOriginalPathFromDocValue,
		getAbsoluteOriginalPathFromDocValue = getAbsoluteOriginalPathFromDocValue,
		getCameraProfileLinkFromDocValue = getCameraProfileLinkFromDocValue,
		getCameraProfileFilenameFromDocValue = getCameraProfileFilenameFromDocValue,
		getOriginalVolumeGuidFromDocValue = getOriginalVolumeGuidFromDocValue,
		getOriginalRelativePathFromDocValue = getOriginalRelativePathFromDocValue,
		getLocalOriginalSha256 = getLocalOriginalSha256,
		getLocalOriginalSha256WithFallback = getLocalOriginalSha256WithFallback,
		buildProxyPathFromDocValue = buildProxyPathFromDocValue,
		getAbsoluteProxyPathFromDocValue = getAbsoluteProxyPathFromDocValue,
		getProxyVolumeGuidFromDocValue = getProxyVolumeGuidFromDocValue,
		getProxyRelativePathFromDocValue = getProxyRelativePathFromDocValue,
		getLocalProxySha256 = getLocalProxySha256,
		getOriginalLinkFromDocValue = getOriginalLinkFromDocValue,
		getProxyLinkFromDocValue = getProxyLinkFromDocValue,
		getXmpDevelopLinkAndSha256FromDocValue = getXmpDevelopLinkAndSha256FromDocValue,
		getSelfLinkFromDocValue = getSelfLinkFromDocValue,
		getTableOfAuxLinksFromDocValue = getTableOfAuxLinksFromDocValue,
		getXmpLinkFromDocValue = getXmpLinkFromDocValue,
		getUrlForLinkKey = getUrlForLinkKey,
		getLinksFromDocValue = getLinksFromDocValue,
		getContentTypeFromDocValue = getContentTypeFromDocValue,
		getMarkedForOfflineUseFromDocValue = getMarkedForOfflineUseFromDocValue,
		getXmpDevelopInfoFromDocValue = getXmpDevelopInfoFromDocValue,
		getProxySizeFromDocValue = getProxySizeFromDocValue,
		getOriginalSizeFromDocValue = getOriginalSizeFromDocValue,
		getXmpSidecarPathFromDocValue = getXmpSidecarPathFromDocValue,
		getXmpSidecarFileSizeFromDocValue = getXmpSidecarFileSizeFromDocValue,
		getXmpSidecarExistsFromDocValue = getXmpSidecarExistsFromDocValue,
		getRenditionLinksFromDocValue = getRenditionLinksFromDocValue,
		checkOzForNonThumbnailRendition = checkOzForNonThumbnailRendition,
		isDocStoreFilePath = isDocStoreFilePath,
		getDefaultDocstorePath = getDefaultDocstorePath,
		getOriginalCannotBeRendered = getOriginalCannotBeRendered,
	},
	
	WFCloseSupport.addCloseSupportMethods,
}

--------------------------------------------------------------------------------
--[[!
	\brief Constructs the WFCatFileSpec with the specified root dir as the base. All managed data will reside underneath this directory
	
	\param rootDir (string) Fully qualified path to directory that should be treated as the base directory
		for all managed content
]]--

function WFCatFileSpec:__init( rootDir, ozCatalogId, optDocstorePathOverride )
	
	check( 'WFCatFileSpec', 'string', 'string', '?string' )
	
	self._rootDir = rootDir
	
	local baseCatalogDir 
		
	baseCatalogDir = KSPathUtils.child( rootDir, ozCatalogId )
		
	self._catalogPath = baseCatalogDir
		
	self._docStorePath = optDocstorePathOverride or 
						getDefaultDocstorePathForFolder( baseCatalogDir )
			
	self._pixelsPath = baseCatalogDir
	
	self._catalogPathInfo = {
		volumeGuid = WFFileUtils.getVolumeGuidForPath( self._catalogPath ),
		relativePath = WFFileUtils.removeVolumeFromPath( self._catalogPath )
	}
	
	local originalsPathInfo = NmbFileManagementSettings.originalStorageLocationInfo()

	-- Check to see if the volumeGuid is saved as a device identifier, if it is get the new version of
	-- the volumeGuid which is saved as the mount point.  See NMB-19641
	if derefOpt( originalsPathInfo, 'volumeGuid' ) and string.match( originalsPathInfo.volumeGuid, "^/dev/disk" ) then

		local volumeGuid = originalsPathInfo.volumeGuid
		local relativePath = originalsPathInfo.relativePath
		local basePath = WFFileUtils.createFullPathFromRelativePathAndGuid( relativePath, volumeGuid )
		local newVolumeGuid = WFFileUtils.getVolumeGuidForPath( basePath )
		if newVolumeGuid ~= volumeGuid then
			NmbFileManagementSettings.setOriginalStorageLocation( basePath )
		end

	end

	self._originalsPathInfo = maybeAddParentFoldersToPath( originalsPathInfo, self._catalogPathInfo, ozCatalogId )

	local proxiesPathInfo = NmbFileManagementSettings.proxyStorageLocationInfo()
	
	self._proxiesPathInfo = maybeAddParentFoldersToPath( proxiesPathInfo, self._catalogPathInfo, ozCatalogId )
	
	self._deletionBacklogPath = KSPathUtils.child( baseCatalogDir, 'Managed Catalog.mru' )
	
end

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

function WFCatFileSpec:rootPath()
	
	return self._rootDir

end

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

function WFCatFileSpec:catalogPath()
	
	return self._catalogPath

end

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

function WFCatFileSpec:catalogPathInfo()
	
	return self._catalogPathInfo

end

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

function WFCatFileSpec:docStorePath()
	
	return self._docStorePath

end

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

function WFCatFileSpec:pixelsPath()
	
	return self._pixelsPath
	
end

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

function WFCatFileSpec:originalsPathInfo()
	
	return self._originalsPathInfo
	
end

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

function WFCatFileSpec:proxiesPathInfo()
	
	return self._proxiesPathInfo
	
end

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

function WFCatFileSpec:deletionBacklogPath()
	
	return self._deletionBacklogPath
	
end

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

function WFCatFileSpec:externalSettingsPath()

	return self._externalSettingsPath 

end

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

function WFCatFileSpec:crUserPathInfo()

	return self._crUserPathInfo

end

--------------------------------------------------------------------------------
--[[!
	\brief Fetches the temporary folder path for a specific volume.
	
	Since we leverage the temp folder path as a staging area when we are
	downloading assets from Oz, it's important that the temp folder
	exists on the same volume as the final destination of the asset.

	\param volumeGuid (uuid) The GUID of the volume.

	\return (String, local file path) A file path to the temporary folder
	on the volume.
]]--

function WFCatFileSpec:tempFolderPathForVolume( volumeGuid )

	local tempFolder = KSPathUtils.getStandardFilePath( "temp" )
	
	if self._catalogPathInfo.volumeGuid ~= volumeGuid then
	
		if self._originalsPathInfo.volumeGuid == volumeGuid then
		
			local basePath = WFFileUtils.createFullPathFromRelativePathAndGuid(
					self._originalsPathInfo.relativePath, 
					self._originalsPathInfo.volumeGuid )

			if basePath then
				tempFolder = KSPathUtils.child( basePath, 'temp' )
			else
				logger:warnf( "Failed to build temp folder path %s", basePath )
			end
				
		elseif self._proxiesPathInfo.volumeGuid == volumeGuid then
		
			local basePath = WFFileUtils.createFullPathFromRelativePathAndGuid(
					self._proxiesPathInfo.relativePath, 
					self._proxiesPathInfo.volumeGuid )

			if basePath then
				tempFolder = KSPathUtils.child( basePath, 'temp' )
			else
				logger:warnf( "Failed to build temp folder path %s", basePath )
			end
		
		end
		
	end
	
	local success = pcall( WFFileUtils.mkdir, tempFolder )
	
	if not success then
		logger:warnf( "Failed to create temp folder at path %s", tempFolder )
	end
	
	return tempFolder

end

--------------------------------------------------------------------------------
--[[!
	\brief Calculate the extension-less file path for a preview.

	\param uuid (uuid) The asset id. It must be a 32-character lowercase
		hexadecimal string

	\param digest (md5 digest) The md5 digest as represented as a 32-character
		lowercase hexadecimal string

	\param longEdge (non-zero positive integer|"full") Must be an even value
		greater than 1 and less than 65535 or the string "full". The string full
		means the longEdge will be math.max( croppedWidth, croppedHeight ) for
		the dev settings matching the digest. If the image is not cropped, "full"
		means math.max( fullImageW, fullImageH )

	\param optCreateDirectory (optional boolean) If true (or omitted) then the
		directory for this preview will be created. Otherwise the directory
		is not created (useful when the caller is only trying to find existing
		files and is not preparing to write one).

	\return (String, local file path) A file path where the preview may be found
		or should be stored. The directory for this preview is guaranteed to
		exist.
]]--

function WFCatFileSpec:pathForPreviewDigestAndLongEdge( uuid, digest,
																  longEdge,
																  optCreateDirectory )

	check( 'WFCatFileSpec', 'uuid', 'uuid', '_', '?boolean' )

	return WFCatPixelPathGenerator.pathForPreviewDigestAndLongEdge( self._pixelsPath,
																uuid,
																digest,
																longEdge,
																optCreateDirectory )

end

--------------------------------------------------------------------------------
--[[!
	\brief Calculate the .jpg file path for a preview.

	\param uuid (uuid) The asset id. It must be a 32-character lowercase
		hexadecimal string

	\param digest (md5 digest) The md5 digest as represented as a 32-character
		lowercase hexadecimal string

	\param longEdge (non-zero positive integer|"full") Must be an even value
		greater than 1 and less than 65535 or the string "full". The string full
		means the longEdge will be math.max( croppedWidth, croppedHeight ) for
		the dev settings matching the digest. If the image is not cropped, "full"
		means math.max( fullImageW, fullImageH )

	\param optCreateDirectory (optional boolean) If true (or omitted) then the
		directory for this preview will be created. Otherwise the directory
		is not created (useful when the caller is only trying to find existing
		files and is not preparing to write one).

	\return (String, local file path) A file path where the preview may be found
		or should be stored. The directory for this preview is guaranteed to
		exist.
]]--

function WFCatFileSpec:pathForPreviewDigestAndLongEdgeJpg( uuid, digest,
																  longEdge,
																  optCreateDirectory )

	check( 'WFCatFileSpec', 'uuid', 'uuid', '_', '?boolean' )

	return WFCatPixelPathGenerator.pathForPreviewDigestAndLongEdgeJpg( self._pixelsPath,
																uuid,
																digest,
																longEdge,
																optCreateDirectory )

end

--------------------------------------------------------------------------------
--[[!
	\brief Extract preview info from a path to a preview file.

	\param previewPath (string) A local file path where the preview
		was found.

	\return (uuid) The asset id. It will be a 32-character lowercase
		hexadecimal string
	\return (md5 digest) The md5 digest as represented as a 32-character
		lowercase hexadecimal string
	\return (non-zero positive integer|"full") the long edge dimension associated with
		this preview or the string "full". "full" means the path represents a
		1:1 render at the current devSettings. In that case, longEdge =
		math.max( croppedWidth, croppedHeight ) for the dev settings in the digest.
		If the image is not cropped, "full" means
		math.max( fullImageW, fullImageH )
]]--

WFCatFileSpec.assetIdDigestAndLongEdgeFromPreviewPath = function( _, previewPath )

	check( 'WFCatFileSpec', '+string' )

	return WFCatPixelPathGenerator.assetIdDigestAndLongEdgeFromPreviewPath( previewPath )

end

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

function WFCatFileSpec:pathForOriginal( fileName, captureDate, useAlternatePathIfDriveOffline, skipCreatePath )

	check( 'WFCatFileSpec', '+string', 'KSDate', '?boolean', '?boolean' )

	local alternatePath

	if useAlternatePathIfDriveOffline then
		alternatePath = self._catalogPath
	end

	local volumeGuid = self._originalsPathInfo.volumeGuid
	local relativePath = self._originalsPathInfo.relativePath

	local basePath = WFFileUtils.createFullPathFromRelativePathAndGuid( relativePath, volumeGuid )

	if not basePath then
		basePath = alternatePath
	end

	return WFCatPixelPathGenerator.pathForOriginal( basePath, fileName, captureDate, alternatePath, skipCreatePath )

end

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

local function isVideoProxyType( proxyType )

	return proxyType == '1080p'
			or proxyType == '1080p709'
			or proxyType == '720p'
			or proxyType == '360p'

end

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

function WFCatFileSpec:pathForProxy( assetId, proxyType )

	check( 'WFCatFileSpec', 'uuid', 'string' )

	local volumeGuid = self._proxiesPathInfo.volumeGuid
	local relativePath = self._proxiesPathInfo.relativePath

	local basePath = WFFileUtils.createFullPathFromRelativePathAndGuid( relativePath, volumeGuid )

	if not basePath then
		basePath = self._catalogPath
	end
	
	if isVideoProxyType( proxyType ) then
		return WFCatPixelPathGenerator.pathForProxyTypeAndUuidWithExtension( basePath, proxyType, assetId )
	else
		return WFCatPixelPathGenerator.pathForProxyTypeAndUuid( basePath, proxyType, assetId )
	end

end

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

function WFCatFileSpec:folderForSettingsXmp()
	
	check( 'WFCatFileSpec' )
	
	return WFCatPixelPathGenerator.folderForSettingsXmp( self._pixelsPath )
	
end

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

function WFCatFileSpec:pathForSettingsXmp( sha256 )
	
	check( 'WFCatFileSpec', 'string' )
	
	return WFCatPixelPathGenerator.pathForSettingsXmp( self._pixelsPath, sha256 )
	
end


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

function WFCatFileSpec:pathForXmpSidecar( assetId )

	check( 'WFCatFileSpec', 'string' )

	return WFCatPixelPathGenerator.pathForXmpSidecar( self._pixelsPath, assetId )

end

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

function WFCatFileSpec:pathForStyle( assetId )

	check( 'WFCatFileSpec', 'string' )

	return WFCatPixelPathGenerator.pathForStyle( self._pixelsPath, assetId )

end

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

function WFCatFileSpec:pathForPerson( personId )

	check( 'WFCatFileSpec', 'string' )

	return WFCatPixelPathGenerator.pathForPerson( self._pixelsPath, personId )

end

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

function WFCatFileSpec:maybeStylePath( path )

	check( 'WFCatFileSpec', 'string' )

	return WFCatPixelPathGenerator.maybeStylePath( self._pixelsPath, path )
end

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

function WFCatFileSpec:pathForDCP( fileName )

	check( 'WFCatFileSpec', '+string', '?boolean' )

	return WFCatPixelPathGenerator.pathForDCP( self._pixelsPath, fileName )

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

function WFCatFileSpec:pathForStandardProfile( relativePath )

	check( 'WFCatFileSpec', '+string' )

	return WFCatPixelPathGenerator.pathForStandardProfile( self._pixelsPath, relativePath )

end
 
--============================================================================--

return Rx.Class 'WFCatFileSpec'( WFCatFileSpec )
