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

WFOzDocUtils.lua

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

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

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

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

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

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

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

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

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

local check = assert( WFArguments.check )

local resolveLinksToAbsoluteUrls = assert( WFCatUrlUtils.resolveLinksToAbsoluteUrls )

local KSTableUtils_derefOpt = assert( KSTableUtils.derefOpt )

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

local WFOzDocUtils = {}

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

function WFOzDocUtils.escapeUnderscoreKeyword( keyword )

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

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

	return keyword

end

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

local function stripPayloadLinkNoise( noisyTable )

	if noisyTable then
		noisyTable.links = nil
		return noisyTable
	end

end

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

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

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

	local result = ozDocument.payload or {}

	result._serverProvidedMetadata = ozDocument
	ozDocument.payload = nil

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

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

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

	if result.type == 'album_asset' then

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

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

	end
	
	if result.type == 'comment' then
	
		result.space = stripPayloadLinkNoise( ozDocument.space )
		ozDocument.space = nil
		
		result.asset = stripPayloadLinkNoise( ozDocument.asset )
		ozDocument.asset = nil
		
	end
	
	if result.type == 'favorite' then
	
		result.space = stripPayloadLinkNoise( ozDocument.space )
		ozDocument.space = nil
		
		result.asset = stripPayloadLinkNoise( ozDocument.asset )
		ozDocument.asset = nil
		
	end
	
	if result.type == 'space_album' then
	
		result.space = stripPayloadLinkNoise( ozDocument.space )
		ozDocument.space = nil
		
		result.album = stripPayloadLinkNoise( ozDocument.album )
		ozDocument.album = nil
		
	end
	
	if result.type == 'stack_asset' then
	
		result.stack = stripPayloadLinkNoise( ozDocument.stack )
		ozDocument.stack = nil

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

	end

	if result.type == 'spaceinvite' then
	
		result.space = stripPayloadLinkNoise( ozDocument.space )
		ozDocument.space = nil

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

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

	end

	if result.type == 'space_user' then
	
		result.space = stripPayloadLinkNoise( ozDocument.space )
		ozDocument.space = nil

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

	end

	if result.type == 'version' then
	
		result.asset = stripPayloadLinkNoise( ozDocument.asset )
		ozDocument.asset = nil
		
		result.importedOnDevice = stripPayloadLinkNoise( ozDocument.importedOnDevice )
		ozDocument.importedOnDevice = nil

	end

	local spm = result._serverProvidedMetadata
	
	-- Oz might send us relative links to the current base url of this request
	-- so we respect the current base url and create absolute url's in the format
	-- /v2/catalogs/123...
	
	if baseUrl and spm and spm.links then
	
		resolveLinksToAbsoluteUrls( baseUrl, spm.links )
		
	end
	
	-- error docs may have additional links in a separate section. if it's present
	-- convert these too
	if baseUrl and spm and spm.source and spm.source.links then
		
		resolveLinksToAbsoluteUrls( baseUrl, spm.source.links )
	
	end
	
	if baseUrl and spm and spm.matching_asset then
		if spm.matching_asset.href then
			spm.matching_asset.href = WFCatUrlUtils.resolveToAbsoluteUrl( baseUrl, spm.matching_asset.href )
		end
	end
	
	-- add the baseUrl to the server provided metadata so that we can fix up 
	-- any unresolved links at a future point, if necessary
	spm.base = baseUrl
	
	-- Docstore requires the revision id list to be non-empty,
	-- if present.
	result._revIds = ozDocument.revision_ids and #ozDocument.revision_ids > 0 
		and ozDocument.revision_ids
		or nil
	
	return result
	
end

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

function WFOzDocUtils.getRevisionId( docValue )
	
	local revIds = docValue.revIds
	
	if type( revIds ) ~= 'table' or #revIds == 0 then 
		return nil
	else
		return revIds[ #revIds ] 
	end

end

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

function WFOzDocUtils.doesDocumentNeedOriginalOrProxyUploaded( docValue )

	check( 'table' )

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

	local needsOriginalOrProxyUploaded = false

	if not ozHasOriginal and not ozHasProxy then

		-- The asset has no original and no proxy.

		needsOriginalOrProxyUploaded = true

	elseif ozHasOriginal and isOriginalOnOzInvalid then

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

		needsOriginalOrProxyUploaded = true

	elseif not ozHasOriginal and ozHasProxy and isProxyOnOzInvalid then

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

		needsOriginalOrProxyUploaded = true

	end

	return needsOriginalOrProxyUploaded

end

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

function WFOzDocUtils.isAssetDocFullySyncedWithOz( docValue )

	check( 'table' )

	local labels = docValue._labels or {}
	local needsMetadataUploaded = not ( labels[ 'oz' ] or labels[ '_dontsendtooz' ] )

	local needsOriginalOrProxyUploaded = WFOzDocUtils.doesDocumentNeedOriginalOrProxyUploaded( docValue )

	local needsXmpSidecarUploaded = WFOzDocUtils.doesDocumentNeedXmpSidecarUploaded( docValue )

	local needsXmpDevelopSettingsUploaded = WFOzDocUtils.doesDocumentNeedXmpDevelopSettingsUploaded( docValue )

	local isFullySynced = true

	if needsMetadataUploaded or
		needsOriginalOrProxyUploaded or
		needsXmpSidecarUploaded or
		needsXmpDevelopSettingsUploaded then

		isFullySynced = false

	end

	return isFullySynced

end

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

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

function WFOzDocUtils.areAssetsFullySyncedWithOz( wfCatalog, assetIds )
	
	check( 'WFCatalog', 'table' )
	
	return WFRxThrottledBatchedBulkDocValue( "WFOzDocUtils_areAssetsFullySyncedWithOz", wfCatalog:docStore(), assetIds, 100, 10 )
			
			:scan( function( docValues, batchOfDocValues )
	
				docValues = KSTableUtils.appendArray( docValues, batchOfDocValues )
				return docValues
				
			end, {} )
			
			:takeLast( 1 )
			
			:map( function( docValues )
			
				for _, docValue in ipairs( docValues ) do
				
					if not WFOzDocUtils.isAssetDocFullySyncedWithOz( docValue ) then
					
						return false
						
					end
				
				end
				
				return true
			
			end )

end

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

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

function WFOzDocUtils.waitUntilAssetsAreFullySyncedWithOz( wfCatalog, assetIds )
	
	check( 'WFCatalog', 'table' )
	
	local isFullySyncedByAssetId = {}
	
	for _, assetId in ipairs( assetIds ) do
		isFullySyncedByAssetId[ assetId ] = false
	end
	
	return Observable.just( assetIds )
	
		:call( WFRxDocValueWatcher.make, wfCatalog:docStore() )
		
		:map( function( docValue )
		
			local isAssetFullySynced = WFOzDocUtils.isAssetDocFullySyncedWithOz( docValue )
		
			isFullySyncedByAssetId[ docValue._id ] = isAssetFullySynced
			
			if not isAssetFullySynced then
				return false --short circuit to avoid the following loop
			end
			
			-- Check to see if all the watched assets are fully synced.
			
			for _, isFullySynced in pairs( isFullySyncedByAssetId ) do
				
				if not isFullySynced then
					return false
				end
				
			end
			
			return true
			
		end )
		
		:filter( function( areAllAssetsFullySynced )
			return areAllAssetsFullySynced
		end )
		
		:take( 1 )

end

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

function WFOzDocUtils.clearNonPayloadKeys( docValue ) 

	for k in pairs( docValue ) do
	
		if k:match( '^_' ) or k == 'type' or k == 'subtype' then
			docValue[ k ] = nil
		end
		
	end
	
	return docValue
	
end

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

function WFOzDocUtils.isOriginalAvailableOnOz( docValue )

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

end

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

function WFOzDocUtils.isValidOriginalAvailableOnOz( docValue )

	local _, isOzOriginalInvalid = WFCatFileSpec.getOriginalLinkFromDocValue( docValue )
	
	return WFOzDocUtils.isOriginalAvailableOnOz( docValue ) and not isOzOriginalInvalid

end


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

function WFOzDocUtils.isProxyAvailableOnOz( docValue )

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

end

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

function WFOzDocUtils.isValidProxyAvailableOnOz( docValue )

	local _, _, isProxyInvalid = WFCatFileSpec.getProxyLinkFromDocValue( docValue )
	
	return WFOzDocUtils.isProxyAvailableOnOz( docValue ) and not isProxyInvalid
end

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

function WFOzDocUtils.doesDocumentNeedXmpSidecarUploaded( docValue )

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

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

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

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

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

	return not not needsXmpSidecarUploaded

end

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

function WFOzDocUtils.doesDocumentNeedXmpDevelopSettingsUploaded( docValue )

	local xmpCameraRaw = KSTableUtils_derefOpt( docValue, 'develop', 'xmpCameraRaw' )
	
	if not ( type( xmpCameraRaw ) == 'table' ) then
		-- if the xmpCameraRaw is not external then there's also nothing to upload
		return false
	end
	
	local isXmpDevelopInvalid = not not KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'invalid' )
	local xmpDevelopUrl = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'href' )
	
	-- Depending on the space settings, the develop settings may be redacted so Oz may not include the href 
	-- in /rels/xmp/develop for us to know that it has been uploaded. Instead we'll look for exists: true to
	-- infer that the xmp_develop has been uploaded even if we can't see the link
	local isAssetInSpace = not not docValue._spaceId
	local impliedXmpDevelopUrl = isAssetInSpace and not WFCatSharedWithYouUtils.canEditAsset( docValue ) and
								KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'exists' ) and
								not KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'invalid' )
								
	if not isXmpDevelopInvalid and ( xmpDevelopUrl or impliedXmpDevelopUrl ) then
		-- it's not missing or invalid, there's nothing to upload
		return false
	end
	
	-- there's external settings specified and no link (or the link is invalid) so Oz does need an update 
	-- (though maybe not from this client)
	return true

end

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

return WFOzDocUtils
