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

WFCatFormattedMetadata.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.

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

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

	-- WF.core.utils
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFRxDocValueWatcher = require 'WFRxDocValueWatcher'
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local WFTableMerge = require 'WFTableMerge'
local WFRxThrottledBatchedBulkApplyOverlay = require 'WFRxThrottledBatchedBulkApplyOverlay'

	-- WF.oz_client
local WFCatClientMetadataUtils = require "WFCatClientMetadataUtils"
local WFCatUndo = require "WFCatUndo"
local WFCatUtils = require 'WFCatUtils'

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

local check = WFArguments.check

local derefOpt = KSTableUtils.derefOpt

local runUntilDone = WFRxRunUntilDone

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

local WFCatFormattedMetadata = {}

local sFormattersById = {}

local function addFormatterFamily( formatterFamily )

	formatterFamily = require( formatterFamily )

	for i, f in ipairs( formatterFamily ) do

		local id = f.id

		sFormattersById[ id ] = f

	end

end

addFormatterFamily "WFCatFormatter_attributes"
addFormatterFamily "WFCatFormatter_dates"
addFormatterFamily "WFCatFormatter_dimensions"
addFormatterFamily "WFCatFormatter_metadata"
addFormatterFamily "WFCatFormatter_exif"
addFormatterFamily "WFCatFormatter_file"
addFormatterFamily "WFCatFormatter_iptc"
addFormatterFamily "WFCatFormatter_video"

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

local function spack( ... )
	return { n = select( '#', ... ), ... }
end

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

local function getOverlay( formatterId, value, wfCatalog )
	local formatter = sFormattersById[ formatterId ]

	if formatter then
		if formatter.asset_overlay == nil then
			return nil, 'readonly'
		end
	else
		return nil, 'notfound'
	end
	return formatter:asset_overlay( value, wfCatalog )
end

WFCatFormattedMetadata.getOverlay = getOverlay

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

local kNilValue = { "< Nil >" }
local kMixedValue = { "< Mixed >" }

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

local function saveMetadataUndoState( imageIds, values, formatterId )

	assert( imageIds )
	assert( values )
	assert( formatterId )
	
	local state = {
		ids = KSTableUtils.deepCopy( imageIds ),
		settings = KSTableUtils.deepCopy( values ),
		formatterId = formatterId,
	}

	-- remove special tokens representing nil

	for k, v in pairs( values ) do
		if v == kNilValue then
			state.settings[ k ] = nil
		end
	end
	
	return WFCatUndo.saveUndoState( state )
end

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

local allValues = {}

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

--[[!
	Update a metadata field on a list of assetIds.

	\param wfCatalog the catalog
	\param assetIds an array of local ids
	\param formatterId (string) the ID of the metadata field to update
	\param value (string) the new value to assign to the metadata field of all the images
	\param optUndoStateToken (optional, number) Token identifying a WFCatUndo state to restore. If provided,
		then 'assetIds' and 'value' args are not used.

	\return RxObservable( result ) sends onCompleted when done with a table that containing an undoToken.
--]]

function WFCatFormattedMetadata.setMetadata( wfCatalog, imageIds, formatterId, value, optUndoStateToken )

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

	local perImageValues = {}
	local undoToken

	if optUndoStateToken then
		local undoState = WFCatUndo.getUndoState( optUndoStateToken )
		imageIds = assert( undoState.ids, "imageIds required in history state" )
		formatterId = assert( undoState.formatterId, "formatterId require in history state" )
		perImageValues = assert( undoState.settings, "settings reqired in history state" )
	else
		undoToken = saveMetadataUndoState( imageIds, allValues[ formatterId ], formatterId )
	end

	assert( #imageIds > 0, "imageIds must contain at least one image ID" )

	return Observable.from( imageIds )

		:selectMany( function( id_local )				

			local imageValue = perImageValues[ id_local ] or value or ""

			if imageValue == kNilValue then
				imageValue = ""
			end		
			if type( imageValue ) == "table" then
				imageValue = ""
			end	
			assert( imageValue ~= nil )
			assert( imageValue ~= kNilValue )
			--assert( string.find( imageValue, "Nil" ) == nil ) -- Caused NMB-24893.

			local overlay, msg = getOverlay( formatterId, imageValue, wfCatalog )
			if not overlay then
				if msg == 'readonly' then
					return Observable.throw( 'formatter ' .. formatterId .. ' is read-only' )
				elseif msg == 'notfound' then
					return Observable.throw( 'formatter ' .. formatterId .. ' is not found' )
				else
					return Observable.just( true )
				end
			end

			local baseOverlay = WFCatUtils.createBaseOverlayWithUserUpdatedField( id_local )
			
			local overlayToApply = WFTableMerge.threeWayMerge( {}, overlay, baseOverlay )
			
			return wfCatalog:docStore():applyOverlay( overlayToApply )

		end )

		:map( function( result )
			return {
				valuesToUpdate = {},
				undoToken = undoToken,
			}
		end )

		:call( runUntilDone )

end

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

function WFCatFormattedMetadata.setMultipleMetadata( wfCatalog, consolidatedMetadataValues )

	check( 'WFCatalog', 'table' )

	local overlays = {}
	
	for _, consolidatedMetadata in ipairs( consolidatedMetadataValues ) do
	
		local overlayToApply = {}
	
		local id_local
		
		for _ , imageFormatterValuePair in  pairs( consolidatedMetadata )  do

			local formatterId = derefOpt( imageFormatterValuePair, "formatterId" )
			local val = derefOpt( imageFormatterValuePair, "value" )
			
			if formatterId and val then

				local overlay, msg = getOverlay( formatterId, val, wfCatalog )
				
				if not overlay then

					if msg == 'readonly' then

						return error( string.format( 'formatter %s is read-only', formatterId ) )

					elseif msg == 'notfound' then

						return error( string.format( 'formatter %s is not found', formatterId ) )
						
					else 
					
						return error( string.format( 'formatter %s is %s', formatterId, msg ) )

					end

				end
					
				overlayToApply = WFTableMerge.threeWayMerge( {}, overlayToApply, overlay )
				
			else

				id_local = consolidatedMetadata.syncSourceId
				overlayToApply._newRevId = consolidatedMetadata._newRevId

			end

		end
			
		local baseOverlay = WFCatUtils.createBaseOverlayWithUserUpdatedField( id_local )
		overlayToApply = WFTableMerge.threeWayMerge( {}, overlayToApply, baseOverlay )
		
		overlays [ #overlays + 1 ] = overlayToApply

	end

	return WFRxThrottledBatchedBulkApplyOverlay( "setMultipleMetadata", wfCatalog:docStore(), overlays, 100, 10 )

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

function WFCatFormattedMetadata.isReadOnly( formatterId )

	check( '+string' )

	return not derefOpt( sFormattersById, formatterId, 'asset_overlay' )

end

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

local function formatField( wfCatalog, options, docValue, formatterId )

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

	local formatter = sFormattersById[ formatterId ]
	local formatted

	if formatter then

		if formatter.format_asset == nil then
			formatted = { 'unused' }
		else
			formatted = spack( formatter.format_asset( formatter, docValue, options, wfCatalog ) )
			if formatted.n == 0 then
				formatted = { 'unused' }
			end
		end

	else

		formatted = { 'literal', '< formatter ' .. formatterId .. ' not found >' }

	end

	return formatted

end

WFCatFormattedMetadata.formatField = formatField

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

function WFCatFormattedMetadata.ongoingHarvest( wfCatalog, imageIds, formatterIds, options )

	check( 'WFCatalog', 'table', 'table', '_', '?table' )
		
	options = options or {}	

	allValues = {}
	local consolidatedValues = {}
	
	local function getConsolidatedValues()
	
		local valueArray = {}
	
		for formatterId, perImageValues in pairs( allValues ) do
		
			local consolidatedValue = consolidatedValues[ formatterId ]
			local mixed = nil
		
			if consolidatedValue == nil then -- needs calculation
							
				for _, value in pairs( perImageValues ) do
					if consolidatedValue == nil then
						consolidatedValue = value
					elseif type( consolidatedValue ) == 'table' and consolidatedValue ~= kNilValue then
						if not KSTableUtils.deepEquals( consolidatedValue, value, 2 ) then
							consolidatedValue = kMixedValue
							break
						end
					elseif consolidatedValue ~= value then
						consolidatedValue = kMixedValue
						break
					end
				end
				
				if consolidatedValue == kNilValue then
					consolidatedValue = nil
				end
				
				if consolidatedValue == kMixedValue then
					consolidatedValue = nil
					mixed = true
				end
			end
			
			table.insert( valueArray, {
				formatterId = formatterId,
				value = consolidatedValue,
				mixed = mixed,
			} )

		end

		return valueArray
	end
	
	local function recordValue( imageId, formatterId, value )

		local formattedValue = WFCatClientMetadataUtils.localizeValue( value )
		
		if formattedValue == nil then
			formattedValue = kNilValue
		end

		if not allValues[ formatterId ] then
			allValues[ formatterId ] = {}
		end							
		
		if not allValues[ formatterId ][ imageId ] then
			allValues[ formatterId ][ imageId ] = {}
		end
		
		local isNewValue = allValues[ formatterId ][ imageId ] == nil
		local isChangedValue = not isNewValue and allValues[ formatterId ][ imageId ] ~= formattedValue
		local consolidatedValue = consolidatedValues[ formatterId ]
		
		if isNewValue and formattedValue ~= consolidatedValue then
			consolidatedValues[ formatterId ] = kMixedValue -- we know this is now mixed
		end
		
		if isChangedValue and consolidatedValue ~= kMixedValue then
			consolidatedValues[ formatterId ] = nil -- needs recalculation
		end
			
		allValues[ formatterId ][ imageId ] = formattedValue 
	
	end

	local mapDocValueMetadataFn = function( docValue )

		local imageId = docValue._localId

		for i, formatterId in ipairs( formatterIds ) do
					
			local value = formatField( wfCatalog, options, docValue, formatterId )
			
			recordValue( imageId, formatterId, value )

		end

	end
	
	local function harvestMetadataFromBatchOfDocValues( docValues )
		for _, docValue in ipairs( docValues ) do
			if not docValue._empty then
				mapDocValueMetadataFn( docValue )
			end
		end
		return getConsolidatedValues()
	end
	
	local function filterEmptyBatches( docValues )
		return docValues and #docValues > 0
	end
	
	return WFRxDocValueWatcher.make( Observable.just( imageIds ), wfCatalog:docStore(), "metadata" )
		:bufferWithTimeOrCount( .1, 500, require 'WFRxScheduler'.sharedScheduler() )
		:filter( filterEmptyBatches )
		:map( harvestMetadataFromBatchOfDocValues )
		
end

--------------------------------------------------------------------------------
-- A simple utility that watches wfCatalog and forwards on
-- with the first non-nil value it receives.

local function makeWithFirstValidCatalog( wfCatalog, defaultValue )

	return function( fn, ... )

		local params = { ... }
		local nParams = select( '#', ... )

		return wfCatalog

			:mapSwitch( function( wfCatalog )
				if not wfCatalog then
					return require 'Rx'.Observable.just( defaultValue )
				end
				
				return fn( wfCatalog, unpack( params, 1, nParams ) )
			end )

	end

end

--------------------------------------------------------------------------------
--[[
	\brief Watch harvested metadata for an array of assets.

	\param (Observable< WFCatalog >) the catalog

	\param (ints or UUIDs) imageIds local or UUID identifiers for an array of assets
		 in this catalog
	\param (strings) formatterIds identifiers of the metadata fields to harvest
	\param (boolean) consolidate if true, the union of values across all images is
		returned, flagging those with mixed values.

	\return A WFModel table
]]--
function WFCatFormattedMetadata.harvestedMetadataModel( wfCatalog, imageIds, formatterIds )
	
	local f = makeWithFirstValidCatalog( wfCatalog )
	
	return {
		
		value = f( WFCatFormattedMetadata.ongoingHarvest, imageIds, formatterIds ),
	}

end

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

return WFCatFormattedMetadata
