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

WFCatDevelopSettings.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 KSData = require 'KSData'
local KSDate = require 'KSDate'
local WFArguments = require 'WFArguments'
local WFFunctionUtils = require 'WFFunctionUtils'

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local KSTableUtils = require 'KSTableUtils'
local WFCrypto = require 'WFCrypto'
local WFAsyncFileUtils = require 'WFAsyncFileUtils'
local WFConfiguration = require 'WFConfiguration'
local WFLogger = require 'WFLogger'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local WFFileUtils = require 'WFFileUtils'

	-- lightroomcc
local WFCrInfo

if WFPlatformInfo.hasACR then
	local NmbAcr = require 'NmbAcr'
	WFCrInfo = NmbAcr.WFCrInfo
end

	-- WF.oz_client.managed_catalog
local WFCatFileSpec = require 'WFCatFileSpec'
local WFCatUtils = require 'WFCatUtils'

	-- WF.oz_client.managed_catalog.develop
local NmbDocValueDevelopSettingsUtils = 
		require "NmbDocValueDevelopSettingsUtils"
local NmbDocValueToRenderInfo = require "NmbDocValueToRenderInfo"

local NmbProfileCatalogUtils = require 'NmbProfileCatalogUtils'

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

local INLINE_XMP_CAMERA_RAW_MAX_LENGTH = WFConfiguration.Nimbus.inlineXmpMaxLength
										or 10 * 1024 --10KB

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

local logger = WFLogger( 'WFCatDevelopSettings' )

local derefOpt = assert( KSTableUtils.derefOpt )

local KSDate_timeInIso8601FormatNearestSecond = assert( KSDate.timeInIso8601FormatNearestSecond, "KSDate.timeInIso8601FormatNearestSecond does not exist" )

local runUntilDone = WFRxRunUntilDone

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

local function createKey( pixStreamId, assetId )

	return string.format( "%s:%d", pixStreamId, assetId )

end

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

local observersWaitingForDevelopSettings = {}

local retainedDevSettingsPathObservers = {}

local xmpDevelopSettingsPathStreamSubscription

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

local _currentSaveSequencesByAssetId = {}

local _pendingDevelopSettingsSaveListeners = {}

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

local function bootstrapXMPDevelopSettingsPathStreamSubscription( pixelProviderProcessor, pixStreamIdToMatch )

	if not xmpDevelopSettingsPathStreamSubscription then
	
		xmpDevelopSettingsPathStreamSubscription = 
			pixelProviderProcessor
				:pixStreamIdPixelPathStream( pixStreamIdToMatch )
				:subscribe( function( pixStreamId, assetId, success, xmpDevelopPath )
				
					local key = createKey( pixStreamId, assetId )
					
					local observers = observersWaitingForDevelopSettings[ key ] or {}
					
					for _, observer in pairs( observers ) do
					
						observer:onNext( success, xmpDevelopPath )
					
					end
				
					observersWaitingForDevelopSettings[ key ] = nil
				
					pixelProviderProcessor:runUntilDone( 'removeRequest',
															pixStreamId, assetId )
				
				end )

	end

end

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

local function getXmpDevelopPath( pixelProviderProcessor, imageId, needsASAP )

	local pixStreamId = 'wfCatDevelopSettings'
	
	return Rx.Observable.create( 

		function( observer )

			bootstrapXMPDevelopSettingsPathStreamSubscription( 
				pixelProviderProcessor, pixStreamId )

			local key = createKey( pixStreamId, imageId )

			local observers = observersWaitingForDevelopSettings[ key ] or KSTableUtils.makeFullyWeak()
			
			observers[ observer ] = observer
			
			observersWaitingForDevelopSettings[ key ] = observers
			
			retainedDevSettingsPathObservers[ observer ] = observer
			
			pixelProviderProcessor:runUntilDone( 
									"requestXmpDevelopForDevelop", 
										pixStreamId,
										imageId,
										needsASAP )

			return Rx.Disposable.create( function() 
			
				retainedDevSettingsPathObservers[ observer ] = nil
			
			end )

		end )

end

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

local function getExternalFileXMPSettingsStream( 
				pixelProviderProcessor, imageId, docValue, xmpCameraRaw,
				baseSettings, needsASAP )

	local xmpPathObservable = 
			getXmpDevelopPath( pixelProviderProcessor, imageId, needsASAP )
			:flatMap( 
				function( success, xmpDevelopPath ) 
					
					if not success or not xmpDevelopPath then
						logger:warnf( "Failed to fetch xmp_develop settings for asset %s: %s", imageId, xmpDevelopPath )
						
						baseSettings.settingsXmpStatus = 'failed'
						
						return Observable.just( baseSettings )
					end
					
					return WFAsyncFileUtils.readFile( xmpDevelopPath )
							:map( function( fileData ) 
								
								if fileData then

									baseSettings.settingsXmp = tostring( fileData )

									baseSettings.settingsXmpStatus = 'complete'

									fileData:close()

								else

									logger:warnf( "failed to read xmp from external develop settings file: %s",
													xmpDevelopPath )

									baseSettings.settingsXmpStatus = 'failed'

								end
								
								return baseSettings

							end )
					
				end )
		
	local xmpDevelopInfo = WFCatFileSpec.getXmpDevelopInfoFromDocValue( 
											docValue )

	if not xmpDevelopInfo or xmpDevelopInfo.sha256 ~= xmpCameraRaw.sha256 then

		-- if we don't think we have this locally yet, mark it pending as it
		-- may take a bit and clients might need to know that we're fetching
		-- the settings
		baseSettings.settingsXmpStatus = 'pending'
		
		xmpPathObservable = xmpPathObservable:startWith( baseSettings )
		
	end
	
	return xmpPathObservable

end

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

local NmbDocValueDevelopSettingsUtils_getSanitizedDocValueDevelopStanza =
		assert( NmbDocValueDevelopSettingsUtils.getSanitizedDocValueDevelopStanza,
				"missing NmbDocValueDevelopSettingsUtils.getSanitizedDocValueDevelopStanza" )

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

local function getValidatedXMPSettingsStream( pixelProviderProcessor, imageId,
												docValue, baseSettings, needsASAP )

	local developStanza = 
			NmbDocValueDevelopSettingsUtils_getSanitizedDocValueDevelopStanza(
				docValue )

	local xmpCameraRaw = developStanza.xmpCameraRaw
		
	if type( xmpCameraRaw ) == 'table' and xmpCameraRaw.sha256 then
		
		return getExternalFileXMPSettingsStream( 
					pixelProviderProcessor, imageId, docValue, xmpCameraRaw,
					baseSettings, needsASAP )

	end
	
	if xmpCameraRaw == nil or type( xmpCameraRaw ) == 'string' then

		baseSettings.settingsXmp = xmpCameraRaw

		baseSettings.settingsXmpStatus = 'complete'
	
		return Observable.just( baseSettings )

	end

	logger:warnf( "bad docValue.develop.xmpCameraRaw value in docstore: %s",
					type( xmpCameraRaw ) ~= "table" and
						type( xmpCameraRaw ) or
						"xmp was a table without a sha256" )

	baseSettings.settingsXmpStatus = 'failed'
	
	return Observable.just( baseSettings )

end

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

local function docValueHasLocalOriginal( docValue )

	local localOriginalVolumeGuid = WFCatFileSpec.getOriginalVolumeGuidFromDocValue( docValue )
	local localOriginalRelativePath = WFCatFileSpec.getOriginalRelativePathFromDocValue( docValue )
	return localOriginalVolumeGuid and localOriginalRelativePath

end

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

local NmbDocValueToRenderInfo_disallowLocalEditsForDocValue =
		assert( NmbDocValueToRenderInfo.disallowLocalEditsForDocValue,
				"NmbDocValueToRenderInfo.disallowLocalEditsForDocValue is missing" )

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

local function developContentStreamImp( pixelProviderProcessor, docValue, needsASAP )
	
	if docValue._empty or docValue.type ~= 'asset' then

		logger:errorf( "Asset does not exist, or has wrong type: id = %s",
								docValue._localId )

		return Observable.empty()

	end
	
	local imageId = docValue._localId

	local originalPath = WFCatFileSpec.buildOriginalPathFromDocValue( docValue )
	
	if originalPath and not WFFileUtils.exists( originalPath ) then
		originalPath = nil
	end
	
	local proxyPath = WFCatFileSpec.buildProxyPathFromDocValue( docValue )
	
	if proxyPath and not WFFileUtils.exists( proxyPath ) then
		proxyPath = nil
	end
	
	local isUnsupportedFormat = WFCatUtils.isUnsupportedFormat( docValue )
	
	-- 2017.07.28 bkruse
	-- 
	-- Nimbus has one asset id per file semantics, use the asset's id as the
	-- fileUUId. This might change in the future, but for now, it's true and
	-- filUUId is used to cache negative paths so we don't want to
	-- assets to have the same fileUUId unless the files are indeed unique.
	-- 
	-- It is possible via catalog migration to have two files with the same
	-- sha256, so we can't rely on the sha256 as unique fileUUId
	local fileUuid = docValue._id -- derefOpt( docValue, 'importSource', 'sha256' ) or docValue._id

	local disallowLocalEdits = 
				NmbDocValueToRenderInfo_disallowLocalEditsForDocValue( docValue )

	local isVideo = derefOpt( docValue, 'subtype') == 'video'

	local disallowClientAccessToProxyAndOrig =
			isUnsupportedFormat or
				( disallowLocalEdits and not isVideo )

	-- rootFile is is a vestige of LrClassic's libImage object interface that
	-- the LrD client code still depends on.
	local rootFile

	local haveLocalProxyOrOrig = originalPath or proxyPath 

	if haveLocalProxyOrOrig and not disallowClientAccessToProxyAndOrig then

		rootFile = 
			{
				filePath = originalPath or proxyPath,
				originalPath = originalPath,
				proxyPath = proxyPath,
			}

	end
	
	local baseSettings = {

			changeCounter = tonumber( docValue._changeCounter ),

			fileUuid = fileUuid,

			historySettingsID = KSIdGenerator.uuid(),
	
			fileFormat = derefOpt( docValue, 'importSource', 'contentType' ) == 'image/jpeg' and "JPG" or "RAW",
				-- TODO [arahn 2015-08-27]: is "RAW" correct?
	
			payloadHeight = derefOpt( docValue, 'importSource', 'originalHeight' ),
			payloadWidth = derefOpt( docValue, 'importSource', 'originalWidth' ),
			noOriginalOnOz = derefOpt( docValue, 'importSource', 'willNotUploadMaster' ),
			id_local = docValue._localId,
			isVideo = isVideo,
			numUnlockedSnapshots = 0,
			numVirtualCopies = 0,
	
			tiffOrientation = derefOpt( docValue, 'develop', 'userOrientation' ) or 1,
			
			defaultCrop = derefOpt( docValue, 'importSource', 'originalCrop' ),
			
			rootFile = rootFile,
			
			settingsID = "default",
			
			virtualCopy = false,
			virtualCopyMap = {},

			hasProxyLink = WFCatFileSpec.getProxyLinkFromDocValue( docValue ) ~= nil,
			hasOriginalLink = WFCatFileSpec.getOriginalLinkFromDocValue( docValue ) ~= nil,
			
			hasLocalOriginal = docValueHasLocalOriginal( docValue ),
			
			isUnsupportedFormat = isUnsupportedFormat,

			disallowLocalEdits = disallowLocalEdits,
			
		}
		
	return getValidatedXMPSettingsStream( 
			pixelProviderProcessor, imageId, docValue, baseSettings,
			needsASAP )
	
end

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

local function docIsNonEmpty( docValue )

	if not docValue._empty then

		return docValue

	end

end

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

local function nonEmptyDocValues( docValues )

	return WFFunctionUtils.filter( docIsNonEmpty, docValues )

end

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

local function buildOutAssetIdTables( assetIdTables, assetIdChangeCounterTuple )

	if type( assetIdChangeCounterTuple ) ~= "table" then

		return assetIdTables

	end

	local assetId, maybeChangeCounter = unpack( assetIdChangeCounterTuple )

	if not assetId then

		return assetIdTables

	end

	assetIdTables.assetIds[ #assetIdTables.assetIds + 1 ] = assetId

	assetIdTables.assetIdSet[ assetId ] = true

	if maybeChangeCounter then

		assetIdTables.assetIdToChangeCounterSet[ assetId ] = 
			maybeChangeCounter

	end

	return assetIdTables

end

--------------------------------------------------------------------------------
-- isUpdateWanted is called from 
-- 	docStore:scannedFilteredMappedChangesFeed( ... )
-- 	
-- 	it's used to scan *all* ongoing changes to the doc store and wantUpdatesSet
-- 	is used to filter out only those docValues that we are interested in.
-- 	
-- it should always return wantUpdatesSet unchanged. wantUpdatesSet will always
-- hold the set of assetIds for which we want updateds
-- 
-- it should return the docValue as a second value if the docValue is of interest
--------------------------------------------------------------------------------

local function isUpdateWanted( wantUpdatesSet, docValue )

	if docValue._empty or docValue._deleted then

		return wantUpdatesSet

	end

	local localAssetId = docValue._localId

	if not localAssetId then

		return wantUpdatesSet

	end

	if not wantUpdatesSet[ localAssetId ] then

		logger:infof( "%s is ignoring update of %s, it's not watched", 
						wantUpdatesSet.desc, localAssetId )

		return wantUpdatesSet

	end

	return wantUpdatesSet, docValue

end

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

local function addToBatch( batches, assetId, batchSize )

	local currBatchSize = #( batches[ #batches ] )

	if currBatchSize == batchSize then

		currBatchSize = 0

		batches[ #batches + 1 ] = {}

	end

	batches[ #batches ][ currBatchSize + 1 ] = assetId

	return batches

end

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

local function batchedDocValuesStream( _, wfCatalog, assetIds, batchSize )

	local batchedAssetIds = 
			WFFunctionUtils.reduce( { {} }, addToBatch, assetIds, batchSize )

	return Rx.Observable.from( batchedAssetIds )
			:map( 
				function( assetIdsBatch )

					return wfCatalog
							:docStore()
							:bulkDocValue( assetIdsBatch )
							:filter( nonEmptyDocValues )
							:flatMapLatest( Rx.Observable.from )

				end )
			-- flatMerge( 1 ) calls bulkDocValue only after previous completes
			:flatMerge( 1 ) 
					
end

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

local _batchSize = 5

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

local function getDocValuesPlusUpdatesStream( 
				desc, wfCatalog, assetIds, assetIdSet, changeCounterAtStart )

	local initialDocValuesStream =
			batchedDocValuesStream( 
				desc, wfCatalog, assetIds, _batchSize )

	local updatesSinceStartStream =
				wfCatalog
					:docStore()
					:scannedFilteredMappedChangesFeedSince( 
						"WFCatDevelopSettings", "isUpdateWanted", 
						assetIdSet, changeCounterAtStart )
	
	return Rx.Observable.concat( 
			initialDocValuesStream, updatesSinceStartStream )

end

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

local function ongoingDevelopContentsStream( 
					desc, wfCatalog, pixelProviderProcessor,
					assetIdChangeCounterTuples, needsASAP )

	local assetIdTables =
				WFFunctionUtils.reduce( 
					{
						assetIds = {},
						assetIdSet = {},
						assetIdToChangeCounterSet = {},
					},
					buildOutAssetIdTables,
						assetIdChangeCounterTuples )

	local assetIds = assetIdTables.assetIds

	if #assetIds == 0  then

		return Rx.Observable.empty()

	end

	local assetIdSet = assetIdTables.assetIdSet
	
	local docValueStream = 
			wfCatalog
				:docStore()
				:changeCounter()
				:take( 1 )
				:flatMapLatest(
					function( changeCounterAtStart )

						-- To guard against race conditions, we need to
						-- establish a "start" doc store change counter. 
						-- 
						-- This guards against this race condition:
						-- 
						-- worker: 
						-- 		changeCounter is at n
						-- this thread: 
						-- 		subscribe initial doc value
						-- worker: 
						-- 		send initial docValue for assetId
						-- worker: 
						-- 		receive update for assetId
						-- worker: 
						-- 		changeCounter is at n + 1
						-- this thread: 
						-- 		subscribe to scannedFilteredMappedChangesFeed
						-- worker: 
						-- 		subscribe to all changes after n + 1
						-- this thread:
						-- 		only receives updates for n + 2 and later
						-- 
						-- In the above sequence, we never see the n + 1
						-- update.  With the changeCounterAtStart the above
						-- sequence is:
						-- 
						-- worker: 
						-- 		changeCounter is at n
						-- this thread: 
						-- 		receives changeCounter with value n
						-- this thread: 
						-- 		subscribe initial doc value
						-- worker: 
						-- 		send initial docValue for assetId
						-- worker: 
						-- 		receive update for assetId
						-- worker: 
						-- 		changeCounter is at n + 1
						-- this thread: 
						-- 		subscribe to scannedFilteredMappedChangesFeedSince
						-- worker: 
						-- 		subscribe to all changes after n
						-- worker: 
						-- 		send all changes from n + 1 to current
						-- this thread: 
						-- 		receive docValue with changeCounter of n + 1

						return getDocValuesPlusUpdatesStream( 
								desc, wfCatalog, assetIds, assetIdSet,
								changeCounterAtStart )

					end )

	local assetIdToChangeCounterSet = assetIdTables.assetIdToChangeCounterSet

	return docValueStream
			:flatMap(
				function( docValue )

					local docValueChangeCounter = 
							tonumber( docValue._changeCounter )

					local lastSeenChangeCounterForAssetId =
							tonumber( assetIdToChangeCounterSet[ docValue._localId ] )

					if type( docValueChangeCounter ) == "number" and
							type( lastSeenChangeCounterForAssetId ) == "number" and
							docValueChangeCounter <= lastSeenChangeCounterForAssetId then

						-- we've already seen this either here or on the client, do nothing
						-- nothing
						return Rx.Observable.empty()

					end

					if type( docValueChangeCounter ) == "number" then
							
						-- update the last seen value
						assetIdToChangeCounterSet[ docValue._localId ] =
							docValueChangeCounter

					end

					-- send the docValue to the client
					return developContentStreamImp( pixelProviderProcessor, 
													docValue, needsASAP )

				end )

end

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

local function makeOngoingDevelopContentsStream(
					desc, wfCatalog, pixelProviderProcessor,
					needDevelopContentStream )

	return needDevelopContentStream
			:flatMapLatest(
				function( assetIdChangeCounterTuples, needsASAP )

					return ongoingDevelopContentsStream( 
							desc, wfCatalog, pixelProviderProcessor,
							assetIdChangeCounterTuples, needsASAP )

				end )

end

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

local function updateFileAccessTimestampStream( 
				wfCatalog, needDevelopContentStream )

	return needDevelopContentStream
			:filter( 
				function( assetIdChangeCounterTuples )

					return 
						type( assetIdChangeCounterTuples ) == "table" and
						#assetIdChangeCounterTuples > 0 and
						type( assetIdChangeCounterTuples[ 1 ] ) == "table" and
						unpack( assetIdChangeCounterTuples[ 1 ] )

				end )
			:map( 
				function( assetIdChangeCounterTuples )

					local localId = unpack( assetIdChangeCounterTuples[ 1 ] )

					wfCatalog:fileAccessDb():touch( localId )

					return string.format( "did update file access for %s", localId )

				end )
	
end

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

local function makeOngoingDevelopContentsStreamWithUpdateFileAccess(
					wfCatalog, pixelProviderProcessor,
					needDevelopContentStream )

	return Rx.Observable.create(
			function( observer )

				local updateFileAccessTimestampSubscription =
						updateFileAccessTimestampStream( 
								wfCatalog, needDevelopContentStream )
							:subscribe(
								function( msg )

									logger:info( msg )

								end )

				local devContentsStream = 
					makeOngoingDevelopContentsStream(
						"active image develop content", wfCatalog,
						pixelProviderProcessor, needDevelopContentStream )

				return Rx.CompositeDisposable(
							updateFileAccessTimestampSubscription,
							devContentsStream:subscribe( observer ) )


			end )

end
 
--------------------------------------------------------------------------------

local function ongoingDevelopContentModel( wfCatalogStream )

	local needDevelopContentStream = Rx.Subject()

	return {
		
		value = wfCatalogStream
					:mapSwitch(
						function( wfCatalog, pixelProviderProcessor )

							return makeOngoingDevelopContentsStreamWithUpdateFileAccess( 
									wfCatalog, pixelProviderProcessor,
									needDevelopContentStream )

						end ),

		events = {

			setAssetId = function( _, assetId, maybeChangeCounter )

				needDevelopContentStream:onNext( { { assetId, maybeChangeCounter } }, true )

			end,

			clearAssetId = function()
				
				needDevelopContentStream:onNext( {}, true )

			end,

		}

	}
	
end
 
--------------------------------------------------------------------------------

local function developContentStream( wfCatalog, pixelProviderProcessor, 
										assetId, needsASAP )

	return wfCatalog
			:docStore()
			:docValue( assetId )
			:tap(
				function( docValue )

					logger:infof( "developContentStream got %sdoc for %s",
									not docIsNonEmpty( docValue ) and
										"empty" or "",
									assetId )
										
				end )
			:filter( docIsNonEmpty )
			:flatMap(
				function( docValue )

					if docValue._localId then

						wfCatalog
							:fileAccessDb()
							:touch( docValue._localId )

					end

					return developContentStreamImp( 
								pixelProviderProcessor, docValue, needsASAP )

				end )

end
 
--------------------------------------------------------------------------------

local function developContentModel( wfCatalogStream, assetId )

	return {
		
		value = wfCatalogStream
					:mapSwitch(
						function( wfCatalog, pixelProviderProcessor )

							return developContentStream( 
									wfCatalog, pixelProviderProcessor, 
									assetId )

						end )

	}
	
end

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

local _developContentWithLocalNegativePathModelId = 0

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

local function settingsXmpStatusIsNotPending( baseSettings )
	
	return baseSettings.settingsXmpStatus ~= "pending"

end

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

local function baseSettingsWithLocalNegativePathStream(
				wfCatalog, pixelProviderProcessor, pixStreamId, assetId ) 

	return pixelProviderProcessor
			:pixStreamIdPixelPathStream( pixStreamId )
			:where( 
				function( _, theAssetId )

					return theAssetId == assetId

				end )
			:tap(
				function( thePixStreamId, localAssetId, havePath )

					logger:infof( "%s.%s state: %s",
									thePixStreamId, localAssetId, 
									havePath and 
										"have local path" or
										"has no local path" )

				end )
			:flatMap(
				function( _, localAssetId, havePath )

					if not havePath then

						return Rx.Observable.throw( 
								string.format( "no local negative path for: %s",
												localAssetId ) )

					end

					return developContentStream( 
							wfCatalog, pixelProviderProcessor, 
							localAssetId, 'asap' )

				end )
			:tap(
				function( baseSettings )

					logger:infof( "%s.%s got preliminary settings with settingsXmpStatus: %s",
									pixStreamId, baseSettings.id_local, 
									baseSettings.settingsXmpStatus )

				end )
			:filter( settingsXmpStatusIsNotPending )
			:tap(
				function( baseSettings )

					logger:infof( "%s.%s got final settings with settingsXmpStatus: %s",
									pixStreamId, baseSettings.id_local, 
									baseSettings.settingsXmpStatus )

				end )
			:take( 1 )

end

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

local function developContentWithLocalNegativePathModel( 
				wfCatalogStream, assetId )

	_developContentWithLocalNegativePathModelId = 
		_developContentWithLocalNegativePathModelId + 1

	local pixStreamId = 
			string.format( "developContentWithLocalNegativePathModel%s", 
							_developContentWithLocalNegativePathModelId )

	return {
		
		value = wfCatalogStream
					:mapSwitch(
						function( wfCatalog, pixelProviderProcessor )

							return Rx.Observable.create( 
								function( observer )

									-- avoid race condition subscribe to
									-- stream before we request a local
									-- negative path
									local baseSettingsWithLocalNegativePathSubscription =
										baseSettingsWithLocalNegativePathStream(
												wfCatalog,
												pixelProviderProcessor,
												pixStreamId, assetId ) 
											:subscribe( observer )

									local negPathSubscription =
										pixelProviderProcessor
											:dispatch( 
												"getLocalNegativePathASAP", 
													pixStreamId, assetId )
											:subscribeOnCompleted(

												function()

													logger:infof( "finished getLocalNegativePathASAP request for %s.%s",
																	pixStreamId, assetId )

												end )

									local disposePixelProviderRequest =
										Rx.Disposable.create(
											function()

												logger:infof( "Cleaning up request: %s",
																pixStreamId )

												pixelProviderProcessor
													:runUntilDone( 
														"abandonAllPixStreamRequests", 
															pixStreamId )

											end )

									return Rx.CompositeDisposable(
												baseSettingsWithLocalNegativePathSubscription,
												negPathSubscription,
												disposePixelProviderRequest )

								end )

						end )

	}
	
end

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

local function ongoingDevelopContentsModel( wfCatalogStream )
	
	local needDevelopContentStream = Rx.Subject()

	return {
		
		value = wfCatalogStream
					:mapSwitch(
						function( wfCatalog, pixelProviderProcessor )

							return makeOngoingDevelopContentsStream( 
									"apron develop contents", wfCatalog,
									pixelProviderProcessor,
									needDevelopContentStream )

						end ),

		events = {

			setAssetIds = function( _, assetIdChangeCounterTuples )

				WFArguments.check( "_", "table" )

				needDevelopContentStream
					:onNext( assetIdChangeCounterTuples )

			end,

		}

	}
	
end

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

local function xmpCameraRawOverlay( wfCatalog, cameraRawXmp, baseOverlay, forceExternalization, maybeCameraRawLimitOverride )
	
	local settingsLength = cameraRawXmp and string.len( cameraRawXmp ) or 0
	
	if maybeCameraRawLimitOverride and maybeCameraRawLimitOverride < INLINE_XMP_CAMERA_RAW_MAX_LENGTH then
		logger:infof( "Overriding max ACR settings length. Was %s, now %s", INLINE_XMP_CAMERA_RAW_MAX_LENGTH, maybeCameraRawLimitOverride )
		INLINE_XMP_CAMERA_RAW_MAX_LENGTH = maybeCameraRawLimitOverride
	end
	
	-- ensure baseOverlay meets the minimum requirements
	baseOverlay = baseOverlay or {}
	baseOverlay.develop = baseOverlay.develop or {}
	
	if settingsLength < INLINE_XMP_CAMERA_RAW_MAX_LENGTH and ( not forceExternalization ) then
		
		baseOverlay.develop.xmpCameraRaw = cameraRawXmp
		
		return Observable.just( baseOverlay )

	end
		
	return Observable.just( baseOverlay )
		:flatMap( function( overlay )
			
			-- too big to send to Oz inline, save to local file, 
			-- send sha256 of develop settings and let the upload 
			-- process attach the file to the asset record on Oz
		
			local settingsAsKSData = KSData.make( cameraRawXmp )
			
			return WFCrypto.asyncDigestData( settingsAsKSData, 'SHA256' )
				:map( function( sha256 )
					
					local pathForSettingsXmp = wfCatalog:fileSpec():pathForSettingsXmp( sha256 )
					
					overlay.develop.xmpCameraRaw = { sha256 = sha256 }
		
					overlay._localOnly = { 
						files = {
							xmp_develop = {
								path = pathForSettingsXmp,
								fileSize = settingsLength,
								sha256 = sha256,
							}
						},
						sourceXmpDevelopLink = { _delete = true } -- WF-1323: settings changes should remove sourceXmpDevelopLink as its no longer valid
					}
					
					return overlay, pathForSettingsXmp
					
				end )
				:flatMap( function( overlayWithSha256, path )
					
					return WFAsyncFileUtils.calculatePathSize( path )
						:flatMap( function( pathSize )
							local settingsLen = KSData.length( settingsAsKSData )
							
							if pathSize == settingsLen then -- settings already exist at this location, don't resave
								return Observable.just( overlayWithSha256 )
							end
							
							return WFAsyncFileUtils.saveToFile( settingsAsKSData, path )
								:tapOnError( function( stdError, platformError )
									logger:errorf( "failed to write external develop settings to %s: %s (%s)", path, stdError, platformError )
								end )
								:catch( function()
									return Observable.just( nil )
								end )
								:map( function()
									return overlayWithSha256
								end )
						end )
				:tap( function()
					settingsAsKSData:close()
				end )
				
			end )

		end )

end

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

local crVersion

local function developSettingsOverlay( wfCatalog, imageId, orientation, croppedWidth, croppedHeight, settingsXmp, dateChanged, fromDefaults )
	
	if not crVersion and WFCrInfo then
		local _, _, _, crVersionString = WFCrInfo.getACRVersion()
		crVersion = crVersionString
	end
	
	local overlayWithoutXMPCameraRaw = {
		_id = imageId,
		
		develop = {
			processingModel = 'lightroom',
			crsVersion = crVersion,
			userOrientation = orientation,
			croppedWidth = croppedWidth,
			croppedHeight = croppedHeight,
			userUpdated = dateChanged,
			fromDefaults = fromDefaults,
		}
	}

	return xmpCameraRawOverlay( wfCatalog, settingsXmp, overlayWithoutXMPCameraRaw )
	
end

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

local function externalizeDevelopSettings( wfCatalog, imageId, maybeMaxLength )

	return wfCatalog:docStore():docValue( imageId )
		:flatMap( function( docValue )
			if docValue._empty then
				-- doc has been deleted, ignore externalizing develop settings
				return Observable.just( true )
			end
			
			local currentSettingsXmp = derefOpt( docValue, 'develop', 'xmpCameraRaw' )
			
			if type( currentSettingsXmp ) ~= 'string' then
				-- doc already has settings externalized, no need to redo
				return Observable.just( true )
			end
			
			local baseOverlay = {
				_id = docValue._localId,
				develop = {
				}
			}
			
			return xmpCameraRawOverlay( wfCatalog, currentSettingsXmp, baseOverlay, true, maybeMaxLength )
				:flatMap( function( overlay )
					if not overlay then
						return Observable.just( false )
					end
					
					return wfCatalog:docStore():applyOverlay( overlay )
						:map( true )
				end )
		end )
	
end

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

local function createPendingSaveKey( assetId, saveSequence )

	return string.format( "%s:%d", assetId, saveSequence )
	
end

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

local function waitForPendingDevelopSettingsSaveForAssetIds( assetIds )

	-- For each asset ID, check to see if there are any existing, pending develop
	-- settings saves in progress. If there are, add it to the list of
	-- pending observables (pendingListeners).
	
	local pendingListeners = {}
	
	for _, assetId in ipairs( assetIds ) do
	
		local currentSaveSequenceForAssetId = _currentSaveSequencesByAssetId[ assetId ]
		
		if currentSaveSequenceForAssetId then

			-- A pending save exists for this asset.
			
			local key = createPendingSaveKey( assetId, currentSaveSequenceForAssetId )
			
			local pendingSaveListener = _pendingDevelopSettingsSaveListeners[ key ] or Rx.Subject()
			_pendingDevelopSettingsSaveListeners[ key ] = pendingSaveListener
				-- pendingSaveListener will resolve when the settings have been saved.
				
			pendingListeners[ #pendingListeners + 1 ] = pendingSaveListener
		
		end
	
	end
	
	if #pendingListeners == 0 then
	
		return Observable.just( true )
		
	end
	
	-- Wait for all of the pending saves to complete.
	
	return Observable.from( pendingListeners )
	
		:flatMap( function( pendingListener )
			return pendingListener
		end )
		
		:takeLast( 1 )
	
end

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

local function pendingDevelopSettingsSaveIsComplete( saveSequence, assetId, wasSaveSuccessful )

	-- Upon a successful save, notify anyone waiting for the pending save to complete
	-- that the save was successful.
	
	local currentSaveSequenceForAssetId = _currentSaveSequencesByAssetId[ assetId ]
	
	if saveSequence == currentSaveSequenceForAssetId then
	
		_currentSaveSequencesByAssetId[ assetId ] = nil
			-- There are no more pending saves for this specific asset.
			
	end
	
	local key = createPendingSaveKey( assetId, saveSequence )
	
	local pendingSaveListener = _pendingDevelopSettingsSaveListeners[ key ]
	_pendingDevelopSettingsSaveListeners[ key ] = nil
	
	if pendingSaveListener then

		pendingSaveListener:onNext( wasSaveSuccessful )
		pendingSaveListener:onCompleted()
		
	end
	
end

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

local function pendingDevelopSettingsSaveFailed( saveSequence, assetId )

	pendingDevelopSettingsSaveIsComplete( saveSequence, assetId, false )

end

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

local function pendingDevelopSettingsSaveSucceeded( saveSequence, assetId )

	pendingDevelopSettingsSaveIsComplete( saveSequence, assetId, true )

end

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

local function pendingDevelopSettingsSaveInProgress( saveSequence, assetId )

	-- Keep track of the current save sequence number for each pending save.
	
	_currentSaveSequencesByAssetId[ assetId ] = saveSequence

end

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

local function setCameraRawSettingsStream( 
					wfCatalog, profileCatalog, saveSequence, assetId,
					croppedWidth, croppedHeight, settingsXmp,
					maybeLensProfileDigest, 
					maybeCameraProfileFingerprint, --UUID
					maybeIsFallbackProfile, maybeCameraProfileDigest,
					maybeCameraProfileName,
					cameraModel, tiffOrientation, isReset )

	local fromDefaults = isReset == 'fromDefaults'
	
	local devSettingsDate = KSDate_timeInIso8601FormatNearestSecond()

	local developSettingsOverlayStream =
			developSettingsOverlay( 
				wfCatalog, assetId, tiffOrientation, croppedWidth, croppedHeight,
				settingsXmp, devSettingsDate, fromDefaults )

	local lensProfileOverlayStream =
			NmbProfileCatalogUtils.lensProfileOverlay( 
				wfCatalog, profileCatalog, assetId, maybeLensProfileDigest )
	
	local cameraProfileOverlayStream =
			NmbProfileCatalogUtils.cameraProfileOverlay( 
				wfCatalog, profileCatalog, assetId, 
				maybeCameraProfileFingerprint, cameraModel,
				maybeIsFallbackProfile, maybeCameraProfileDigest )

	return Observable.combineLatest( 
				developSettingsOverlayStream,
				lensProfileOverlayStream,
				cameraProfileOverlayStream )
			:map( 
				function( baseDevelopOverlay, lensProfileOverlay, cameraProfileOverlay )

					local baseOverlay = WFCatUtils.createBaseOverlayWithUserUpdatedField()

					local baseDevelopOverlayWithUserUpdatedField =
						KSTableUtils.merge( 
								{},  baseOverlay, baseDevelopOverlay )
			
					local developSettingsOverlayWithLensProfile = 
							KSTableUtils.merge( 
								{},  baseDevelopOverlayWithUserUpdatedField, lensProfileOverlay )

					local developSettingsOverlayWithCameraAndLensProfile = 
							KSTableUtils.merge( 
								{},  developSettingsOverlayWithLensProfile, cameraProfileOverlay )
								
					
					return developSettingsOverlayWithCameraAndLensProfile
					
				end )
			:flatMap( 
				function( settingsOverlay )
					
					return NmbProfileCatalogUtils.getCameraProfileInfoOverlay( 
							wfCatalog:ozCatalogId(), 
							profileCatalog,
							settingsOverlay, 
							maybeIsFallbackProfile, 
							maybeCameraProfileDigest,
							maybeCameraProfileName,
							cameraModel )
						
						:map( function( customProfileOverlay )
						
						-- In the case where a custom camera profile which does NOT yet
						-- exist on Oz (and maybe not locally yet either; e.g. pasted settings),
						-- is replacing a custom camera profile which DOES exist on Oz,
						-- we may be looking at a situation here where the profile link
						-- still in the asset payload is referring to the latter (the previously
						-- applied custom camera profile). One could argue that the profile link
						-- should point to a 'fallback' profile in this case. If that is so,
						-- then the fact that it doesn't is a bug in code that already
						-- executed upstream in this Rx chain - NmbProfileCatalogUtils.cameraProfileOverlay.
						
						-- It shouldn't matter too much whether the link left behind points to
						-- the already-on-Oz formerly-referred-to custom camera profile or
						-- and Adobe provided 'fallback' profile, as long as it gets fixed
						-- properly if/when the currently-referred-to custom camera profile
						-- becomes available on Oz. (Fixed by the Lr CC instance which uploaded
						-- the profile.)
								
						return KSTableUtils.merge( 
									{},  settingsOverlay, customProfileOverlay )
					end )
					
				end )
			:flatMap(
				function( settingsOverlay )

					if not isReset and not fromDefaults then

						-- If we are requesting to set fromDefaults to false make sure it
						-- previously existed, only change it to false if it did.

						-- Since the client code doesn't know whether fromDefaults is present
						-- it always specifies false when an edit occurs. So this code removes
						-- it if it wasn't already in the payload as it should only be set if
						-- the asset was imported or reset to original/default when not using
						-- Adobe Default (catalog.settings.universal.editDefaults.enabled == true).

						-- Setting it to false instead of not present will make it easier for
						-- oz engineers to debug issues. I think seeing it as false instead of
						-- not present will allow them to know this asset was originally imported
						-- with editDefaultsEnabled and has now been edited.

						return wfCatalog:docStore():docValue( assetId )
							:map( function( docValue )
								if derefOpt( docValue, 'develop', 'fromDefaults' ) == nil then
									settingsOverlay.develop.fromDefaults = nil
								end
								return settingsOverlay
							end )

					else

						if isReset then
							-- Remove userUpdated when resetting so the photo doesn't show
							-- up in searches for edited. Do this regardless of wether it
							-- is a raw or jpeg so search behaves consistently.
							settingsOverlay.develop.userUpdated = { _delete = true }
						end

						return Observable.just( settingsOverlay )

					end

				end )
			:flatMap( 
				function( settingsOverlay )
			
					return wfCatalog:docStore():applyOverlay( settingsOverlay )
				
				end )
			:call( runUntilDone )
			:map(
				function()

					return saveSequence, assetId

				end )
		
end

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

local function setCameraRawSettings( wfCatalogStream )

	local cameraRawSettingsStream = Rx.Subject()

	return  {

		value = wfCatalogStream
					:mapSwitch(
						function( wfCatalog, _, profileCatalog )

							return cameraRawSettingsStream
									:flatMap(
										function( saveSequence, assetId,
													croppedWidth,
													croppedHeight, xmpSettings,
													maybeLensProfileDigest,
													maybeCameraProfileFingerprint,
													maybeIsFallbackProfile, 
													maybeCameraProfileDigest,
													maybeCameraProfileName,
													cameraModel, tiffOrientation,
													isReset )

											logger:infof( "starting save of imageId %s", assetId )
					
											return 
												setCameraRawSettingsStream(
													wfCatalog, profileCatalog,
													saveSequence, assetId,
													croppedWidth,
													croppedHeight, xmpSettings,
													maybeLensProfileDigest,
													maybeCameraProfileFingerprint,
													maybeIsFallbackProfile, 
													maybeCameraProfileDigest,
													maybeCameraProfileName,
													cameraModel, tiffOrientation,
													isReset )
													
												:catch(
													function( ... )
													
														pendingDevelopSettingsSaveFailed( saveSequence, assetId )
													
														return( ... )
													
													end )

										end )
									:map(
										function( saveSequence, savedAssetId )

											pendingDevelopSettingsSaveSucceeded( saveSequence, savedAssetId )

											logger:infof( "Saved settings for imageId %s with sequence", 
															savedAssetId, saveSequence )
										
											return { saveSequence, savedAssetId }

										end )

						end ),

		events = {

			save = function( _, saveSequence, assetId, croppedWidth,
								croppedHeight, xmpSettings,
								maybeLensProfileDigest,
								maybeCameraProfileFingerprint,
								maybeIsFallbackProfile,
								maybeCameraProfileDigest,
								maybeCameraProfileName,
								cameraModel, tiffOrientation,
								isReset )
				
				pendingDevelopSettingsSaveInProgress( saveSequence, assetId )
				
				cameraRawSettingsStream
					:onNext( saveSequence, assetId, croppedWidth,
								croppedHeight, xmpSettings,
								maybeLensProfileDigest,
								maybeCameraProfileFingerprint,
								maybeIsFallbackProfile,
								maybeCameraProfileDigest,
								maybeCameraProfileName,
								cameraModel, tiffOrientation,
								isReset )

			end,
				
		}
	}

end

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

local function saveExternalizedXmp( wfCatalog, xmpKSData, versionDocVal )

	local xmpSha256 = KSTableUtils.derefOpt( versionDocVal, "develop",
													"xmpCameraRaw", "sha256" )

	if not xmpKSData then
		return Observable.throw(
			string.format( "invalid externalized xmp data for version doc %s",
														versionDocVal._id ) )
	end
	
	local path = wfCatalog:fileSpec():pathForSettingsXmp( xmpSha256 )
	
	return WFAsyncFileUtils.saveToFile( xmpKSData, path )
		:map( function( success )
			if success then
				return path, KSData.length( xmpKSData ), xmpSha256
			else
				error( string.format( "failed to save externalized xmp to %s", path ) )
			end
		end )
			
end

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

return {

	isUpdateWanted = isUpdateWanted,

	ongoingDevelopContentModel = ongoingDevelopContentModel,

	developContentModel = developContentModel,

	developContentWithLocalNegativePathModel = 
		developContentWithLocalNegativePathModel,

	ongoingDevelopContentsModel = ongoingDevelopContentsModel,

	developSettingsOverlay = developSettingsOverlay,
	
	externalizeDevelopSettings = externalizeDevelopSettings,

	setCameraRawSettings = setCameraRawSettings,
	
	saveExternalizedXmp = saveExternalizedXmp,
		
	waitForPendingDevelopSettingsSaveForAssetIds = waitForPendingDevelopSettingsSaveForAssetIds,

	_DEBUG_pendingDevelopSettingsSaveFailed = pendingDevelopSettingsSaveFailed,

	_DEBUG_pendingDevelopSettingsSaveSucceeded = pendingDevelopSettingsSaveSucceeded,

	_DEBUG_pendingDevelopSettingsSaveInProgress = pendingDevelopSettingsSaveInProgress
	
}
