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

WFCatPixelFileIOWorker.lua

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

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

NOTICE: All information contained herein is, and remain 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 may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

--------------------------------------------------------------------------------]]
	
	-- wf.core
local KSDate = require 'KSDate'
local KSPathUtils = require 'KSPathUtils'
local WFArguments = require 'WFArguments'
local WFArguments_uuid = require 'WFArguments_uuid'
local WFArguments_check = assert( WFArguments.check,
										"missing WFArguments.check" )
local WFCloseSupport = require "WFCloseSupport"
local WFFileUtils = require 'WFFileUtils'
local WFFunctionUtils = require 'WFFunctionUtils'
local WFFolderWalker = require 'WFFolderWalker'

	-- WF.core.lua.threading
local WFRequestDispatcher = require 'WFRequestDispatcher'

-- WF.oz_client.managed_catalog.pixel_provider
local WFCatPixelPathGenerator = require 'WFCatPixelPathGenerator'

	-- Rx
local Rx = require "Rx"

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

local logger = require 'WFLogger'( "WFCatPixelFileIOWorker" )

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

local WFFileUtils_deleteFile = assert( WFFileUtils.deleteFile, 
										"WFFileUtils.deleteFile not defined" )

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

local WFFunctionUtils_map = WFFunctionUtils.map

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

local function setupDispatcher( methName, objectInstance )

	WFRequestDispatcher.onMessageDo {

		message = methName,

		target = objectInstance,

	}

end

--==============================================================================
-- class level methods. 
--==============================================================================

local function initMessageProcessor( WFCatPixelFileIOWorkerClass, name )

	local wfCatPixelFilePurgeImp = WFCatPixelFileIOWorkerClass( name  )

	WFFunctionUtils_map( setupDispatcher, 
							{ 
							  'purgeResultsStream', 
							  'deleteAssetRenditions', 
							  'scanRenditionFolderNamed',
							  'deleteRenditions',
							  'scanSettingsFolder',
							  'deleteSettings',
							},
							wfCatPixelFilePurgeImp )

end

--==============================================================================
-- instance methods
--==============================================================================

local WFCatPixelFileIOWorker = {
	
	_baseBinariesPath = false,

	_purgeResultsStream = false,

	WFCloseSupport.addCloseSupportMethods,

}

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

function WFCatPixelFileIOWorker:__init( baseBinariesPath )

	WFArguments_check( 'WFCatPixelFileIOWorker', 'string' )

	self._baseBinariesPath = baseBinariesPath

	self._purgeResultsStream = Rx.Subject()

end

--------------------------------------------------------------------------------
-- This is an ongoing stream of rendition file deletion results. This is useful
-- for monitoring purge results for the entire preview cache.
--
-- Returns:
--
-- 	A stream of arrays of tuples for each rendition file that we attempted to delete
--
-- 	{ { boolean, path, maybeErr },
-- 		{ boolean, path, maybeErr },
-- 		{ boolean, path, maybeErr } }

function WFCatPixelFileIOWorker:purgeResultsStream()

	return self._purgeResultsStream

end

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

local function reduceFolderFiles( folder, initVal, f, ... )

	local retVal = initVal

	for fname in WFFolderWalker( folder ) do

		retVal = f( retVal, fname, ... )

	end

	return retVal

end

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

local WFCatPixelPathGenerator_assetIdDigestAndLongEdgeFromPreviewPath =
		WFCatPixelPathGenerator.assetIdDigestAndLongEdgeFromPreviewPath

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

local WFArguments_uuid_isUuid = WFArguments_uuid.isUuid

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

local string_lower = string.lower

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

local function isAssetIdRendition( path, assetId )

	local ok, pathAssetId = 
			pcall( WFCatPixelPathGenerator_assetIdDigestAndLongEdgeFromPreviewPath, 
						path )

	return ok and WFArguments_uuid_isUuid( pathAssetId ) and 
			pathAssetId == string_lower( assetId )

end

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

local function deleteFileWithNormalizedResult( fname )

	local ok, maybeErr = pcall( WFFileUtils_deleteFile, fname )

	 return ok, fname, not ok and maybeErr or nil

end

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

local function deleteFileWithAppendedNormalizedResult( deletedFileResults, 
														fname )

	deletedFileResults[ #deletedFileResults + 1 ] = 
		{ deleteFileWithNormalizedResult( fname ) }

	return deletedFileResults

end

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

local function deletePathIfRenditionForAssetId( deletedRenditionResults, fname, 
												assetId )

	if not isAssetIdRendition( fname, assetId ) then

		return deletedRenditionResults

	end

	return deleteFileWithAppendedNormalizedResult( 
				deletedRenditionResults, fname )

end

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

local WFCatPixelPathGenerator_renditionFolderPathForAssetId =
		WFCatPixelPathGenerator.renditionFolderPathForAssetId

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

local function wrapInMulticastReturnValueToPurgeResultsStream( f )

	return function( self, ... )

		local ret = { f( self, ... ) }

		self._purgeResultsStream:onNext( unpack( ret ) )

		return unpack( ret )

	end

end

--------------------------------------------------------------------------------
-- This attempts to delete all rendition files that are linked to this assetId.
--
-- Returns:
--
-- 	An array of tuples for each rendition file that we attempted to delete
--
-- 	{ { boolean, path, maybeErr },
-- 		{ boolean, path, maybeErr },
-- 		{ boolean, path, maybeErr } }
--
-- 	This is also multicast to self:purgeResultsStream()

function WFCatPixelFileIOWorker:deleteAssetRenditions( assetId )

	WFArguments_check( 'WFCatPixelFileIOWorker', 'uuid' )

	local assetIdRenditionsPath = 
		WFCatPixelPathGenerator_renditionFolderPathForAssetId( 
							self._baseBinariesPath, assetId )
	
	return reduceFolderFiles( assetIdRenditionsPath, {}, 
								deletePathIfRenditionForAssetId, 
									assetId )
	
end

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

WFCatPixelFileIOWorker.deleteAssetRenditions =
	wrapInMulticastReturnValueToPurgeResultsStream(
		WFCatPixelFileIOWorker.deleteAssetRenditions )

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

local function maybeAddRenditionInfo( renditionInfos, maybeRenditionPath )

	local ok, assetId, digest, longSide = 
			pcall( WFCatPixelPathGenerator_assetIdDigestAndLongEdgeFromPreviewPath, 
						maybeRenditionPath )

	if ok then

		renditionInfos[ assetId ] = renditionInfos[ assetId ] or {}

		renditionInfos[ assetId ][ digest ] = 
			renditionInfos[ assetId ][ digest ] or {}

		renditionInfos[ assetId ][ digest ][ longSide ] = true

	end

	return renditionInfos

end

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

local WFCatPixelPathGenerator_getPreviewsSubfolder =
			WFCatPixelPathGenerator.getPreviewsSubfolder

--------------------------------------------------------------------------------
-- This walks a rendition folder and returns all rendition files in that folder.
--
-- Returns:
--
-- 	An map of cracked rendition paths in that folder partioned as follows
--
-- 	{ [ assetId1 ] = {
-- 			[ digest1 ] = { [ longSide1 ] = true, 
-- 							[ longSide2 ] = true,
-- 							... },
-- 			[ digest2 ] = { ... },
-- 			...
-- 		}
-- 	  [ assetId2 ] = { ... }
-- 	  ...
--
-- 	 }
--

function WFCatPixelFileIOWorker:scanRenditionFolderNamed( renditionFolderName )

	WFArguments_check( 'WFCatPixelFileIOWorker', 'string' )

	local ok, folderPath = 
			pcall( WFCatPixelPathGenerator_getPreviewsSubfolder,
					self._baseBinariesPath, renditionFolderName )

	if not ok then

		logger:warnf( "Bad subfolder '%s' passed to cache", 
						renditionFolderName )

		return {}

	end
	
	return reduceFolderFiles( folderPath, {}, maybeAddRenditionInfo )
	
end

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

local function addSettingsInfoIfOlderThan( maybeSettingsInfos, maybeSettingsPath, settingsAgeToAlwaysKeep )
	
	if maybeSettingsPath then
		
		local fileAttributes = WFFileUtils.fileAttributes( maybeSettingsPath )
		
		local fileTouchTime = fileAttributes.fileModificationDate or fileAttributes.fileCreationDate
		
		-- only consider settings files that are older than a certain age
		if fileTouchTime and fileTouchTime < settingsAgeToAlwaysKeep then
			local settingsSha256 = KSPathUtils.leafName( maybeSettingsPath )
			maybeSettingsInfos[ settingsSha256 ] = true
		end
		
	end

	return maybeSettingsInfos

end

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

function WFCatPixelFileIOWorker:scanSettingsFolder( settingsAgeInSeconds )

	WFArguments_check( 'WFCatPixelFileIOWorker' )
	
	local ok, folderPath = 
		pcall( WFCatPixelPathGenerator.folderForSettingsXmp, self._baseBinariesPath )
		
	if not ok then
	
		logger:warnf( "Failed to find subfolder for develop settings XMP: %s", folderPath )
		
		return {}
	end
	
	return reduceFolderFiles( folderPath, {}, addSettingsInfoIfOlderThan, KSDate.timeNow() - settingsAgeInSeconds )
	
end
				

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

local WFCatPixelPathGenerator_pathForPreviewDigestAndLongEdge =
			WFCatPixelPathGenerator.pathForPreviewDigestAndLongEdge

local WFCatPixelPathGenerator_pathForPreviewDigestAndLongEdgeJpg =
			WFCatPixelPathGenerator.pathForPreviewDigestAndLongEdgeJpg

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

local function reduceRenditionInfos( baseBinariesPath, renditionInfos,
											initVal, f, ... )

	WFArguments_check( 'string', 'table', '_', 'function' )

	local ret = initVal

	for assetId, digests in pairs( renditionInfos ) do

		if type( digests ) == "table" then

			for digest, longSides in pairs( digests ) do

				if type( longSides ) == "table" then

					for longSide in pairs( longSides ) do

						-- Delete newer renditions that don't have file extensions.
						
						local ok, path = 
							pcall( WFCatPixelPathGenerator_pathForPreviewDigestAndLongEdge,
									baseBinariesPath, assetId, digest, longSide, false )

						if ok then

							ret = f( ret, path, ... )

						end
						
						-- Delete older renditions ( created before May 15th, 2017 ) that have file extensions.
						
						ok, path = 
							pcall( WFCatPixelPathGenerator_pathForPreviewDigestAndLongEdgeJpg,
									baseBinariesPath, assetId, digest, longSide, false )
						
						if ok then
						
							ret = f( ret, path, ... )
							
						end

					end

				else

					logger:warnf( "longSides assetId %s digest %s is not a table, it's %s",
									assetId, digest, type( longSides ) )

				end

			end

		else

			logger:warnf( "digests for assetId %s is not a table, it's %s",
							assetId, type( digests ) )

		end

	end

	return ret

end

--------------------------------------------------------------------------------
-- This attempts to delete all rendition files encoded in renditionsInfo
--
-- renditionsInfo is the same format as the output of 
-- self:scanRenditionFolderNamed():
--
-- 	{ [ assetId1 ] = {
-- 			[ digest1 ] = { [ longSide1 ] = true, 
-- 							[ longSide2 ] = true,
-- 							... },
-- 			[ digest2 ] = { ... },
-- 			...
-- 		}
-- 	  [ assetId2 ] = { ... }
-- 	  ...
--
-- 	 }
--
-- Returns:
--
-- 	An array of tuples for each rendition file that we attempted to delete
--
-- 	{ { boolean, path, maybeErr },
-- 		{ boolean, path, maybeErr },
-- 		{ boolean, path, maybeErr } }
--
-- 	This is also multicast to self:purgeResultsStream()

function WFCatPixelFileIOWorker:deleteRenditions( renditionInfos )

	WFArguments_check( 'WFCatPixelFileIOWorker', 'table' )

	return reduceRenditionInfos( 
			self._baseBinariesPath, renditionInfos, 
			{}, deleteFileWithAppendedNormalizedResult )

end

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

WFCatPixelFileIOWorker.deleteRenditions = 
	wrapInMulticastReturnValueToPurgeResultsStream(
		WFCatPixelFileIOWorker.deleteRenditions )

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

local WFCatPixelPathGenerator_pathForSettingsXmp = 
	WFCatPixelPathGenerator.pathForSettingsXmp
--------------------------------------------------------------------------------

local function reduceSettingsInfos( baseBinariesPath, settingsSha256Infos,
											initVal, f, ... )

	WFArguments_check( 'string', 'table', '_', 'function' )

	local ret = initVal

	for _, sha256 in ipairs( settingsSha256Infos ) do

		local ok, path = 
			pcall( WFCatPixelPathGenerator_pathForSettingsXmp,
					baseBinariesPath, sha256 )

		if ok then

			ret = f( ret, path, ... )

		else
		
			logger:warnf( "Failed to generate pathForSettingsXmp for %s and %s", baseBinariesPath, sha256 )
		
		end
						

	end

	return ret

end

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

function WFCatPixelFileIOWorker:deleteSettings( settingsSha256Values )

	WFArguments_check( 'WFCatPixelFileIOWorker', 'table' )

	return reduceSettingsInfos( 
			self._baseBinariesPath, settingsSha256Values, 
			{}, deleteFileWithAppendedNormalizedResult )

end

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

WFCatPixelFileIOWorker.deleteSettings = 
	wrapInMulticastReturnValueToPurgeResultsStream(
		WFCatPixelFileIOWorker.deleteSettings )
		
--------------------------------------------------------------------------------

WFCatPixelFileIOWorker.class = { 

	initMessageProcessor = initMessageProcessor,

}

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

return Rx.Class( 'WFCatPixelFileIOWorker', WFCatPixelFileIOWorker )
