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

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

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

This is the public API to KSDataCache.

-- FIXME [scouten 2011-04-05]: Document it.

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

	-- RxLua
local Rx = require 'Rx'

	-- WF.core.rxlua
local WFRxPromiseBridge = require 'WFRxPromiseBridge'
local WFRxEventBridge = require "WFRxEventBridge"

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

	-- WF.core.rxlua
local WFRxRunUntilDone = require 'WFRxRunUntilDone'

	-- WF.core.utils
local WFArguments = require 'WFArguments'
local WFCloseSupport = require 'WFCloseSupport'

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

local check = WFArguments.check

local asKSPromise = WFRxPromiseBridge

local asKSEvent = WFRxEventBridge

local runUntilDone = WFRxRunUntilDone

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

-- @mark KSDataCache class

--[[!
	\class KSDataCache
	\brief This namepace allows manipulation of a database.
	A database is represented as a table of simple keys whose
	values are KSData objects.

]]--

local dataCacheMethods = {

	_readEPStream = false,
		-- (Observable<WFWorkerThread>) event processor that implements reading from the cache
	
	_readEP = false,
		-- (WFWorkerThread) event processor that implements reading from the cache.
		-- This may be nil, so it behooves you to use the _readEPP instead of this directly.
		-- (This is here to ensure proper close behavior.)
		
	_writeEP = false,
		-- (WFWorkerThread) event processor that implements writing to the cache
	
	_path = false,
		-- (Observable< string >) path to data cache file (should be used only for disk space
		-- management)

	WFCloseSupport.addCloseSupportMethods,

}

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

function dataCacheMethods:__init( optName )

	local writeEP = WFWorkerThread( 'KSDataCacheImp.lua', 'write:' .. ( optName or '' ) )
	
	-- Wait for the write thread before attempting the read thread. We use the
	-- fact that the worker thread API can take a promise for the argument
	-- as a way to delay construction of the result.
	
	local readArg = writeEP:threadIsReady()
		:map( function()
			return 'read:' .. ( optName or '' )
		end )
	
	local readEPStream = readArg
		:map( function( readArg )
	
			if WFCloseSupport.hasBeenOrWillBeClosed( self ) then
				return nil
			else
				local readEP = WFWorkerThread( 'KSDataCacheImp.lua', readArg )
				self:setOwned {
					_readEP = readEP
				}
				return readEP
			end

		end )
		:shareReplay( 1 )
	
	self:setOwned { 
		_writeEP = writeEP,
	}

	rawset( self, '_readEPStream', readEPStream )
	
	WFCloseSupport.mustClose( self )

end

--------------------------------------------------------------------------------
-- Create a request against the write processor but do so only after the
-- read processor is ready. This balances with the fact that we won't start
-- the read processor until write processor is ready.

local function writeRequest( self, ... )
	local n = select( '#', ... )
	local args = { ... }
	return self._readEPStream
		:flatMap( function( readEP )
			return readEP:threadIsReady()
		end )
		:flatMap( function()
			return self._writeEP:request( unpack( args, 1, n ) )
		end )
end

--------------------------------------------------------------------------------
-- (Observable< string >) path to database file (should be used only for disk space
-- management)

--[[!
      Retrieves the physical path to the database file, for the purpose of disk
      space management. Do not manipulate the database file directly.

      \return The file path.
]]--

function dataCacheMethods:path()

	if not self._path then
		self._path = self._readEPStream
		:flatMap( function( readEP )
			return readEP:request( 'path' )
		end )
	end
	
	return self._path

end

function dataCacheMethods:pathP()
	return self:path():call( asKSPromise )
end


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

local function read( self, path, debugMe, highPriority )
			
	local selector = highPriority and '+fetchCachedData' or 'fetchCachedData'
	
	return self._readEPStream
		:where( function( readEP ) 
			return readEP ~= nil 
		end )
		:flatMap( function( readEP )
			return readEP:request( selector, path, debugMe )
		end )
	
end

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

--[[!
       \brief Creates an Observable that provides a data value.

       \param path (string) The key of the data value.
       \param debugMe (boolean) When \c true, tracks retain/release
       \param highPriority (boolean) When \c true, we will process earlier
			issues with the data object that is created.
       \param highPriority (boolean) When \c true, we will process earlier

       \return The Observable
]]--

function dataCacheMethods:get( --[[string]] path, --[[boolean]] debugMe, --[[boolean]] highPriority )
	
	check( 'KSDataCache', '+string', '?boolean', '?boolean' )
	
	-- debugMe helps you to debug retain/release issues with the NSData object
	-- that gets created.

	-- FIXME [mhamburg 2011-04-13]: Is it worth tracking these so that they
	-- could be closed if need be?
	
	local o = read( self, path, debugMe, highPriority )
	
	-- Since we're multithreading, a failure to read might actually be caused
	-- by the data just not having been written yet. So, we try again on the
	-- write EP. This has the downside of resulting in two attempts to read, but
	-- if we're routinely missing in the data cache then it probably isn't
	-- doing its job.
	
	-- Note that we terminate on an actual error from the data cache. A nil
	-- result just means that the data wasn't present.
	
	o = o:flatMap( function( data )
	
		if data then

			return Rx.Observable.just( data )

		elseif WFCloseSupport.hasBeenOrWillBeClosed( self._writeEP ) then
		
			return Rx.Observable.throw( "Read request outlived the data cache" )

		else

			local w = writeRequest( self, 'fetchCachedData', path, debugMe )
				:flatMap( function( data )

					return Rx.Observable.just( data )

				end )

			return w
				
		end

	end )
	:tapOnError( function( message )

		debug.print( "Data cache read error:", message )

	end )

	-- And return it.

	return o
	
end

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

function dataCacheMethods:getP( ... )
	
	return self:get( ... )
		:call( asKSPromise )
	
end

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

function dataCacheMethods:getB( ... )
	
	return self:get( ... )
		:call( asKSEvent )
		:closingHoldB( 'initial_value' )
		:autoCloseRelayB()
	
end

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

--[[!

	\brief Creates an Observable that provides keys in the database.

	\param optStartAfterPath  (string) The key of a data value at which to start.
		If not supplied, starts at the beginning.
	\param optLimit	(number) The number of keys to retrieve. If not supplied,
		retrieves all keys after the start key.

	\return The observable object.

]]--

function dataCacheMethods:enumerateKeys( --[[string]] optStartAfterPath, --[[number]] optLimit )

	-- This is a read operation but we do it on the write thread because we
	-- don't want to block the read thread waiting for the enumeration.

	return writeRequest( self, 'enumerateKeys', optStartAfterPath, optLimit )

end

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

function dataCacheMethods:enumerateKeysP( ... )

	return self:enumerateKeys( ... )
		:call( asKSPromise )

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

--[[!

	\brief Creates an observable that stores a data value in the database.

	\param path (string) The key of the data value.
	\param data (KSData) The new data value.

	\return The promise object.

]]--

function dataCacheMethods:put( --[[string]] path, --[[KSData]] data )

	-- If we already have data with this hash (path), then assume it matches
	-- the data we're about to write. We don't bother checking the actual
	-- data or checking the database at this point. 
	
	check( 'KSDataCache', '+string', 'KSData' )
	
	-- Go put the data in the cache
	
	local result = writeRequest( self, 'storeCachedData', path, data )
	
	return result:call( runUntilDone )
		-- No cancel support since we just want to put.

end

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

function dataCacheMethods:putP( ... )

	return self:put( ... )
		:call( asKSPromise )

end


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

--[[!
 	\brief Creates an Observable that removes a data value from the database.

       \param path (string) The key of the data value.

	\return The Observable.
]]--

function dataCacheMethods:remove( --[[string]] path )

	check( 'KSDataCache', 'string' )

	return writeRequest( self, 'removeData', path )
		:call( runUntilDone )

end

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

--[[!
 	\brief Creates an Observable that removes a data value from the database.
		   This does not remove it from the caches.

       \param path (string) The key of the data value.

	\return The Observable.
]]--

function dataCacheMethods:removeBulk( --[[array of strings]] paths )

	check( 'KSDataCache', 'table' )

	return writeRequest( self, 'removeBulkData', paths )

end

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

function dataCacheMethods:removeP( ... )

	return self:remove( ... )
		:call( asKSPromise )

end


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

--[[!
 	\brief Creates an Observable that removes all data from the database.

	\return The Observable.
]]--

function dataCacheMethods:removeAll()

	check( 'KSDataCache' )

	return writeRequest( self, 'removeAll' )

end

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

--[[!
	\brief Garbage collects the database.
]]--

function dataCacheMethods:vacuum()

	return writeRequest( self, 'vacuum' )

end

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

function dataCacheMethods:vacuumP()
	return self:vacuum()
		:call( asKSPromise )

end

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

--[[!
	\brief Shuts down the connection to the database and deletes it from disk.

]]--

function dataCacheMethods:haltAndDelete()

	assert( self._writeEP and self._readEPStream )
	
	-- Shutdown the write event processor and wait for confirmation.

	local stopWrite = writeRequest( self, 'haltAndDelete', false )

	-- Then stop the read EP, ask it to delete the files, and wait for confirmation.
	
	local stopRead = stopWrite
		:flatMap( function()
	
			return self._readEPStream
				:flatMap( function(readEP )
					return readEP:request( 'haltAndDelete', true )
				end )
			
		end )
		
	return stopRead
		:tapOnCompleted( function()

			self:close()
		
		end )
		:call( runUntilDone )
	
end

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

function dataCacheMethods:haltAndDeleteP()
	return self:haltAndDelete( )
		:call( asKSPromise )
end
--------------------------------------------------------------------------------

return Rx.Class 'KSDataCache'( dataCacheMethods )
