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

PixelProviderRenditionDiskStatusCache.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 WFArguments = require 'WFArguments'
	-- imported to add 'uuid' as a WFArguments option
local _ = require 'WFArguments_uuid' 
local WFCloseSupport = require "WFCloseSupport"
local WFFunctionUtils = require "WFFunctionUtils"

	-- wf.sqlite
local WFSQLiteDatabase = require "WFSQLiteDatabase"
local WFSQLiteDatabaseSchema = require "WFSQLiteDatabaseSchema"

	-- Rx
local Rx = require "Rx"

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

local logger = require 'WFLogger'( 'PixelProviderRenditionDiskStatusCache' )

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

local WFArguments_check = assert( WFArguments.check, 
										"missing WFArguments.check" )

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

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

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

local function execSQL( db, sql )

	db:execOnce( sql )

	return db

end

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

local function makeDB()

	local dummySchema = WFSQLiteDatabaseSchema( "1.0" )

	local db = WFSQLiteDatabase( ':memory:', dummySchema )

	local schemaSQL = {
		------------------------------------------------------------
		-- main ExistingRenditionData table
		------------------------------------------------------------

		[[
		CREATE TABLE ExistingRenditionData(
			localAssetId NOT NULL, 
			longSide NOT NULL, 
			digest NOT NULL,
			changeCounter NOT NULL
		);
		]],

		-- we only expect at most 5 unique ( localAssetId, longSide ) pairs per
		-- localAssetId, so we really will only be getting 5 rows or less when
		-- filtering by localAssetId, therefore there's no value to adding
		-- additional INDEXES for sorting.
		[[
		CREATE UNIQUE INDEX ExistingRenditionData_primaryKey
						ON ExistingRenditionData( localAssetId, longSide );
		]],

		-- store paths in separate table so the lookup is on a compact table
		[[
		CREATE TABLE ExistingRenditionPath(
			localAssetId NOT NULL, 
			longSide NOT NULL, 
			path NOT NULL
		);
		]],

		[[
		CREATE UNIQUE INDEX ExistingRenditionPath_primaryKey
						ON ExistingRenditionPath( localAssetId, longSide );
		]],

		[[
		CREATE TABLE LatestDigest(
			localAssetId PRIMARY KEY, 
			digest NOT NULL
		);
		]],

		[[
		CREATE TABLE LatestDigestCounter(
			localAssetId PRIMARY KEY NOT NULL,
			changeCounter NOT NULL
		);
		]],

		-- create some views and triggers that update the change counter
		-- whenever we add a different rendition digest
		[[
		CREATE TRIGGER update_LatestDigestCounter
		AFTER INSERT ON LatestDigest
		BEGIN

			INSERT OR REPLACE INTO LatestDigestCounter
				SELECT NEW.localAssetId, IFNULL( MAX( changeCounter ), 0 ) + 1
				FROM LatestDigestCounter
				WHERE localAssetId = NEW.localAssetId;

		END;
		]],

		[[
		CREATE VIEW ExistingRenditionPathDataUpdater AS
			SELECT 1 AS localAssetId, 1 AS longSide, 1 AS digest, 1 AS path;
		]],

		[[
		CREATE TRIGGER ExistingRenditionPathDataUpdater_impl
		INSTEAD OF INSERT ON  ExistingRenditionPathDataUpdater
		BEGIN

			INSERT OR REPLACE INTO LatestDigest
				SELECT NEW.localAssetId, NEW.digest
					EXCEPT
				SELECT localAssetId, digest 
				FROM LatestDigest
				WHERE localAssetId = NEW.localAssetId;

			INSERT OR REPLACE INTO ExistingRenditionData
				SELECT NEW.localAssetId, NEW.longSide, NEW.digest,
						changeCounter
				FROM LatestDigestCounter
				WHERE localAssetId = NEW.localAssetId;

			INSERT OR REPLACE INTO ExistingRenditionPath
				SELECT NEW.localAssetId, NEW.longSide, NEW.path;

		END;
		]],

		[[
		CREATE VIEW ExistingRenditionNoPathDataUpdater AS
			SELECT 1 AS localAssetId, 1 AS longSide, 1 AS digest;
		]],

		[[
		CREATE TRIGGER ExistingRenditionNoPathDataUpdater_impl
		INSTEAD OF INSERT ON  ExistingRenditionNoPathDataUpdater
		BEGIN

			INSERT OR REPLACE INTO LatestDigest
				SELECT NEW.localAssetId, NEW.digest
					EXCEPT
				SELECT localAssetId, digest 
				FROM LatestDigest
				WHERE localAssetId = NEW.localAssetId;

			INSERT OR REPLACE INTO ExistingRenditionData
				SELECT NEW.localAssetId, NEW.longSide, NEW.digest,
						changeCounter
				FROM LatestDigestCounter
				WHERE localAssetId = NEW.localAssetId;

			DELETE FROM ExistingRenditionPath
			WHERE localAssetId = NEW.localAssetId AND longSide = NEW.longSide;

		END;
		]],

		[[
		CREATE VIEW RemoveAllCachedRenditionStatusesForLocalId AS
			SELECT 1 AS localAssetId;
		]],

		[[
		CREATE TRIGGER RemoveAllCachedRenditionStatusesForLocalId_impl
		INSTEAD OF INSERT ON  RemoveAllCachedRenditionStatusesForLocalId
		BEGIN

			DELETE FROM LatestDigest
			WHERE localAssetId = NEW.localAssetId;

			DELETE FROM ExistingRenditionData
			WHERE localAssetId = NEW.localAssetId;

			DELETE FROM ExistingRenditionPath
			WHERE localAssetId = NEW.localAssetId;

			DELETE FROM LatestDigestCounter
			WHERE localAssetId = NEW.localAssetId;

		END;
		]],

	}

	db = WFFunctionUtils_reduce( db, execSQL, schemaSQL )

	return db

end

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

local PixelProviderRenditionDiskStatusCachePrototype = {

	_diskStatusDB = false,
		-- WFSQLiteDatabase

	_renditionCacheRootFolder = false,
		-- string
		
	_didScanBestPath = false,
		-- table

	_removeDeletedAssetIdsSubscription = false,
		-- Rx.Disposable

	WFCloseSupport.addCloseSupportMethods,

}

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

local function removeAllCachedRenditionStatusesForLocalId( self, localAssetId )

	WFArguments_check( "PixelProviderRenditionDiskStatusCache",
						"number" )

	self._diskStatusDB:exec( 
		[[INSERT INTO RemoveAllCachedRenditionStatusesForLocalId 
			SELECT ?]],
			localAssetId )

	return self

end

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

function PixelProviderRenditionDiskStatusCachePrototype:__init( 
			renditionCacheRootFolder, deletedLocalAssetIdsStream )

	self._diskStatusDB = makeDB()

	self._renditionCacheRootFolder = renditionCacheRootFolder

	self._removeDeletedAssetIdsSubscription =
			deletedLocalAssetIdsStream
				:subscribe(

					function( deletedLocalAssetIds )

						WFFunctionUtils_reduce( 
							self, 
							removeAllCachedRenditionStatusesForLocalId,
								deletedLocalAssetIds )

					end )

	self._didScanBestPath = {}

end

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

local function warnClosed( methName, defaultReturn ) 

	return function()

		logger:warnf( "Method '%s' called on closed DB", methName )

		return defaultReturn

	end

end

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

function PixelProviderRenditionDiskStatusCachePrototype:close()

	self._diskStatusDB:close()

	self._diskStatusDB = {

		exec = warnClosed( 'exec', nil ),

		gatherArray = warnClosed( 'gatherArray', {} ),

		gatherArrayOfArray = warnClosed( 'gatherArrayOfArray', {} ),

	}

	self._removeDeletedAssetIdsSubscription:dispose()

end

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

function PixelProviderRenditionDiskStatusCachePrototype:addRendition( 
									localAssetId, longSide, digest, path )

	WFArguments_check( "PixelProviderRenditionDiskStatusCache",
						"number", "number|string", "uuid", "string" )

	self._diskStatusDB:exec( 
		[[INSERT INTO ExistingRenditionPathDataUpdater
			SELECT ?, ?, ?, ?]],
			localAssetId, longSide, digest, path )

end

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

function PixelProviderRenditionDiskStatusCachePrototype:addNoRendition( 
									localAssetId, longSide, digest )

	WFArguments_check( "PixelProviderRenditionDiskStatusCache",
						"number", "number|string", "uuid" )

	self._diskStatusDB:exec( 
		[[INSERT INTO ExistingRenditionNoPathDataUpdater
			SELECT ?, ?, ?]],
			localAssetId, longSide, digest )

end

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

local function getRenditionStatusImp( self, localAssetId, longSide, 
										maybeDigest )

	local digest = maybeDigest or "%"

	local matchingLocalAssetIds =
			self._diskStatusDB:gatherArray( 
					[[SELECT localAssetID FROM ExistingRenditionData
					  WHERE localAssetId = ? AND longSide = ? AND 
					  		digest LIKE ?]],
					localAssetId, longSide, digest )

	if #matchingLocalAssetIds == 0 then

		return 0

	end
	
	local matchingPaths =
			self._diskStatusDB:gatherArray( 
					[[SELECT localAssetID FROM ExistingRenditionPath
					  WHERE localAssetId = ? AND longSide = ?]],
					localAssetId, longSide )

	return #matchingPaths > 0 and 1 or -1

end

--------------------------------------------------------------------------------
-- getRenditionStatus returns
--
-- 	-1 if we've checked disk and DID NOT find rendition
--
-- 	1  if we've checked disk and DID find rendition
--
-- 	0  if we haven't checked disk

function PixelProviderRenditionDiskStatusCachePrototype:getRenditionStatus( 
										localAssetId, longSide, digest )

	WFArguments_check( "PixelProviderRenditionDiskStatusCache",
						"number", "number|string", "?uuid" )

	return getRenditionStatusImp( self, localAssetId, longSide, digest )

end

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

local function addHaveCheckedStatus( haveChecked, longSide, localAssetId, self )

	if not haveChecked then

		return false

	end

	return getRenditionStatusImp( self, localAssetId, longSide ) ~= 0

end

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

function PixelProviderRenditionDiskStatusCachePrototype:haveCheckedAllRenditionSizes( 
												localAssetId, longSides )

	WFArguments_check( "PixelProviderRenditionDiskStatusCache", "number",
							"table" )

	return WFFunctionUtils_reduce( true, addHaveCheckedStatus,
										longSides, localAssetId, self )

end

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

function PixelProviderRenditionDiskStatusCachePrototype:maybeGetPath( 
										localAssetId, longSide )

	WFArguments_check( "PixelProviderRenditionDiskStatusCache",
						"number", "number|string" )

	local matchingPathArray =
			self._diskStatusDB:gatherArrayOfArray( 
						[[SELECT path, ExistingRenditionPath.longSide, digest
						  FROM ExistingRenditionData, ExistingRenditionPath
						  WHERE ExistingRenditionData.localAssetId = 
						  			ExistingRenditionPath.localAssetId AND
						  		ExistingRenditionData.longSide = 
						  			ExistingRenditionPath.longSide AND
						  		ExistingRenditionPath.localAssetId = ? AND 
								ExistingRenditionPath.longSide = ?]],
						localAssetId, longSide )

	if #matchingPathArray == 0 then

		return false

	end

	-- path, longSide, digest
	return unpack( matchingPathArray[ 1 ] )

end

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

function PixelProviderRenditionDiskStatusCachePrototype:maybeGetHighestResCurrentRendition( 
										localAssetId )

	WFArguments_check( "PixelProviderRenditionDiskStatusCache", "number" )

	local matchingPathArrayOfArray =
			self._diskStatusDB:gatherArrayOfArray( 
						[[SELECT path, ExistingRenditionPath.longSide, digest
						  FROM ExistingRenditionData, ExistingRenditionPath
						  WHERE ExistingRenditionData.localAssetId = 
						  			ExistingRenditionPath.localAssetId AND
						  		ExistingRenditionData.longSide = 
						  			ExistingRenditionPath.longSide AND
						  		ExistingRenditionPath.localAssetId = ? AND 
								digest IN
								( SELECT digest
						 		  FROM ExistingRenditionData 
								  WHERE localAssetId = ?
								  ORDER BY changeCounter DESC
								  LIMIT 1 )
						  ORDER BY ExistingRenditionPath.longSide DESC
						  LIMIT 1]],
						localAssetId, localAssetId )

	if #matchingPathArrayOfArray == 0 then

		return false

	end

	return unpack( matchingPathArrayOfArray[ 1 ] )

end

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

local function pack( ... )

	return { ... }, select( "#", ... )

end

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

function PixelProviderRenditionDiskStatusCachePrototype:runInBatchMode( 
										f, ... )

	self._diskStatusDB:exec( [[SAVEPOINT runInTransaction]] )

	local ret, retN = pack( pcall( f, ... ) )

	if not ret[ 1 ] then
	
		self._diskStatusDB:exec( [[ROLLBACK]] )

		error( ret[ 2 ] )

	end

	self._diskStatusDB:exec( [[RELEASE runInTransaction]] )

	return unpack( ret, 2, retN )
	
end

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

local PixelProviderRenditionDiskStatusCache = 
		Rx.Class( "PixelProviderRenditionDiskStatusCache", 
					PixelProviderRenditionDiskStatusCachePrototype )

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

return PixelProviderRenditionDiskStatusCache 
