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

WFCatCopies.lua

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

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

NOTICE: All information contained herein is, and remains the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and are protected by all applicable
intellectual property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

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

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

	-- WF.core.os
local WFAsyncFileUtils = require 'WFAsyncFileUtils'
local WFFileUtils = require 'WFFileUtils'

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

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local KSPathUtils = require 'KSPathUtils'
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFLogger = require 'WFLogger'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFTableMerge = require 'WFTableMerge'

	-- WF.oz_client.managed_catalog
local LrAssetUtils = require 'LrAssetUtils'
local WFCatAlbums = require 'WFCatAlbums'
local WFCatAssetUtils = require "WFCatAssetUtils"
local WFCatFileSpec = require 'WFCatFileSpec'
local WFCatIdUtils = require 'WFCatIdUtils'
local WFCatIndexUtils = require 'WFCatIndexUtils'
local WFCatStacks = require 'WFCatStacks'
local WFCatAssetAux = require 'WFCatAssetAux'

local OzFilePaths, OzCatalogUploadStatus

if WFPlatformInfo.isAndroid then

	OzCatalogUploadStatus = require 'OzCatalogUploadStatus'
	OzFilePaths = require 'OzFilePaths'

elseif WFPlatformInfo.isiOS then

	OzFilePaths = require 'OzFilePaths'

end

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

local logger = WFLogger( 'WFCatCopies' )

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

local derefOpt = assert( KSTableUtils.derefOpt )
local deepCopy = assert( KSTableUtils.deepCopy )
local check = assert( WFArguments.check )

local getFullOriginalPathFromDocValue = assert( WFCatAssetUtils.getFullOriginalPathFromDocValue )
local getOriginalRelativePathFromDocValue = assert( WFCatAssetUtils.getOriginalRelativePathFromDocValue )
local getOriginalSizeFromDocValue = assert( WFCatAssetUtils.getOriginalSizeFromDocValue )
local getOriginalContentIdFromDocValue = assert( WFCatAssetUtils.getOriginalContentIdFromDocValue )

local getFullProxyPathFromDocValue = assert( WFCatAssetUtils.getFullProxyPathFromDocValue )
local getProxyRelativePathFromDocValue = assert( WFCatAssetUtils.getProxyRelativePathFromDocValue )
local getProxySizeFromDocValue = assert( WFCatAssetUtils.getProxySizeFromDocValue )
local getProxyContentIdFromDocValue = assert( WFCatAssetUtils.getProxyContentIdFromDocValue )

local getLocalExternalDevelopSettingsPath = assert( WFCatAssetUtils.getLocalExternalDevelopSettingsPath )
local getLocalExternalDevelopSettingsSize = assert( WFCatAssetUtils.getLocalExternalDevelopSettingsSize )
local getLocalExternalDevelopSettingsContentId = assert( WFCatAssetUtils.getLocalExternalDevelopSettingsContentId )

local kBatchSize = 40

local underscoreByte = string.byte( '_' )

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

local WFCatCopies = {}

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

--[[!
	\brief Set the given set of assets as a copy of each other. If any one of the
	assets already has a copy_id, it is propagated through to all the assets. If
	there is a conflict, throw an error

	\param wfCatalog the catalog
	\param assetIdArray An array of asset-ids

	\return Rx.Observable( array of strings ) Array of revision IDs for each document.
--]]

function WFCatCopies.setAssetsAsCopies( wfCatalog, assetIdArray )

	check( 'WFCatalog', 'table' )

	if #assetIdArray < 2 then
		return Observable.empty()
	end

	local docStore = wfCatalog:docStore()

	return docStore:bulkDocValue( assetIdArray )
		:flatMap( function( docValueArray )

			local copyId
			local docOverlays = {}

			-- Check if these assets are already a part of a copy
			for _, docValue in ipairs( docValueArray ) do

				local docCopyId = docValue and docValue.copy and docValue.copy.copy_id

				if docCopyId and copyId == nil then
					copyId = docCopyId
					break
				end
			end

			-- Create a copy id if none of them are already a copy
			if not copyId then
				copyId = KSIdGenerator.uuidInLowercaseHex( )
			end

			-- Form the overlays
			for _, docValue in ipairs( docValueArray ) do

				if docValue.copy and docValue.copy.copy_id then
					assert( docValue.copy.copy_id == copyId, "Trying to change the copy-id of an asset" )
				else
					local overlay = {
						_id = docValue._id,
						copy = {
							copy_id = copyId,
						}
					}

					table.insert( docOverlays, overlay )
				end
			end

			if #docOverlays == 0 then
				-- If all copy ids are already applied
				-- then nop
				return Observable.just()
			end

			return docStore:bulkApplyOverlay( docOverlays )

		end )

end

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

local function createCopyOfDocValue( docValue )

	local copyDocValue = deepCopy( docValue )

	-- Remove all keys that start with underscores.

	for k, _ in pairs( copyDocValue ) do

		if string.byte( k, 1 ) == underscoreByte then
			copyDocValue[ k ] = nil
		end

	end

	-- Remove aux section. This is populated by getAuxOverlayForCopiedDocValue

	copyDocValue.aux = nil

	-- Add in a new document ID, as well as the space ID if applicable.

	copyDocValue._id = KSIdGenerator.uuidInLowercaseHex()
	copyDocValue._newRevId = KSIdGenerator.uuidInLowercaseHex()

	-- Remove any labels associated with the document.

	copyDocValue.label = nil

	-- Clear out importSource.uniqueDeviceId to work around a problem where copies
	-- are deleted by lightroom mobile if it sees them before the copy is completed.

	if copyDocValue.importSource then
		copyDocValue.importSource.uniqueDeviceId = nil
	end

	-- Copy the info about the xmp sidecar as it may hint that a sidecar needs to be
	-- uploaded if we just copied a file with one that wasn't uploaded yet. Because we
	-- currently cannot tell if a /rels/xmp link represents data extracted from the master
	-- or a separate sidecar file entirely we have to assume during a copy that if there's
	-- a /rels/xmp link already that it's from a sidecar. And if there's not, then we'll look
	-- at the source document's xmpSidecarExists
	local xmpSidecarExists = ( derefOpt( docValue, '_localOnly', 'xmpSidecarExists' ) or
							( derefOpt( docValue, '_localOnly', 'xmpSidecarExists' ) ~= false
								and not not WFCatFileSpec.getXmpLinkFromDocValue( docValue ) ) )

	local copyDocLocalOnly = {
		xmpSidecarExists = xmpSidecarExists,
	}

	-- Create a copy payload which is used by the binary upload code.

	local copyId = derefOpt( docValue, 'copy', 'copy_id' )

	local docValueOverlay

	if not copyId then

		local copyInfo = { copy_id = KSIdGenerator.uuidInLowercaseHex() }
		copyDocValue.copy = copyInfo

		-- Add a copy.copy_id to the original document as well.
		docValueOverlay = {
			_id = docValue._id,
			copy = copyInfo
		}

	end

	-- If the source exists on oz then copy the links for the copy operations that will
	-- be issued by the uploader. This is needed to get the correct develop settings
	-- if an asset doesn't have them downloaded locally when copied as they are
	-- usually modified and end up different from other copied assets.

	-- It's possible that the source document being copied from is itself a copy, and does
	-- not contain local binaries or Oz-links to the binaries. When this happens, we
	-- fall back to copying over the source links.

	copyDocLocalOnly.sourceOriginalLink = WFCatFileSpec.getOriginalLinkFromDocValue( docValue )
			or derefOpt( docValue, '_localOnly', 'sourceOriginalLink' )

	copyDocLocalOnly.sourceXmpLink = WFCatFileSpec.getXmpLinkFromDocValue( docValue )
			or derefOpt( docValue, '_localOnly', 'sourceXmpLink' )

	-- have to get xmpDevelopLink a little differently because need to ensure link
	-- and sha256 come from the same source

	local xmpDevelopLink, xmpDevelopSha256 = WFCatFileSpec.getXmpDevelopLinkAndSha256FromDocValue( docValue )

	if not xmpDevelopLink then
		xmpDevelopLink = derefOpt( docValue, '_localOnly', 'sourceXmpDevelopLink' )
		xmpDevelopSha256 = derefOpt( docValue, '_localOnly', 'sourceXmpDevelopSha256' )
	end

	copyDocLocalOnly.sourceXmpDevelopLink = xmpDevelopLink
	copyDocLocalOnly.sourceXmpDevelopSha256 = xmpDevelopSha256

	copyDocLocalOnly.sourceProxyLink = WFCatFileSpec.getProxyLinkFromDocValue( docValue )
			or derefOpt( docValue, '_localOnly', 'sourceProxyLink' )

	copyDocLocalOnly.sourceAuxLinks = WFCatFileSpec.getTableOfAuxLinksFromDocValue( docValue )
			or derefOpt( docValue, '_localOnly', 'sourceAuxLinks' )

	if copyDocLocalOnly.sourceAuxLinks then

		copyDocLocalOnly.sourceAssetLinkForAux = WFCatFileSpec.getSelfLinkFromDocValue( docValue )
				or derefOpt( docValue, '_localOnly', 'sourceAssetLinkForAux' )

	end

	copyDocLocalOnly.sourceAssetId = docValue._id

	if next( copyDocLocalOnly ) ~= nil then
		copyDocValue._localOnly = copyDocLocalOnly
	end

	return copyDocValue, docValueOverlay

end

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

local function maybeCopyFile( sourceFilePath, copyFilePath )

	logger:infof( 'maybeCopyFile: source %s, dest %s', sourceFilePath, copyFilePath )

	if not sourceFilePath then
		return Observable.just( nil )
	end

	local copyFilePathFolder = KSPathUtils.parent( copyFilePath )

	return Observable.just( WFFileUtils.mkdir( copyFilePathFolder ) )

		:flatMap( function()

			return WFAsyncFileUtils.copyToUniquelyNamedFile( sourceFilePath, copyFilePath )

		end )

end

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

local function generateDesktopProxiesInFileInfo( wfCatalog, docValue, copyDocValue, proxies, optProxyType )

	local proxyPath = getFullProxyPathFromDocValue( docValue, optProxyType )

	local proxySize, proxyType = getProxySizeFromDocValue( docValue, optProxyType )

	local proxyCopyPath = proxyType and wfCatalog:fileSpec():pathForProxy( copyDocValue._id, proxyType )

	if proxyPath and proxySize then

		proxies[ #proxies + 1 ] = {

			proxyPath = proxyPath,
			proxyCopyPath = proxyCopyPath,
			proxyType = proxyType,
			proxySize = proxySize,

		}

	end

end

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

local function getLocalAuxReferencedbyDevelopSettingsAndC2pa( currentAuxDigestList, downloadedAux )

	local auxToApply = {}

	for _, auxDigest in pairs( currentAuxDigestList or {} ) do

		local _, downloadInfo = KSTableUtils.findInTable( downloadedAux, function( digest, _ )
			return digest == auxDigest
		end)

		if downloadInfo then
			auxToApply[ auxDigest ] = downloadInfo
		end

	end

	return auxToApply

end

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

local function getAuxReferencedByDevelopSettingsAndC2pa( currentAuxList, auxSection )

	local reducedAuxSection = {}

	for auxId in pairs( currentAuxList or {} ) do
	
		local auxInfo = auxSection[ auxId ]

		if auxInfo then
			reducedAuxSection[ auxId ] = auxInfo
		end

	end

	return reducedAuxSection

end

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

local function getAuxOverlayForCopiedDocValue( sourceDocValue, isCopyingToVersion )

	local currentMasksApplied = derefOpt( sourceDocValue, 'develop', 'masks' ) or {}
	local currentFiltersApplied = derefOpt( sourceDocValue, 'develop', 'filters' ) or {}
	local currentC2paApplied = derefOpt( sourceDocValue, 'importSource', 'c2paIngredients' ) or {}

	local downloadedAux = derefOpt( sourceDocValue, '_localOnly', 'files', 'aux' ) or {}
	local auxSection = derefOpt( sourceDocValue, 'aux' ) or {}

	local auxOverlay = {}

	if next( currentMasksApplied ) then
	
		KSTableUtils.setNested( auxOverlay, currentMasksApplied, 'develop', 'masks' )
		
	end
	
	if next( currentFiltersApplied ) then
	
		KSTableUtils.setNested( auxOverlay, currentFiltersApplied, 'develop', 'filters' )
		
	end
	
	if next( currentC2paApplied ) then
		
		KSTableUtils.setNested( auxOverlay, currentC2paApplied, 'importSource', 'c2paIngredients' )
		
	end
	
	local developAuxDigestList = WFCatAssetAux.extractDevelopAuxDigestsFromDocValue( sourceDocValue )
	local c2paDigestList = WFCatAssetAux.extractC2paAuxDigestFromDocValue( sourceDocValue )
	local currentAuxDigestList = WFTableMerge.applyOverlay( developAuxDigestList, c2paDigestList )
	
	local localAuxAnnotations = getLocalAuxReferencedbyDevelopSettingsAndC2pa( currentAuxDigestList, downloadedAux )

	if next( localAuxAnnotations ) then

		KSTableUtils.setNested( auxOverlay, localAuxAnnotations, '_localOnly', 'files', 'aux' )

	end

	if not isCopyingToVersion then

		if next( auxSection ) then

			auxOverlay.aux = getAuxReferencedByDevelopSettingsAndC2pa( currentAuxDigestList, auxSection )

		end

	end

	return auxOverlay

end

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

local NmbLocalRenditionDigest

local function getFileInfo_Desktop( wfCatalog, docValue, copyDocValue, optArgs )

	if not NmbLocalRenditionDigest then

		NmbLocalRenditionDigest = require 'NmbLocalRenditionDigest'
			-- WF.lightroomcc

	end

	local originalPath = getFullOriginalPathFromDocValue( docValue )
	local originalCopyPath = originalPath and WFFileUtils.ensureFileNameIsUnique( originalPath )
	local originalSize = getOriginalSizeFromDocValue( docValue )

	local xmpSidecarPath = WFCatFileSpec.getXmpSidecarPathFromDocValue( docValue )
	local xmpSidecarCopyPath = wfCatalog:fileSpec():pathForXmpSidecar( copyDocValue._id )
	local xmpSidecarSize = WFCatFileSpec.getXmpSidecarFileSizeFromDocValue( docValue )
	local xmpSidecarExists = WFCatFileSpec.getXmpSidecarExistsFromDocValue( docValue )

	local xmpDevelopInfo = WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue )
		-- Don't bother copying devSettings as it shares the same path for all
		-- assets that have the same settings sha256.

	local renditions = {}

	local md5XmpCameraRawSrc = NmbLocalRenditionDigest.maybeComputeFromDocValue( docValue )

	local md5XmpCameraRawCpy = NmbLocalRenditionDigest.maybeComputeFromDocValue( copyDocValue )

	if md5XmpCameraRawSrc and md5XmpCameraRawCpy then

		for _, longEdge in ipairs( { 320, 640, 1280, 2048, 'full' } ) do

			local renditionPath = wfCatalog:fileSpec():pathForPreviewDigestAndLongEdge(
					docValue._id, md5XmpCameraRawSrc, longEdge, false )

			if renditionPath and WFFileUtils.exists( renditionPath ) == "file" then

				local renditionCopyPath = wfCatalog:fileSpec():pathForPreviewDigestAndLongEdge(
						copyDocValue._id, md5XmpCameraRawCpy, longEdge, true )

				table.insert( renditions, {
						renditionPath = renditionPath,
						renditionCopyPath = renditionCopyPath
					} )

			end

		end

	end

	if #renditions == 0 then
		-- Think this will only ever happen in unit tests.
		table.insert( renditions, {} )
				-- add an empty value so takeLast( 1 ) below will always get something.
	end

	local isVersion = WFCatIndexUtils.isVersionDocument( copyDocValue )

	local fileInfo = {

			renditions = renditions,

			originalPath = originalPath,
			originalCopyPath = originalCopyPath,
			originalSize = originalSize,

			xmpSidecarPath = xmpSidecarPath,
			xmpSidecarCopyPath = xmpSidecarCopyPath,
			xmpDevelopInfo  = xmpDevelopInfo,
			xmpSidecarSize = xmpSidecarSize,
			xmpSidecarExists = xmpSidecarExists,

			auxOverlay = getAuxOverlayForCopiedDocValue( docValue, isVersion ),

		}

	local proxies = {}
	
	if WFCatIndexUtils.isVideoDocument( docValue ) then 
		
		for _, proxyType in ipairs( { '1080p', '1080p709', '360p', '720p' } ) do
		
			generateDesktopProxiesInFileInfo( wfCatalog, docValue, copyDocValue, proxies, proxyType )

		end

	else

		generateDesktopProxiesInFileInfo( wfCatalog, docValue, copyDocValue, proxies )

	end

	if not next( proxies ) then
		proxies = nil
	end

	fileInfo.proxies = proxies

	return fileInfo

end

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

local function extractCopyOptions( copyOptions )

	copyOptions = copyOptions or {}

	local needToCopyOriginal = copyOptions.needToCopyOriginal ~= false
	local needToCopyProxy = copyOptions.needToCopyProxy ~= false
	local needToCopyRenditions = copyOptions.needToCopyRenditions ~= false
	local needToCopyExternalDevelopSettings = copyOptions.needToCopyExternalDevelopSettings ~= false
	local needToCopyXmpSidecar = copyOptions.needToCopyXmpSidecar ~= false
	local needToCopyAux = copyOptions.needToCopyAux ~= false

	return needToCopyOriginal, needToCopyProxy, needToCopyRenditions,
			needToCopyExternalDevelopSettings, needToCopyAux, needToCopyXmpSidecar

end

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

local function updateDocValueAndOverlay_Desktop( copyDocValue, fileInfo, copyOptions )

	local needToCopyOriginal, needToCopyProxy, _,
			needToCopyExternalDevelopSettings, needToCopyAux, needToCopyXmpSidecar
					= extractCopyOptions( copyOptions )

	local originalInfo = {

		master = needToCopyOriginal and
			fileInfo.originalCopyPath and {
				relativePath = WFFileUtils.removeVolumeFromPath( fileInfo.originalCopyPath ),
				volumeGuid = WFFileUtils.getVolumeGuidForPath( fileInfo.originalCopyPath ),
				size = fileInfo.originalSize
			} or nil,

	}

	if not WFCatIndexUtils.isVideoDocument( copyDocValue ) then

		originalInfo [ 'xmp' ] = needToCopyXmpSidecar and
			fileInfo.xmpSidecarCopyPath and {
				path = fileInfo.xmpSidecarCopyPath,
				fileSize = fileInfo.xmpSidecarSize
			} or nil

	end

	if next( originalInfo ) == nil then
		originalInfo = nil
	end

	local proxyInfo = { }

	if needToCopyProxy and fileInfo.proxies then

		for _, proxy in ipairs( fileInfo.proxies ) do

			local proxyType = proxy.proxyType

			if proxyType and proxy.proxyCopyPath then

				proxyInfo[ proxyType ] = {
					relativePath = WFFileUtils.removeVolumeFromPath( proxy.proxyCopyPath ),
					volumeGuid = WFFileUtils.getVolumeGuidForPath( proxy.proxyCopyPath ),
					size = proxy.proxySize,
				}

			end

		end

	end

	if next( proxyInfo ) == nil then
		proxyInfo = nil
	end

	local xmpSidecarExists = needToCopyXmpSidecar and fileInfo.xmpSidecarExists

	local overlay = {
		_localOnly = {
			xmpSidecarExists = xmpSidecarExists,
		}
	}

	local files = {
		original = originalInfo,
		xmp_develop = needToCopyExternalDevelopSettings and fileInfo.xmpDevelopInfo or nil,
		proxy = proxyInfo
	}

	if next( files ) then
		KSTableUtils.setNested( overlay, files, '_localOnly', 'files' )
	end

	if needToCopyAux and fileInfo.auxOverlay then
		WFTableMerge.applyOverlay( overlay, fileInfo.auxOverlay )
	end

	WFTableMerge.applyOverlay( copyDocValue, overlay )

	return copyDocValue

end

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

local function absolutePath( catalogId, relativePath )

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

	local absolutePath

	if WFPlatformInfo.isiOS then

		if relativePath then

			if string.sub( relativePath, 1, 1 ) == '/' then

				absolutePath = relativePath

			else

				absolutePath = OzFilePaths.getAbsolutePathInCatalog( catalogId, relativePath )

			end

		end

	elseif WFPlatformInfo.isAndroid then

		absolutePath = OzCatalogUploadStatus.checkAndCorrectLocalPath( relativePath )

	end

	return absolutePath

end

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

local function buildPathsFromSourceRelativePath( catalogId, sourceRelativePath, maybeSourceAbsolutePath )

	local sourceAbsolutePath = maybeSourceAbsolutePath

	if not sourceAbsolutePath then
		sourceAbsolutePath = sourceRelativePath and absolutePath( catalogId, sourceRelativePath )
	end

	local copyAbsolutePath = sourceAbsolutePath
			and WFFileUtils.ensureFileNameIsUnique( sourceAbsolutePath )

	local copyFileName = copyAbsolutePath and KSPathUtils.leafName( copyAbsolutePath )
	local relativeBasePath = sourceRelativePath and KSPathUtils.parent( sourceRelativePath )
	local copyRelativePath = relativeBasePath and copyFileName
			and KSPathUtils.child( relativeBasePath, copyFileName )

	return sourceAbsolutePath, copyAbsolutePath, copyRelativePath

end

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

local function generateMobileProxiesTableInFileInfo( catalogId, docValue, proxies, optProxyType )

	local proxyRelativePath = getProxyRelativePathFromDocValue( docValue, optProxyType )
	local proxySize = getProxySizeFromDocValue( docValue, optProxyType )
	local proxyContentId = getProxyContentIdFromDocValue( docValue, optProxyType )

	if proxyRelativePath and proxySize then

		local proxyPath, proxyCopyPath, proxyCopyRelativePath = buildPathsFromSourceRelativePath( catalogId, proxyRelativePath )

		proxies[ #proxies + 1 ] = {
			proxyPath = proxyPath,
			proxyCopyPath = proxyCopyPath,
			proxyCopyRelativePath = proxyCopyRelativePath,
			proxySize = proxySize,
			proxyContentId = proxyContentId,
			proxyType = optProxyType,
		}

	end

end

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

local function getMobileFileInfo_proxies( catalogId, docValue, fileInfo )

	local proxies = {}

	if WFCatIndexUtils.isVideoDocument( docValue ) then

		for _, proxyType in ipairs( { '1080p', '360p' } ) do
			generateMobileProxiesTableInFileInfo( catalogId, docValue, proxies, proxyType )
		end

	else

		generateMobileProxiesTableInFileInfo( catalogId, docValue, proxies )

	end

	if not next( proxies ) then
		proxies = nil
	end

	fileInfo.proxies = proxies

end

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

local function getMobileFileInfo( catalogId, docValue )

	local fileInfo = {}

	-- Original

	local originalRelativePath = getOriginalRelativePathFromDocValue( docValue )

	local originalPath, originalCopyPath, originalCopyRelativePath
			= buildPathsFromSourceRelativePath( catalogId, originalRelativePath )

	fileInfo.originalPath = originalPath
	fileInfo.originalCopyPath = originalCopyPath
	fileInfo.originalCopyRelativePath = originalCopyRelativePath

	fileInfo.originalSize = getOriginalSizeFromDocValue( docValue )
	fileInfo.originalContentId = getOriginalContentIdFromDocValue( docValue )

	getMobileFileInfo_proxies( catalogId, docValue, fileInfo )

	return fileInfo

end

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

local function getFileInfo_iOS( wfCatalog, docValue, copyDocValue, optArgs )

	local catalogId = wfCatalog:ozCatalogId()

	local fileInfo = getMobileFileInfo( catalogId, docValue )

	-- On iOS, the rendition and external xmp paths follows this pattern:
	-- path = 'blobs/<first 2 letters of asset id>/asset id/filename

	local documentId = copyDocValue._id

	local function getBlobPathsFromSourceRelativePath( sourceRelativePath )

		local sourceAbsolutePath, copyRelativePath, copyAbsolutePath

		if sourceRelativePath then

			local fileName = KSPathUtils.leafName( sourceRelativePath )

			sourceAbsolutePath = absolutePath( catalogId, sourceRelativePath )

			local relativeBlobPath = OzFilePaths.getRelativeBlobPath( documentId )

			copyRelativePath = string.format( "%s/%s", relativeBlobPath, fileName )
			copyAbsolutePath = absolutePath( catalogId, copyRelativePath )

		end

		return sourceAbsolutePath, copyAbsolutePath, copyRelativePath

	end

	local externalXmpRelativePath = getLocalExternalDevelopSettingsPath( docValue )

	local externalXmpPath, externalXmpCopyPath, externalXmpCopyRelativePath
			= getBlobPathsFromSourceRelativePath( externalXmpRelativePath )

	fileInfo.externalXmpCopyRelativePath = externalXmpCopyRelativePath
	fileInfo.externalXmpPath = externalXmpPath
	fileInfo.externalXmpCopyPath = externalXmpCopyPath

	fileInfo.externalXmpFileSize = getLocalExternalDevelopSettingsSize( docValue )
	fileInfo.externalXmpContentId = getLocalExternalDevelopSettingsContentId( docValue )
			 or derefOpt( docValue, 'develop', 'xmpCameraRaw', 'sha256' )

	local renditions = {}

	for _, renditionName in ipairs( { 'thumbnail2x', 'preview', 'fullsize' } ) do

		local renditionRelativePath = derefOpt( docValue, '_localOnly', 'files', renditionName, 'path' )

		local renditionPath, renditionCopyPath, renditionCopyRelativePath
				= getBlobPathsFromSourceRelativePath( renditionRelativePath )

		local renditionContentId = derefOpt( docValue, '_localOnly', 'files', renditionName, 'contentId' )
		local renditionSize = derefOpt( docValue, '_localOnly', 'files', renditionName, 'fileSize' )

		table.insert( renditions, {
				renditionPath = renditionPath,
				renditionCopyPath = renditionCopyPath,
				renditionCopyRelativePath = renditionCopyRelativePath,
				renditionContentId = renditionContentId,
				renditionSize = renditionSize,
				renditionName = renditionName
			} )

	end

	fileInfo.renditions = renditions

	local isVersion = WFCatIndexUtils.isVersionDocument( copyDocValue )

	fileInfo.auxOverlay = getAuxOverlayForCopiedDocValue( docValue, isVersion )

	return fileInfo

end

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

local function getFileInfo_Android( wfCatalog, docValue, copyDocValue, optArgs )

	local catalogId = wfCatalog:ozCatalogId()

	optArgs = optArgs or {}

	local fileInfo = getMobileFileInfo( catalogId, docValue )

	fileInfo.xmpDevelopInfo = WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue )

	if optArgs.isXmpDevelopCopyingToNewPath then

		local externalXmpSha256 = KSTableUtils.derefOpt( fileInfo, 'xmpDevelopInfo', 'sha256' )
		local spaceId = copyDocValue._spaceId

		if externalXmpSha256 and spaceId then

			local _, externalXmpPath = OzFilePaths.getXmpDevelopPath( catalogId, docValue )

			local externalXmpCopyRelativePath, externalXmpCopyPath = OzFilePaths.getXmpDevelopPath( catalogId, copyDocValue )

			fileInfo.externalXmpCopyRelativePath = externalXmpCopyRelativePath
			fileInfo.externalXmpPath = externalXmpPath
			fileInfo.externalXmpCopyPath = externalXmpCopyPath

		end

	end


	local renditions = {}

	for _, renditionName in ipairs( { 'thumbnail2x', 'preview', 'fullsize' } ) do

		-- On Android, renditions are stored within a DB and are referenced by an MD5
		-- hash of the rendition. This means that if multiple images have the same
		-- rendition blobs, they will exist within the same place on the DB; there's
		-- not need to copy renditions bits on Android.

		local renditionInfo = derefOpt( docValue, '_localOnly', renditionName )

		-- renditionInfo is a table containing the following:

		--[[ local renditionInfo = {
				md5 = < MD5 has of the rendition KSData blob >,
				revId = < the asset revision ID >,
				epoch = < epoch value >,
				size = < size of the rendition> ,
			} ]]--

		-- To copy a rendition from one doc to another, all we need to do is copy this
		-- rendition info to the new document and adjust the revId.

		if renditionInfo then

			renditionInfo.revId = copyDocValue._newRevId

			table.insert( renditions, {
					renditionName = renditionName,
					renditionInfo = renditionInfo
				} )

		end

	end

	fileInfo.renditions = renditions

	local isVersion = WFCatIndexUtils.isVersionDocument( copyDocValue )

	fileInfo.auxOverlay = getAuxOverlayForCopiedDocValue( docValue, isVersion )

	return fileInfo

end

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

local function updateDocValueAndOverlay_iOS( copyDocValue, fileInfo, copyOptions )

	local needToCopyOriginal, needToCopyProxy, needToCopyRenditions,
			needToCopyExternalDevelopSettings, needToCopyAux = extractCopyOptions( copyOptions )

	local files = {}

	if needToCopyOriginal and fileInfo.originalCopyRelativePath then

		files.master = {

			path = fileInfo.originalCopyRelativePath,
			contentId = fileInfo.originalContentId,
			fileSize = fileInfo.originalSize

		}

	end

	if needToCopyProxy then

		if WFCatIndexUtils.isVideoDocument( copyDocValue ) and fileInfo.proxies then

			local proxies = {}

			for _, proxy in ipairs( fileInfo.proxies ) do

				local proxyType = proxy.proxyType

				if proxyType and proxy.proxyCopyRelativePath then

					proxies[ proxyType ] = {
						relativePath = proxy.proxyCopyRelativePath,
						contentId = proxy.proxyContentId,
						fileSize = proxy.proxySize
					}
				end

			end

			files.proxies = proxies

		else
			-- image assets only have one proxy currently if available
			local proxyCopyRelativePath = derefOpt( fileInfo, 'proxies', 1, 'proxyCopyRelativePath' )

			if proxyCopyRelativePath then

				files.proxy = {
					path = derefOpt( fileInfo, 'proxies', 1, 'proxyCopyRelativePath' ),
					contentId = derefOpt( fileInfo, 'proxies', 1, 'proxyContentId' ),
					fileSize = derefOpt( fileInfo, 'proxies', 1, 'proxySize' )
				}

			end

		end

	end

	if needToCopyRenditions then

		for _, rendition in ipairs( fileInfo.renditions ) do

			local renditionName = rendition.renditionName

			if renditionName and rendition.renditionCopyRelativePath then

				files[ renditionName ] = {

					path = rendition.renditionCopyRelativePath,
					contentId = rendition.renditionContentId,
					fileSize = rendition.renditionSize

				}

			end

		end

	end

	if needToCopyExternalDevelopSettings and fileInfo.externalXmpCopyRelativePath then

		files.xmp_develop = {

			path = fileInfo.externalXmpCopyRelativePath,
			contentId = fileInfo.externalXmpContentId,
			fileSize = fileInfo.externalXmpFileSize

		}

	end

	local overlay = {}
	
	if next( files ) then
	
		overlay._localOnly = {
			files = files
		}

	end

	if needToCopyAux and fileInfo.auxOverlay then
		WFTableMerge.applyOverlay( overlay, fileInfo.auxOverlay )
	end

	if next( overlay ) then
		WFTableMerge.applyOverlay( copyDocValue, overlay )
	end

	return copyDocValue

end

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

local function updateDocValueAndOverlay_Android( copyDocValue, fileInfo, copyOptions )

	local needToCopyOriginal, needToCopyProxy, needToCopyRenditions,
			needToCopyExternalDevelopSettings, needToCopyAux = extractCopyOptions( copyOptions )

	local localOnly = {}

	if needToCopyOriginal and fileInfo.originalCopyRelativePath then

		localOnly.originalFilePath = fileInfo.originalCopyRelativePath

		localOnly.original = {

			size = fileInfo.originalSize

		}

	end

	if needToCopyProxy then

		if WFCatIndexUtils.isVideoDocument( copyDocValue ) and fileInfo.proxies then
			local proxies = {}

			for _, proxy in ipairs( fileInfo.proxies ) do

				local proxyType = proxy.proxyType

				if proxyType and proxy.proxyCopyRelativePath then

					proxies[ proxyType ] = {
						relativePath = proxy.proxyCopyRelativePath,
						contentId = proxy.proxyContentId,
						fileSize = proxy.proxySize
					}
				end

			end

			KSTableUtils.setNested( localOnly, proxies, 'files', 'proxies' )

		else

			-- image assets only have one proxy currently if available
			local proxyCopyRelativePath = derefOpt( fileInfo, 'proxies', 1, 'proxyCopyRelativePath' )

			if proxyCopyRelativePath then

				localOnly.proxy = {
					filePath = derefOpt( fileInfo, 'proxies', 1, 'proxyCopyRelativePath' ),
					size = derefOpt( fileInfo, 'proxies', 1, 'proxySize' )
				}

			end

		end

	end

	if needToCopyRenditions then

		for _, rendition in ipairs( fileInfo.renditions ) do

			local renditionName = rendition.renditionName

			if renditionName and rendition.renditionInfo then

				localOnly[ renditionName ] = rendition.renditionInfo

			end

		end

	end

	if not WFCatIndexUtils.isVideoDocument( copyDocValue ) and needToCopyExternalDevelopSettings and fileInfo.xmpDevelopInfo then

		KSTableUtils.setNested( localOnly, fileInfo.xmpDevelopInfo, 'files', 'xmp_develop' )

	end

	local overlay = {
		_localOnly = localOnly
	}

	if needToCopyAux and fileInfo.auxOverlay then
		WFTableMerge.applyOverlay( overlay, fileInfo.auxOverlay )
	end

	if next( overlay ) then
		WFTableMerge.applyOverlay( copyDocValue, overlay )
	end

	return copyDocValue

end

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

local function copyFilesToNewDoc( wfCatalog, docValue, copyDocValue, docValueOverlay, copyRenditionsFn, optArgs )

	local fileInfo = {}

	if WFPlatformInfo.isDesktop then
		fileInfo = getFileInfo_Desktop( wfCatalog, docValue, copyDocValue, optArgs )
	elseif WFPlatformInfo.isiOS then
		fileInfo = getFileInfo_iOS( wfCatalog, docValue, copyDocValue, optArgs )
	elseif WFPlatformInfo.isAndroid then
		fileInfo = getFileInfo_Android( wfCatalog, docValue, copyDocValue, optArgs )
	end

	local renditions = fileInfo.renditions or {}

	local shouldCopyOriginal = false
	local shouldCopyProxy = false

	if optArgs.isContributorCopyingToSharedAlbum then

		-- For In-App Contribute, 'contributor' or 'editor' role has the ability to add images into
		-- a shared album. Once added to the album, the 'contributor' does not have access to the Original
		-- or the Smart Preview, but 'editor' continue to have access to the Original or the Smart Preview.

		-- When copying a document into a shared album as a contributor or editor, we should
		-- only copy across the binaries when the source asset is not on Oz or when there are no
		-- local renditions. In these cases, the binaries are necessary in order to a.) perform the
		-- binary upload to Oz or b.) render something in the grid. In all other cases, copying the
		-- binaries isn't necessary and should not be done.

		-- Note that the following logic isn't perfect, but it should greatly reduce the amount
		-- of unneccessary binary copies we perform. Because the user's subscription status can affect
		-- what needs to be copied - which can change after an asset is added to an album but before
		-- that asset is uploaded - copying too often is the safe approach. It should be noted that
		-- performing a copy when we shouldn't isn't a huge issue, other than that it takes up
		-- extra resources. Known bugs caused by this needless copying have already been addressed.

		local sourceDocHasOzMasterUrl, isOzOriginalInvalid = WFCatFileSpec.getOriginalLinkFromDocValue( docValue )

		if not sourceDocHasOzMasterUrl or isOzOriginalInvalid then

			shouldCopyOriginal = true

		end

		local sourceDocHasOzProxyUrl, _, isOzProxyInvalid = WFCatFileSpec.getProxyLinkFromDocValue( docValue )

		if not sourceDocHasOzProxyUrl or isOzProxyInvalid then

			shouldCopyProxy = true

		end

		if not next( renditions ) then

			shouldCopyOriginal = true
			shouldCopyProxy = true

		end

	else

		shouldCopyOriginal = true
		shouldCopyProxy = true

	end

	if not next( renditions ) then
		renditions = { {} }
	end

	return Observable.from( renditions )

		:flatMap( function( renditionInfo )

			-- Copy the renditions so the image appears immediately and copy will
			-- work without a proxy or original when offline.

			if copyRenditionsFn then

				local docId = copyDocValue._id or copyDocValue._localId

				return copyRenditionsFn ( renditionInfo, docId )

					:take ( 1 )

					:map( function ( newRenditionInfo )

						renditionInfo = newRenditionInfo

						return renditionInfo

					end)
			else
				return Observable.just( nil )
			end

		end )

		:takeLast( 1 )

		:flatMap( function()

			local maybeCopyOriginal = Observable.just( nil )
			local maybeCopyProxiesArray = {}

			if shouldCopyOriginal then
				maybeCopyOriginal = maybeCopyFile( fileInfo.originalPath, fileInfo.originalCopyPath )
			end

			if shouldCopyProxy and fileInfo.proxies then

				for _,proxy in ipairs( fileInfo.proxies ) do
						local maybeCopyProxy = maybeCopyFile( proxy.proxyPath, proxy.proxyCopyPath )
						table.insert( maybeCopyProxiesArray, maybeCopyProxy )

				end

			end

			if not next( maybeCopyProxiesArray ) then
				maybeCopyProxiesArray = { Observable.just( nil ) }
			end

			return Observable.combineLatest(
				maybeCopyOriginal,
				maybeCopyFile( fileInfo.xmpSidecarPath, fileInfo.xmpSidecarCopyPath ),
				maybeCopyFile( fileInfo.externalXmpPath, fileInfo.externalXmpCopyPath ),
				-- unpack table here
				unpack( maybeCopyProxiesArray ))

		end )

		:map( function( originalCopyPath, xmpSidecarCopyPath, externalXmpCopyPath, ... )

	 		-- Update absolute paths in fileInfo

			fileInfo.originalCopyPath = originalCopyPath

			fileInfo.xmpSidecarCopyPath = xmpSidecarCopyPath
			fileInfo.externalXmpCopyPath = externalXmpCopyPath

			-- Update relative paths in fileInfo

			local getNewRelativeCopyPath = function( copyAbsolutePath, copyRelativePath )

				local copyFileName = copyAbsolutePath and KSPathUtils.leafName( copyAbsolutePath )
				local relativeBasePath = copyRelativePath and KSPathUtils.parent( copyRelativePath )
				local newCopyRelativePath = relativeBasePath and copyFileName
						and KSPathUtils.child( relativeBasePath, copyFileName )

				return newCopyRelativePath

			end

			fileInfo.originalCopyRelativePath = getNewRelativeCopyPath( originalCopyPath, fileInfo.originalCopyRelativePath )

			fileInfo.externalXmpCopyRelativePath = getNewRelativeCopyPath( externalXmpCopyPath, fileInfo.externalXmpCopyRelativePath )

			local hasProxies = ...

			if not hasProxies then

				fileInfo.proxies = nil

			else

				--Update proxies if available
				for i, proxyCopyPath in ipairs( { hasProxies } ) do
					KSTableUtils.setNested( fileInfo, proxyCopyPath, 'proxies', i, 'proxyCopyPath' )
					local proxyCopyRelativePath = derefOpt( fileInfo, 'proxies', i, 'proxyCopyRelativePath' )
					KSTableUtils.setNested( fileInfo, getNewRelativeCopyPath( proxyCopyPath, proxyCopyRelativePath ), 'proxies', i, 'proxyCopyRelativePath' )
				end

			end

			if WFPlatformInfo.isDesktop then
				copyDocValue = updateDocValueAndOverlay_Desktop( copyDocValue, fileInfo )
			elseif WFPlatformInfo.isiOS then
				copyDocValue = updateDocValueAndOverlay_iOS( copyDocValue, fileInfo )
			elseif WFPlatformInfo.isAndroid then
				copyDocValue = updateDocValueAndOverlay_Android( copyDocValue, fileInfo )
			end

			return copyDocValue, docValueOverlay

	end )

end

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

local function getAllAlbumsForAssets( wfCatalog, assetIds )

	if WFPlatformInfo.isDesktop then

		return wfCatalog:indexStore():queryOnce {
				sql = [[SELECT album_asset_v2.assetId, album_asset_v2.albumId
							FROM CARRAY( ?, ?, 'int32' ) as ai
							JOIN assets ON ai.value == assets.docId
							JOIN album_asset_v2 ON assets.assetId == album_asset_v2.assetId]],
				quiesceUntilUpToDate = true,
				assetIds,
				#assetIds,
			}

	else

	 	return wfCatalog:indexStore():queryOnce {
				sql = [[SELECT album_asset.assetId, album_asset.albumId
							FROM CARRAY( ?, ?, 'char*' ) as ai
							JOIN album_asset ON ai.value == album_asset.assetId]],
				quiesceUntilUpToDate = true,
				assetIds,
				#assetIds,
			}

	end

end

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

local function getAlbumAssetMappings( wfCatalog, assetIds, albumIds )

	if ( not albumIds ) or ( not next( albumIds ) ) then

		return Observable.just( {} )

	end

	-- get the albums that each asset is in

	return Observable.combineLatest(
			getAllAlbumsForAssets( wfCatalog, assetIds ),
			WFCatIdUtils.getOzAlbumIdsForLocalId( wfCatalog, albumIds ) )

		:take( 1 )

		:map( function( albumAssetRows, selectedAlbumIds )

			-- build the albums we care about so we can easily filter

			local selectedAlbumMap = {}

			for _, albumId in ipairs( selectedAlbumIds ) do
				selectedAlbumMap[ albumId ] = true
			end

			local assetToAlbumMap = {}

			for _, albumAsset in ipairs( albumAssetRows ) do

				local album = albumAsset[ 2 ]

				if selectedAlbumMap[ album ] then

					local asset = albumAsset[ 1 ]

					local albums = assetToAlbumMap[ asset ] or {}
					table.insert( albums, album )

					assetToAlbumMap[ asset ] = albums

				end

			end

			return assetToAlbumMap

		end )

end

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

local defaultCopyRenditionsFn = function( renditionInfo )

	local sourceFilePath = renditionInfo.renditionPath
	local copyFilePath = renditionInfo.renditionCopyPath

	logger:infof( 'defaultCopyRenditionsFn: source %s, dest %s', sourceFilePath, copyFilePath )

	if not sourceFilePath then
		return Rx.Observable.just( nil )
	end

	local copyFilePathFolder = KSPathUtils.parent( copyFilePath )

	return Rx.Observable.just( WFFileUtils.mkdir( copyFilePathFolder ) )

		:flatMap( function()
			return WFAsyncFileUtils.copyToUniquelyNamedFile( sourceFilePath, copyFilePath )
		end )

end

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

--[[!
	\brief Create a copy of each of the assets in the provided assetIds array.

	\param wfCatalog (WFCatalog) the catalog
	\param assetIds (table) An array of asset-ids that are being copied.
	\param albumIds (optional table) An array of album-ids
	\param stackIdsByAlbumIds (optional table) A table of stack-ids whithin which
		copies are being created, keyed by ids of assets having copies created
	\param copiedDocModifierFn (optional function)
	\param optIncludeAssetId (optional boolean) If false, the source asset will
		not be modified to include the copy ID. Defaults to true.
	\param copyRenditionsFn (optional function) Needed for android to copy
		the renditions of previewCache from one asset to another asset,
		The method returns an observable which emits a renditionInfo value after successful copy of the rendition
	\param optArgs (optional table) A table of optional arguments. Current args include:
		isContributorCopyingToSharedAlbum (boolean): Set to true if the caller of this API
			is a contributor to the specified shared album
		isXmpDevelopCopyingToNewPath (boolean): Set to true if the caller of this API is creating
			copies to a different catalog or space

	\return Rx.Observable() that completes when done.
--]]

function WFCatCopies.createCopiesOfAssets( wfCatalog, assetIds, albumIds,
		stackIdsByAssetIds, copiedDocModifierFn, optIncludeAssetId, copyRenditionsFn,
		optArgs )

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

	optArgs = optArgs or {}

	local stackIds = {}
	local stackAssetIds = {}

	local noopFn = function( ... ) return ... end

	copiedDocModifierFn = copiedDocModifierFn or noopFn
	optIncludeAssetId = optIncludeAssetId ~= false

	stackIdsByAssetIds = stackIdsByAssetIds or {}

	for assetId, stackId in pairs( stackIdsByAssetIds ) do

		table.insert( stackIds, stackId )
		table.insert( stackAssetIds, assetId )

	end

	local copiedAssetIds = {}

	local docStore = wfCatalog:docStore()

	copyRenditionsFn = copyRenditionsFn or defaultCopyRenditionsFn

	return Observable.from( assetIds )

		:bufferWithCount( kBatchSize )

		:flatMap( function( batchOfAssetIds )

			return Observable.combineLatest( docStore:bulkDocValue( batchOfAssetIds ),
											getAlbumAssetMappings( wfCatalog, batchOfAssetIds, albumIds ),
											WFCatIdUtils.getOzAssetIdsForLocalIds( wfCatalog, stackIds ),
											WFCatIdUtils.getOzAssetIdsForLocalIds( wfCatalog, stackAssetIds ) )

				:flatMap( function( docValues, assetToAlbumMap, ozStackIds, ozAssetIds )

					-- Construct stackIdsByAssetIds' equivalent with Oz ids.
					local ozStackIdsByOzAssetIds = {}

					for i, localAssetId in ipairs( stackAssetIds ) do
						if ozAssetIds[ i ] and ozStackIds[ i ] then
							ozStackIdsByOzAssetIds[ ozAssetIds[ i ] ] = ozStackIds[ i ]
						end
					end

					return Observable.from( docValues )

						:map( function( docValue )

							local copyValue, docValueOverlay = createCopyOfDocValue( docValue )

							copyValue = copiedDocModifierFn( copyValue )

							docValueOverlay = optIncludeAssetId and docValueOverlay or nil

							return docValue, copyValue, docValueOverlay

						end )

						:flatMap( function( docValue, copyDocValue, docValueOverlay )
							return copyFilesToNewDoc( wfCatalog, docValue, copyDocValue, docValueOverlay, copyRenditionsFn, optArgs )
						end )

						:map( function( newDocValue, docValueOverlay )

							-- WF-700/WF-1366: Due to some bad LrClassic behavior there can be assets with willNotUploadMaster = true
							-- but also a master. If we have a sourceOriginalLinkForCopy or a master file locally, make
							-- sure that willNotUploadMaster is definitely not in the copied docValue's importSource section

							if derefOpt( newDocValue, 'importSource', 'willNotUploadMaster' ) and
									( derefOpt( newDocValue, '_localOnly', 'sourceOriginalLink' ) or
										LrAssetUtils.hasMasterLocally( newDocValue ) ) then

								KSTableUtils.setNested( newDocValue, nil, 'importSource', 'willNotUploadMaster' )

							end

							return newDocValue, docValueOverlay

						end )

						:scan( function( newDocValues, existingDocOverlays, copyDocValue, existingDocOverlay )

							table.insert( copiedAssetIds, copyDocValue._id )

							table.insert( newDocValues, copyDocValue )

							local sourceAssetId = KSTableUtils.derefOpt( copyDocValue, "_localOnly", "sourceAssetId" )

							if sourceAssetId then

								local albumsForCopiedAsset = assetToAlbumMap[ sourceAssetId ]

								if albumsForCopiedAsset then

									for _, albumId in ipairs( albumsForCopiedAsset ) do

										-- for the album, create an album_asset record for the copy

										local albumAssetDocValue = WFCatAlbums.createAlbumAssetDocument( albumId, copyDocValue._id )

										albumAssetDocValue = copiedDocModifierFn( albumAssetDocValue )

										table.insert( newDocValues, albumAssetDocValue )

									end
								end

								local ozStackId = ozStackIdsByOzAssetIds[ sourceAssetId ]

								if ozStackId then
									table.insert( newDocValues, WFCatStacks.createStackAssetDocument( ozStackId, copyDocValue._id ) )
								end

							end

							if existingDocOverlay then
								table.insert( existingDocOverlays, existingDocOverlay )
							end

							return newDocValues, existingDocOverlays

						end, {}, {} )

						:takeLast( 1 )

						:flatMap( function( copyDocValues, existingDocOverlays )

							local result

							if #existingDocOverlays > 0 then
								result = docStore:bulkApplyOverlay( existingDocOverlays )

							else
								result = Observable.just( true )
							end

							if #copyDocValues > 0 then
								result = result:merge( docStore:bulkSetDocValue( copyDocValues ) )
							end

							return result:takeLast( 1 )

						end )

				end )

				:call( WFRxRunUntilDone )
					-- Using runUntilDone within the flatMap so each buffer of
					-- kBatchSize runs to completion to ensure corresponding files
					-- are copied. This is to handle the case if the model was
					-- closed to terminate the action.

		end )

		:takeLast( 1 )

		:map( function()
			return copiedAssetIds
		end )

end

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

--[[!
	\brief Copy file paths from the source doc value to the destination doc value.
		This updates the file paths to be unique and document specific.

	\param wfCatalog (WFCatalog) the catalog
	\param srcDocValue (table) The source doc value.
	\param dstDocValue (table) The destination doc value.
	\param copyOptions (optional table) A table of options that affect the operation:
		needToCopyOriginal (boolean) Defaults to true. If false the original path will be
			copied from the srcDocValue to the dstDocValue, but the filesToCopy return value
			won't include an entry for the original.
		needToCopyProxy (boolean) Defaults to true. If false the proxy path will be
			copied from the srcDocValue to the dstDocValue, but the filesToCopy return value
			won't include an entry for the proxy.
		needToCopyRenditions (boolean) Defaults to true. If false the rendition paths will be
			copied from the srcDocValue to the dstDocValue, but the filesToCopy return value
			won't include entries for the renditions.
		needToCopyExternalDevelopSettings (boolean) Defaults to true. If false the external develop
			settings path will be copied from the srcDocValue to the dstDocValue, but the filesToCopy
			return value won't include an entry for the settings.
		needToCopyXmpSidecar (boolean) Defaults to true. If false the xmp sidecar path will be
			copied from the srcDocValue to the dstDocValue, but the filesToCopy return value
			won't include an entry for the sidecar.
	\return
		updatedDestDocValue (table) The new destination docValue updated to include
			files paths.
		filesToCopy (table) A table of source and destination file paths that have yet to be
			moved or copied.
--]]

function WFCatCopies.calculateCopyPathsAndUpdateDocValue( wfCatalog, srcDocValue, dstDocValue, copyOptions )

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

	local needToCopyOriginal, needToCopyProxy, needToCopyRenditions,
			needToCopyExternalDevelopSettings, _, needToCopyXmpSidecar
					= extractCopyOptions( copyOptions )

	local fileInfo = {}

	if WFPlatformInfo.isDesktop then
		fileInfo = getFileInfo_Desktop( wfCatalog, srcDocValue, dstDocValue )
	elseif WFPlatformInfo.isiOS then
		fileInfo = getFileInfo_iOS( wfCatalog, srcDocValue, dstDocValue )
	elseif WFPlatformInfo.isAndroid then
		fileInfo = getFileInfo_Android( wfCatalog, srcDocValue, dstDocValue )
	end

	local filesToCopy = {}

	local function maybeAddToFilesToCopy( srcPath, dstPath )

		if srcPath and dstPath then

			table.insert( filesToCopy, {
					srcFilePath = srcPath,
					dstFilePath = dstPath
				} )

		end

	end

	local renditions = fileInfo.renditions or {}

	if needToCopyRenditions then

		for _, rendition in ipairs( renditions ) do
			maybeAddToFilesToCopy( rendition.renditionPath, rendition.renditionCopyPath )
		end

	end

	if needToCopyOriginal then
		maybeAddToFilesToCopy( fileInfo.originalPath, fileInfo.originalCopyPath )
	end

	local proxies = fileInfo.proxies or {}

	if needToCopyProxy then

		for _, proxy in ipairs( proxies ) do
			maybeAddToFilesToCopy( proxy.proxyPath, proxy.proxyCopyPath )
		end

	end

	if needToCopyExternalDevelopSettings then
		maybeAddToFilesToCopy( fileInfo.externalXmpPath, fileInfo.externalXmpCopyPath )
	end

	if not WFCatIndexUtils.isVideoDocument( dstDocValue ) and needToCopyXmpSidecar then
		maybeAddToFilesToCopy( fileInfo.xmpSidecarPath, fileInfo.xmpSidecarCopyPath )
	end

	if WFPlatformInfo.isDesktop then
		dstDocValue = updateDocValueAndOverlay_Desktop( dstDocValue, fileInfo, copyOptions )
	elseif WFPlatformInfo.isiOS then
		dstDocValue = updateDocValueAndOverlay_iOS( dstDocValue, fileInfo, copyOptions )
	elseif WFPlatformInfo.isAndroid then
		dstDocValue = updateDocValueAndOverlay_Android( dstDocValue, fileInfo, copyOptions )
	end

	return dstDocValue, filesToCopy

end

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

--[[!
	\brief Copy exteranl xmp_develop file to destication path

	\param sourceExternalXmpPath (string) source external xmp file path
	\param copyExternalXmpPath (string) destication external xmp file path

	\return Observable that sends the complete path to the new file once it
		completes. Or sends an error tuple if unable to complete this operation.

--]]


function WFCatCopies.copyExternalXmpFile( sourceExternalXmpPath, copyExternalXmpPath )

	check( '+string|nil', '+string|nil' )

	logger:infof( 'copyExternalXmpFile: source %s, dest %s', sourceExternalXmpPath, copyExternalXmpPath )

	if not sourceExternalXmpPath or not copyExternalXmpPath or sourceExternalXmpPath == copyExternalXmpPath then
		return Observable.just( nil )
	end

	local copyFilePathFolder = KSPathUtils.parent( copyExternalXmpPath )

	return Observable.just( WFFileUtils.mkdir( copyFilePathFolder ) )

		:flatMap( function()

			return WFAsyncFileUtils.copyToUniquelyNamedFile( sourceExternalXmpPath, copyExternalXmpPath )

		end )

end

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

return WFCatCopies
