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

WFCatAssetAux.lua

This is a bag of utility functions for domain logic specifically
involving auxiliary objects and metadata. See
http://apidocs.adbecrsl.com/v2/data-model/asset/aux.html for details.

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

ADOBE CONFIDENTIAL
------------------
Copyright 2022 Adobe
All Rights Reserved.

NOTICE: All information contained herein is, and remains
the property of Adobe and its suppliers, if any. The intellectual
and technical concepts contained herein are proprietary to Adobe
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.

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

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

	-- WF.core.rxlua
local WFRxRetryObservable = require 'WFRxRetryObservable'

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

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local KSTableUtils = require 'KSTableUtils'
local WFAnalytics = require 'WFAnalytics'
local WFArguments = require 'WFArguments'
local WFAsyncFileUtils = require 'WFAsyncFileUtils'
local WFCrypto = require 'WFCrypto'
local WFFileUtils = require 'WFFileUtils'
local WFLogger = require 'WFLogger'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFTableMerge = require 'WFTableMerge'
local WFCapabilities = require 'WFCapabilities'

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

	-- WF.web.json
local KSJsonEncoder = require "KSJsonEncoder"

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

local logger = WFLogger 'WFCatAssetAux'

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

local KSTableUtils_derefOpt = assert( KSTableUtils.derefOpt )
local KSTableUtils_setNested = assert( KSTableUtils.setNested )
local WFCatTrashUtils_getOriginalAssetAuxLink = assert( WFCatTrashUtils.getOriginalAssetAuxLink )

local check = assert( WFArguments.check )
local child = assert( KSPathUtils.child )

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

-- Resample logic for original instance boundary can be found here:
-- https://wiki.corp.adobe.com/display/lightroom/Downsampling+Eclipse+Filters

local kResampleBoundaryLongEdge = 3072
local kResampleBoundaryFileSize = 512 * 1024

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

local WFCatAssetAux = {}

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

local function pathFragmentForDigest( digest )

	local first3Characters = digest:sub( 1, 3 )
	local remainder = digest:sub( 4 )

	return child( first3Characters, remainder )

end

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

local WFCatAssetAux_externalAuxModule_auxExistenceChecker

local function getEternalAuxExistenceChecker()

	if not WFCatAssetAux_externalAuxModule_auxExistenceChecker then

		local externalAuxModule = WFCapabilities.getCapability( "WFCatAssetAux.externalAuxModule" )

		if externalAuxModule then

			local WFCatAssetAux_externalAuxModule = require( externalAuxModule )

			assert( type( WFCatAssetAux_externalAuxModule.auxExistenceChecker) == "function",
				"missing expected function WFCatAssetAux_externalAuxModule.auxExistenceChecker" )

			WFCatAssetAux_externalAuxModule_auxExistenceChecker = WFCatAssetAux_externalAuxModule.auxExistenceChecker

		end

	end

	return WFCatAssetAux_externalAuxModule_auxExistenceChecker

end

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

--[[
	\brief Finds an aux document by digest in an id => aux table

	\param auxList (table) - The curinvokeResampleAux
	t Auxiliary List of an asset document
	\param digest (string) - an ACR digest of an auxiliary

	\return id (string), aux (table) or nil, nil
--]]

function WFCatAssetAux.findAuxByDigest( auxList, digest )

	check( 'table', '+string' )

	return KSTableUtils.findInTable( auxList, function( _, auxDetails )

		return KSTableUtils.findInTable( auxDetails, function( k, v )

			return k == 'digest' and v == digest

		end)
	end )
end

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

local function maybeGetC2pa( docValue )

	local allAuxEntries = KSTableUtils_derefOpt( docValue, 'aux' ) or {}
	local c2paAuxInfo = {}

	local aux_id = KSTableUtils_derefOpt( docValue, 'importSource', 'c2paIngredients', 'aux_id' )

	if aux_id then
		c2paAuxInfo[ aux_id ] = allAuxEntries[ aux_id ]
	end

	return c2paAuxInfo

end

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

local function maybeGetAuxSection( docValue )

	return KSTableUtils_derefOpt( docValue, 'aux' )

end

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

local function maybeGetMasks( docValue )

	return KSTableUtils_derefOpt( docValue, 'develop', 'masks' )

end

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

local function maybeGetFilters( docValue )

	return KSTableUtils_derefOpt( docValue, 'develop', 'filters' )

end

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

--[[
	\brief Return a normalized filter info list, an id => filter info table

	\param docValue (table) - The docValue with develop.filters info

	\return (table) a filters info by auxId table
	{
		{
			digest = digest,
			groupDigest = groupDigest,
			sourceLongEdge = 6000,
			crsSizeX = 1024,
			crsSizeY = 768
		},
		{
			digest = digest,
			groupDigest = groupDigest,
			targetLongEdge = 2560,
			crsSizeX = 437,
			crsSizeY = 328
		},
	}
--]]

local function getFiltersInfoByAuxId( docValue )

	local developFilters = maybeGetFilters( docValue ) or {}
	local developFiltersInfoByAuxId = {}

	for groupDigest, groupInfo in pairs( developFilters ) do

		for auxId, digestInfo in pairs( groupInfo ) do
			local copiedDigestInfo = KSTableUtils.deepCopy( digestInfo )
			copiedDigestInfo.groupDigest = groupDigest
			developFiltersInfoByAuxId[ auxId ] = copiedDigestInfo
		end

	end

	return developFiltersInfoByAuxId

end

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

--[[

	\brief Retrieve the aux information of an auxiliary object given the sha256.

	\param docValue (table)
	\param sha256 (string)

	\return <code>table</code> The associated aux table. If there is no table,
		nil is returned.

]]--

function WFCatAssetAux.getAuxInfoBySha256( docValue, auxSha256 )

	check( 'table', '+string' )

	local auxEntries = maybeGetAuxSection( docValue ) or {}

	for _, auxInfo in pairs( auxEntries ) do

		if auxInfo.sha256 == auxSha256 then
			return auxInfo
		end

	end

end

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

local function getDevelopAuxNormalizedList( docValue )

	local developMasks = maybeGetMasks( docValue ) or {}
	local developFiltersInfoByAuxId = getFiltersInfoByAuxId( docValue )

	-- Use developFiltersInfoByAuxId as base table, because it's a copied new table,
	-- modify it won't affect docValue

	local result = WFTableMerge.applyOverlay( developFiltersInfoByAuxId, developMasks )

	return result

end

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

--[[!
	\brief Returns a normalized list of auxiliary information for a document.

	This function returns auxiliary information in a consistent format regardless of whether
	the document is an asset or version. For assets, it includes both the aux section and
	group digests from the develop.filters section. For versions, it returns the normalized
	develop aux list.

	\param docValue (table) The document to get aux info from. Can be either an asset or version document.

	\return (table) A table mapping auxiliary IDs to their information. Format:
		{
			[auxId] = {
				type = "mask"|"filter"|"c2pa",  -- Type of auxiliary (optional for version)
				fileSize = number,              -- Size of the auxiliary file in bytes (optional for version)
				digest = string,                -- Unique digest identifying the auxiliary
				sha256 = string,                -- SHA256 hash of the auxiliary file (optional for version)
				-- Filter only fields
				groupDigest = string,           -- (Optional) For develop filters only, the group digest this aux belongs to
				sourceLongEdge = number,        -- (Optional) The source long edge of the auxiliary
				targetLongEdge = number,        -- (Optional) The target long edge of the auxiliary
				crsSizeX = number,              -- (Optional) The crsSizeX of the auxiliary
				crsSizeY = number,              -- (Optional) The crsSizeY of the auxiliary
			},
			...
		}

	For version documents, the fields `type`, `fileSize`, and `sha256` may be absent.
]]--

function WFCatAssetAux.getAuxInfoList( docValue )

	check( 'table' )

	local auxInfoList = {}

	if docValue.type == 'asset' then

		auxInfoList = maybeGetAuxSection( docValue ) or {}

		-- Add group digests from develop section filters

		local filters = KSTableUtils_derefOpt( docValue, 'develop', 'filters' ) or {}

		for groupDigest, filterGroup in pairs( filters ) do

			for auxId, _ in pairs( filterGroup ) do

				if auxInfoList[ auxId ] then
					KSTableUtils_setNested( auxInfoList, groupDigest, auxId, 'groupDigest' )
				end

			end

		end

	elseif docValue.type == 'version' then

		auxInfoList = getDevelopAuxNormalizedList( docValue )

	end

	return auxInfoList

end

--------------------------------------------------------------------------------
--[[!
	\brief returns a boolean indicating whether a docValue has aux values

	\param docValue (table)

	\return (boolean) indicates whether the docValue has masks. Note: no checks
		are made if the aux values are local or available on Oz
--]]
--
function WFCatAssetAux.hasAuxValues( docValue )

	return next( WFCatAssetAux.getAuxInfoList( docValue ) ) ~= nil

end

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

--[[!
	\brief Gets the aux ID of an auxiliary object, given the digest.

	\param docValue (table)
	\param digest (string)

	\return (string) The aux ID.
--]]

function WFCatAssetAux.getAuxIdForDigest( docValue, digest )

	check( 'table', '+string' )

	local auxList = WFCatAssetAux.getAuxInfoList( docValue )

	local id = WFCatAssetAux.findAuxByDigest( auxList, digest )

	return id

end

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

--[[!
	\brief Gets the digest of an auxiliary object, given the aux ID.

	\param docValue (table)
	\param auxId (string)

	\return (string) The digest.
--]]

function WFCatAssetAux.getDigestForAuxId( docValue, auxId )

	check( 'table', '+string' )

	local auxList = WFCatAssetAux.getAuxInfoList( docValue )

	local digest

	if auxList[ auxId ] then
		digest = auxList[ auxId ].digest
	end

	return digest

end

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

--[[!
	\brief If digest belong to a aux in develop.filters, get the groupDigest.

	\param docValue (table)
	\param digest (string)

	\return (string) The groupDigest if exists.
--]]

function WFCatAssetAux.maybeGetGroupDigestForDigest( docValue, digest )
	
	check( 'table', '+string' )

	local maybeFilters = maybeGetFilters( docValue )

	if maybeFilters then

		local maybeGroupDigest = KSTableUtils.findInTable( maybeFilters, function( _, auxListInGroup )
			return KSTableUtils.findInTable( auxListInGroup, function( _, auxDetails )
				return KSTableUtils.findInTable( auxDetails, function( k, v )
					return k == 'digest' and v == digest
				end )
			end  )
		end )

		return maybeGroupDigest

	end

end

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

--[[!
	\brief Extracts the develop masks and filters digests from aux section of the document.

	\param docValue (table)

	\return (table) A table containing the digests:
		key (string): Auxiliary object UUID
		value (string): Auxiliary object digest
--]]

function WFCatAssetAux.extractDevelopAuxDigestsFromDocValue( docValue )

	check( 'table' )

	local digestsByAuxId = {}

	if docValue.type == 'asset' or docValue.type == 'version' then

		local developAuxNormalizedList = getDevelopAuxNormalizedList( docValue ) or {}

		for auxId, maskOrFilter in pairs( developAuxNormalizedList ) do
			digestsByAuxId[ auxId ] = maskOrFilter.digest
		end

	end

	return digestsByAuxId

end

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

--[[!
	\brief Extracts the develop mask digest and digest from longest edge from
		filter group from the aux section of the document.

	\param docValue (table)

	\return (table) A table containing the digests:
		key (string): Auxiliary object UUID
		value (string): Auxiliary object digest
--]]

function WFCatAssetAux.extractLongestDevelopAuxDigestsFromDocValue( docValue )

	check( 'table' )

	local digestsByAuxId = {}

	if docValue.type == 'asset' or docValue.type == 'version' then

		-- First include all the masks as they don't have groups
		local developMasks = maybeGetMasks( docValue ) or {}
		for auxId, mask in pairs( developMasks ) do
			digestsByAuxId[ auxId ] = mask.digest
		end

		-- Now process the filters

		local developFilters = maybeGetFilters( docValue ) or {}

		for groupDigest, groupInfo in pairs( developFilters ) do

			local longestAuxId, longestDigest

			local longestEdge = 0
			for auxId, digestInfo in pairs( groupInfo ) do

				local longEdge = digestInfo.sourceLongEdge or digestInfo.targetLongEdge or 0
				if longEdge > longestEdge then
					longestEdge = longEdge
					longestAuxId = auxId
					longestDigest = digestInfo.digest
				end

			end

			if longestAuxId then
				digestsByAuxId[ longestAuxId ] = longestDigest
			end

		end

	end

	return digestsByAuxId

end

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

function WFCatAssetAux.extractC2paAuxDigestFromDocValue( docValue )

	check( 'table' )

	local digestByAuxId = {}

	if docValue.type == 'asset' then

		local c2paInfo = maybeGetC2pa( docValue ) or {}

		for auxId, maybeC2pa in pairs( c2paInfo ) do

			digestByAuxId[ auxId ] = maybeC2pa.digest

		end

	end

	return digestByAuxId

end

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

--[[!

	\brief Creates a well formed overlay to add auxiliary documents to an asset.

	\param auxList (table) - The current auxillary list of the asset document
	\param auxDataListToApply (table) - Array of auxillary documents. Example entry:
		{
			{
				digest = digest,
			},
			{
				digest = digest,
				groupDigest = groupDigest,
				sourceLongEdge = 6000,
				crsSizeX = 1024,
				crsSizeY = 768
			},
			{
				digest = digest,
				groupDigest = groupDigest,
				targetLongEdge = 2560,
				crsSizeX = 437,
				crsSizeY = 328
			},
			...
		}
	\param auxFileInfoByDigest (table) - Array of aux file info by digest. Example entry:
		{
			[ digest ] = {
				relativeFilePath = relativeFilePath,
				fileSize = fileSize,
				sha256 = sha256
			},
			...
		}
	\param overrideOpts (table) Supports the following override parameters
		auxIDGenerator (function () -> string) - Function which assigns auxiliary ID for new aux document
		auxIdsByDigest (table) A map of auxiliary IDs by Digest.
			key: digest
			value: auxiliary ID

	\return <code>table</code> the aux overlay to apply to the original document,
			and a map of auxiliary IDs by Digest for auxListToApply

--]]

function WFCatAssetAux.createAuxOverlay( auxList, auxDataListToApply, auxFileInfoByDigest, overrideOpts )

	check( 'table', 'table', 'table',  '?table' )

	-- default options
	overrideOpts = overrideOpts or {}

	local auxIdsByDigest = overrideOpts.auxIdsByDigest or {}

	local opts = {

		-- This function is called in case of ID Collision (eg, editing an existing mask)
		auxIDGenerator = KSIdGenerator.uuidInLowercaseHex

	}

	-- override default options
	for k,v in pairs( overrideOpts ) do opts[ k ] = v end

	-- Loop through auxDataListToApply to get overlay info
	local overlay = {}
	local localOnlyOverlay = {}

	if next( auxDataListToApply ) then
		overlay.aux = {}
	end

	local latestAuxIdsByDigest = {}

	for _, auxData in ipairs( auxDataListToApply ) do

		local digest = auxData.digest

		-- Check if aux binary is present in the list already - unique by digest

		local auxId = WFCatAssetAux.findAuxByDigest( auxList, digest )
						or auxIdsByDigest[ digest ]
							-- auxIdsByDigest is the map created by getAuxIdByDigestAndExistingFiltersFromRelatedDocuments
						or opts.auxIDGenerator()

		local auxFileInfo = auxFileInfoByDigest[ digest ]

		local groupDigest = auxData.groupDigest
		local auxType = groupDigest and 'filter' or 'mask'

		overlay.aux[ auxId ] = {
			type = auxType,
			fileSize = auxFileInfo.fileSize,
			digest = digest,
			sha256 = auxFileInfo.sha256
		}

		latestAuxIdsByDigest[ digest ] = auxId

		if auxFileInfo.relativeFilePath then

			localOnlyOverlay = WFCatAssetAux.createAuxLocalOnlyOverlay(
					digest, auxFileInfo.relativeFilePath, localOnlyOverlay )

		end

	end

	WFTableMerge.applyOverlay( overlay, localOnlyOverlay )

	return overlay, latestAuxIdsByDigest

end

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

function WFCatAssetAux.addDevelopToOverlay( overlay, existingFilters, auxDataList, latestAuxIdsByDigest )

	overlay = overlay or {}
	existingFilters = existingFilters or {}

	local latestMasks = {}
	local latestFilters = {}

	-- Update develop masks and filters

	for _, auxData in ipairs( auxDataList ) do

		local digest = auxData.digest
		local groupDigest = auxData.groupDigest
		local auxId = latestAuxIdsByDigest[ digest ]

		if groupDigest then

			local filterGroup = latestFilters[ groupDigest ] or {}

			-- If the group already exists in develop.filters, preserve the multi-resolution data
			local existingGroup = existingFilters[ groupDigest ]

			-- If latestFilters has this group, it means the group data is already copied
			if not next( filterGroup ) and existingGroup then
				filterGroup = KSTableUtils.deepCopy( existingGroup )
			end

			-- If same digest aux already exists in the group, we should not add the filter info again
			local existingSameDigestAuxId = KSTableUtils.findInTable( filterGroup, function( _, auxDetails )
				return KSTableUtils.findInTable( auxDetails, function( k, v )
					return k == 'digest' and v == digest
				end )
			end )

			if not existingSameDigestAuxId then
				filterGroup[ auxId ] = {
					digest = digest,
					targetLongEdge = auxData.targetLongEdge,
					sourceLongEdge = auxData.sourceLongEdge,
					crsSizeX = auxData.crsSizeX,
					crsSizeY = auxData.crsSizeY
				}
			end

			latestFilters[ groupDigest ] = filterGroup

		else

			latestMasks[ auxId ] = {
				digest = digest
			}

		end

	end

	if next( latestMasks ) then

		-- always replace this section instead of appending
		latestMasks[ '_replace' ] = true

		KSTableUtils_setNested( overlay, latestMasks, 'develop', 'masks' )

	else
		KSTableUtils_setNested( overlay, { _delete = true }, 'develop', 'masks' )
	end

	if next( latestFilters ) then

		-- always replace this section instead of appending
		latestFilters[ '_replace' ] = true

		KSTableUtils_setNested( overlay, latestFilters, 'develop', 'filters' )

	else
		KSTableUtils_setNested( overlay, { _delete = true }, 'develop', 'filters' )
	end

	return overlay

end

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

--[[
	\brief Merge a base overlay with an aux overlay.

	\param baseOverlay (table)
	\param auxOverlay (table)

	\return (table) The merged overlay.
]]--

function WFCatAssetAux.mergeAuxOverlay( baseOverlay, auxOverlay )

	-- Remove the aux, develop.masks & develop.filters sections from the existing overlay.
	-- They are likely not present, but just to be safe we nil them out.

	KSTableUtils.setNested( baseOverlay, nil, 'aux' )
	KSTableUtils.setNested( baseOverlay, nil, 'develop', 'masks' )
	KSTableUtils.setNested( baseOverlay, nil, 'develop', 'filters' )

	-- Now we merge the auxOverlay with the docValue overlay.
	-- It's critical that we use KSTableUtils.merge for this operation
	-- stead of WFTableMerge, since auxOverlay has magic keys
	-- (_replace and _delete) that we do not want interpreted at this time.

	local mergedOverlay = KSTableUtils.merge( {}, baseOverlay, auxOverlay )

	return mergedOverlay

end

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

--[[

	\brief creates an overlay to register a downloaded aux binary

	\return overlay (table) to be applied to the base document

--]]

function WFCatAssetAux.createAuxLocalOnlyOverlay( digest, relativeFilePath, maybeOverlay )

	check( '+string', '+string', '?table' )

	local overlay = maybeOverlay or {}

	KSTableUtils_setNested( overlay, relativeFilePath, '_localOnly', 'files', 'aux', digest, 'relativePath' )

	return overlay

end

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

--[[

	\brief creates an overlay to register a resampled aux binary. It would contain resample aux info.

	\return overlay (table) to be applied to the base document

--]]

local function createAuxLocalOnlyOverlayForResampledAux( digest, relativeFilePath, resampledAuxInfo, maybeOverlay )

	check( '+string', '+string', 'table', '?table' )

	local overlay = maybeOverlay or {}

	local auxFileInfoOverlay = KSTableUtils.deepCopy( resampledAuxInfo )
	auxFileInfoOverlay.relativePath = relativeFilePath

	KSTableUtils_setNested( overlay, auxFileInfoOverlay, '_localOnly', 'files', 'aux', digest )

	return overlay

end

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

--[[
	\brief Get the file size of a specified auxiliary.

	\param docValue (table)
 	\param digest (string) The digest of the auxiliary object.

	\return (number) Returns the size of the auxiliary object.
--]]

function WFCatAssetAux.getFileSize( docValue, digest )

	check( 'table', '+string' )

	local fileSize = 0

	local auxiliaries = maybeGetAuxSection( docValue ) or {}

	for _, auxInfo in pairs( auxiliaries ) do

		if auxInfo.digest == digest then
			fileSize = auxInfo.fileSize or 0
		end

	end

	return fileSize

end

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

function WFCatAssetAux.hasSomeLocalAuxFiles( docValue )

	local localOnlyAuxTable =  KSTableUtils_derefOpt( docValue, '_localOnly', 'files', 'aux' )

	return not not ( localOnlyAuxTable and next( localOnlyAuxTable ) )

end

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

function WFCatAssetAux.getAuxLocalOnlyFilePath( docValue, digest )

	return KSTableUtils_derefOpt( docValue, '_localOnly', 'files', 'aux', digest, 'relativePath' )

end

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

-- @mark Logic Functions

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

--[[
	\brief Get an OZ download link from an asset for a given digest

	\return url (string) or nil

--]]

function WFCatAssetAux.getDownloadLink( docValue, digest )

	check( 'table', '+string' )

	local auxList = maybeGetAuxSection( docValue ) or {}

	local auxId = WFCatAssetAux.findAuxByDigest( auxList, digest )

	if auxId then

		local auxLinkPath = string.format( '/rels/aux/%s', auxId )

		local maybeAux = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', auxLinkPath )
				or WFCatTrashUtils_getOriginalAssetAuxLink( docValue, auxId ) or {}

		local auxLink = KSTableUtils_derefOpt( maybeAux, 'href' )

		return auxLink

	end

end

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

--[[
	\brief Retrieve the Copy Aux Files API link for a specified document:
		http://apidocs.adbecrsl.com/v2/docs/multi_page/#method_copyauxfiles

	\return url (string) or nil
--]]

function WFCatAssetAux.getCopyUrl( docValue )

	check( 'table' )

	return KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/aux_copy_to', 'href' )

end

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

--[[
	\brief Retrieve the Aux DU templated link from the document document:
		http://apidocs.adbecrsl.com/v2/docs/multi_page/#method_getdirecturlstopartsofaux

	\return url (string) or nil
--]]

function WFCatAssetAux.getAuxPartsRequestUrlTemplate( docValue )

	check( 'table' )

	return KSTableUtils_derefOpt( docValue, "_serverProvidedMetadata", "links", "/rels/aux_create/parts", "href" )

end

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

--[[!
	\brief Creates an analytics event if the number of auxiliaries (count) exceeds
		the threshold value.

	\param count (number) The number of auxiliaries.
	\param appWorkflow (string) A workflow identifer string.

	\return (void)
--]]

function WFCatAssetAux.maybeTrackAuxAnalytics( count, appWorkflow )

	check( 'number', '+string' )

	local kAuxCountThreshold = 100 -- This number may need to be adjusted.

	if count > kAuxCountThreshold then

		WFAnalytics.trackEvent {
			['event.type'] = 'warning',
			['event.appWorkflow'] = appWorkflow,
			['event.subtype'] = 'many-aux-warning',
			['event.count'] = count,
		}

	end

end

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

--[[!
	\brief Calculates the absoluate path of a mask given the mask digest and the
		base binaries path (catalog path).

	\param catalogPath (string) The base binaries path. For example on Mac the
		path is: `~/Pictures/Lightroom Library.lrlibrary/<catalog id>/

 	\param digest (string) The fingerprint of the auxiliary object.

	\return (string) The path to the mask. Note that a path will be returned
		regardless of if a mask exists on disk.
--]]

function WFCatAssetAux.calculatePathUsingDigest( basePath, digest )

	check( 'string', '+string' )

	local relativePath = 'auxiliary'

	local pathFragment = pathFragmentForDigest( digest )

	relativePath = child( relativePath, pathFragment )

	local absolutePath = KSPathUtils.child( basePath, relativePath )

	return absolutePath, relativePath

end

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

--[[!
	\brief Determines if an auxiliary object has been uploaded to Oz.

	\param docValue (table)
 	\param auxId (string) The ID of the auxiliary object.

	\return (boolean) Returns true if the auxiliary object has been uploaded to Oz.
--]]

function WFCatAssetAux.isAuxObjectOnOz( docValue, auxId )

	check( 'table', '+string' )

	local auxRels = string.format( "/rels/aux/%s", auxId )

	local auxLinks = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', auxRels ) or {}

	local auxLinkHref = KSTableUtils_derefOpt( auxLinks, 'href' )

	local invalid = KSTableUtils_derefOpt( auxLinks, 'invalid' )
		-- This should be impossible; aux objects can never be invalid since they
		-- are immutable once created.

	local isAssetInSpace = not not docValue._spaceId

	local impliedAuxUrl = isAssetInSpace
			and not WFCatSharedWithYouUtils.canEditAsset( docValue )
			and KSTableUtils_derefOpt( auxLinks, 'exists' )

	local existsOnOz = not not ( auxLinkHref or impliedAuxUrl )

	return existsOnOz, invalid

end

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

--[[

	\brief Retrieve the Oz link to the source asset which contains the auxiliary
		binary. This is used in COPY workflows.

	\param docValue (table)

	\returns (string) The link, else nil.

--]]

function WFCatAssetAux.getSourceAssetLinkForAuxFromDocValue( docValue )

	check( 'table' )

	-- This function works in tandem with WFCatFileSpec.getSelfLinkFromDocValue,
	-- which is used to create the data stored within _localOnly->sourceAssetLinkForAux.

	return KSTableUtils_derefOpt( docValue, '_localOnly', 'sourceAssetLinkForAux' )

end

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

--[[

	\brief Retrieve the Oz link to the source auxiliary which this auxiliary was
			copied from. This is used in COPY workflows.

	\param docValue (table)
	\param digest (string) aux digest

	\returns (boolean)

--]]

function WFCatAssetAux.isSourceAuxUploadedToOz( docValue, digest )

	check( 'table' )

	-- This function works in tandem with WFCatFileSpec.getTableOfAuxLinksFromDocValue,
	-- which is used to create the data stored within _localOnly->sourceAuxLinks.

	return not not KSTableUtils_derefOpt( docValue, '_localOnly', 'sourceAuxLinks', digest )

end

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

--[[

	\brief Determine if a document has auxiliaries which need to be uploaded.
		Uploads are required when a link for an aux is not detected in the server
		provided metadata by id: 'rels/aux/{auxId}'

	\param docValue (table)
	\param onlyConsiderDevelopAndC2pa (boolean) When true, only aux entries within
		the develop.masks, develop.filters section of the document and c2pa aux are
		considered. When false or nil, all entries within the aux section of
		the document are considered.

	\returns {bool, table} If the bool is true, the table contains tables
		corresponding to the payload.aux format.

--]]

function WFCatAssetAux.uploadsRequired( docValue, onlyConsiderDevelopAndC2pa )

	check( 'table', '?boolean' )

	local auxMetadata

	if onlyConsiderDevelopAndC2pa then

		auxMetadata = getDevelopAuxNormalizedList( docValue ) or {}
		if WFPlatformInfo.isClassic() then
			local maybeC2paInfo = maybeGetC2pa( docValue )
			auxMetadata = WFTableMerge.applyOverlay( maybeC2paInfo, auxMetadata )
		end
	else
		auxMetadata = maybeGetAuxSection( docValue ) or {}
	end

	local uploadsRequired = false
	local auxObjectsToUpload = {}

	for auxId, auxDetails in pairs( auxMetadata ) do

		local existsOnOz, auxLinkInvalid = WFCatAssetAux.isAuxObjectOnOz( docValue, auxId )

		if existsOnOz and not auxLinkInvalid then

			-- it's not missing or invalid, there's nothing to upload

		else

			-- there's external settings specified and no link (or the link is invalid) so Oz does need an update
			-- (though maybe not from this client)

			uploadsRequired = true
			auxObjectsToUpload[ auxId ] = auxDetails

		end

	end

	return uploadsRequired, auxObjectsToUpload

end

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

local function getAdditionalAuxFileInfo( basePath, auxDataList, maybeDocValue )

	return Observable.from( auxDataList )

		:flatMap( function( auxData )

			local digest = auxData.digest

			if not digest then
				return Observable.throw( "Must provide digest" )
			end

			local filePath, relativeFilePath = WFCatAssetAux.calculatePathUsingDigest( basePath, digest )

			local auxList = maybeDocValue and WFCatAssetAux.getAuxInfoList( maybeDocValue )
			local auxId = auxList and WFCatAssetAux.findAuxByDigest( auxList, digest )
			local auxInfo = auxId and auxList[ auxId ]

			local auxFileExists = WFFileUtils.exists( filePath ) == 'file'

			if type( auxInfo ) == 'table' -- This auxiliary already exists in the docValue
					and auxInfo.fileSize
					and auxInfo.sha256 then

				if not auxFileExists then

					WFAnalytics.trackEvent {
						['event.type'] = 'warning',
						['event.appWorkflow'] = 'develop',
						['event.subtype'] = 'missing-aux-during-develop-session'
					}

					logger:warnf( "Aux %s does not exist at expected path %s, but this is recoverable because the aux annotation already exists.", digest, filePath )

					-- Don't store relativeFilePath in _localOnly if not exists
					relativeFilePath = nil

				end

				return Observable.just( digest, relativeFilePath, auxInfo.fileSize, auxInfo.sha256 )

			elseif auxFileExists then

				local fileAttributes = WFFileUtils.fileAttributes( filePath )

				local fileSize = 0

				if fileAttributes then

					fileSize = fileAttributes.fileSize

				end

				return WFCrypto.asyncDigestFile( filePath, 'SHA256' )

					:map( function( fileSha256 )

						return digest, relativeFilePath, fileSize, fileSha256

					end )

			else

				return Observable.throw( "AuxNotFound", digest, filePath )

			end

		end )

		:scan( function( auxFileInfoByDigest, digest, relativeFilePath, fileSize, sha256 )

			auxFileInfoByDigest[ digest ] = {
				relativeFilePath = relativeFilePath,
				fileSize = fileSize,
				sha256 = sha256
			}

			return auxFileInfoByDigest

		end, {} )

		:takeLast( 1 )

end

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

--[[!
	\brief Returns the base path in which the auxiliary folder resides.

	\param catalogId (string)

	\return (string) The base path for auxiliary objects.
					If unit test has temp test folder set in wfCatalog,
					return test folder as basePath for multithread test environment
--]]

function WFCatAssetAux.getBasePath( wfCatalog )

	check( 'WFCatalog' )

	if WFPlatformInfo.isMobile() then

		local catalogId = wfCatalog:ozCatalogId()

		local documentsDir = KSPathUtils.getStandardFilePath 'carouselDocuments'
		return KSPathUtils.child( documentsDir, catalogId )

	else

		return wfCatalog:fileSpec():catalogPath()

	end

end

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

--[[!
	\brief Calculates the absoluate path of a mask given the mask digest and the
		base binaries path (catalog path).

	\param wfCatalog (WFCatalog)

	\param relativePath (string) relative path of the aux binary

	\return (string) The path to the mask. Note that a path will be returned
		regardless of if a mask exists on disk.
--]]

function WFCatAssetAux.getAbsolutePath( wfCatalog, relativePath )

	check( 'WFCatalog', '+string' )

	-- cannot call WFFileUtils.isAbsolutePath on mobile platforms and on mac same simple
	-- check that works for mobile platforms will also work, so to keep things simple
	-- only call isAbsolutePath on windows, where it is necessary.
	if ( WFPlatformInfo.isWindows and WFFileUtils.isAbsolutePath(relativePath) )
		or ( not WFPlatformInfo.isWindows and relativePath:sub(1,1) == '/' ) then
		return relativePath
	end

	local basePath = WFCatAssetAux.getBasePath( wfCatalog )

	local absolutePath = KSPathUtils.child( basePath, relativePath )

	return absolutePath

end

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

--[[
	\brief Reports whether a given auxiliary ID exists is accessible, meaning it
		either exists on Oz or exists on the device.

	\paramn wfCatalog (WFCatalog)
	\param docValue (table)
	\param auxId (string)

	\return <code>boolean</code>
]]--

function WFCatAssetAux.isAuxiliaryAccessible( wfCatalog, docValue, auxId )

	check( 'WFCatalog', 'table', '+string' )

	local ozHasAuxObject = WFCatAssetAux.isAuxObjectOnOz( docValue, auxId )

	local digest = WFCatAssetAux.getDigestForAuxId( docValue, auxId )

	local auxLocalFilePath = not WFCatAssetAux.hasLocalAuxError( docValue, auxId )
								and WFCatAssetAux.getAuxLocalOnlyFilePath( docValue, digest )

	local absolutePath = auxLocalFilePath and WFCatAssetAux.getAbsolutePath( wfCatalog, auxLocalFilePath )

	local isAccessible = ozHasAuxObject or ( absolutePath and WFFileUtils.exists( absolutePath ) )

	return not not isAccessible

end

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

local function addDeleteLocalAuxErrorAndResampledInfoToOverlay( overlay, auxDataList )

	for _, auxData in ipairs( auxDataList ) do

		local digest = auxData.digest
		KSTableUtils_setNested( overlay, { _delete = true }, '_localOnly', 'files', 'aux', digest, 'error' )

		-- Only need to remove possible resampled auxInfo for proxy filter
		if auxData.groupDigest and auxData.targetLongEdge then
			KSTableUtils_setNested( overlay, { _delete = true }, '_localOnly', 'files', 'aux', digest, 'groupDigest' )
			KSTableUtils_setNested( overlay, { _delete = true }, '_localOnly', 'files', 'aux', digest, 'crsSizeX' )
			KSTableUtils_setNested( overlay, { _delete = true }, '_localOnly', 'files', 'aux', digest, 'crsSizeY' )
			KSTableUtils_setNested( overlay, { _delete = true }, '_localOnly', 'files', 'aux', digest, 'fileSize' )
			KSTableUtils_setNested( overlay, { _delete = true }, '_localOnly', 'files', 'aux', digest, 'targetLongEdge' )
		end
	end

end

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

local function getAuxIdByDigestAndExistingFiltersFromRelatedDocuments( wfCatalog, docId, optParentAssetId, optParentDocValue )

	-- Note: optParentAssetId/optParentDocValue are only set when docId corresponds
	-- to a Version document.

	-- The following code builds up a map of all existing auxIds/digests that
	-- are related to an Asset or Version document. This map is then used downstream
	-- when building the aux overlay, and prevents us from generating new aux Ids
	-- for auxiliaries that already have known Ids.

	-- First we fetch all related Version documents. If docId corresponds to an Asset
	-- then these are the child Versions. If docId corresponds to a Version, then
	-- these are the sibling Versions.

	local assetId = optParentAssetId
			or ( optParentDocValue and optParentDocValue._id )
			or docId

	if not assetId then
		return Observable.just( {} )
	end

	-- Helper functions

	local function addAuxIdByDigest( auxIdByDigestMap, auxList )

		for auxId, auxInfo in pairs( auxList ) do

			local digest = auxInfo.digest

			auxIdByDigestMap[ digest ] = auxId

		end

		return auxIdByDigestMap

	end

	local function addFilters( existingFilters, filtersSection )

		for groupDigest, groupFilterList in pairs( filtersSection ) do

			local existingFilterGroup = existingFilters[ groupDigest ]

			if not existingFilterGroup then
				existingFilters[ groupDigest ] = KSTableUtils.deepCopy( groupFilterList )
			else

				-- If there is any filter aux not included, we need to add to filter group

				local existingDigestsInGroup = {}
				for _, auxInfo in pairs( existingFilterGroup ) do
					existingDigestsInGroup[ auxInfo.digest ] = true
				end

				for auxId, auxInfo in pairs( groupFilterList ) do
					if not existingFilterGroup[ auxId ] and not existingDigestsInGroup[ auxInfo.digest ] then
						existingFilterGroup[ auxId ] = KSTableUtils.deepCopy( auxInfo )
					end
				end
			end
		end

		return existingFilters

	end

	-- get all related versions doscument and asset document

	return wfCatalog:indexStore():queryOnce(
		{
			sql = [[ SELECT versions.docId
						FROM versions
						WHERE assetId = ? ]],
			singleColumnQuery = true,
			quiesceUntilUpToDate = true,
			assetId
		} )

		:flatMap( function( versionIds )

			if #versionIds == 0 then
				return Observable.just( {} )
			end

			return wfCatalog:docStore():bulkDocValue( versionIds )

		end )

		:map( function( versionDocValues )

			local auxIdByDigestMap = {}
			local existingFilters = {}

			for _, versionDocValue in ipairs( versionDocValues ) do

				-- Add all digest/auxId pairs from the related Versions to the map.

				local developAuxNormalizedList = getDevelopAuxNormalizedList( versionDocValue ) or {}
				auxIdByDigestMap = addAuxIdByDigest( auxIdByDigestMap, developAuxNormalizedList )

				-- Add all filter groups from related Versions

				local filtersSection = maybeGetFilters( versionDocValue ) or {}
				existingFilters = addFilters( existingFilters, filtersSection )

			end

			return auxIdByDigestMap, existingFilters

		end )

		:flatMap( function( auxIdByDigestMap, existingFilters )

			-- When docId corresponds to a Version or an Asset, extract all the digest/auxId
			-- pairs from the parent Asset document and add those to the map.

			local assetDocValueStream = wfCatalog:docStore():docValue( assetId )

			if optParentDocValue then

				assetDocValueStream = Observable.just( optParentDocValue )

			elseif optParentAssetId then

				assetDocValueStream = wfCatalog:docStore():docValue( optParentAssetId )

			end

			return assetDocValueStream

				:map( function( assetDocValue )

					-- Add to auxIdByDigestMap
					local auxSection = maybeGetAuxSection( assetDocValue ) or {}
					auxIdByDigestMap = addAuxIdByDigest( auxIdByDigestMap, auxSection )

					-- Add to existingFilters

					local filtersSection = maybeGetFilters( assetDocValue ) or {}
					existingFilters = addFilters( existingFilters, filtersSection )

					return auxIdByDigestMap, existingFilters

				end )

		end )

end

WFCatAssetAux._TEST_getAuxIdByDigestAndExistingFiltersFromRelatedDocuments = getAuxIdByDigestAndExistingFiltersFromRelatedDocuments -- Exposed For Testing

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

local function generateAuxOverlayForAuxFileInfoDigest( auxFileInfoByDigest, docValue, auxDataList, overrideArgs )

	check( 'table', '?table', 'table', '?table' )

	docValue = docValue or {}

	local existingAuxSection = maybeGetAuxSection( docValue ) or {}
	local overlay, latestAuxIdsByDigest = WFCatAssetAux.createAuxOverlay( existingAuxSection, auxDataList, auxFileInfoByDigest, overrideArgs )

	local maybeParentDocValue = overrideArgs and overrideArgs.parentDocValue
	local existingFilters = overrideArgs and overrideArgs.existingFilters
								or ( maybeParentDocValue and maybeGetFilters( maybeParentDocValue ) )
								or maybeGetFilters( docValue )
								or {}

	WFCatAssetAux.addDevelopToOverlay( overlay, existingFilters, auxDataList, latestAuxIdsByDigest )

	-- The applyOverlay above will remove any _delete = true directives
	-- so apply them afterwards so that we will clear out the error in
	-- the subsequent applyOverlay call that is using this overlay.
	-- Any digest that was passed in the auxDigests table should have
	-- been re-created so we should try uploading it again.
	addDeleteLocalAuxErrorAndResampledInfoToOverlay( overlay, auxDataList )

	return overlay

end

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

-- Create c2pa overlay(aux section) and apply to original document
local function createC2paOverlay( auxFileInfoByDigest, c2paInfo, docValue )

	check( 'table', 'table', '?table' )

	local overlay = {}
	local localOnlyOverlay = {}
	overlay.aux = {}

	docValue = docValue or {}
	local digest = KSTableUtils_derefOpt( c2paInfo, "digest" )

	local auxId = WFCatAssetAux.getAuxIdForDigest( docValue, digest ) or KSIdGenerator.uuidInLowercaseHex()

	local auxFileInfo = auxFileInfoByDigest[ digest ]

	overlay.aux[ auxId ] = {
		type = "c2pa",
		fileSize = auxFileInfo.fileSize,
		digest = digest,
		sha256 = auxFileInfo.sha256,
	}

	localOnlyOverlay = WFCatAssetAux.createAuxLocalOnlyOverlay( digest, auxFileInfo.relativeFilePath, localOnlyOverlay )

	WFTableMerge.applyOverlay( overlay, localOnlyOverlay )

	return overlay, auxId

end

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

-- Create importSource.c2paIngredients section and apply to original document
local function addC2paImportSourceToOverlay( overlay, c2paInfo, auxId )

	overlay = overlay or {}
	local version = KSTableUtils_derefOpt( c2paInfo, "version" )
	local state = KSTableUtils_derefOpt( c2paInfo, "state" )

	local c2pa = {
		_replace = true,
		aux_id = auxId,
		version = version,
		state = state,
	}

	KSTableUtils_setNested( overlay, c2pa, 'importSource', 'c2paIngredients' )

	return overlay

end

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

local function generateC2paOverlayForAuxFileInfoDigest( auxFileInfoByDigest, c2paInfo, docValue )

	check( 'table', 'table', '?table' )

	docValue = docValue or {}

	local overlay, auxId = createC2paOverlay( auxFileInfoByDigest, c2paInfo, docValue )

	overlay = addC2paImportSourceToOverlay( overlay, c2paInfo, auxId )

	return overlay

end

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

--[[

	\brief Given a table of c2paInfo, creates an overlay.

	\param wfCatalog (WFCatalog)
	\param c2paInfo (table) A table containing c2pa object information. Format:
		{
			digest = digest,
			state = state,
			version = version,
		}
	\param docValue (table) The docValue(optional)

	\return <code>Observable</code> Fires with an overlay (table) to be applied to the
		base document.

]]--

function WFCatAssetAux.getC2paOverlay( wfCatalog, c2paInfo, docValue )

	check( 'WFCatalog', 'table', '?table' )

	local basePath = WFCatAssetAux.getBasePath( wfCatalog )

	return getAdditionalAuxFileInfo( basePath, { c2paInfo } )

		:map( function( auxFileInfoByDigest )

			return generateC2paOverlayForAuxFileInfoDigest( auxFileInfoByDigest, c2paInfo, docValue )

		end )

end

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

--[[

	\brief Given a table of aux digests, creates an overlay.

	\param wfCatalog (WFCatalog)
	\param auxDataList (table) A table containing auxiliary object information. Format:
		{
			{
				digest = digest,
			},
			{
				digest = digest,
				groupDigest = groupDigest,
				sourceLongEdge = 6000,
				crsSizeX = 1024,
				crsSizeY = 768
			},
			{
				digest = digest,
				groupDigest = groupDigest,
				targetLongEdge = 2560,
				crsSizeX = 437,
				crsSizeY = 328
			},
			...
		}

	\return <code>Observable</code> Fires with an overlay (table) to be applied to the
		base document. If one of the specified auxiliaries does not exist on disk,
		this function will return an error and the doc value will not be modified.

]]--

function WFCatAssetAux.getAuxOverlayForAuxInfo( wfCatalog, auxDataList )

	check( 'WFCatalog', 'table' )

	local basePath = WFCatAssetAux.getBasePath( wfCatalog )

	return getAdditionalAuxFileInfo( basePath, auxDataList )

		:map( function( auxFileInfoByDigest )

			return generateAuxOverlayForAuxFileInfoDigest( auxFileInfoByDigest, nil, auxDataList )

		end )

end

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

--[[

	\brief Given a document Id and table of aux digests, creates an overlay
		add the specified aux objects to an asset document.

	\param wfCatalog (WFCatalog)
	\param docId (string|int) The document Id
	\param auxDataList (table) A table containing auxiliary object information. Format:
		{
			{
				digest = digest,
			},
			{
				digest = digest,
				groupDigest = groupDigest,
				sourceLongEdge = 6000,
				crsSizeX = 1024,
				crsSizeY = 768
			},
			{
				digest = digest,
				groupDigest = groupDigest,
				targetLongEdge = 2560,
				crsSizeX = 437,
				crsSizeY = 328
			},
			...
		}
	\param overrideArgs (table)
		parentDocValue (table) If docId corresponds to a Version, parentDocValue is the
			docValue of the parent asset document.

	\return <code>Observable</code> Fires with an overlay (table) to be applied to the
		base document. If one of the specified auxiliaries does not exist on disk,
		this function will return an error and the doc value will not be modified.

]]--

function WFCatAssetAux.getAuxOverlay( wfCatalog, docId, auxDataList, overrideArgs )

	check( 'WFCatalog', '?string|number', 'table', '?table' )

	overrideArgs = overrideArgs or {}

	local basePath = WFCatAssetAux.getBasePath( wfCatalog )

	local maybeDocValue = docId and wfCatalog:docStore():docValue( docId ) or Observable.just( {} )

	return maybeDocValue

		:flatMap( function( docValue )

			-- Use parentDocValue if getAuxOverlay for version
			local assetDocValue = overrideArgs.parentDocValue or docValue

			return getAdditionalAuxFileInfo( basePath, auxDataList, assetDocValue )

				:map( function( auxFileInfoByDigest )

					return docValue, auxFileInfoByDigest

				end )

		end )

		:flatMap( function( docValue, auxFileInfoByDigest )

			local maybeParentAssetId = KSTableUtils_derefOpt( docValue, 'asset', 'id' )

			local maybeParentDocValue = overrideArgs.parentDocValue

			return getAuxIdByDigestAndExistingFiltersFromRelatedDocuments( wfCatalog, docId, maybeParentAssetId, maybeParentDocValue )

				:map( function( auxIdsByDigest, existingFilters )

					overrideArgs.auxIdsByDigest = auxIdsByDigest
					overrideArgs.existingFilters = existingFilters

					return docValue, auxFileInfoByDigest

				end )

		end )

		:map( function( docValue, auxFileInfoByDigest )

			return generateAuxOverlayForAuxFileInfoDigest( auxFileInfoByDigest, docValue, auxDataList, overrideArgs )

		end )

end

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

--[[

	\brief Given a document Id and table of aux digests, creates an overlay
		add the specified aux objects to a Version document.

	\param wfCatalog (WFCatalog)
	\param docId (string|int) The version Id
	\param auxDataList (table) A table containing auxiliary object information. Format:
		{
			{
				digest = digest,
			},
			{
				digest = digest,
				groupDigest = groupDigest,
				sourceLongEdge = 6000,
				crsSizeX = 1024,
				crsSizeY = 768
			},
			{
				digest = digest,
				groupDigest = groupDigest,
				targetLongEdge = 2560,
				crsSizeX = 437,
				crsSizeY = 328
			},
			...
		}
	\param parentDocValue (table) The docValue of the parent asset document.

	\return <code>Observable</code> Fires with an overlay (table) to be applied to the
		Version document. If one of the specified auxiliaries does not exist on disk,
		this function will return an error and the doc value will not be modified.

]]--

function WFCatAssetAux.getAuxOverlayForVersion( wfCatalog, docId, auxDataList, parentDocValue, overrideArgs )

	check( 'WFCatalog', 'string|number', 'table', 'table', '?table' )

	overrideArgs = overrideArgs or {}

	overrideArgs.parentDocValue = parentDocValue

	return WFCatAssetAux.getAuxOverlay( wfCatalog, docId, auxDataList, overrideArgs )

		:map( function( overlay )

			if overlay then
				overlay.aux = nil
			end

			return overlay

		end )

end

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

--[[

	\brief Given an asset doc Id and a table of aux info, creates an overlay
		to add only non-tracked resampled aux

	\param wfCatalog (WFCatalog)
	\param docId (string|int) The asset doc Id
	\param auxDataList (table) A table containing auxiliary object information. Format:
		{
			{
				digest = digest,
			},
			{
				digest = digest,
				groupDigest = groupDigest,
				sourceLongEdge = 6000,
				crsSizeX = 1024,
				crsSizeY = 768
			},
			{
				digest = digest,
				groupDigest = groupDigest,
				targetLongEdge = 2560,
				crsSizeX = 437,
				crsSizeY = 328
			},
			...
		}
	\param optDocValue (optional table) The docValue of the asset document.

	\return <code>Observable</code> Fires with an overlay (table) to be applied to the
		asset document. If one of the specified auxiliaries does not exist on disk,
		this function will return an error and the doc value will not be modified.

]]--

function WFCatAssetAux.getAuxOverlayForLocalOnlyResampledAux( wfCatalog, docId, auxDataList, optDocValue )

	check( 'WFCatalog', 'string|number', 'table', '?table' )

	if #auxDataList == 0 then
		return Observable.just( {} )
	end

	local basePath = WFCatAssetAux.getBasePath( wfCatalog )

	local docValueObs = ( optDocValue and Observable.just( optDocValue ) )
							or wfCatalog:docStore():docValue( docId )

	return docValueObs

		:flatMap( function( docValue )

			-- find localOnly resampled auxillaries

			local localOnlyResampledAuxDataList = {}

			for _, auxData in ipairs( auxDataList ) do
				
				if auxData.groupDigest and auxData.targetLongEdge then

					local maybeAuxId = WFCatAssetAux.getAuxIdForDigest( docValue, auxData.digest )

					if not maybeAuxId then
						localOnlyResampledAuxDataList[ #localOnlyResampledAuxDataList + 1 ] = auxData
					end

				end

			end

			-- create overlay

			if #localOnlyResampledAuxDataList == 0 then
				return Observable.just( {} )
			end

			return getAdditionalAuxFileInfo( basePath, localOnlyResampledAuxDataList )
				
				:map( function( auxFileInfoByDigest )

					local auxSectionOverlay = {}
					local developFiltersOverlay = {}

					for _, localOnlyResampledAuxData in ipairs( localOnlyResampledAuxDataList ) do 

						local digest = localOnlyResampledAuxData.digest
						local groupDigest = localOnlyResampledAuxData.groupDigest

						local auxFileInfo = auxFileInfoByDigest[ digest ]
						local auxId = KSIdGenerator.uuidInLowercaseHex()

						auxSectionOverlay[ auxId ] = {
							type = 'filter',
							fileSize = auxFileInfo.fileSize,
							digest = digest,
							sha256 = auxFileInfo.sha256
						}

						if KSTableUtils_derefOpt( docValue, 'develop', 'filters', groupDigest ) then
						
							developFiltersOverlay[ groupDigest ] = {
								[ auxId ] = {
									digest = digest,
									targetLongEdge = localOnlyResampledAuxData.targetLongEdge,
									crsSizeX = localOnlyResampledAuxData.crsSizeX,
									crsSizeY = localOnlyResampledAuxData.crsSizeY
								}
							}

						end

					end

					local overlay = {
						aux = auxSectionOverlay
					}

					if next( developFiltersOverlay ) then
						KSTableUtils_setNested( overlay, developFiltersOverlay, 'develop', 'filters' )
					end

					return overlay
					
				end )

		end )
end

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

local function hasUnsyncedAuxObjectsLocally( docValue, onlyIncludeDevelop )

	check( 'table', '?boolean' )

	local auxObjects

	if onlyIncludeDevelop then
		auxObjects = getDevelopAuxNormalizedList( docValue ) or {}
	else
		auxObjects = maybeGetAuxSection( docValue ) or {}
	end

	for auxId, auxInfo in pairs( auxObjects ) do

		local ozHasAuxObject = WFCatAssetAux.isAuxObjectOnOz( docValue, auxId )

		if not ozHasAuxObject then

			local auxLocalFilePath = WFCatAssetAux.getAuxLocalOnlyFilePath( docValue, auxInfo.digest )

			if auxLocalFilePath then
				return true
			end

		end

	end

	return false

end

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

--[[

	\brief Given a docValue, returns a boolean indicating if the document has
		unsynced auxiliary objects that exist locally.

	\param docValue (table)

	\return <code>boolean</code>

]]--

function WFCatAssetAux.hasUnsyncedAuxObjectsLocally( docValue )

	check( 'table' )

	local onlyIncludeDevelop = false

	return hasUnsyncedAuxObjectsLocally( docValue, onlyIncludeDevelop )

end

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

--[[

	\brief Given a docValue, returns a boolean indicating if the document has
		unsynced auxiliary objects that exist locally. This function only
		considers masks and filters that are referenced by the current develop
		settings.

	\param docValue (table)

	\return <code>boolean</code>

]]--

function WFCatAssetAux.hasUnsyncedDevelopAuxLocally( docValue )

	check( 'table' )

	local onlyIncludeDevelop = true

	return hasUnsyncedAuxObjectsLocally( docValue, onlyIncludeDevelop )

end

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

--[[

	\brief If the local binary associated with the auxiliary object is marked with
		an error (such as in the event of a locally corrupt binary), the associated
		error string is returned.

	\param docValue (table)
	\param auxId (string)

	\return <code>boolean</code>
]]--

function WFCatAssetAux.hasLocalAuxError( docValue, auxId )

	check( 'table', '+string' )

	local maybeAuxInfo = KSTableUtils_derefOpt( docValue, 'aux', auxId )

	if maybeAuxInfo then

		return not not KSTableUtils_derefOpt(
				docValue, '_localOnly', 'files', 'aux', maybeAuxInfo.digest, 'error' )

	end

	return false

end

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

--[[

	\brief Returns true if any of the local auxiliary objects associated with the
		current set of develop settings contain local errors.

	\param docValue (table)

	\return <code>boolean</code>

]]--

function WFCatAssetAux.hasLocalAuxErrorForCurrentDevelopSettings( docValue )

	check( 'table' )

	local developAuxNormalizedList = getDevelopAuxNormalizedList( docValue ) or {}

	for auxId in pairs( developAuxNormalizedList ) do

		local hasError = WFCatAssetAux.hasLocalAuxError( docValue, auxId )

		if hasError then

			return true

		end

	end

	return false

end

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

--[[

	\brief Given a document Id and table of aux digests, filter auxes that are not uploaded.

		NOTE:This API is currently Classic specific during develop update process

	\param wfCatalog (WFCatalog)
	\param docId (string|int) The document Id
	\param auxDigests (table) A table containing auxiliary object information. Format:
		{
			{ digest = <digest> },
			{ digest = <digest> }
			...
		}

	\return <code>Observable</code> Fires with a list of tables with not uploaded aux digests
		and expected filePath.

]]--

function WFCatAssetAux.filterAuxThatAreNotUploaded( wfCatalog, docId, auxDataList )

	check( 'WFCatalog', 'string|number', 'table' )

	local basePath = WFCatAssetAux.getBasePath( wfCatalog )

	return wfCatalog:docStore():docValue( docId )

		:map( function( docValue )

			local notUploadedAuxList = {}

			for _, auxDigest in ipairs( auxDataList ) do

				local digest = auxDigest.digest
				local auxId = WFCatAssetAux.getAuxIdForDigest( docValue, digest )

				if not auxId or not WFCatAssetAux.isAuxObjectOnOz( docValue, auxId ) then

					local filePath = WFCatAssetAux.calculatePathUsingDigest( basePath, digest )

					notUploadedAuxList[ #notUploadedAuxList + 1 ] = {
						digest = digest,
						filePath = filePath
					}

				end

			end

			return notUploadedAuxList

		end )

end

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

local function auxFileExistsLocally( docValue, digest, performFileExistenceCheck, optBasePath )

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

	local auxLocalFilePath = WFCatAssetAux.getAuxLocalOnlyFilePath( docValue, digest )

	local hasLocalAuxFile = auxLocalFilePath

	if performFileExistenceCheck then
		-- Currently LrC saves aux in RocksDb and this file expects aux at a specific location.
		-- In order to check if aux is present with client currently

		local eternalAuxExistenceChecker = getEternalAuxExistenceChecker()
		if eternalAuxExistenceChecker then
			hasLocalAuxFile = eternalAuxExistenceChecker( digest )
		elseif auxLocalFilePath and optBasePath then
			local auxAbsolutePath = KSPathUtils.child( optBasePath, auxLocalFilePath )
			hasLocalAuxFile = not not WFFileUtils.exists( auxAbsolutePath )
		end
	end

	return not not hasLocalAuxFile

end

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

local function isAuxMissingFromDevice( docValue, maybeParentDocValue, auxId, digest, ignoreOnOzCheck, performFileExistenceCheck, optBasePath )

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

	local hasLocalAuxFile = auxFileExistsLocally( docValue, digest, performFileExistenceCheck, optBasePath )

	if ignoreOnOzCheck then
		return not hasLocalAuxFile
	end

	local ozHasAuxObject = WFCatAssetAux.isAuxObjectOnOz( docValue, auxId )
		or ( maybeParentDocValue and WFCatAssetAux.isAuxObjectOnOz( maybeParentDocValue, auxId ) )

	return ozHasAuxObject and not hasLocalAuxFile

end

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

local function checkSoleAuxMissingFromDevice( docValue, auxList, maybeParentDocValue, ignoreOnOzCheck, performFileExistenceCheck, optBasePath )

	-- Please note that performFileExistenceCheck should only be passed in when
	-- this function is called from outside an index store map functions; in general,
	-- we avoid tying reindexing logic to file existence checks and other disk-related
	-- operations. We do this for performance reasons.

	check( 'table', 'table', '?table', '?boolean', '?boolean', '?string' )

	local auxMissingFromDevice = {}

	for auxId, auxInfo in pairs( auxList ) do

		local auxIsMissing = isAuxMissingFromDevice( docValue, maybeParentDocValue, auxId, auxInfo.digest, ignoreOnOzCheck, performFileExistenceCheck, optBasePath )

		if auxIsMissing then

			local copyAuxInfo = KSTableUtils.deepCopy( auxInfo )
			copyAuxInfo.auxId = auxId

			auxMissingFromDevice[ #auxMissingFromDevice + 1 ] = copyAuxInfo

		end

	end

	return auxMissingFromDevice

end

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

function WFCatAssetAux.maybeResampledDigestAndAuxInfoLocally( docValue, groupDigest, performFileExistenceCheck, optBasePath, optTargetLongEdge )

	check( 'table', 'string', '?boolean', '?string', '?number' )

	optTargetLongEdge = optTargetLongEdge or 2560

	local localOnlyFilesAux = KSTableUtils_derefOpt( docValue, '_localOnly', 'files', 'aux' ) or {}

	local resampleDigest, resampleAuxInfo = KSTableUtils.findInTable( localOnlyFilesAux, 
		function( _, auxFileInfo )
			return auxFileInfo.groupDigest == groupDigest 
				and auxFileInfo.targetLongEdge == optTargetLongEdge
				and not not auxFileInfo.relativePath
		end )

	if not resampleDigest then
		return 
	end

	local hasLocalAuxFile = auxFileExistsLocally( docValue, resampleDigest, performFileExistenceCheck, optBasePath )

	if not hasLocalAuxFile then
		return
	end
	
	return resampleDigest, {
		groupDigest = resampleAuxInfo.groupDigest,
		targetLongEdge = resampleAuxInfo.targetLongEdge,
		crsSizeX = resampleAuxInfo.crsSizeX,
		crsSizeY = resampleAuxInfo.crsSizeY,
		fileSize = resampleAuxInfo.fileSize
	}

end

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

local function checkMultiResAuxMissingAndSelectedFromDevice( docValue, groupedAuxList, maybeParentDocValue, optImageType, ignoreOnOzCheck, performFileExistenceCheck, optBasePath )

	-- Please note that performFileExistenceCheck should only be passed in when
	-- this function is called from outside an index store map functions; in general,
	-- we avoid tying reindexing logic to file existence checks and other disk-related
	-- operations. We do this for performance reasons.

	check( 'table', 'table', '?table', '?string', '?boolean', '?boolean', '?string' )

	optImageType = optImageType or 'original'

	local auxSelected = {}
	local auxMissingFromDevice = {}

	for groupDigest, auxList in pairs( groupedAuxList ) do

		local function findResolution( auxListInGroup, selectFn )
			return KSTableUtils.findInTable( auxListInGroup, function( _, auxDetails )
				return KSTableUtils.findInTable( auxDetails, selectFn )
			end )
		end

		local selectedAuxId, selectedAuxInfo

		local sourceAuxId, sourceAuxInfo = findResolution( auxList,
			function( k, v )
				return k == 'sourceLongEdge' and v ~= nil
			end )

		if not sourceAuxId then

			logger:errorf( "Filter should contain exact one aux with sourceLongEdge. Missing aux in groupDigest %s", groupDigest )

		elseif optImageType == 'original'
			or auxFileExistsLocally( docValue, sourceAuxInfo.digest, performFileExistenceCheck, optBasePath ) then

			-- If original instance (with sourceLongEdge) exists locally
			-- or optImageType is "original", always use original instance
			-- Original instance might be full-res or proxy.
			-- If original instance is proxy size, there's nothing we can do. It's be best we can get for original image type

			selectedAuxId = sourceAuxId
			selectedAuxInfo = sourceAuxInfo

		elseif optImageType == 'proxy-2560' then

			-- If optImageType is proxy-2560, check first if original instance is already proxy size.
			-- If not, then check tartget instances.

			-- If parentDocValue aux section is not reachable, we always assuem source filter 
			-- is larger than resample boundary
			
			local sourceFileSize = KSTableUtils_derefOpt( maybeParentDocValue or docValue, 'aux', sourceAuxId, 'fileSize' )
									or kResampleBoundaryFileSize + 1

			if sourceAuxInfo.sourceLongEdge <= kResampleBoundaryLongEdge
				or sourceFileSize <= kResampleBoundaryFileSize then

				-- if original insatnce is small enough, don't need to resample

				selectedAuxId = sourceAuxId
				selectedAuxInfo = sourceAuxInfo

			else

				selectedAuxId, selectedAuxInfo = findResolution( auxList,
					function( k, v )
						return k == 'targetLongEdge' and v <= kResampleBoundaryLongEdge
					end )

			end

		end

		if selectedAuxId and selectedAuxInfo then

			local copySelectedAuxInfo = KSTableUtils.deepCopy( selectedAuxInfo )
			copySelectedAuxInfo.auxId = selectedAuxId
			copySelectedAuxInfo.groupDigest = groupDigest

			local auxIsMissing = isAuxMissingFromDevice( docValue, maybeParentDocValue, selectedAuxId, selectedAuxInfo.digest, ignoreOnOzCheck, performFileExistenceCheck, optBasePath )

			if auxIsMissing then

				auxMissingFromDevice[ #auxMissingFromDevice + 1 ] = copySelectedAuxInfo

			end

			auxSelected[ #auxSelected + 1 ] = copySelectedAuxInfo

		elseif sourceAuxId and sourceAuxInfo then

			-- If no matching resolution, then a resample is required
			-- First check _localOnly if filter is already resampled

			local maybeResampledDigest, maybeResampledAuxInfo = WFCatAssetAux.maybeResampledDigestAndAuxInfoLocally( docValue, groupDigest, performFileExistenceCheck, optBasePath )

			if maybeResampledDigest and maybeResampledAuxInfo then

				local alreadyResampledAuxInfo = KSTableUtils.deepCopy( maybeResampledAuxInfo )
				alreadyResampledAuxInfo.digest = maybeResampledDigest
				alreadyResampledAuxInfo.resampled = true

				auxSelected[ #auxSelected + 1 ] = alreadyResampledAuxInfo

			else

				-- Need Oz reample

				local needResampleAuxInfo = KSTableUtils.deepCopy( sourceAuxInfo )
				needResampleAuxInfo.auxId = sourceAuxId
				needResampleAuxInfo.groupDigest = groupDigest
				needResampleAuxInfo.resample = true
				needResampleAuxInfo.estimatedFileSize = WFCatAssetAux.estimateProxyAuxFileSize( sourceAuxInfo.sourceLongEdge, sourceAuxInfo.crsSizeX, sourceAuxInfo.crsSizeY )

				auxMissingFromDevice[ #auxMissingFromDevice + 1 ] = needResampleAuxInfo

			end

		end

	end

	return auxMissingFromDevice, auxSelected

end

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

function WFCatAssetAux._TEST_checkMultiResAuxMissingAndSelectedFromDevice( ... )

	local auxMissing, auxSelected = checkMultiResAuxMissingAndSelectedFromDevice( ... )

	return { auxMissing, auxSelected  }

end

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

local function missingAuxAndSelectedMultiResAuxForDevelop( docValue, maybeParentDocValue, optImageType, ignoreOnOzCheck, performFileExistenceCheck, optBasePath )

	check( 'table', '?table', '?string', '?boolean', '?boolean', '?string' )

	optImageType = optImageType or 'original'

	local masks = maybeGetMasks( docValue ) or {}
	local filters = maybeGetFilters( docValue ) or {}

	local missingMasks = checkSoleAuxMissingFromDevice( docValue, masks, maybeParentDocValue, ignoreOnOzCheck, performFileExistenceCheck, optBasePath )
	local missingFilters, selectedFilters = checkMultiResAuxMissingAndSelectedFromDevice( docValue, filters, maybeParentDocValue, optImageType,
																						ignoreOnOzCheck, performFileExistenceCheck, optBasePath )
	return KSTableUtils.appendArray( missingMasks, missingFilters ), selectedFilters

end

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

local function maybeMissingFromDeviceC2paAuxInfo( docValue, maybeParentDocument, ignoreOnOzCheck, performFileExistenceCheck, optBasePath )

	check( 'table', '?table', '?boolean', '?boolean', '?string' )

	local maybeC2paNeed = maybeGetC2pa( docValue ) or {}

	local missingC2paAuxInfoList = checkSoleAuxMissingFromDevice( docValue, maybeC2paNeed, maybeParentDocument, ignoreOnOzCheck, performFileExistenceCheck, optBasePath )

	if #missingC2paAuxInfoList > 0 then

		return missingC2paAuxInfoList[ 1 ]

	end

end

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

--[[
	\brief Given a docValue, returns true if any of the auxiliary files referenced
		by the develop settings are on Oz and not on the device.
	\param docValue (table)
	\return <code>boolean</code>
]]--

function WFCatAssetAux.needsAuxDownloadForDevelop( docValue, optImageType )

	check( 'table', '?string' )

	local maybeParentDocValue = nil
	local missingFromDeviceAuxInfoList = missingAuxAndSelectedMultiResAuxForDevelop( docValue, maybeParentDocValue, optImageType )

	return #missingFromDeviceAuxInfoList > 0

end

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

function WFCatAssetAux.needsAuxDownloadForC2pa( docValue )

	check( 'table' )

	local missingFromDeviceC2paAuxInfo = maybeMissingFromDeviceC2paAuxInfo( docValue )

	return not not missingFromDeviceC2paAuxInfo

end

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

function WFCatAssetAux.needsAuxDownloadForDevelopAndC2pa( docValue )

	check( 'table' )

	local needsAuxDownload = WFCatAssetAux.needsAuxDownloadForDevelop( docValue ) or WFCatAssetAux.needsAuxDownloadForC2pa( docValue )

	return needsAuxDownload

end

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

 -- Exposed For Testing
function WFCatAssetAux._TEST_needsAuxDownloadForDevelop( docValue, performFileExistenceCheck )

	check( 'table', '?boolean' )

	local maybeParentDocument, imageType, ignoreOnOzCheck
	local missingFromDeviceAuxInfoList = missingAuxAndSelectedMultiResAuxForDevelop( docValue, maybeParentDocument, imageType, ignoreOnOzCheck, performFileExistenceCheck )

	return #missingFromDeviceAuxInfoList > 0

end

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

function WFCatAssetAux._TEST_needsAuxDownloadForDevelopAndC2pa( docValue )

	check( 'table' )

	local missingFromDeviceAuxInfoList = missingAuxAndSelectedMultiResAuxForDevelop( docValue )
	local missingFromDeviceC2paAuxInfo = maybeMissingFromDeviceC2paAuxInfo( docValue )

	return #missingFromDeviceAuxInfoList > 0 or missingFromDeviceC2paAuxInfo ~= nil

end

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

--[[
	\brief Returns a table including auxiliary object information for auxiliaries
		associated with the document that are *not* referenced within the
		local only section of the document.

		This API works for both Versions and Assets.

	\param docValue (table)
 	\param digest (string) The digest of the auxiliary object.

	\return (number) Returns the a table of auxiliary object information:

		{
		    {
			    auxId = <auxId>,
				digest = <digest>
			},
			{
			    auxId = <auxId>,
				digest = <digest>,
				type = "c2pa",
				fileSize = <fileSize>,
				sha256 = <sha256>,
			},
			{
			    auxId = <auxId>,
				digest = <digest>,
				groupDigest = <groupDigest>,
				sourceLongEdge = 6000,
				crsSizeX = 1024,
				crsSizeY = 768
			},
			{
			    auxId = <auxId>,
				digest = <digest>,
				groupDigest = <groupDigest>,
				targetLongEdge = 2560,
				crsSizeX = 437,
				crsSizeY = 328
			},
			{
			    auxId = <auxId>,
				digest = <digest>,
				groupDigest = <groupDigest>,
				sourceLongEdge = 6000,
				crsSizeX = 1024,
				crsSizeY = 768,
				resample = true
			},
			...
		}

--]]

function WFCatAssetAux.getListOfAuxThatAreNotLocal( docValue, optImageType )

	check( 'table', '?string' )

	optImageType = optImageType or 'original'

	local maybeParentDocument = nil
	local ignoreOnOzCheck = true

	local auxThatAreNotLocal = missingAuxAndSelectedMultiResAuxForDevelop( docValue, maybeParentDocument, optImageType, ignoreOnOzCheck )

	auxThatAreNotLocal[ #auxThatAreNotLocal + 1 ] = maybeMissingFromDeviceC2paAuxInfo( docValue, maybeParentDocument, ignoreOnOzCheck )

	return auxThatAreNotLocal

end

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

--[[!
	\brief Determines if an asset has aux which are not on Oz.

	\param assetDocValue (table)
	\param versionDocValue (table)
	\param optImageType ( optional string ) Indicates what kind of aux type is being requested.
		Available types:
		'original' - Selected by deefault. Determines if the original is on Oz.
		'proxy-2560' - Looks to see if proxy size is on Oz.

	\return (boolean, boolean) Return true if the version is complete; the second boolean is true if
		develop settings and all masks are local
--]]

function WFCatAssetAux.isVersionComplete( assetDocValue, versionDocValue, optImageType )

	check( 'table', 'table', '?string' )

	optImageType = optImageType or 'original'

	local versionXmpDevelopOnOz, versionXmpDevelopLocal, _ = false, false, nil

	local xmpCameraRaw = KSTableUtils_derefOpt( versionDocValue, 'develop', 'xmpCameraRaw' )
	local isExternalized = type( xmpCameraRaw ) == 'table'

	if isExternalized then

		_, versionXmpDevelopOnOz = WFCatAssetUtils.doesAssetNeedDevelopSettingsUploaded( versionDocValue )

		versionXmpDevelopLocal = not not WFCatAssetUtils.getLocalExternalDevelopSettingsPath( versionDocValue )

	else

		versionXmpDevelopLocal = true

	end

	local versionXmpIncomplete = ( versionXmpDevelopOnOz == false ) and ( versionXmpDevelopLocal == false )

	if versionXmpIncomplete then
		return false, versionXmpDevelopLocal
	end

	local ignoreOnOzCheck = true
	local auxMissingWithoutOnOzCheck = missingAuxAndSelectedMultiResAuxForDevelop( versionDocValue, assetDocValue, optImageType,
												ignoreOnOzCheck )

	ignoreOnOzCheck = false
	local auxMissingWithOnOzCheck = missingAuxAndSelectedMultiResAuxForDevelop( versionDocValue, assetDocValue, optImageType,
												ignoreOnOzCheck )

	if #auxMissingWithoutOnOzCheck ~= #auxMissingWithOnOzCheck then
		return false, false
	end

	local allAuxAreLocal = ( #auxMissingWithoutOnOzCheck == 0 )

	return true,
		versionXmpDevelopLocal and allAuxAreLocal

end

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

local function downloadAuxAndSaveToDisk( wfCatalog, docValue, digest, maybeParentDocument )

	-- In the event of success, this function returns an observable that fires
	-- with two values: < digest, relative file path >

	-- In the event of error, an observable is returned that fires with a single value: < digest >

	-- Construct the aux file paths

	local basePath = WFCatAssetAux.getBasePath( wfCatalog )

	local absoluteFilePath, relativeFilePath = WFCatAssetAux.calculatePathUsingDigest( basePath, digest )

	-- If the file already exists ( maybe it was downloaded as part of another document ),
	-- then this is a no-op.

	if WFFileUtils.exists( absoluteFilePath ) then

		logger:infof( "No need to download aux digest: %s, it already exists at %s", digest, absoluteFilePath )

		return Observable.just( digest, relativeFilePath )

	end

	-- Fetch the download link.

	local auxLink = WFCatAssetAux.getDownloadLink( docValue, digest )

	if not auxLink and maybeParentDocument then

		logger:infof( "Cannot find download link for aux %s. Checking parent document.", digest )

		auxLink = WFCatAssetAux.getDownloadLink( maybeParentDocument, digest )

	end

	if not auxLink then

		local errorString = string.format( "No download link for aux %s", digest )

		logger:warn( errorString )

		return Observable.just( digest )

	end

	-- Download the binary using the save-to-file functionality.

	local options = {
		requestHeaders = {
			{
				header = "Content-Type",
				value = "application/octet-stream",
			}
		}
	}

	local wfCatSpec = wfCatalog:ozCatalogSpec()

	return wfCatSpec:getBinary( auxLink, options )

		:catch( function( responseData, responseHeader )

			local statusCode = responseHeader and responseHeader.status_code or ''

			if string.sub( statusCode, 1, 1 ) == '4' then

				 -- 4XX HTTP status code, log error to Dunamis

				WFAnalytics.trackEvent {
					['event.type'] = 'error',
					['event.appWorkflow'] = 'develop',
					['event.subtype'] = 'cannot-download-aux',
					['event.statusCode'] = statusCode,
				}

			end

			return Observable.throw( responseData, responseHeader )

		end )

		:flatMap( function( data )

			-- Save aux to disk

			local parentPath = KSPathUtils.parent( absoluteFilePath )

			WFFileUtils.mkdir( parentPath )

			return WFAsyncFileUtils.saveToFile( data, absoluteFilePath )

		end )

		:map( function()

			-- Return the digest and the relative file path, used to create
			-- the _localOnly file annotation.

			return digest, relativeFilePath

		end )

		:catch( function()

			logger:warnf( "Unable to download auxiliary digest %s using link %s:", digest, auxLink )

			return Observable.just( digest )

		end )

end

WFCatAssetAux._TEST_downloadAuxAndSaveToDisk = downloadAuxAndSaveToDisk -- Exposed For Testing

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

--[[
	\brief Requests Oz API to resample Auxiliary and save the resulting auxilary to disk

	\param wfCatalog (WFCatalog)
	\param assetId
	\param auxId
	\param optVersionId
	\param groupDigest
	\param optArgs
		sourceImageType - defaults to 'original', only available version
		targetImageType - defaults to 'proxy-2560', only available version

	\return <code>Observable< ></code> Fires a custom response upon completion. The response includes
		the status code of the http call, the file path where the auxilary has been temporarily stored,
		and auxiliary information, including width, height, and digest of the auxiliary.
		Throws an error if decoding fails or got a non 200 response from server.

		Reponse format:
			< Auxilary digest obstained from response headers >,
			< Relative file path to aux file >,
			< Resampled auxilary Information >

		Example response:
			'abc123def456',
			'/path/to/auxiliary/file',
			{
				groupDigest = groupDigest,
				crsSizeX = '602',
				crsSizeY = '803',
				fileSize = <fileSize>,
				sha256 = <sha256>
			}


]]--

local function invokeResampleAux( wfCatalog, assetId, auxId, optVersionId, optSpaceId, optGroupDigest, optArgs )

	check(  'WFCatalog', 'string',  'string', '?string', '?string', '?string','?table' )

	optArgs = optArgs or {}

	local catalogId = wfCatalog:ozCatalogId()

	local sourceImageType = optArgs.sourceImageType or 'original'
	local targetImageType = optArgs.targetImageType or 'proxy-2560'

	local requestUrl

	if optSpaceId then
		requestUrl = string.format( "/v2/spaces/%s/assets/%s/aux/%s/resample", optSpaceId, assetId, auxId )
	else
		requestUrl = string.format( "/v2/catalogs/%s/assets/%s/aux/%s/resample", catalogId, assetId, auxId )
	end
	
	if optVersionId then
		requestUrl = requestUrl .. string.format( "?version_id=%s", optVersionId )
	end

	local wfCatSpec = wfCatalog:ozCatalogSpec()

	local requestHeaders = {
		{
			header = 'Content-Type',
			value = 'application/json',
		}
	}

	local requestOptions = {
		requestMethod = 'POST',
		returnHttpErrorsAsData = true,
		requestHeaders = requestHeaders,
	}

	local requestData = KSJsonEncoder.encodeOzUploadJson {
			source_image_type = sourceImageType,
			target_image_type = targetImageType,
	}

	return WFRxRetryObservable {

		maxAttempts = 3,

		retryDelay = WFPlatformInfo.isUnitTest() and 0.1 or 2,

		factory = function()

			return wfCatSpec:put( requestUrl, requestData, requestOptions )

				:flatMap( function( response, headers )

					local status_code = headers.status_code

					if not headers or status_code ~= '200' then
            
						return Observable.throw( status_code, assetId, auxId, optVersionId, optSpaceId, optGroupDigest, targetImageType )
						
					end

					local digest = headers[ 'X-Aux-Digest' ]
					local crsSizeX = tonumber ( headers[ 'X-Aux-Width' ] )
					local crsSizeY = tonumber ( headers[ 'X-Aux-Height' ] )

					-- Save aux to disk

					local basePath = WFCatAssetAux.getBasePath( wfCatalog )
					local absoluteFilePath, relativeFilePath = WFCatAssetAux.calculatePathUsingDigest( basePath, digest )

					local parentPath = KSPathUtils.parent( absoluteFilePath )

					WFFileUtils.mkdir( parentPath )

					return WFAsyncFileUtils.saveToFile( response, absoluteFilePath )

						:map( function()

							local resampledInfo = {
								groupDigest = optGroupDigest,
								crsSizeX = crsSizeX,
								crsSizeY = crsSizeY
							}
							return digest, relativeFilePath, resampledInfo, absoluteFilePath

						end )

				end )

				:flatMap( function( digest, relativeFilePath, resampledInfo, absoluteFilePath )

					local fileAttributes = WFFileUtils.fileAttributes( absoluteFilePath )

					local fileSize = 0

					if fileAttributes then
						fileSize = fileAttributes.fileSize
					end

					return WFCrypto.asyncDigestFile( absoluteFilePath, 'SHA256' )

						:map( function( fileSha256 )

							resampledInfo.fileSize = fileSize
							resampledInfo.sha256 = fileSha256

							return digest, relativeFilePath, resampledInfo

						end )

				end )
		end
	}
	:catch( function( status_code, assetId, auxId, versionId, spaceId, groupDigest, targetImageType )
					
		local errorMessage = string.format( "Resample fail for asset %s, auxId %s, versionId %s, spaceId %s, groupDigest %s, targetImageType %s", 
													assetId, auxId, versionId, spaceId, groupDigest, targetImageType )
		WFAnalytics.trackEvent {
			['event.type'] = 'error',
			['event.appWorkflow'] = 'develop',
			['event.subtype'] = 'cannot-resample-aux',
			['event.error_code'] = status_code,
			['event.error_desc'] = errorMessage,
			['content.id'] = assetId
		}
						
		return Observable.throw( errorMessage )
					
	end )

end

WFCatAssetAux._TEST_invokeResampleAux = invokeResampleAux -- Exposed For Testing

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

local function ensureAuxFilesExistLocallyImp( wfCatalog, docValue, optImageType )

	check( 'WFCatalog', 'table', '?string' )

	-- Iterate through the list, downloading each auxiliary.

	local parentDocumentStream = Observable.just( nil )

	local isVersion = docValue.type == 'version'
	local maybeAssetId = KSTableUtils_derefOpt( docValue, 'asset', 'id' )

	if isVersion and maybeAssetId then

		parentDocumentStream = wfCatalog:docStore():docValue( maybeAssetId )

	end

	local result = {
		success = true
	}

	return parentDocumentStream

		:flatMap( function( maybeParentDocument )

			-- Identify which auxiliary objects need to be downloaded, or resampled
			-- Also identify which auxiliary objects is selected for multi-res aux type

			local ignoreOnOzCheck = false
			local performFileExistenceCheck = true
			local basePath = WFCatAssetAux.getBasePath( wfCatalog )

			local auxMissingFromDevice, auxSelectedForDevelopMultiRes = missingAuxAndSelectedMultiResAuxForDevelop( docValue, maybeParentDocument, optImageType,
																			ignoreOnOzCheck, performFileExistenceCheck, basePath )

			if #auxSelectedForDevelopMultiRes > 0 then
				result.selectedDigests = auxSelectedForDevelopMultiRes
			end

			-- Append missing c2pa auxInfo to array

			auxMissingFromDevice[ #auxMissingFromDevice + 1 ] = maybeMissingFromDeviceC2paAuxInfo( docValue, maybeParentDocument, ignoreOnOzCheck, performFileExistenceCheck, basePath )

			return Observable.from( auxMissingFromDevice )

				:map( function( auxInfo )

					return auxInfo, maybeParentDocument

				end )

		end )

		:flatMap( function( auxInfo, maybeParentDocument )

			local digest = auxInfo.digest

			local resampleIsNeeded = auxInfo.resample

			if resampleIsNeeded then

				local groupDigest = auxInfo.groupDigest
				local auxId = auxInfo.auxId
				local assetId = isVersion and maybeAssetId or docValue._id
				local versionId = isVersion and docValue._id or nil

				local spaceId = docValue._spaceId
				if spaceId then
					local assetDocValue = isVersion and maybeParentDocument or docValue
					local catalogDocId = KSTableUtils.derefOpt( assetDocValue, '_serverProvidedMetadata', 'catalog_doc', 'id' )
					assetId = catalogDocId or assetId
				end

				local optArgs = {
					targetImageType = optImageType
				}

				return invokeResampleAux( wfCatalog, assetId, auxId, versionId, spaceId, groupDigest, optArgs )
					:map(function( resampledDigest, resampledRelativeFilePath, resampledInfo )

						return resampledDigest, resampledRelativeFilePath, resampledInfo

					end )
					:catch( function()
						return Observable.just( digest )
					end )

			else

				return downloadAuxAndSaveToDisk( wfCatalog, docValue, digest, maybeParentDocument )

			end

		end )

		:scan( function( overlay, digest, relativeFilePath, maybeResampledInfo  )

			if relativeFilePath then

				-- Construct a joint overlay that includes the file path annotations
				-- so that we only have to call applyOverlay once.

				if not maybeResampledInfo then

					overlay = WFCatAssetAux.createAuxLocalOnlyOverlay( digest, relativeFilePath, overlay )

				else

					local resampledLocalOnlyAuxInfo = {
						groupDigest = maybeResampledInfo.groupDigest,
						crsSizeX = maybeResampledInfo.crsSizeX,
						crsSizeY = maybeResampledInfo.crsSizeY,
						fileSize = maybeResampledInfo.fileSize,
						targetLongEdge = 2560
					}
					
					overlay = createAuxLocalOnlyOverlayForResampledAux( digest, relativeFilePath, resampledLocalOnlyAuxInfo, overlay )

					local resultResampledInfo = {
						groupDigest = maybeResampledInfo.groupDigest,
						digest = digest,
						crsSizeX = maybeResampledInfo.crsSizeX,
						crsSizeY = maybeResampledInfo.crsSizeY,
						targetLongEdge = 2560,
						resampled = true
					}

					result.selectedDigests = result.selectedDigests or {}
					result.selectedDigests[ #result.selectedDigests + 1 ] = resultResampledInfo

				end

			else

				result.success = false
				KSTableUtils.setNested( result, true, 'failedToDownloadDigests', digest )

			end

			return overlay

		end, {} )

		:takeLast( 1 )

		:flatMap( function( overlay )

			local applyOverlayObs = Observable.just( true )

			if next( overlay ) then

				-- There's a chance the docValue sent into this function is created
				-- using the legacy OzCatalogService object, with modifies doc values
				-- to appear in the old OzDocStore format. When this is the case,
				-- docValue._docId contains the document Id.

				overlay._id = docValue._id or docValue._docId

				-- Write the annotation to the doc store.

				applyOverlayObs = wfCatalog:docStore():applyOverlay( overlay )

			end

			return applyOverlayObs

				:map( function()

					return result

				end )

		end )

end

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

local ensureAuxFilesObservableList = {}

function WFCatAssetAux.ensureAuxFilesExistLocally( wfCatalog, docValue, optImageType )

	check( 'WFCatalog', 'table', '?string' )

	local docId = docValue._id

	if not ensureAuxFilesObservableList[ docId ] then

		ensureAuxFilesObservableList[ docId ] = ensureAuxFilesExistLocallyImp( wfCatalog, docValue, optImageType )
			:replay( nil, 1 )
			:refCount()

	end

	return ensureAuxFilesObservableList[ docId ]
		:tap( function()
			ensureAuxFilesObservableList[ docId ] = nil
		end )

end

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

--[[
	\brief MOBILE-Only: Downloads all auxiliary objects referenced by the current
		develop settings in the given document.

	\param ozCatalogService ( OzCatalogService )
	\param docId ( string )
	\param optImageType ( optional string ) Variable that indicates what kind of aux is being requested. Available types:
		'original' - Fetch full-size if possible, otherwise proxy size
		'proxy-2560' - Fetch proxy size. If not available WF will make a resample request to Oz.

	\return <code>Observable< ></code> Fires with a table when done:
		{
			success = <boolean>,
			failedToDownloadDigests = <table of aux digests that cannot be downloaded>
			selectedDigests = {
			    {
                    auxId = <existedAuxId>,
					digest = <digest>,
					groupDigest = <groupDigest>,
					crsSizeX = <crsSizeX>,
					crsSizeY = <crsSizeY>
                },
				{
                    auxId = <resampledAuxId>,
					digest = <digest>,
					groupDigest = <groupDigest>,
					crsSizeX = <crsSizeX>,
					crsSizeY = <crsSizeY>,
					resampled = true
                },
                ...
            }
		}
]]--

function WFCatAssetAux.ozCatalogService_ensureAuxFilesExistLocally_Mobile( ozCatalogService, docId, optImageType )

	return ozCatalogService._wfCatalogStream

		:take( 1 )

		:flatMap( function( wfCatalog )

			return wfCatalog:docStore():docValue( docId )

				:flatMap( function( docValue )

					return WFCatAssetAux.ensureAuxFilesExistLocally( wfCatalog, docValue, optImageType )

				end )

		end )

end

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

--[[
	\brief Downloads all auxiliary objects referenced by the current
		develop settings for the provided document Ids.

	\param wfCatalog (WFCatalog)
	\param docIds (table) Table of document Ids

	\return <code>Observable< ></code> Fires true when done. Throws an error
		if one or more auxiliaries cannot be downloaded.

]]--

function WFCatAssetAux.bulkDownloadAuxiliariesForOfflineUse( wfCatalog, docIds, optImageType )

	check( 'WFCatalog', 'table', '?string' )

	local unableToDownloadAuxForIds = {}

	return wfCatalog:docStore():bulkDocValue( docIds )

		:flatMap( function( docValues )

			return Observable.from( docValues )

		end )

		:flatMap( function( docValue )

			return WFCatAssetAux.ensureAuxFilesExistLocally( wfCatalog, docValue, optImageType )

				:map( function( result )

					if type( result ) == 'table' and result.success then
						-- noop, everything downloaded correctly
					else
						unableToDownloadAuxForIds[ #unableToDownloadAuxForIds + 1 ] = docValue._id
					end

					return true

				end )

		end )

		:takeLast( 1 )

		:flatMap( function()

			if next( unableToDownloadAuxForIds ) then

				local errorMessage = string.format( "Unable to download all auxiliaries for documents %s", unableToDownloadAuxForIds )
				return Observable.throw( errorMessage )

			else

				return Observable.just( true )

			end

		end )

end

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

local function resampleAux( wfCatalog, assetId, auxId, optVersionId, optArgs )

	check(  'WFCatalog', 'string',  'string', '?string', '?table' )

	return invokeResampleAux( wfCatalog, assetId, auxId, optVersionId, nil, nil, optArgs )

		:flatMap( function( digest, relativeFilePath, resampledInfo )

			if not relativeFilePath then

				local err = string.format( "Unable to resample auxiliary %s", auxId )

				return Observable.throw( err )

			end

			resampledInfo = {
				filepath = relativeFilePath,
				crsSizeX = resampledInfo.crsSizeX,
				crsSizeY = resampledInfo.crsSizeY,
				digest = digest
			}

			return Observable.just( resampledInfo )

		end )

end

WFCatAssetAux._TEST_resampleAux = resampleAux

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

--[[
	\brief Esitmate proxy aux fileSize based on original instance info

	\param sourceLongEdge
	\param crsSizeX
	\param crsSizeY

	\return <code>number< ></code> Estimated proxy aux fileSize

]]--

function WFCatAssetAux.estimateProxyAuxFileSize( sourceLongEdge, crsSizeX, crsSizeY )

	check( 'number', '?number', '?number' )

	local defaultSize = kResampleBoundaryFileSize

	-- The following formula is copied from WFCatAssetUtils.estimateProxySize 
	-- which comes from Eric Chan:
	-- https://git.corp.adobe.com/acr/acr/commit/efaa06e90f3ba8053c6d7ebfec7e2effea535dac
	
	if not crsSizeX or not crsSizeY then
		return defaultSize
	end
	
	local targetLongEdge = math.min( 2560, sourceLongEdge )

	local ratio = targetLongEdge / sourceLongEdge

	local proxyCrsSizeX = ratio * crsSizeX
	local proxyCrsSizeY = ratio * crsSizeY

	local bytesPerPixel = 3 -- RGB
	local jpegCompressionFudgeFactor = 0.1

	local size = proxyCrsSizeX *
			proxyCrsSizeY *
			bytesPerPixel *
			jpegCompressionFudgeFactor

	return size or defaultSize

end

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

return WFCatAssetAux
