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

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 WFCatStacks = require 'WFCatStacks'

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

	-- 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.
	
	copyDocLocalOnly.sourceOriginalLink = WFCatFileSpec.getOriginalLinkFromDocValue( docValue )
	copyDocLocalOnly.sourceXmpLink = WFCatFileSpec.getXmpLinkFromDocValue( docValue )
	copyDocLocalOnly.sourceXmpDevelopLink = WFCatFileSpec.getXmpDevelopLinkFromDocValue( docValue )
	copyDocLocalOnly.sourceProxyLink = WFCatFileSpec.getProxyLinkFromDocValue( docValue )

	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 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 proxyPath = getFullProxyPathFromDocValue( docValue )
	local proxySize, proxyType = WFCatFileSpec.getProxySizeFromDocValue( docValue )
	local proxyCopyPath = proxyType and wfCatalog:fileSpec():pathForProxy( copyDocValue._id, proxyType )

	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 } ) 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 fileInfo = {
	
		renditions = renditions,
		
		originalPath = originalPath,
		originalCopyPath = originalCopyPath,
		originalSize = originalSize,
		
		proxyPath = proxyPath,
		proxySize = proxySize,
		proxyType = proxyType,
		proxyCopyPath = proxyCopyPath,
		
		xmpSidecarPath = xmpSidecarPath,
		xmpSidecarCopyPath = xmpSidecarCopyPath,
		xmpDevelopInfo  = xmpDevelopInfo,
		xmpSidecarSize = xmpSidecarSize,
		xmpSidecarExists = xmpSidecarExists
		
	}
	
	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

	return needToCopyOriginal, needToCopyProxy, needToCopyRenditions, 
			needToCopyExternalDevelopSettings, needToCopyXmpSidecar

end

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

local function updateDocValueAndOverlay_Desktop( copyDocValue, fileInfo, copyOptions )

	local needToCopyOriginal, needToCopyProxy, _, 
			needToCopyExternalDevelopSettings, 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,
			
		xmp = needToCopyXmpSidecar and 
				fileInfo.xmpSidecarCopyPath and {
					path = fileInfo.xmpSidecarCopyPath,
					fileSize = fileInfo.xmpSidecarSize
				} or nil,
			
	}

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

	local proxyInfo

	if needToCopyProxy and fileInfo.proxyCopyPath then
	
		proxyInfo = {
		
			[ fileInfo.proxyType ] = {
			
				relativePath = WFFileUtils.removeVolumeFromPath( fileInfo.proxyCopyPath ),
				volumeGuid = WFFileUtils.getVolumeGuidForPath( fileInfo.proxyCopyPath ),
				size = fileInfo.proxySize,
				
			}
			
		}
		
	end

	local xmpSidecarExists = needToCopyXmpSidecar and fileInfo.xmpSidecarExists

	local files = {
		original = originalInfo,
		xmp_develop = needToCopyExternalDevelopSettings and fileInfo.xmpDevelopInfo or nil,
		proxy = proxyInfo,
	}
	
	if not next( files ) then
		files = nil
	end

	WFTableMerge.applyOverlay( copyDocValue, 
		{
			_localOnly = {
				files = files,
					-- Upload or copy the xmp sidecar if we know one exists or if
					-- we don't know. Don't bother uploading xmp if we know
					-- one doesn't exist.
				xmpSidecarExists = xmpSidecarExists,
			}
		} )

	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 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 )
	 
	-- Proxy
	
	local proxyRelativePath = getProxyRelativePathFromDocValue( docValue )
	
	local proxyPath, proxyCopyPath, proxyCopyRelativePath 
			= buildPathsFromSourceRelativePath( catalogId, proxyRelativePath )
	
	fileInfo.proxyPath = proxyPath
	fileInfo.proxyCopyPath = proxyCopyPath
	fileInfo.proxyCopyRelativePath = proxyCopyRelativePath
	
	fileInfo.proxySize = getProxySizeFromDocValue( docValue )
	fileInfo.proxyContentId = getProxyContentIdFromDocValue( docValue )

	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' } ) 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

	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' } ) 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

	return fileInfo

end

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

local function updateDocValueAndOverlay_iOS( copyDocValue, fileInfo, copyOptions )

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

	local files = {}

	if needToCopyOriginal and fileInfo.originalCopyRelativePath then
		
		files.master = {
		
			path = fileInfo.originalCopyRelativePath,
			contentId = fileInfo.originalContentId,
			fileSize = fileInfo.originalSize
			
		}

	end

	if needToCopyProxy and fileInfo.proxyCopyRelativePath then
	
		files.proxy = {
		
			path = fileInfo.proxyCopyRelativePath,
			contentId = fileInfo.proxyContentId,
			fileSize = fileInfo.proxySize
			
		}

	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

	if next( files ) then
	
		WFTableMerge.applyOverlay( copyDocValue, 
			{
				_localOnly = {
					files = files,
				}
			} )

	end

	return copyDocValue

end

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

local function updateDocValueAndOverlay_Android( copyDocValue, fileInfo, copyOptions )

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

	local localOnly = {}

	if needToCopyOriginal and fileInfo.originalCopyRelativePath then
		
		localOnly.originalFilePath = fileInfo.originalCopyRelativePath
		
		localOnly.original = {
		
			size = fileInfo.originalSize
			
		}

	end

	if needToCopyProxy and fileInfo.proxyCopyRelativePath then
	
		localOnly.proxy = {
		
			filePath = fileInfo.proxyCopyRelativePath,
			size = fileInfo.proxySize
			
		}
	
	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 needToCopyExternalDevelopSettings and fileInfo.xmpDevelopInfo then 

		localOnly.files = {
		
			xmp_develop = fileInfo.xmpDevelopInfo
		
		}

	end

	if next( localOnly ) then
	
		WFTableMerge.applyOverlay( copyDocValue, 
			{
				_localOnly = localOnly
			} )
		
	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 maybeCopyProxy = Observable.just( nil )
			
			if shouldCopyOriginal then
				maybeCopyOriginal = maybeCopyFile( fileInfo.originalPath, fileInfo.originalCopyPath )
			end
			
			if shouldCopyProxy then
				maybeCopyProxy = maybeCopyFile( fileInfo.proxyPath, fileInfo.proxyCopyPath )
			end

			return Observable.combineLatest(
				maybeCopyOriginal,
				maybeCopyProxy,
				maybeCopyFile( fileInfo.xmpSidecarPath, fileInfo.xmpSidecarCopyPath ),
				maybeCopyFile( fileInfo.externalXmpPath, fileInfo.externalXmpCopyPath ) )

		end )
		
		:map( function( originalCopyPath, proxyCopyPath, xmpSidecarCopyPath, externalXmpCopyPath )

	 		-- Update absolute paths in fileInfo
			
			fileInfo.originalCopyPath = originalCopyPath
			fileInfo.proxyCopyPath = proxyCopyPath
			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.proxyCopyRelativePath = getNewRelativeCopyPath( proxyCopyPath, fileInfo.proxyCopyRelativePath )
			fileInfo.externalXmpCopyRelativePath = getNewRelativeCopyPath( externalXmpCopyPath, fileInfo.externalXmpCopyRelativePath )
	
			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
	
	if needToCopyProxy then
		maybeAddToFilesToCopy( fileInfo.proxyPath, fileInfo.proxyCopyPath )
	end

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

	if 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
