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

NmbDeletionBacklogImp.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2022 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.

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

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

	-- WF.core.logger
local WFLogger = require 'WFLogger'

	-- WF.core.utils
local WFArguments = require 'WFArguments'
local WFCloseSupport = require 'WFCloseSupport'
local WFRxScheduler = require 'WFRxScheduler'
local WFFileUtils = require 'WFFileUtils'
local WFRxRunUntilDone = require 'WFRxRunUntilDone'

	-- WF.catalog
local WFCatDelete = require 'WFCatDelete'
local WFCatFileSpec = require 'WFCatFileSpec'

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

local check = assert( WFArguments.check )

local logger = WFLogger 'NmbDeletionBacklogImp'

local runUntilDone = WFRxRunUntilDone

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

local function buildWhereClause( onlineVolumeGuids )

 	local guidExpression = table.concat( onlineVolumeGuids, "' OR volumeGuid = '" )
	return string.format( " WHERE ( volumeGuid = '%s' )", guidExpression )
	
end

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

local function transferTableContents( self )

	local sql = 'SAVEPOINT transferBacklog'

	self._db:exec( sql )

	local statement = 
			[[INSERT INTO deletionBacklogV2( volumeGuid, relativePath )
			SELECT volumeGuid, relativePath
			FROM deletionBacklog]]

	self._db:exec( statement )

	self._db:exec( [[DELETE FROM deletionBacklog]] )
	
	sql = 'RELEASE SAVEPOINT transferBacklog'
	
	self._db:exec( sql )
	
end


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

-- @mark NmbDeletionBacklogImp namespace

--[[!

	\class NmbDeletionBacklogImp
	
--]]

local NmbDeletionBacklogImp = {

	_docStore = false,
		-- (WFRxDocStore)

	_db = false,
		-- (WFSQLiteDatabase)
	
	_backlogCache = false,
		-- (table) Used to track backlog updates that haven't yet been written to the db.
		
	_processedDeletionBacklogStream = false,
		-- (Rx.Subject) Fires when deletions are processed. Used by unit tests.
		
	_updateDatabaseSubscription = false,
		-- ( Rx.Subscription ) Subscription to a timer that periodically commits updates to the db.
		
	_processBacklogSubscription = false,
		-- ( Rx.Subscription ) Subscription to task that periodically processes the deletion backlog.	
	
	WFCloseSupport.addCloseSupportMethods,	
	
}

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

local function commitUpdatesToDatabase( self )

	if not next( self._backlogCache ) then
		return
	end

	if WFCloseSupport.hasBeenOrWillBeClosed( self ) then
		return
	end

	local sql = 'SAVEPOINT updateDeletionBacklog'

	self._db:exec( sql )

	for _, entry in pairs( self._backlogCache ) do
	
		local volumeGuid = entry.volumeGuid
		local relativePath = entry.relativePath
		local localId = entry.localId
		local binaryType = entry.binaryType
		
		if entry.key == 'remove' then
	
			local deleteSQL = [[DELETE FROM deletionBacklogV2 WHERE volumeGuid = ? AND relativePath = ?]]
			self._db:exec( deleteSQL, volumeGuid, relativePath )
						
		elseif entry.key == 'add' then

			if localId and binaryType then

				local insertSQL = 
						[[INSERT OR IGNORE INTO deletionBacklogV2( volumeGuid, relativePath, localId, binaryType ) 
						VALUES( ?, ?, ?, ? )]]
				self._db:exec( insertSQL, volumeGuid, relativePath, localId, binaryType )

			else
			
				local insertSQL = [[INSERT OR IGNORE INTO deletionBacklogV2( volumeGuid, relativePath ) VALUES( ?, ? )]]
				self._db:exec( insertSQL, volumeGuid, relativePath )
			
			end
		
		end
		
	end
	
	self._backlogCache = {}
	
	sql = 'RELEASE SAVEPOINT updateDeletionBacklog'
	
	self._db:exec( sql )

end

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

local function addToBacklogCache( self, key, volumeGuid, relativePath, localId, binaryType )

	local cacheKey = volumeGuid .. '::' .. relativePath

	local entry = {
		key = key,
		volumeGuid = volumeGuid,
		relativePath = relativePath,
		localId = localId,
		binaryType = binaryType
	}

	self._backlogCache[ cacheKey ] = entry
	
	return Observable.just( true )

end

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

local function gatherArrayOfArray( self, sql, dontBringUpToDate )

	if not dontBringUpToDate then
		commitUpdatesToDatabase( self )
	end
	
  	return self._db:gatherArrayOfArray( sql )

end

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

local function processDeletionBacklog( self )

	local volumeGuidRows = gatherArrayOfArray( self,
			[[SELECT volumeGuid FROM deletionBacklogV2 GROUP BY volumeGuid]] )
			
	local onlineVolumeGuids = {}
			
	for _, row in ipairs( volumeGuidRows ) do
	
		local volumeGuid = row[ 1 ]

		if WFFileUtils.isVolumeGuidOnline( volumeGuid ) then
		
			table.insert( onlineVolumeGuids, volumeGuid )
		
		end
		
	end
	
	if #onlineVolumeGuids > 0 then
	
		local whereClause = buildWhereClause( onlineVolumeGuids )
		
		local rows = gatherArrayOfArray( self, 
				[[SELECT * FROM deletionBacklogV2 ]] .. whereClause )
		
		local needDocValues = {}
	
		for i, row in ipairs( rows ) do
		
			local localId = row[ 1 ]
			
			if localId then
				table.insert( needDocValues, row[ 1 ] )
			end
			
		end
		
		local maybeBulkDocValue = Observable.just( {} )
		
		if #needDocValues > 0 then
			maybeBulkDocValue = self._docStore:bulkDocValue( needDocValues )
		end
		
		return maybeBulkDocValue
		
			:map( function( docValues )
				
				local docValueLookUpTable = {}
				
				for _, docValue in ipairs( docValues ) do
				
					if docValue._localId then
						docValueLookUpTable[ docValue._localId ] = docValue
					end
					
				end

				for _, row in ipairs( rows ) do

					local localId = row[ 1 ]
					local binaryType = row[ 2 ]
					local volumeGuid = row[ 3 ]
					local relativePath = row[ 4 ]
					
					local docStoreVolumeGuid
					local docStoreRelativePath
					
					if localId then
					
						local docValue = docValueLookUpTable[ localId ] or {}
					
						if binaryType == 'original' then
							docStoreVolumeGuid = WFCatFileSpec.getOriginalVolumeGuidFromDocValue( docValue )
							docStoreRelativePath = WFCatFileSpec.getOriginalRelativePathFromDocValue( docValue )
						elseif binaryType == 'proxy' then
							docStoreVolumeGuid = WFCatFileSpec.getProxyVolumeGuidFromDocValue( docValue )
							docStoreRelativePath = WFCatFileSpec.getProxyRelativePathFromDocValue( docValue )
						elseif binaryType == 'xmp' then
						
							local absolutePath = WFCatFileSpec.getXmpSidecarPathFromDocValue( docValue )
							
							if absolutePath then 
								docStoreVolumeGuid = WFFileUtils.getVolumeGuidForPath( absolutePath )
								docStoreRelativePath = WFFileUtils.removeVolumeFromPath( absolutePath )
							end
							
						end
					
					end
					
					if relativePath and volumeGuid then
					
						-- SPAM [jandrew 2017-10-23]: If the path is still tracked in the doc store, do not
						-- delete the asset but do remove it from the backlog. This can happen if the app quits
						-- in the middle of a purge; this behavior is intentional and prevents us from leaking
						-- files and/or creating dangling pointers.
					
						if not localId or ( volumeGuid ~= docStoreVolumeGuid or relativePath ~= docStoreRelativePath ) then
					
							local success, path = pcall( 
									WFFileUtils.createFullPathFromRelativePathAndGuid,
									relativePath, volumeGuid )
									
							if success and path then
							
								-- If were were able to create a path, we know that the associated volume
								-- is connected.
							
								logger:infof( "Now removing asset at path: %s", path )
							
								local localPathExists = path and WFFileUtils.exists( path )

								if localPathExists then
								
									-- If the file exists, delete it from disk.
								
									local deleteWasSuccessful, maybeErrorMessage = WFCatDelete.deleteAssetAndMaybeParentFoldersByAbsolutePath( path )
									
									if not deleteWasSuccessful then
										logger:warnf( "Failed to remove asset at path: %s, Error: %s", path, maybeErrorMessage or "Not Available" )
									end
									
								end
								
								-- If the path doesn't exist or if the asset was deleted from disk, 
								-- then remove the entry from the backlog.
		
								addToBacklogCache( self, 'remove', volumeGuid, relativePath )
								
							end

						else
						
							addToBacklogCache( self, 'remove', volumeGuid, relativePath )
						
						end
						
					end

				end

				self._processedDeletionBacklogStream:onNext( #rows )

				return true

			end )
			
			:call( runUntilDone )

	end

	self._processedDeletionBacklogStream:onNext( 0 )

	return Observable.just( true )

end

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

function NmbDeletionBacklogImp:__init( docStore, db, optOverrides )

	check( 'NmbDeletionBacklogImp', 'WFRxDocStore', 'WFSQLiteDatabase', '?table' )

	logger:infof( "Creating NmbDeletionBacklogImp at path" )

	optOverrides = optOverrides or {}

	self._db = db

	transferTableContents( self )

	self._docStore = docStore

	self._backlogCache = {}

	self._processedDeletionBacklogStream = Rx.ReplaySubject( 1 )

	self._updateDatabaseSubscription = Observable.timer( 
			optOverrides.writeInterval or 0, 
			optOverrides.writeInterval or 10, 
			WFRxScheduler.sharedScheduler() )
	
		:subscribe(
			function()
				commitUpdatesToDatabase( self )
			end )
			
	self._processBacklogSubscription = Observable.timer( 
			optOverrides.processBacklogDelay or 60, 
			optOverrides.processBacklogDelay or 120, 
			WFRxScheduler.sharedScheduler() )
			
		:subscribe(
			function()
				processDeletionBacklog( self )
			end )
			
end

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

function NmbDeletionBacklogImp:close()
										
	local updateDatabaseSubscription = self._updateDatabaseSubscription
	self._updateDatabaseSubscription = nil
	
	if updateDatabaseSubscription then
		updateDatabaseSubscription:dispose()
	end
	
	local processBacklogSubscription = self._processBacklogSubscription
	self._processBacklogSubscription = nil
	
	if processBacklogSubscription then
		processBacklogSubscription:dispose()
	end
	
	-- Clear the cache
	commitUpdatesToDatabase( self )
	
	local db = self._db
	self._db = nil
	
	if db then
		db:close()
	end
	
	self._processedDeletionBacklogStream = nil
	
end

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

function NmbDeletionBacklogImp:addToDeletionBacklog( volumeGuid, relativePath, localId, binaryType )

	check( 'NmbDeletionBacklogImp', 'string', 'string', '?number', '?string' )
	
	logger:debugf( "addToDeletionBacklog %s:%s:%s:%s", localId, binaryType, volumeGuid, relativePath )
	
	return addToBacklogCache( self, 'add', volumeGuid, relativePath, localId, binaryType )
	
end

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

function NmbDeletionBacklogImp:removeFromDeletionBacklog( volumeGuid, relativePath )

	check( 'NmbDeletionBacklogImp', 'string', 'string' )
	
	logger:debugf( "removeFromDeletionBacklog %s:%s", volumeGuid, relativePath )
	
	return addToBacklogCache( self, 'remove', volumeGuid, relativePath )
	
end

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

function NmbDeletionBacklogImp:bulkAddToDeletionBacklog( localAssetsToDelete )

	check( 'NmbDeletionBacklogImp', 'table' )
	
	return Observable.just( true )
	
		:map( function()
		
			logger:infof( "Adding %d assets to the deletion backlog.", #localAssetsToDelete )
		
			for _, entry in ipairs( localAssetsToDelete ) do	
				
				local localId = entry.localId
				local binaryType = entry.binaryType
				
				local absolutePath = entry.absolutePath
					-- XMP sidecar files are stored via absolute paths.
					
				local volumeGuid = entry.volumeGuid
				local relativePath = entry.relativePath
					-- Originals and proxies are stored via relative paths.
					
				if absolutePath then

					volumeGuid = WFFileUtils.getVolumeGuidForPath( absolutePath )
					relativePath = WFFileUtils.removeVolumeFromPath( absolutePath )
					
				end
			
				if volumeGuid and relativePath then

					self:addToDeletionBacklog( volumeGuid, relativePath, localId, binaryType )
					
				end
			
			end
		
			return true
		
		end )
	
end

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

function NmbDeletionBacklogImp:bringDeletionBacklogUpToDate()

	check( 'NmbDeletionBacklogImp' )
	commitUpdatesToDatabase( self )
	
end

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

function NmbDeletionBacklogImp:processDeletionBacklog()

	check( 'NmbDeletionBacklogImp' )
	return processDeletionBacklog( self )
	
end

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

function NmbDeletionBacklogImp:DEBUG_processedDeletionBacklogStream()

	check( 'NmbDeletionBacklogImp' )
	return self._processedDeletionBacklogStream
	
end

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

local function convertSqlRowsToUsefulLuaTables( arr )

	local usefulArr = {}

	for idx, row in pairs( arr ) do

		local localId = row[ 1 ]
		local binaryType = row[ 2 ]
		local volumeGuid = row[ 3 ]
		local relativePath = row[ 4 ]
		
		usefulArr[ idx ] = {
			localId = localId,
			binaryType = binaryType,
			volumeGuid = volumeGuid,
			relativePath = relativePath,
		}

	end

	return usefulArr

end

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

--[[!

	\brief TEST USE ONLY - dump the contents of the deletionBacklogV2 table.

	\param dontBringUpToDate (optional boolean) If true, this will not update the DB before duping the contents.
	
	\return Contents of the deletionBacklogV2 table.
	
--]]

function NmbDeletionBacklogImp:DEBUG_dumpDeletionBacklogContents( dontBringUpToDate )

	check( 'NmbDeletionBacklogImp', '?boolean' )
	
	return Observable.just( convertSqlRowsToUsefulLuaTables( 
			gatherArrayOfArray( self, [[SELECT * FROM deletionBacklogV2]], dontBringUpToDate ) 
		) )

end

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

--[[!

	\brief TEST USE ONLY - dump the contents of the deletionBacklog table.

	\return Contents of the deletionBacklog table.
	
--]]

function NmbDeletionBacklogImp:DEBUG_dumpDeletionBacklogContentsV1()

	check( 'NmbDeletionBacklogImp' )
	
	return Observable.just( gatherArrayOfArray( self, [[SELECT * FROM deletionBacklog]], true ) )

end

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

--[[!

	\brief TEST USE ONLY 
	
--]]

function NmbDeletionBacklogImp:DEBUG_buildDeletionBacklogWhereClause( onlineVolumeGuids )

	check( 'NmbDeletionBacklogImp', 'table' )
	
	return Observable.just( buildWhereClause( onlineVolumeGuids ) )

end

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

return Rx.Class 'NmbDeletionBacklogImp'( NmbDeletionBacklogImp )
