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

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

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

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

THIS CLASS IS USED BY ALL PLATFORMS. ALL NEW FUNCTIONS ADDED TO THIS CLASS MUST
WORK ON ALL PLATFORMS AND BE VERIFIED AS SUCH BY UNIT TESTS.

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

	-- WF.core.os
local WFFileUtils = require 'WFFileUtils'
local WFPlatformInfo = require "WFPlatformInfo"
local isDesktop = WFPlatformInfo.isDesktop
local isAndroid = WFPlatformInfo.isAndroid
local isiOS = WFPlatformInfo.isiOS
local isMobile = isAndroid or isiOS
local isClassic = WFPlatformInfo.isClassic()

	-- WF.core.utils
local KSPathUtils = require 'KSPathUtils'
local KSTableUtils = require "KSTableUtils"
local WFArguments = require 'WFArguments'

	-- WF.managed_catalog
local WFCatIndexUtils = require 'WFCatIndexUtils'

	-- WF.oz_client.utils
local LrmAssetUtils, WFCatFileSpec
local NmbLocalRenditionDigest
local OzFilePaths, OzCatalogUploadStatus

if isDesktop then

	WFCatFileSpec = require 'WFCatFileSpec'
	NmbLocalRenditionDigest = require 'NmbLocalRenditionDigest'

else

	LrmAssetUtils = require 'LrmAssetUtils'

	if isAndroid then

		OzCatalogUploadStatus = require 'OzCatalogUploadStatus'

	elseif isiOS then

		OzFilePaths = require 'OzFilePaths'

	end

end

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

local check = assert( WFArguments.check )

local derefOpt = assert( KSTableUtils.derefOpt )

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

--[[

The format at the API level adopts the conventions laid out in
https://git.corp.adobe.com/lua/wf/blob/master/doc/ozdocstore-localOnly.md

]]--

local legacy_localKeyMap_iOS = {
	relativePath = 'path',
	fileSize = 'fileSize',
	contentId = 'contentId',
}

local modern_localKeyMap_iOS = {
	relativePath = 'relativePath',
	fileSize = 'fileSize',
	contentId = 'contentId'
}

local function derefUniversal_iOS( docValue, ... )

	if select( 1, ... ) == 'files' then

		local keyMap = legacy_localKeyMap_iOS

		-- special cases for old structure in iOS

		local info
		local depth = 4

		local l2 = select( 2, ... )
		local l3 = select( 3, ... )

		if l2 == 'renditions' then
			if l3 == 'thumbnail2x' then
				info = derefOpt( docValue, '_localOnly', 'files', 'thumbnail2x' )
			elseif l3 == '2048' then
				info = derefOpt( docValue, '_localOnly', 'files', 'preview' )
			elseif l3 == 'fullsize' then
				info = derefOpt( docValue, '_localOnly', 'files', 'fullsize' )
			end
		elseif l2 == 'proxy' then
			if l3 == '2560' then
				info = derefOpt( docValue, '_localOnly', 'files', 'proxy' )
			elseif l3 == '1080p' then
				keyMap = modern_localKeyMap_iOS
				info = derefOpt( docValue, '_localOnly', 'files', 'proxies', '1080p' )
			elseif l3 == '360p' then
				keyMap = modern_localKeyMap_iOS
				info = derefOpt( docValue, '_localOnly', 'files', 'proxies', '360p' )
			elseif l3 == 'panorama' then
				keyMap = modern_localKeyMap_iOS
				info = derefOpt( docValue, '_localOnly', 'files', 'proxies', 'panorama' )
			end
		elseif l2 == 'original' then
			if l3 == 'master' then
				info = derefOpt( docValue, '_localOnly', 'files', 'master' )
			elseif l3 == 'latestMaster' and select( 4, ... ) == 'relativePath' then
				return derefOpt( docValue, '_localOnly', 'files', 'latestMaster', 'filePath' )
			end
		elseif l2 == 'xmp_develop' then
			info = derefOpt( docValue, '_localOnly', 'files', 'xmp_develop' )
			depth = 3
		end

		if not info or not next( info ) then
			return nil
		end

		local key = keyMap[ select( depth, ... )  ]

		if not key then
			return nil
		else
			return info[ key ]
		end

	end

	return derefOpt( docValue, '_localOnly', 'universal', unpack( arg ) )

end

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

local function universalPath_iOS( ... )

	-- return nil for unknown paths to make errors more obviou

	if select( 1, ... ) ~= 'files' then
		return nil
	end

	local l2 = select( 2, ... )
	local l3 = select( 3, ... )

	if l2 == 'renditions' then
		if l3 == 'thumbnail' then
			return '_localOnly', 'files', 'thumbnail'
		elseif l3 == 'thumbnail2x' then
			return '_localOnly', 'files', 'thumbnail2x'
		elseif l3 == '2048' then
			return '_localOnly', 'files', 'preview'
		elseif l3 == 'fullsize' then
			return '_localOnly', 'files', 'fullsize'
		else
			return nil
		end
	elseif l2 == 'proxy' then
		if l3 == '2560' then
			return '_localOnly', 'files', 'proxy'
		elseif l3 == '1080p' then
			return '_localOnly', 'files', 'proxies', '1080p'
		elseif l3 == '360p' then
			return '_localOnly', 'files', 'proxies', '360p'
		elseif l3 == 'panorama' then
			return '_localOnly', 'files', 'proxies', 'panorama'
		else
			return nil
		end
	elseif l2 == 'original' then
		if l3 == 'master' then
			return '_localOnly', 'files', 'master'
		elseif l3 == 'latestMaster' then
			return '_localOnly', 'files', 'latestMaster'
		else
			return nil
		end
	elseif l2 == 'xmp_develop' then
		return '_localOnly', 'files', 'xmp_develop'
	else
		return nil
	end

end

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

local localRenditionKeyMap_Android = {
	relativePath = 'md5',
	fileSize = 'size',
	contentId = 'contentId',
}

local localXmpKeyMap_Android = {
	relativePath = 'path',
	contentId = 'sha256',
	fileSize = 'fileSize'
}

local localKeyMap_Android = {
	relativePath = 'path',
	fileSize = 'size',
}

local legacy_proxyKeyMap_Android = {
	relativePath = 'filePath',
	fileSize = 'size'
}

local modern_proxyKeyMap_Android = {
	relativePath = 'relativePath',
	fileSize = 'fileSize'
}

local function derefUniversal_Android( docValue, ... )

	if select( 1, ... ) == 'files' then

		local keyMap = localKeyMap_Android
		local depth = 4
		local info

		local l2 = select( 2, ... )
		local l3 = select( 3, ... )

		if l2 == 'renditions' then

			keyMap = localRenditionKeyMap_Android

			if l3 == 'thumbnail2x' then
				info = derefOpt( docValue, '_localOnly', 'thumbnail2x' )
			elseif l3 == '2048' then
				info = derefOpt( docValue, '_localOnly', 'preview' )
			elseif l3 == 'fullsize' then
				info = derefOpt( docValue, '_localOnly', 'fullsize' )
			end
		elseif l2 == 'proxy' then

			keyMap = legacy_proxyKeyMap_Android

			if l3 == '2560' then
				info = derefOpt( docValue, '_localOnly', 'proxy' )
			elseif l3 == '1080p' then
				keyMap = modern_proxyKeyMap_Android
				info = derefOpt( docValue, '_localOnly', 'files', 'proxies', '1080p' )
			elseif l3 == '360p' then
				keyMap = modern_proxyKeyMap_Android
				info = derefOpt( docValue, '_localOnly', 'files', 'proxies', '360p' )
			elseif l3 == 'panorama' then
				keyMap = modern_proxyKeyMap_Android
				info = derefOpt( docValue, '_localOnly', 'files', 'proxies', 'panorama' )
			end

		elseif l2 == 'latestOriginal' then

			if l3 == 'relativePath' then

				return derefOpt( docValue, '_localOnly', 'latestOriginal', 'filePath' )

			end
		elseif l2 == 'original' then

			if l3 == 'master' then

				if select( 4, ... ) == 'relativePath' then
					return derefOpt( docValue, '_localOnly', 'originalFilePath' )
				end

				info = derefOpt( docValue, '_localOnly', 'original' )

			elseif l3 == 'latestMaster' then

				if select( 4, ... ) == 'relativePath' then
					return derefOpt( docValue, '_localOnly', 'latestOriginal', 'filePath' )
				end

			end

		elseif l2 == 'xmp_develop' then

			keyMap = localXmpKeyMap_Android

			info = derefOpt( docValue, '_localOnly', 'files', 'xmp_develop' )
			depth = 3
		elseif l2 == 'presetProfile' then

			keyMap = localXmpKeyMap_Android

			info = derefOpt( docValue, '_localOnly', 'files', 'presetProfile' )
			depth = 3
		end

		if not info or not next( info ) then
			return nil
		end

		local key = keyMap[ select( depth, ... ) ]
		if not key then
			return nil
		end

		return info[ key ]

	end

	return derefOpt( docValue, '_localOnly', 'universal', unpack( arg ) )

end

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

local function universalPath_Android( ... )

	if select( 1, ... ) ~= 'files' then
		return nil
	end

	local l2 = select( 2, ... )
	local l3 = select( 3, ... )

	if l2 == 'renditions' then
		if l3 == 'thumbnail' then
			return '_localOnly', 'thumbnail'
		elseif l3 == 'thumbnail2x' then
			return '_localOnly', 'thumbnail2x'
		elseif l3 == '2048' then
			return '_localOnly', 'preview'
		elseif l3 == 'fullsize' then
			return '_localOnly', 'fullsize'
		else
			return nil
		end
	elseif l2 == 'proxy' then
		if l3 == '2560' then
			return '_localOnly', 'proxy'
		elseif l3 == '1080p' then
			return '_localOnly', 'files', 'proxies', '1080p'
		elseif l3 == '360p' then
			return '_localOnly', 'files', 'proxies', '360p'
		elseif l3 == 'panorama' then
			return '_localOnly', 'files', 'proxies', 'panorama'
		else
			return
		end
	elseif l2 == 'original' then
		if l3 == 'master' then
			return '_localOnly', 'original'
		elseif l3 == 'latestMaster' then
			return '_localOnly', 'latestOriginal'
		else
			return nil
		end
	elseif l2 == 'xmp_develop' then
		return '_localOnly', 'files', 'xmp_develop'
	else
		return nil
	end

end

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

local function derefUniversal_Desktop( docValue, ... )

	if select( 1, ... ) == 'files' then

		local keys = { ... }

		local l2 = select( 2, ... )
		local l3 = select( 3, ... )
		local l4 = select( 4, ... )

		if l2 == 'xmp_develop' then

			if l3 == 'relativePath' then
				keys[ 3 ] = 'path'
			elseif l3 == 'contentId' then
				keys[ 3 ] = 'sha256'
			end

		elseif l4 == 'fileSize' then

			keys[ 4 ] = 'size'

		elseif l4 == 'contentId' then

			keys[ 4 ] = 'sha256'

		end

		return derefOpt( docValue, '_localOnly', unpack( keys ) )

	end

	return derefOpt( docValue, '_localOnly', 'universal', unpack( arg ) )

end

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

local function derefUniversal( ... )

	if isDesktop then
		return derefUniversal_Desktop( ... )
	elseif isAndroid then
		return derefUniversal_Android( ... )
	elseif isiOS then
		return derefUniversal_iOS( ... )
	end

end

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

local function universalPath( ... )

	if isDesktop then
		return ...
	elseif isAndroid then
		return universalPath_Android( ... )
	elseif isiOS then
		return universalPath_iOS( ... )
	end

end

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

local function getFullProxyPathFromDocValue( docValue, optProxyType )

	check( 'table', '?string' )

	local path

	if isDesktop then

		path = WFCatFileSpec.buildProxyPathFromDocValue( docValue, optProxyType )

	else

		path = LrmAssetUtils.getProxyPath( docValue, optProxyType )

	end

	if path == "" then
		path = nil
	end

	return path

end

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

local function getProxyRelativePathFromDocValue( docValue, optProxyType )

	check( 'table', '?string' )

	local path

	if isDesktop then
		path = WFCatFileSpec.getProxyRelativePathFromDocValue( docValue, optProxyType )
	else
		path = getFullProxyPathFromDocValue( docValue, optProxyType )
			-- On mobile, the relative path _is_ the full path.
	end

	return path

end

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

local function getProxyVolumeGuidFromDocValue( docValue, optProxyType )

	check( 'table', '?string' )

	local guid

	if isDesktop then
		guid = WFCatFileSpec.getProxyVolumeGuidFromDocValue( docValue, optProxyType )
	else
		-- noop. Mobile does have volume guids.
	end

	return guid

end

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

local function getProxySizeFromDocValue( docValue, optProxyType )

	check( 'table', '?string' )

	local size

	local proxyType = optProxyType

	if isDesktop then

		size, proxyType = WFCatFileSpec.getProxySizeFromDocValue( docValue, optProxyType )

	else

		if WFCatIndexUtils.isVideoDocument( docValue ) then

			if optProxyType then

				size = derefOpt( docValue, '_localOnly', 'files', 'proxies', optProxyType, 'fileSize' )

			else

				size = derefOpt( docValue, '_localOnly', 'files', 'proxies', '1080p', 'fileSize' )

				if size then

					proxyType = '1080p'

				else

					size = derefOpt( docValue, '_localOnly', 'files', 'proxies', '360p', 'fileSize' )
					proxyType = '360p'

				end

			end

		else

			--The default proxy type for images should be 2560
			proxyType = '2560'

			if isAndroid then
				size = derefOpt( docValue, '_localOnly', 'proxy', 'size' )
			elseif isiOS then
				size = derefOpt( docValue, '_localOnly', 'files', 'proxy', 'fileSize' )
			end

		end

	end

	return size, proxyType

end

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

local function getProxyContentIdFromDocValue( docValue, optProxyType )

	check( 'table', '?string' )

	local contentIdOrSha256

	if isDesktop then
		contentIdOrSha256 = WFCatFileSpec.getLocalProxySha256( docValue, optProxyType )
	else
		if WFCatIndexUtils.isVideoDocument( docValue ) then

			if optProxyType then

				contentIdOrSha256 = derefOpt( docValue, '_localOnly', 'files', 'proxies', optProxyType, 'contentId' )

			else

				contentIdOrSha256 = derefOpt( docValue, '_localOnly', 'files', 'proxies', '1080p', 'contentId' )
						or derefOpt( docValue, '_localOnly', 'files', 'proxies', '360p', 'contentId' )

			end

		else

			if isAndroid then
				-- On Android, there is no SHA256 or contentId stored for the proxy.
			elseif isiOS then
				contentIdOrSha256 = derefOpt( docValue, '_localOnly', 'files', 'proxy', 'contentId' )
			end

		end

	end

	return contentIdOrSha256

end

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

local function doesWatermarkNeedBinaryUploaded( docValue )

	local isWatermarkInvalid = not not derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'invalid' )

	local href = not not derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href' )

	if not ( isWatermarkInvalid or not href ) then
		-- it's not invalid or missing, so there's nothing to upload
		return false
	end

	-- there is no link or the link is invalid. So Oz does need an update
	-- (though may not from this client)
	return true

end

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

local function getLocalWatermarkBinaryRelativePath( docValue )

	check( 'table' )

	local path = derefOpt( docValue, '_localOnly', 'universal', 'files', 'original', 'master', 'relativePath' )
		-- Woo, platform unity!

	return path

end

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

local function getLocalWatermarkBinarySize( docValue )

	check( 'table' )

	local size = derefOpt( docValue, '_localOnly', 'universal', 'files', 'original', 'master', 'fileSize' )
		-- Woo, platform unity!

	return size

end

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

local function getLocalWatermarkBinarySha256( docValue )

	check( 'table' )

	local sha256 = derefOpt( docValue, '_localOnly', 'universal', 'files', 'original', 'master', 'sha256' )
	-- Woo, platform unity!

	return sha256

end

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

local function getLocalWatermarkVolumeGuid( docValue )

	check( 'table' )
    local guid
    if isDesktop then
		guid = derefOpt( docValue, '_localOnly', 'universal', 'files', 'original', 'master', 'volumeGuid' )
	else
		-- noop. Mobile doesn't have volume guids.
	end

	return guid

end

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

local function getLocalWatermarkBinaryPath( docValue )

	check( 'table' )

	local path = getLocalWatermarkBinaryRelativePath( docValue )

	if WFPlatformInfo.isUnitTest() then
		path = KSPathUtils.child( KSPathUtils.getStandardFilePath( "testdata" ), path )
	end

	return path

end

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

local function getFullOriginalPathFromDocValue( docValue )

	check( 'table' )

	if( docValue.type == "watermark" ) then
		return getLocalWatermarkBinaryPath( docValue )
	end

	local path

	if isDesktop then
		path = WFCatFileSpec.buildOriginalPathFromDocValue( docValue )
	elseif isAndroid then
		path = LrmAssetUtils.buildOriginalPathFromDocValue( docValue )
	elseif isiOS then
		-- iOS handles "camera_profile" like a regular profile
		if docValue.subtype == "preset" or docValue.subtype == "profile" or docValue.subtype == "camera_profile" then
			path = LrmAssetUtils.getPresetPath( docValue )
		else
			path = LrmAssetUtils.getMasterPath( docValue )
		end
	end

	if path == "" then
		path = nil
	end

	return path

end

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

local function getOriginalRelativePathFromDocValue( docValue )

	check( 'table' )

	if docValue.type == "watermark" then
		return getLocalWatermarkBinaryRelativePath( docValue )
	end

	local path

	if isDesktop then
		path = WFCatFileSpec.getOriginalRelativePathFromDocValue( docValue )
	else
		path = getFullOriginalPathFromDocValue( docValue )
			-- On mobile, the relative path _is_ the full path.
	end

	return path

end


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

local function getOriginalVolumeGuidFromDocValue( docValue )

	check( 'table' )

	if( docValue.type == "watermark" ) then
		return getLocalWatermarkVolumeGuid( docValue )
	end

	local guid

	if isDesktop then
		guid = WFCatFileSpec.getOriginalVolumeGuidFromDocValue( docValue )
	else
		-- noop. Mobile does have volume guids.
	end

	return guid

end

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

local function getOriginalSizeFromDocValue( docValue )

	check( 'table' )

	if ( docValue.type == "watermark" ) then
		return getLocalWatermarkBinarySize( docValue )
	end

	local size

	if isDesktop then
		size = derefOpt( docValue, '_localOnly', 'files', 'original', 'master', 'size' )
				or derefOpt( docValue, 'importSource', 'fileSize' )
	elseif isAndroid then
		-- presets/profiles are handled like iOS presets/profiles, camera_profiles like originals
		if docValue.subtype == "preset" or docValue.subtype == "profile"  then
			size = derefOpt( docValue, '_localOnly', 'files', 'presetProfile', 'fileSize' )
		else
			size = derefOpt( docValue, '_localOnly', 'original', 'size' )
					or derefOpt( docValue, 'importSource', 'fileSize' )
		end
	elseif isiOS then
		if docValue.subtype == "preset" or docValue.subtype == "profile" or docValue.subtype == "camera_profile" then
			size = derefOpt( docValue, '_localOnly', 'files', 'presetProfile', 'fileSize' )
		else
			size = derefOpt( docValue, '_localOnly', 'files', 'master', 'fileSize' )
					or derefOpt( docValue, 'importSource', 'fileSize' )
		end
	end

	return size

end

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

local function getOriginalContentIdFromDocValue( docValue )

	check( 'table' )

	if docValue.type == "watermark" then
		return getLocalWatermarkBinarySha256( docValue )
	end

	local contentIdOrSha256

	if isDesktop then
		contentIdOrSha256 = WFCatFileSpec.getLocalOriginalSha256WithFallback( docValue )
	elseif isAndroid then
		contentIdOrSha256 = LrmAssetUtils.getlocalOriginalSha256 ( docValue )
	elseif isiOS then
		if docValue.subtype == "preset" or docValue.subtype == "profile" or docValue.subtype == "camera_profile" then
			contentIdOrSha256 = derefOpt( docValue, '_localOnly', 'files', 'presetProfile', 'sha256' )
		else
			contentIdOrSha256 = derefOpt( docValue, '_localOnly', 'files', 'master', 'contentId' )
					or derefOpt( docValue, 'importSource', 'sha256' ) -- Fallback
		end
	end

	return contentIdOrSha256

end

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

local function getLocalExternalDevelopSettingsPath( docValue )

	check( 'table' )

	local path = derefOpt( docValue, '_localOnly', 'files', 'xmp_develop', 'path' )
		-- Woo, platform unity!

	return path

end

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

local function getLocalExternalDevelopSettingsSize( docValue )

	check( 'table' )

	local size = derefOpt( docValue, '_localOnly', 'files', 'xmp_develop', 'fileSize' )
		-- On Android, external develop settings size may not stored in local only for image
		-- and video assets, but it should be stored there for version documents


	return size

end

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

local function getLocalExternalDevelopSettingsContentId( docValue )

	check( 'table' )

	local contentIdOrSha256

	if isDesktop or isAndroid then

		contentIdOrSha256 = derefOpt( docValue, '_localOnly', 'files', 'xmp_develop', 'sha256' )

	elseif isiOS then

		contentIdOrSha256 = derefOpt( docValue, '_localOnly', 'files', 'xmp_develop', 'contentId' )

	end

	return contentIdOrSha256


end

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

local function getLocalAuxObjectPath( docValue, digest )

	check( 'table', '+string' )

	local relativePath = derefOpt( docValue, '_localOnly', 'files', 'aux', digest, 'relativePath' )

	return relativePath

end

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

local function getXmpSidecarPathFromDocValue( docValue )

	if isDesktop then
		return WFCatFileSpec and WFCatFileSpec.getXmpSidecarPathFromDocValue( docValue )  -- desktop
	else
		return nil -- android and iOS don't keep local sidecar files currently
	end

end

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

local function hasLocalOriginal( docValue )

	check( 'table' )

	local result

	if isDesktop then
		result = WFCatIndexUtils.hasLocalOriginal( docValue )
	else
		result = LrmAssetUtils.hasMasterLocally( docValue )
	end

	return not not result

end

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

local function hasLocalProxy( docValue, optProxyType )

	check( 'table' )

	local result

	if isDesktop then
		result = WFCatIndexUtils.hasLocalProxy( docValue, optProxyType )
	else
		result = LrmAssetUtils.hasProxyLocally( docValue, optProxyType )
	end

	return not not result

end

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

local function hasLocalOriginalOrProxy( docValue )

	check( 'table' )


	local hasLocalOriginalOrProxyValue = hasLocalProxy( docValue ) or hasLocalOriginal( docValue )

	return not not hasLocalOriginalOrProxyValue

end


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

local function hasLocalOriginalOrAnyProxy( docValue )

	check( 'table' )

	local hasLocalOriginalOrProxyValue = hasLocalOriginalOrProxy( docValue )

	if WFPlatformInfo.isWindows and WFCatIndexUtils.isVideoDocument( docValue ) then

		hasLocalOriginalOrProxyValue = hasLocalOriginalOrProxyValue or hasLocalProxy( docValue, '1080p709' )

	end

	return not not hasLocalOriginalOrProxyValue

end

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

local function getSourceOriginalLinkFromDocValue( docValue )

	check( 'table' )

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

end

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

local function getSourceProxyLinkFromDocValue( docValue )

	check( 'table' )

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

end

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

local function getSourceXmpLinkFromDocValue( docValue )

	check( 'table' )

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

end

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

local function getSourceXmpDevelopLinkAndSha256FromDocValue( docValue )

	check( 'table' )

	local link = derefOpt( docValue, '_localOnly', 'sourceXmpDevelopLink' )

	if not link then
		return nil, nil
	end

	local linkSha256 = derefOpt( docValue, '_localOnly', 'sourceXmpDevelopSha256' )
	local developSha256 = derefOpt( docValue, 'develop', 'xmpCameraRaw', 'sha256' )

	-- if sourceXmpDevelopSha256 is set and it is different from current develop settings,
	-- then we do not want to copy from the source. If it is not set, then later
	-- we will decide whether to copy based on whether local binary is available or not.

	if linkSha256 and developSha256 ~= linkSha256 then
		return nil, nil
	end

	return link, linkSha256

end

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

local function doesAssetDocumentHaveLocalBinary( docValue )

	check( 'table' )

	local hasLocalBinary = false

	if isMobile then
		hasLocalBinary = LrmAssetUtils and LrmAssetUtils.doesAssetDocumentHaveLocalBinary( docValue ) -- android and ios
	elseif isDesktop then
		hasLocalBinary = WFCatIndexUtils and WFCatIndexUtils.doesAssetDocumentHaveLocalBinary( docValue ) -- desktop
	end

	return not not hasLocalBinary

end

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

local function absolutePath( catalogId, relativePath )

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

	local absolutePath

	if isiOS then

		if relativePath then

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

				absolutePath = relativePath

			else

				absolutePath = OzFilePaths.getAbsolutePathInCatalog( catalogId, relativePath )

			end

		end

	elseif isAndroid then

		absolutePath = OzCatalogUploadStatus.checkAndCorrectLocalPath( relativePath )

	end

	return absolutePath

end

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

local function absolutePathToDevelopSettings( catalogId, maybeExternalXmpRelativePath )

	local externalXmpAbsolutePath

	if WFPlatformInfo.isDesktop then

		externalXmpAbsolutePath = maybeExternalXmpRelativePath

	elseif WFPlatformInfo.isMobile() and maybeExternalXmpRelativePath then

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

			externalXmpAbsolutePath = maybeExternalXmpRelativePath

		elseif WFPlatformInfo.isiOS then

			externalXmpAbsolutePath = OzFilePaths.getAbsolutePathInCatalog( catalogId, maybeExternalXmpRelativePath )

		elseif WFPlatformInfo.isAndroid then

			local documentsDir = KSPathUtils.getStandardFilePath( 'carouselDocuments' )

			externalXmpAbsolutePath = KSPathUtils.child( documentsDir, maybeExternalXmpRelativePath )

		end

	end

	return externalXmpAbsolutePath

end

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

local function getLocalRenditionInfo( wfCatalog, docValue )

	local renditions = {}

	if isDesktop then

		local renditionDigest = NmbLocalRenditionDigest.maybeComputeFromDocValue( docValue )

		if renditionDigest then

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

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

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

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

				end

			end

		end

	elseif isiOS then

		local catalogId = wfCatalog:ozCatalogId()

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

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

			local renditionPath = absolutePath( catalogId, renditionRelativePath )

			if renditionRelativePath then

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

			end

		end

	elseif isAndroid then

		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> ,
				} ]]--

			if renditionInfo then

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

			end

		end

	end

	return renditions

end

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

local doesAssetNeedDevelopSettingsUploaded -- forward declaration

local function binaryUploadPending( docValue )

	-- Returns true if the asset was created locally but had not finished
	-- uploading all of the associated binaries.

	if isDesktop then

		-- Desktop's binary uploader supports uploading masters, proxies,
		-- external develop settings, and xmp sidecars.
		-- Note proxies are only uploaded if the master is unavailable or a
		-- Lightroom Classic proxy-only asset was copied.
		
		local hasOriginalLink = WFCatFileSpec.getOriginalLinkFromDocValue( docValue ) ~= nil
		local wasCopiedLocally = derefOpt( docValue, "_localOnly", "sourceAssetId" ) ~= nil

		local originalOrProxyUploadPending = false
		local xmpSidecarUploadPending = false
		
		local xmpDevelopUploadPending = not WFCatFileSpec.getXmpDevelopLinkAndSha256FromDocValue( docValue )
				and ( getLocalExternalDevelopSettingsPath( docValue ) ~= nil or wasCopiedLocally )
		
		if isClassic then
		
			local willNotUploadMaster = KSTableUtils.derefOpt( docValue, 'importSource', 'willNotUploadMaster' )
			
			if willNotUploadMaster then
				local hasproxyLink, _, isInvalid = WFCatFileSpec.getProxyLinkFromDocValue( docValue )
				originalOrProxyUploadPending = not hasproxyLink or not ( isInvalid == nil )
			end
		
		else
		
			originalOrProxyUploadPending = not hasOriginalLink 
					and ( hasLocalOriginal( docValue ) or wasCopiedLocally )

				or not hasOriginalLink and not WFCatFileSpec.getProxyLinkFromDocValue( docValue )
					and ( hasLocalProxy( docValue ) or wasCopiedLocally )
			
			-- if we don't have an xmp link yet but we could upload it, then consider this
			-- upload pending
			xmpSidecarUploadPending = not WFCatFileSpec.getXmpLinkFromDocValue( docValue )
				and ( WFCatFileSpec.getXmpSidecarExistsFromDocValue( docValue ) or
					getSourceXmpLinkFromDocValue( docValue ) )
		
		end
		
		return originalOrProxyUploadPending or xmpDevelopUploadPending or xmpSidecarUploadPending

	else

		-- Mobile's binary uploader only supports uploading masters and external
		-- develop settings.

		return LrmAssetUtils.doesAssetNeedMasterUploaded( docValue )
			or doesAssetNeedDevelopSettingsUploaded( docValue )

	end

end

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

local function isValidOriginalOnOz( docValue )

	local originalLink = derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master' )

	local ozSha256Error = derefOpt( docValue,
			'importSource',
			'status',
			'error',
			'all',
			'reason',
			'sha256' ) == "does not match"

	return not not ( originalLink
			and originalLink.href
			and not originalLink.invalid
			and not ozSha256Error )

end

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

--[[!
	\brief Describe the latest edits by a user

	\param assetDocValue (table) Full Asset Document
	\param feedbackUserDocValues (table) list of full feedbackUser documents
	\param tempDevelopInfo (table) /rels/temp/ link info

	\return table describing the latest edit details
--]]

local function getLatestEditDetails( docValue, feedbackUserDocs, tempDevelopInfo )

	local editDetails = {
		userUpdated = derefOpt( docValue, 'develop', 'userUpdated' ),
		userDevice = derefOpt( docValue, 'develop', 'device' ),
	}

	local function getFeedbackUserDoc( id )
		for _, feedbackUserDoc in ipairs( feedbackUserDocs ) do
			if feedbackUserDoc._id == id then
				return feedbackUserDoc
			end
		end
	end

	local userId = derefOpt( docValue, '_serverProvidedMetadata', 'developedByUser', 'id' )
	if userId then
		editDetails.userName = derefOpt( getFeedbackUserDoc( userId ), '_serverProvidedMetadata', 'full_name' )
	end

	if tempDevelopInfo then

		-- we have already checked that both of these fields are present
		local tempDetails = {
			userUpdated = tempDevelopInfo.develop.userUpdated,
			userDevice = tempDevelopInfo.develop.device,
		}

		userId = derefOpt( tempDevelopInfo, 'uploadedByUser', 'id' )
		if userId then
			tempDetails.userName = derefOpt( getFeedbackUserDoc( userId ), '_serverProvidedMetadata', 'full_name' )
		end

		editDetails.tempBinary = tempDetails

	end

	return editDetails

end

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

--[[!
	\brief Get info on any temp develop binary links that may supercede
			the current develop settings if the revision completes syncing
			to Oz from some client.

	\param docValue (table) Full Asset Document

	\return table describing the edit details from most recent temp binary link
--]]

local function getMostRecentTempDevelopInfo( docValue )

	local links = derefOpt( docValue, '_serverProvidedMetadata', 'links' )

	if links == nil then
		return
	end

	local mostRecentTempDevelopInfo

	for link, info in pairs( links ) do

		if string.find( link, '/rels/temp/xmp/develop/' ) or string.find( link, '/rels/temp/aux/' ) then

			-- also check that device is populated as if for some reason it is NOT populated
			-- (should not happen) we do not want to use that one

			local compUserUpdated = derefOpt( info, 'develop', 'device' ) and
					derefOpt( info, 'develop', 'userUpdated' )

			if compUserUpdated and ( not mostRecentTempDevelopInfo or
					compUserUpdated > mostRecentTempDevelopInfo.develop.userUpdated ) then

				mostRecentTempDevelopInfo = info

			end

		end

	end

	-- if the userUpdated timestamp from the current develop settings is more recent
	-- than the most recent value from the /rels/temp links, then do not return
	-- the link info

	if mostRecentTempDevelopInfo then

		local developUserUpdated = derefOpt( docValue, 'develop', 'userUpdated')

		if developUserUpdated and developUserUpdated >= mostRecentTempDevelopInfo.develop.userUpdated then
			mostRecentTempDevelopInfo = nil
		end

	end

	return mostRecentTempDevelopInfo

end

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

--[[!
	\brief Given a doc value, estimate the size of the proxy. For images, this
		function estimates the size of the 2560 proxy. For videos, it estimates
		the size of the 1080p proxy.

	\return The estimated size.
--]]

local function estimateProxySize( docValue, optProxyType )

	local defaultSize = 943718
		-- Same default used by the cacheReferences index store table.

	local size

	if WFCatIndexUtils.isVideoDocument( docValue ) then

		-- Because we know the bitrate that Oz uses to transcode proxies, both
		-- the video bitrate and audio bitrate, and we know the length of the
		-- video, getting an accurate estimated proxy size is as simple as:
		-- ( video bitrate * duration ) + ( audio bitrate * duration )

		local videoBitrate = 8 * 1024 * 1024 -- 8mbps

		local audioBitrate = 128 * 1024 -- 128kbps

		local durationRatio = derefOpt( docValue, 'video', 'duration' )

		if durationRatio then

			local durationInSeconds = durationRatio[ 1 ] / durationRatio[ 2 ]

			size = ( videoBitrate * durationInSeconds ) + ( audioBitrate * durationInSeconds )

		end

		if size and optProxyType then

			-- TODO [kcarlson 2023-03-01]: JpegXL proxies will need a new factor

			local proxyTypeCompressionFactor = {
				[ '360p' ] = 1, -- not used by desktop
				[ '720p' ] = 1/30, -- compression factor not included in videos above?
				[ '1080p' ] = 1/7, -- compression factor?
			}

			local compressionFactorForProxySize = proxyTypeCompressionFactor[ optProxyType ] or 1
			size = size * compressionFactorForProxySize

		end

	else

		-- The following formula comes from Eric Chan:
		-- https://git.corp.adobe.com/acr/acr/commit/efaa06e90f3ba8053c6d7ebfec7e2effea535dac

		local originalWidth = derefOpt( docValue, "importSource", "originalWidth" )
		local originalHeight = derefOpt( docValue, "importSource", "originalHeight" )

		if originalWidth and originalHeight then

			local originalLongSide = math.max( originalWidth, originalHeight )
			local originalShortSide = math.min( originalWidth, originalHeight )

			local proxyLongSide = math.min( 2560, originalLongSide )

			local ratio = proxyLongSide / originalLongSide

			local proxyShortSide = ratio * originalShortSide

			local bytesPerPixel = 3 -- RGB
			local jpegCompressionFudgeFactor = 0.1

			size = proxyLongSide *
					proxyShortSide *
					bytesPerPixel *
					jpegCompressionFudgeFactor

		end

	end

	return size or defaultSize

end

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

doesAssetNeedDevelopSettingsUploaded = function( docValue )

	local hasXmpDevelopUrl = not not derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'href' )

	local hasDevelopXmpLocally

	if isDesktop then

		local xmpCameraRawSha256 = KSTableUtils.derefOpt( docValue, 'develop', 'xmpCameraRaw', 'sha256' )

		local localXmpCameraRawPath = KSTableUtils.derefOpt( docValue, '_localOnly', 'files', 'xmp_develop', 'path' )
		local localXmpCameraRawSha256 = KSTableUtils.derefOpt( docValue, '_localOnly', 'files', 'xmp_develop', 'sha256' )

		hasDevelopXmpLocally = xmpCameraRawSha256 == localXmpCameraRawSha256 and localXmpCameraRawPath

	else

		hasDevelopXmpLocally = LrmAssetUtils.hasExternalXmpDevelopLocally( docValue )

	end

	local isXmpDevelopInvalid = not not derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'invalid' )

	local isSpaceAsset = not not docValue._spaceId
	local isSpaceAssetWithImpliedXmpDevelop = isSpaceAsset and derefOpt( docValue, '_serverProvidedMetadata',
											 'links',
											 '/rels/xmp/develop',
											 'exists' ) == true

	local ozHasXmpDevelop = hasXmpDevelopUrl or isSpaceAssetWithImpliedXmpDevelop

	local needsUpload = false

	if hasDevelopXmpLocally and not ozHasXmpDevelop then

		needsUpload = true

	elseif hasDevelopXmpLocally and ozHasXmpDevelop and isXmpDevelopInvalid then

		needsUpload = true

	end

	return needsUpload, ozHasXmpDevelop

end

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

local function hasExternalXmpDevelop( docValue )

	local xmpCameraRawTable =
			KSTableUtils.derefOpt( docValue, 'develop', 'xmpCameraRaw' )

	return type( xmpCameraRawTable ) == "table" and
			type( xmpCameraRawTable.sha256 ) == "string"

end

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

local function maybeGetExternalXmpDevelopSha256( docValue )

	return KSTableUtils.derefOpt(
			docValue, 'develop', 'xmpCameraRaw', 'sha256' )

end

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

local function hasEmbeddedXmpDevelop( docValue )

	local xmpCameraRawStr =
			KSTableUtils.derefOpt( docValue, 'develop', 'xmpCameraRaw' )

	return type( xmpCameraRawStr ) == "string"

end

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

local function hasLocalXmpDevelopError( docValue )

	check( 'table' )

	local maybeLocalXmpDevelopError = KSTableUtils.derefOpt( docValue, '_localOnly', 'files', 'xmp_develop', 'error' )

	if maybeLocalXmpDevelopError then

		local payloadXmpSha256 = maybeGetExternalXmpDevelopSha256( docValue )

		if payloadXmpSha256 then

			local localXmpSha256 = getLocalExternalDevelopSettingsContentId( docValue )

			-- if the local xmp_develop file is the current active one and it is marked
			-- with an error, than we should return true

			return payloadXmpSha256 == localXmpSha256

		end

	end

	return false

end

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

return {

	-- get functions for local originals

	getFullOriginalPathFromDocValue = getFullOriginalPathFromDocValue,
	getOriginalRelativePathFromDocValue = getOriginalRelativePathFromDocValue,
	getOriginalVolumeGuidFromDocValue = getOriginalVolumeGuidFromDocValue,
	getOriginalSizeFromDocValue = getOriginalSizeFromDocValue,
	getOriginalContentIdFromDocValue = getOriginalContentIdFromDocValue,

	-- get functions for local proxies

	getFullProxyPathFromDocValue = getFullProxyPathFromDocValue,
	getProxyRelativePathFromDocValue = getProxyRelativePathFromDocValue,
	getProxyVolumeGuidFromDocValue = getProxyVolumeGuidFromDocValue,
	getProxySizeFromDocValue = getProxySizeFromDocValue,
	getProxyContentIdFromDocValue = getProxyContentIdFromDocValue,

	-- get functions for watermark

	getLocalWatermarkBinaryRelativePath = getLocalWatermarkBinaryRelativePath,
	doesWatermarkNeedBinaryUploaded = doesWatermarkNeedBinaryUploaded,

	-- get functions for local external develop settings

	getLocalExternalDevelopSettingsPath = getLocalExternalDevelopSettingsPath,
	getLocalExternalDevelopSettingsSize = getLocalExternalDevelopSettingsSize,
	getLocalExternalDevelopSettingsContentId = getLocalExternalDevelopSettingsContentId,
	doesAssetNeedDevelopSettingsUploaded = doesAssetNeedDevelopSettingsUploaded,

	-- get function for aux objects

	getLocalAuxObjectPath = getLocalAuxObjectPath,

	-- get functions for xmp sidecar files

	getXmpSidecarPathFromDocValue = getXmpSidecarPathFromDocValue,

	-- get functions for rendition information

	getLocalRenditionInfo = getLocalRenditionInfo,

	-- get functions for doc source links (for copies, IAC, etc.)

	getSourceOriginalLinkFromDocValue = getSourceOriginalLinkFromDocValue,
	getSourceProxyLinkFromDocValue = getSourceProxyLinkFromDocValue,
	getSourceXmpLinkFromDocValue = getSourceXmpLinkFromDocValue,
	getSourceXmpDevelopLinkAndSha256FromDocValue = getSourceXmpDevelopLinkAndSha256FromDocValue,

	-- get functions for edit meta information

	getLatestEditDetails = getLatestEditDetails,
	getMostRecentTempDevelopInfo = getMostRecentTempDevelopInfo,

	-- various does-exist style functions

	hasLocalOriginal = hasLocalOriginal,
	hasLocalProxy = hasLocalProxy,
	hasLocalOriginalOrProxy = hasLocalOriginalOrProxy, -- Should only be used for non-trash assets.
	hasLocalOriginalOrAnyProxy = hasLocalOriginalOrAnyProxy,
	doesAssetDocumentHaveLocalBinary = doesAssetDocumentHaveLocalBinary,-- Works for assets and trash assets

	-- other local-only utilities

	binaryUploadPending = binaryUploadPending,
	derefUniversal = derefUniversal,
	universalPath = universalPath,
	absolutePathToDevelopSettings = absolutePathToDevelopSettings,

	-- other

	isValidOriginalOnOz = isValidOriginalOnOz,
	estimateProxySize = estimateProxySize,

	-- develop info
	hasExternalXmpDevelop = hasExternalXmpDevelop,
	maybeGetExternalXmpDevelopSha256 = maybeGetExternalXmpDevelopSha256,
	hasEmbeddedXmpDevelop = hasEmbeddedXmpDevelop,
	hasLocalXmpDevelopError = hasLocalXmpDevelopError,

}
