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

WFOzDocUtils.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2013 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.

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

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

	-- WF.core.utils
local WFArguments = require 'WFArguments'
local WFLogger = require 'WFLogger'
local KSTableUtils = require 'KSTableUtils'

	-- WF.oz_client.managed_catalog
local WFCatAssetAux = require 'WFCatAssetAux'
local WFCatFileSpec = require 'WFCatFileSpec'
local WFCatSharedWithYouUtils = require 'WFCatSharedWithYouUtils'
local WFCatUrlUtils = require 'WFCatUrlUtils'

local WFRxDocValueWatcher = require 'WFRxDocValueWatcher'
local WFRxThrottledBatchedBulkDocValue = require 'WFRxThrottledBatchedBulkDocValue'

local LrAssetUtils = require 'LrAssetUtils'
local isLatestMetadataUploaded = assert( LrAssetUtils.isLatestMetadataUploaded )

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

local check = assert( WFArguments.check )

local logger = WFLogger( 'WFOzDocUtils' )

local resolveLinksToAbsoluteUrls = assert( WFCatUrlUtils.resolveLinksToAbsoluteUrls )

local string_byte = assert( string.byte )

local underscoreByte = string_byte( '_' )

local KSTableUtils_derefOpt = assert( KSTableUtils.derefOpt )

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

local WFOzDocUtils = {}

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

function WFOzDocUtils.escapeUnderscoreKeyword( keyword )

	-- Ensure we don't build any keys that start with reserved
	-- character _ -- if a keyword starts with that, we replace
	-- it with the UTF "FULLWIDTH LOW LINE" U+FF3F (UTF-8 EF BC BF)

	if keyword:sub( 1, 1 ) == "_" then
		keyword = "\239\188\191" .. keyword:sub( 2 )
	end

	return keyword

end

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

local function stripPayloadLinkNoise( noisyTable )

	if noisyTable then
		noisyTable.links = nil
		return noisyTable
	end

end

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

function WFOzDocUtils.remapBasicOzDocument( ozDocument, baseUrl )

	-- Rearrange document to fit WF conventions instead of Oz conventions.
	-- This is a destructive transformation, but that's OK because the original
	-- document is no longer used after this call.

	-- Invert the relationship between payload and server-provided metadata.

	local result = ozDocument.payload or {}

	result._serverProvidedMetadata = ozDocument
	ozDocument.payload = nil

	result._id = ozDocument.id
	ozDocument.id = nil

	result.type = ozDocument.type
	ozDocument.type = nil

	result.subtype = ozDocument.subtype
	ozDocument.subtype = nil

	result.original = ozDocument.original
	ozDocument.original = nil

	if result.type == 'album_asset' then

		result.album = stripPayloadLinkNoise( ozDocument.album )
		ozDocument.album = nil

		result.asset = stripPayloadLinkNoise( ozDocument.asset )
		ozDocument.asset = nil

	end

	if result.type == 'comment' then

		result.space = stripPayloadLinkNoise( ozDocument.space )
		ozDocument.space = nil

		result.asset = stripPayloadLinkNoise( ozDocument.asset )
		ozDocument.asset = nil

	end

	if result.type == 'favorite' then

		result.space = stripPayloadLinkNoise( ozDocument.space )
		ozDocument.space = nil

		result.asset = stripPayloadLinkNoise( ozDocument.asset )
		ozDocument.asset = nil

	end

	if result.type == 'space_album' then

		result.space = stripPayloadLinkNoise( ozDocument.space )
		ozDocument.space = nil

		result.album = stripPayloadLinkNoise( ozDocument.album )
		ozDocument.album = nil

	end

	if result.type == 'stack_asset' then

		result.stack = stripPayloadLinkNoise( ozDocument.stack )
		ozDocument.stack = nil

		result.asset = stripPayloadLinkNoise( ozDocument.asset )
		ozDocument.asset = nil

	end

	if result.type == 'spaceinvite' then

		result.space = stripPayloadLinkNoise( ozDocument.space )
		ozDocument.space = nil

		result.user = stripPayloadLinkNoise( ozDocument.user )
		ozDocument.user = nil

		result.space_user = stripPayloadLinkNoise( ozDocument.space_user )
		ozDocument.space_user = nil

	end

	if result.type == 'space_user' then

		result.space = stripPayloadLinkNoise( ozDocument.space )
		ozDocument.space = nil

		result.user = stripPayloadLinkNoise( ozDocument.user )
		ozDocument.user = nil

	end

	if result.type == 'version' then

		result.asset = stripPayloadLinkNoise( ozDocument.asset )
		ozDocument.asset = nil

		result.importedOnDevice = stripPayloadLinkNoise( ozDocument.importedOnDevice )
		ozDocument.importedOnDevice = nil

	end

	local spm = result._serverProvidedMetadata

	-- Oz might send us relative links to the current base url of this request
	-- so we respect the current base url and create absolute url's in the format
	-- /v2/catalogs/123...

	if baseUrl and spm and spm.links then

		resolveLinksToAbsoluteUrls( baseUrl, spm.links )

	end

	-- error docs may have additional links in a separate section. if it's present
	-- convert these too
	if baseUrl and spm and spm.source and spm.source.links then

		resolveLinksToAbsoluteUrls( baseUrl, spm.source.links )

	end

	if baseUrl and spm and spm.matching_asset then
		if spm.matching_asset.href then
			spm.matching_asset.href = WFCatUrlUtils.resolveToAbsoluteUrl( baseUrl, spm.matching_asset.href )
		end
	end

	-- add the baseUrl to the server provided metadata so that we can fix up 
	-- any unresolved links at a future point, if necessary
	spm.base = baseUrl

	-- Docstore requires the revision id list to be non-empty,
	-- if present.
	result._revIds = ozDocument.revision_ids and #ozDocument.revision_ids > 0 
		and ozDocument.revision_ids
		or nil

	return result

end

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

function WFOzDocUtils.getRevisionId( docValue )

	local revIds = docValue.revIds

	if type( revIds ) ~= 'table' or #revIds == 0 then 
		return nil
	else
		return revIds[ #revIds ] 
	end

end

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

function WFOzDocUtils.doesDocumentNeedOriginalOrProxyUploaded( docValue )

	check( 'table' )

	local ozHasOriginal, isOriginalOnOzInvalid = WFCatFileSpec.getOriginalLinkFromDocValue( docValue )
	local ozHasProxy, _, isProxyOnOzInvalid = WFCatFileSpec.getProxyLinkFromDocValue( docValue )

	local isSpaceAsset = not not docValue._spaceId
	local isSpaceAssetWithImpliedMaster = isSpaceAsset and KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'exists' )

	local needsOriginalOrProxyUploaded = false

	if isSpaceAssetWithImpliedMaster then

		-- this indicates contributor workflow in which original binary is complete.

	elseif not ozHasOriginal and not ozHasProxy then

		-- The asset has no original and no proxy.

		needsOriginalOrProxyUploaded = true

	elseif ozHasOriginal and isOriginalOnOzInvalid then

		-- The asset has an original, but it's invalid.

		needsOriginalOrProxyUploaded = true

	elseif not ozHasOriginal and ozHasProxy and isProxyOnOzInvalid then

		-- The asset has no original, and a proxy that is invalid.

		needsOriginalOrProxyUploaded = true

	end

	return needsOriginalOrProxyUploaded

end

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

function WFOzDocUtils.isAssetDocFullySyncedWithOz( docValue )

	check( 'table' )

	local needsMetadataUploaded = not LrAssetUtils.isLatestMetadataUploaded( docValue )

	local needsOriginalOrProxyUploaded = WFOzDocUtils.doesDocumentNeedOriginalOrProxyUploaded( docValue )

	local needsXmpSidecarUploaded = WFOzDocUtils.doesDocumentNeedXmpSidecarUploaded( docValue )

	local needsXmpDevelopSettingsUploaded = WFOzDocUtils.doesDocumentNeedXmpDevelopSettingsUploaded( docValue )

	local onlyConsiderDevelopAndC2pa = true
	local needsAuxUploaded = WFCatAssetAux.uploadsRequired( docValue, onlyConsiderDevelopAndC2pa )

	local isFullySynced = true

	if needsMetadataUploaded or
		needsOriginalOrProxyUploaded or
		needsXmpSidecarUploaded or
		needsXmpDevelopSettingsUploaded or
		needsAuxUploaded then

		isFullySynced = false

	end

	return isFullySynced

end

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

--[[!
	\brief Checks the validity of a given docId.
	
	\param docId ( string ) A string containing a docId
										
	\return ( boolean ) Returns true if valid docId. Logs a warning and 
		returns false in case of an invalid docId 
		
--]]

function WFOzDocUtils.isValidDocId( docId )

	local stringDocId = tostring( docId )
	
	if docId:byte( 1 ) == underscoreByte then

		logger:warnf( "document ID must not start with _" )
		return false

	elseif not stringDocId:match( "^[0-9a-zA-Z_.%-]+$" ) then

		logger:warnf( "document ID must only contain letters, numbers, and _.-" )
		return false

	elseif string.len( stringDocId ) ~= 32 then

		logger:warnf( "docId should be 32-character hexadecimal and instead was " .. docId )
		return false

	else
		return true
	end

end

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

-- The following function resolves to `true` if all of the assets
-- are fully synced with Oz, and there are no pending local changes
-- that have yet to sync.

function WFOzDocUtils.areAssetsFullySyncedWithOz( wfCatalog, assetIds )

	check( 'WFCatalog', 'table' )

	return WFRxThrottledBatchedBulkDocValue( "WFOzDocUtils_areAssetsFullySyncedWithOz", wfCatalog:docStore(), assetIds, 100, 10 )

			:scan( function( docValues, batchOfDocValues )

				docValues = KSTableUtils.appendArray( docValues, batchOfDocValues )
				return docValues

			end, {} )

			:takeLast( 1 )

			:map( function( docValues )

				for _, docValue in ipairs( docValues ) do

					if not WFOzDocUtils.isAssetDocFullySyncedWithOz( docValue ) then

						return false

					end

				end

				return true

			end )

end

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

-- The following function resolves to `true` once all of the assets
-- are fully synced with Oz, and there are no pending local changes
-- that have yet to sync.

function WFOzDocUtils.waitUntilAssetsAreFullySyncedWithOz( wfCatalog, assetIds, debugMode )

	check( 'WFCatalog', 'table' )

	local isFullySyncedByAssetId = {}

	for _, assetId in ipairs( assetIds ) do
		isFullySyncedByAssetId[ assetId ] = false
	end

	local result = Observable.just( assetIds )

		:call( WFRxDocValueWatcher.make, wfCatalog:docStore(), "WfOzDocUtils" )

		:map( function( docValue )

			local isAssetFullySynced = WFOzDocUtils.isAssetDocFullySyncedWithOz( docValue )

			isFullySyncedByAssetId[ docValue._id ] = isAssetFullySynced

			if not isAssetFullySynced then
				return false --short circuit to avoid the following loop
			end

			-- Check to see if all the watched assets are fully synced.

			for _, isFullySynced in pairs( isFullySyncedByAssetId ) do

				if not isFullySynced then
					return false
				end

			end

			return true

		end )

	if not debugMode then

		result = result

			:filter( function( areAllAssetsFullySynced )
				return areAllAssetsFullySynced
			end )

			:take( 1 )

	end

	return result

end

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

function WFOzDocUtils.waitUntilAssetsAreFullySyncedWithOzAndHasRendition( wfCatalog, assetIds, debugMode )

	check( 'WFCatalog', 'table' )

	local isFullySyncedByAssetId = {}

	for _, assetId in ipairs( assetIds ) do
		isFullySyncedByAssetId[ assetId ] = false
	end

	local result = Observable.just( assetIds )

		:call( WFRxDocValueWatcher.make, wfCatalog:docStore() )

		:map( function( docValue )

			local isAssetFullySynced = WFOzDocUtils.isAssetDocFullySyncedWithOz( docValue )

			if not isAssetFullySynced then				
				return false
			end

			if not isLatestMetadataUploaded(docValue ) 
				or not WFCatFileSpec.checkOzForNonThumbnailRendition( docValue )
				or not WFOzDocUtils.isValidProxyAvailableOnOz( docValue ) then

				return false

			end

			isFullySyncedByAssetId[ docValue._id ] = isAssetFullySynced

			-- Check to see if all the watched assets are fully synced.

			for _, isFullySynced in pairs( isFullySyncedByAssetId ) do

				if not isFullySynced then
					return false
				end

			end

			return true

		end )

	if not debugMode then

		result = result

			:filter( function( areAllAssetsFullySynced )
				return areAllAssetsFullySynced
			end )

			:take( 1 )

	end

	return result

end

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

function WFOzDocUtils.clearNonPayloadKeys( docValue ) 

	for k in pairs( docValue ) do

		if k:match( '^_' ) or k == 'type' or k == 'subtype' then
			docValue[ k ] = nil
		end

	end

	return docValue

end

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

function WFOzDocUtils.isOriginalAvailableOnOz( docValue )

	local ozMasterUrl, _ = WFCatFileSpec.getOriginalLinkFromDocValue( docValue )

	-- A space asset may not have a master href visible so we need to instead look at a field 'exists'
	-- under /rels/master which will be set to true once the master is uploaded even if we don't have
	-- access to redownload it
	local isSpaceAsset = not not docValue._spaceId
	local isSpaceAssetWithImpliedMaster = isSpaceAsset and KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'exists' )

	return not not ( ozMasterUrl or isSpaceAssetWithImpliedMaster )

end

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

function WFOzDocUtils.isValidOriginalAvailableOnOz( docValue )

	local _, isOzOriginalInvalid = WFCatFileSpec.getOriginalLinkFromDocValue( docValue )

	return WFOzDocUtils.isOriginalAvailableOnOz( docValue ) and not isOzOriginalInvalid

end


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

function WFOzDocUtils.isProxyAvailableOnOz( docValue )

	local ozProxyUrl, _, _ = WFCatFileSpec.getProxyLinkFromDocValue( docValue )

	-- A space asset may not have a proxy href visible so we need to instead look at a field 'exists'
	-- under /rels/proxy_type/2560 which will be set to true if proxy is available on Oz
	local isSpaceAsset = not not docValue._spaceId
	local isSpaceAssetWithImpliedProxy = isSpaceAsset and KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/proxy_type/2560', 'exists' )

	return not not ( ozProxyUrl or isSpaceAssetWithImpliedProxy )

end

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

function WFOzDocUtils.isValidProxyAvailableOnOz( docValue )

	local _, _, isProxyInvalid = WFCatFileSpec.getProxyLinkFromDocValue( docValue )

	return WFOzDocUtils.isProxyAvailableOnOz( docValue ) and not isProxyInvalid
end

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

function WFOzDocUtils.doesDocumentNeedXmpSidecarUploaded( docValue )

	local xmpSidecarExists = WFCatFileSpec.getXmpSidecarExistsFromDocValue( docValue )
	local xmpLocalPath = WFCatFileSpec.getXmpSidecarPathFromDocValue( docValue )

	local sourceXmpLinkForCopy =  KSTableUtils_derefOpt( docValue, '_localOnly', 'sourceXmpLink' )

	local xmpSidecarNeedsUpload = KSTableUtils_derefOpt( docValue, '_localOnly', 'xmpSidecarNeedsUpload' )

	local existingXmpUrl = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp', 'href' ) or
		KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp', 'exists' )

	local needsXmpSidecarUploaded = 
			( ( xmpSidecarExists and xmpLocalPath and not existingXmpUrl ) or -- locally imported xmp sidecar
			( sourceXmpLinkForCopy and not existingXmpUrl ) or -- copy/share of existing asset with possible xmp sidecar
			( xmpSidecarNeedsUpload and existingXmpUrl ) ) -- legacy case where we upload after the xmp is extracted by Oz

	return not not needsXmpSidecarUploaded

end

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

--[[

	\brief return true if MAYBE this client should upload xmpDevelop binary
		to Oz, assuming it has a local develop binary with the correct sha256
		(those checks are made outside of this method). This method is a helper
		both to WFOzDocUtils.doesDocumentNeedXmpDevelopSettingsUploaded() (next
		method in this file) and also to WFCatBinaryUploader.maybeEmitXmpDevelopUploadRecord()
		where it is used in trash and copy asset workflows where it might
		be appropriate to wait for Oz to async copy the xmp develop (trash workflow)
		or to request that Oz copy it across assets (copy workflow).

		There are edge cases where we cannot be certain whether the local value
		supersedes the Oz value and in these cases we assume that the local value
		supersedes the Oz value to be safe.

		Note that if the document does NOT contain a /rels/xmp/develop link, then
		this method returns FALSE. There are many workflows where in the case that
		/rels/xmp/develop is missing, this means YES the local xmp develop should
		be uploaded, but this method is specifically to check if Oz has a value and
		it is possible that the local binary is the correct binary and the Oz binary
		might be out of date.

		One check in this method is whether the xmp develop link is invalid.

		Another check is for when atomic develop settings are enabled. Since
		the xmp_develop uploads before the metadata, we cannot rely on invalid
		with ADS. Instead the xmpDevelop sha256 from local settings is
		compared with the sha256 from the /rels/xmp/develop link. If the sha256
		is missing and latest metadata has not synced to Oz, then we must assume
		that upload may be necessary (see comment in the code for more). If
		the sha256 IS present in the link and does not match, then the answer
		is YES, otherwise NO.

		There are other preconditions to being able to upload xmp develop binary
		from this client, such as there is no local xmp develop binary, the asset
		is in a space and the user does not have the appropriate role to upload
		a binary to this asset. The ability to upload xmp develop binary is implied
		by things like having access to develop.xmpDevelop.sha256 value and having
		local xmp_develop file available locally and those checks are not made
		within this helper method so they should be made by the calling method.

	\param docValue (table) a document

	\return boolean

]]
function WFOzDocUtils.maybeLocalXmpDevelopSupersedesOzXmpDevelop( docValue )

	-- this first check is redundant with checks already made in
	-- doesDocumentNeedXmpDevelopSettingsUploaded() but since we want to
	-- make it safe to call this method independently, add a few redundant
	-- checks should not have a significant performance impact.

	local xmpCameraRawSha256 = KSTableUtils_derefOpt( docValue, 'develop', 'xmpCameraRaw', 'sha256' )
	local xmpDevelopLink = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop' )

	-- definitely nothing to upload if xmp is not externalized, and also
	-- we are checking for superseding local case, not no binary on Oz case,
	-- so return false if /rels/xmp/develop link is missing
	if not xmpCameraRawSha256 or not xmpDevelopLink then
		return false
	end

	if xmpDevelopLink.invalid then
		-- it is invalid, upload is required (though maybe not from this client)
		return true
	end

	-- for atomic develop settings, the metadata with the updated sha256 for the xmp_develop is accepted
	-- by Oz after the binary uploads, which means binary may need upload even though there is an
	-- xmp_develop link on Oz and it is not marked invalid. Check for case where local xmp_develop sha256
	-- does not match the value in the link and this should mean that xmp_develop upload is required and
	-- likely from this client.

	local linkSha256 = xmpDevelopLink.sha256

	-- the sha256 should always be provided with the link now that Oz supports atomic develop settings.
	-- If it is missing, this means the document has not synced an update from Oz for a long time. In
	-- this case if there is any local change that is not synced to Oz, while there may not be an
	-- unsynced develop settings sha256 change, we have to assume there is one. If there is not, then
	-- when the latest syncs back from Oz with a sha256 value in the /rels/xmp/develop link, this
	-- function will go back to returning false for this document.

	if not linkSha256 then
		return not LrAssetUtils.isLatestMetadataUploaded( docValue )
	end

	-- if the local xmpCameraRaw sha256 does not match the value in the link, then assume this is an
	-- atomic develop settings upload scenario

	if xmpCameraRawSha256 ~= linkSha256 then
		return true
	end

	-- no superseding upload required
	return false

end

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

--[[

	\brief return true if document needs xmp develop binary uploaded. Might not need
		to be uploaded from THIS client, but maybe from some client. Further checks are
		required outside of this method to decide wither THIS client should upload,
		or wait for a Oz async trash related copy, or request Oz to copy the binary
		from another asset.

		First checks if develop xmpCameraRaw is not a table, in which case NO.

		Second makes some version specific checks both for spaces and catalog
		versions. xmp_develop is only uploaded once to version so we get a definitive
		YES or NO for versions at this point.

		Third makes a space specific check for users who cannot edit the document,
		if either xmp develop link is missing or is marked invalid then YES,
		otherwise NO.

		If we get this far, the remaining checks are for whether the xmp_develop
		in the link may need to be updated because the current value Oz has is
		out of date. This logic is i the previous method,
		WFOzDocUtils.maybeLocalXmpDevelopSupersedesOzXmpDevelop() so look at
		comment on that method for more info.

	\param docValue (table) a document

	\return boolean

]]
function WFOzDocUtils.doesDocumentNeedXmpDevelopSettingsUploaded( docValue )

	local xmpCameraRaw = KSTableUtils_derefOpt( docValue, 'develop', 'xmpCameraRaw' )

	if not ( type( xmpCameraRaw ) == 'table' ) then
		-- if the xmpCameraRaw is not external then there's also nothing to upload
		return false
	end

	local xmpDevelopLink = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop' )

	if docValue.type == 'version' then

		local redacted = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'redacted' )

		return not redacted and not xmpDevelopLink
			-- there are SWY workflows where redacted version documents are synced and a client
			-- never uploads to a redacted version document. For non-redacted version documents,
			-- the logic is pretty simple as xmp_develop is only uploaded once to a version and
			-- is never marked invalid, so the only scenario where it needs to be uploaded is
			-- when Oz has no /rels/xmp/develop link

	elseif not xmpDevelopLink then

		return true
			-- Oz has no /rels/xmp/develop link at all, must have to upload

	end

	local isXmpDevelopInvalid = not not xmpDevelopLink.invalid

	-- Depending on the space settings, the develop settings may be redacted so Oz may not include the href 
	-- in /rels/xmp/develop for us to know that it has been uploaded. Instead we'll look for exists: true to
	-- infer that the xmp_develop has been uploaded even if we can't see the link

	local isAssetInSpace = not not docValue._spaceId

	if isAssetInSpace then

		local cannotEdit = not KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'editable' )
			-- atomic develop settings upload will not be possible if not able to push revisions
			-- so only consider if initial revision needs upoad and do not go into sha256 checks

		if cannotEdit then

			local impliedXmpDevelopUrl = xmpDevelopLink.exists

			return not impliedXmpDevelopUrl or isXmpDevelopInvalid

		end

	end

	-- now assuming either NOT in a space or in a space and current user is an editor, logic is the same

	-- this missing check is sort of weird and probably useless but leaving it just in case.
	-- if /rels/xmp/develop is there, either href should be present or value might be exists=true
	-- in some space cases, but we already checked for those cases AND we do not want to upload
	-- if exists=true is present. So probably this next check is useless, but leaving it just
	-- in case. jkamerer 2023.12.06

	if not xmpDevelopLink.href then
		-- it is missing, upload is required (though maybe not from this client)
		return true
	end

	return WFOzDocUtils.maybeLocalXmpDevelopSupersedesOzXmpDevelop( docValue )

end

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

return WFOzDocUtils
