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

WFCatCaptureDateTask.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2017 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.logging
local WFLogger = require 'WFLogger'

	-- WF.core.utils
local KSTableUtils = require 'KSTableUtils'
local WFRxBatchedBulkApplyOverlay = require 'WFRxBatchedBulkApplyOverlay'
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local WFFunctionUtils = require 'WFFunctionUtils'
local WFArguments = require 'WFArguments'

	-- WF.core.os
local KSDate = require 'KSDate'

	-- WF.oz_client.managed_catalog
local WFCatUndo = require 'WFCatUndo'
local WFCatTask = require 'WFCatTask'
local WFCatUtils = require 'WFCatUtils'

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

local logger = WFLogger( "WFCatCaptureDateTask" )

local derefOpt = assert( KSTableUtils.derefOpt )

local WFFunctionUtils_reduce = assert( WFFunctionUtils.reduce,
			"WFFunctionUtils.reduce does not exist" )

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

local kDefaultBatchSize = 25

local kInfinity = 1/0

local kNegativeInfinity = -kInfinity

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

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 getCount( arrayOfArray )
    if not arrayOfArray or #arrayOfArray == 0 then
        return 0
    end

    local count = 0
	
    for _, array in ipairs( arrayOfArray ) do
        count = #array + count
    end
	
    return count
end

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

local function isNaN( value )
	return type( value ) == 'number' and value ~= value
end

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

local function isValidTimeDelta( timeDelta )
	return timeDelta ~= kInfinity and timeDelta ~= kNegativeInfinity and not isNaN( timeDelta )
end

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

local function isEmptyDate( dateString )
	if dateString == nil or dateString == "" then
		return true
	end
	
	return not not string.find( dateString, '0000%-00%-00' )
end

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

local function countOfMap( map )
	if map == nil then return 0 end
	
	local count = 0
	for _, _ in pairs( map ) do
		count = count + 1
	end
	
	return count
end

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

local function undoEditCaptureDate( docStore, optUndoStateToken, progressReporter, cancelObservable )

    local undoState = WFCatUndo.getUndoState( optUndoStateToken )

    assert ( undoState and type(undoState) == "table" )
    assert ( undoState.arrayOfOverlaysArray and type (undoState.arrayOfOverlaysArray) == "table" )

    progressReporter:setTotal( getCount( undoState.arrayOfOverlaysArray ) )

    return Observable.from( undoState.arrayOfOverlaysArray )

        :map( function( arrayOfOverlays )

           return WFRxBatchedBulkApplyOverlay( "undo edit capture date", docStore, arrayOfOverlays )
               :map( function( ret )
                    progressReporter:reportBulkSuccess( arrayOfOverlays and #arrayOfOverlays or 0 )
                    return arrayOfOverlays
                end )
				
        end )

        :flatMerge( 1 )
		
		:tapOnError( function( err )
			progressReporter:reportBulkError( 0, { errorMessage = err }, true )
			
			logger:errorf( "WFCatCaptureDateTask.undoEditCaptureDate() error : %s", err )
		end )

        :finally( function()
            progressReporter:done()
        end )

        :call( WFRxRunUntilDone )
		
end

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

local function editOneDoc( timeDelta, newCaptureDate, docValue )
	local assetId = docValue._id
	local originalCaptureDate = derefOpt( docValue, 'captureDate' )
	local originalDateTimeOriginal = derefOpt( docValue, 'xmp', 'exif', 'DateTimeOriginal' )
	local originalDateCreated = derefOpt( docValue, 'xmp', 'photoshop', 'DateCreated' )
	local originalCreateDate = derefOpt( docValue, 'xmp', 'xmp', 'CreateDate' )
	
	local originalDate = originalCaptureDate or originalDateTimeOriginal 
		or originalDateCreated or originalCreateDate

	originalDate = originalDate and KSDate.maybeKSDateFromString( originalDate ) or nil

	local function shiftCaptureDate()
		local shiftDate = KSDate.shiftIso8601DateTime( originalDate, timeDelta )
		
		-- 	1) 'captureDate' will always be shifted to the new date base on the timeDelta value.
		-- 	2) 'xmp.exif.DateTimeOriginal' will be updated *ONLY* if the field already exists.
		-- 	3) 'xmp.photoshop.DateCreated' will be updated *ONLY* if the field already exists.
		-- 	4) 'xmp.xmp.CreateDate' will be set to the original capture date *ONLY* if the field is empty.
	
		local overlays = WFCatUtils.createBaseOverlayWithUserUpdatedField( assetId )
		
		overlays[ "captureDate" ] = shiftDate
		
		local xmp = {}
		
		if originalDateTimeOriginal then
			xmp[ "exif" ] = { DateTimeOriginal = shiftDate }
		end
		
		if originalDateCreated then
			xmp[ "photoshop" ] = { DateCreated = shiftDate }
		end
		
		if not originalCreateDate then
			xmp[ "xmp" ] = { CreateDate = originalCaptureDate }
		end
		
		if countOfMap( xmp ) > 0 then
			overlays[ "xmp" ] = xmp
		end
		
		-- Undo
		-- 	1) 'captureDate' will be reset if the original field exist, or be deleted.
		--	2) 'xmp.exif.DateTimeOriginal' will be reset only if the field exists.
		--	3) 'xmp.photoshop.DateCreated' will be reset onlyl if the field exists.
		-- 	4) 'xmp.xmp.CreateDate' will be deleted if the original field does not exist.
	
		local undoOverlays = {}
		undoOverlays[ "_id" ] = assetId
		
		undoOverlays[ "captureDate" ]  = originalCaptureDate or { _delete = true }
		
		local undoXmp = {}
		
		if originalDateTimeOriginal then
			undoXmp[ "exif" ] = { DateTimeOriginal = originalDateTimeOriginal }
		end
		
		if originalDateCreated then
			undoXmp[ "photoshop" ] = { DateCreated = originalDateCreated }
		end
		
		if not originalCreateDate then
			undoXmp[ "xmp" ] = { CreateDate = { _delete = true } }
		end

		if countOfMap( undoXmp ) > 0 then
			undoOverlays[ "xmp" ] = undoXmp
		end
		
		return overlays, undoOverlays
	end
	
	local function setCaptureDate()
	
		-- 	1) 'captureDate' will be set to the new date.
		-- 	2) 'xmp.exif.DateTimeOriginal' will be updated *ONLY* if the field already exists.
		-- 	3) 'xmp.photoshop.DateCreated' will be updated *ONLY* if the field already exists.
		-- 	4) 'xmp.xmp.CreateDate' has no change for now.
	
		local overlays = {}
		overlays[ "_id" ] = assetId
		
		overlays[ "captureDate" ] = newCaptureDate
		
		local xmp = {}
		
		if originalDateTimeOriginal then
			xmp[ "exif" ] = { DateTimeOriginal = newCaptureDate }
		end
		
		if originalDateCreated then
			xmp[ "photoshop" ] = { DateCreated = newCaptureDate }
		end

		if countOfMap( xmp ) > 0 then
			overlays[ "xmp" ] = xmp
		end
		
		-- Undo
		--	1) 'captureDate' will be set to zero date.
		--	2) 'xmp.exif.DateTimeOriginal' will be reset only if the field exists.
		--	3) 'xmp.photoshop.DateCreated' will be reset onlyl if the field exists.
	
		local undoOverlays = {}
		undoOverlays[ "_id" ] = assetId
		
		undoOverlays[ "captureDate" ]  = "0000-00-00T00:00:00" 
		
		local undoXmp = {}
		
		if originalDateTimeOriginal then
			undoXmp[ "exif" ] = { DateTimeOriginal = originalDateTimeOriginal }
		end
		
		if originalDateCreated then
			undoXmp[ "photoshop" ] = { DateCreated = originalDateCreated }
		end

		if countOfMap( undoXmp ) > 0 then
			undoOverlays[ "xmp" ] = undoXmp
		end
		
		return overlays, undoOverlays
	end

	-- If no capture date, set the specified capture date, otherwise, shift capture date by the same delta.
	if isEmptyDate( originalDate ) then
		return setCaptureDate()
	else
		return shiftCaptureDate()
	end
end

--------------------------------------------------------------------------------
	
--[[!
	
	If the new capture date is a valid date string, capture date releated fields will be updated

	params:
		-- assetIds : table, the selected assets that will be updated.
		-- timeDelta : number, if the time delta is invalid ( nan or infinity ), the error will be reported.
		-- newCaptureDate : string, the image has no capture date will be set to the value.
	
--]]

local function editCaptureDate( docStore, assetIds, timeDelta, newCaptureDate, progressReporter, cancelObservable )
	
	assert( assetIds and type( assetIds ) == "table" )
	assert( timeDelta )
	
	local batchSize = kDefaultBatchSize
		
	progressReporter:setTotal( #assetIds )

	if not isValidTimeDelta( timeDelta ) then
		progressReporter:reportBulkError( 0, { errorMessage = "Invalid timeDelta" }, true )
		progressReporter:done()
		
		logger:errorf( "WFCatCaptureDateTask.editCaptureDate() invalid timeDelta value : %s",  tostring( timeDelta ) )
		
		return Observable.just( true )
	end

	local batchedDocIds = WFFunctionUtils_reduce( { {} }, addToBatch, assetIds, batchSize )
		
	return Observable.from( batchedDocIds )
		
		:map( function( docIdsBatch )
		
			return docStore:bulkDocValue( docIdsBatch )
			
				:flatMapLatest( function( docValueArray )
					local arrayOfOverlays = {}
					local undoOverlayArray = {}
					
					for _, docValue in ipairs( docValueArray or {} ) do
						if not docValue._empty and not docValue._delete then
							local overlays, undoOverlays = editOneDoc( timeDelta, newCaptureDate, docValue )
							
							arrayOfOverlays[ #arrayOfOverlays + 1 ] = overlays
							undoOverlayArray[ #undoOverlayArray + 1 ] = undoOverlays
						end
					end 
									
					if #arrayOfOverlays == 0 then
						progressReporter:reportBulkSuccess( docValueArray and #docValueArray or 0 )
						return Observable.just( nil )
					else
						return WFRxBatchedBulkApplyOverlay( "edit capture date", docStore, arrayOfOverlays )
							:map( function( ret )
								progressReporter:reportBulkSuccess( docValueArray and #docValueArray or 0 )
								return undoOverlayArray
							end )
					end	
				end )
		end )
		
		:flatMerge( 1 )
		
		:takeUntil( cancelObservable )
		
        :filter( function( arrayOfOverlays )
            return arrayOfOverlays
        end )

        :toArray()

        :map( function( processedDocValueArrays )
            local undoStateToken

            undoStateToken = WFCatUndo.saveUndoState {
                arrayOfOverlaysArray = processedDocValueArrays,
            }

            progressReporter:reportBulkSuccess( 0, {
                undoToken = undoStateToken,
                totalCount = getCount( processedDocValueArrays )
            }, true )
        end )
		
		:tapOnError( function( err )
			progressReporter:reportBulkError( 0, { errorMessage = err }, true )
	
			logger:errorf( "WFCatCaptureDateTask.editCaptureDate() error : %s", err )
		end )
		
		:finally( function()
			progressReporter:done()
		end )
		
		:call( WFRxRunUntilDone )
		
end

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

local function createCaptureDateTask( wfCatalog, assetIds, timeDelta, newCaptureDate, optUndoStateToken )
	
	WFArguments.check( 'WFCatalog', '?table', '?number', '?string', '?number' )
	
	local docStore = wfCatalog:docStore()
	assert ( docStore )
		
	local function editCaptureDateFunc( args, progressReporter, cancelObservable )
		if optUndoStateToken then
			undoEditCaptureDate( docStore, optUndoStateToken, progressReporter )
		else
			editCaptureDate( docStore, assetIds, timeDelta, newCaptureDate, progressReporter, cancelObservable )
		end
	end
	
	local request = {
		args = {
			wfCatalog = wfCatalog, 
			assetIds = assetIds, 
			timeDelta = timeDelta,
			optUndoStateToken = optUndoStateToken
		},
		
		action = editCaptureDateFunc, 
	}
	
	local task = WFCatTask( request )
	
	return task
end

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

return {

	createCaptureDateTask = createCaptureDateTask,

}
