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

KSMemoryCache.lua

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

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

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

local Rx = require 'Rx'

local KSTableUtils = require 'KSTableUtils'

local WFCloseSupport = require 'WFCloseSupport'
local WFCloseSupport_hasBeenOrWillBeClosed = assert( WFCloseSupport.hasBeenOrWillBeClosed )

local assert = assert
local type = type

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

local DEBUG = false

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

-- Remove an entry from an MRU ring. If it isn't part of a ring, this also
-- succeeds. It would mostly succeed if it were part of some other ring except
-- that it wouldn't be able to update the head correctly.

local function removeFromMruRing( mruRingHead, mruEntry )

	-- Advance around the ring if we are at the head.

	if mruRingHead == mruEntry then
		if mruRingHead == mruEntry._next then
			mruRingHead = false
		else
			mruRingHead = mruEntry._next
		end
	end
	
	-- Unlink.
	
	-- Fetch neighbors
	
	local next = mruEntry._next
	local prev = mruEntry._prev
	
	-- Connect entry back to self. This means that even if something were still
	-- pointing to the entry, it would be part of a valid ring.

	mruEntry._next = mruEntry
	mruEntry._prev = mruEntry

	-- Link neighbors together. Redundant but harmless if the entry was already
	-- a single element ring.
		
	next._prev = prev
	prev._next = next
	
	-- Return the new ring head.
	
	return mruRingHead

end

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

local function addToHeadOfMruRing( mruRingHead, mruEntry )

	-- The entry should point to itself.
	
	assert( mruEntry._next == mruEntry,
		"Bad next link for mruEntry." )
		
	assert( mruEntry._prev == mruEntry,
		"Bad next link for mruEntry." )
		
	-- The entry should not already be the head of the MRU ring.
	
	assert( mruRingHead ~= mruEntry, "Can't add an item that is already in the ring." )
	
	-- If the ring wasn't empty, then insert into the ring.
	
	if mruRingHead then
		local next = mruRingHead
		local prev = mruRingHead._prev
		mruEntry._next = next
		mruEntry._prev = prev
		next._prev = mruEntry
		prev._next = mruEntry
	end
	
	-- The entry is the new ring head
	
	return mruEntry

end

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

local function getTailOfMruRing( mruRingHead )

	return mruRingHead and mruRingHead._prev
	
end

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

local function moveToHeadOfMruRing( mruRingHead, mruEntry )

	-- Short-circuit removal and re-add when already at the head.

	if mruRingHead == mruEntry then
		return mruRingHead
	end

	mruRingHead = removeFromMruRing( mruRingHead, mruEntry )
	
	mruRingHead = addToHeadOfMruRing( mruRingHead, mruEntry )
	
	return mruRingHead
	
end

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

--[[

	dataMap is a fully weak map from keys to actual data
	mruMap is a fully weak map from keys to mru entries
	
	We can find any piece of data that has been added and not removed that
	is still alive. We keep some of them alive via an MRU mechanism.

]]

-- Precondition: there is an oldest entry

local function removeOldestEntry( self )

	local oldest = getTailOfMruRing( self.mruRing )
	
	if oldest then
		self.mruRing = removeFromMruRing( self.mruRing, oldest )

		self.mruMap[ oldest._key ] = nil

		self.count = self.count - 1

		if DEBUG then debug.print( "removed oldest entry: ", oldest._key ) end
	end
end

local KSMemoryCache = Rx.Class 'KSMemoryCacheClass' {
	
	__init = function( self, optMaxEntries )
		if optMaxEntries then
			assert( type( optMaxEntries ) == 'number' )
			assert( optMaxEntries > 2 )
			
			self.maxEntries = optMaxEntries
		end
		self.mruMap = KSTableUtils.makeFullyWeak()
		self.mruRing = false
		self.dataMap = KSTableUtils.makeFullyWeak()
	end,

	maxEntries = false,
	
	mruMap = false,
	
	dataMap = false,

	count = 0,

	mruRing = false,

	add = function( self, key, data )

		assert( key )
		assert( data )
		
		if DEBUG then debug.print( "add: ", key ) end
		
		local tdata = type( data )
		
		assert( tdata == 'table' or tdata == 'userdata' or tdata == 'string', "Only cache tables, userdata and strings." )
		
		self.dataMap[ key ] = data
		
		local mruEntry = self.mruMap[ key ]
		if mruEntry then
			-- just update the data
			mruEntry._data = data
		else
			self.count = self.count + 1
			mruEntry = { _data = data, _key = key, _prev = false, _next = false }
			mruEntry._next = mruEntry -- false was just for space allocation
			mruEntry._prev = mruEntry
			self.mruMap[ key ] = mruEntry
			
			if DEBUG then debug.print( "added entry: ", key ) end

		end
		
		self.mruRing = moveToHeadOfMruRing( self.mruRing, mruEntry )
		
		if self.maxEntries and self.count > self.maxEntries then
			removeOldestEntry( self )
		end
	end,
	
	adjustMaximumEntries = function( self, newMaxEntries )
	
		assert( newMaxEntries )
		assert( type( newMaxEntries ) == 'number' )
		assert( newMaxEntries > 2 )
			
		self.maxEntries = newMaxEntries
		
		while self.count > self.maxEntries do
			removeOldestEntry( self )
		end
		
	
	end,
	
	getData = function( self, key )
	
		assert( key )

		local data = self.dataMap[ key ]
		
		if not data then
			if DEBUG then debug.print( "getData: no data" ) end
			return
		end
		
		-- Data that has been closed should have been removed and treated
		-- as if it isn't there.
		
		if data and WFCloseSupport_hasBeenOrWillBeClosed( data ) then
			self:remove( key )
			return
		end
	
		local mruEntry = self.mruMap[ key ]
		
		if mruEntry then
			self.mruRing = moveToHeadOfMruRing( self.mruRing, mruEntry )
		else
			self:add( key, data )
		end
		
		if DEBUG then debug.print( "getData: returning data" ) end
		return data
		
	end,
	
	remove = function( self, key )
	
		assert( key )
		
		self.dataMap[ key ] = nil
		
		local mruEntry = self.mruMap[ key ]
		
		if mruEntry then
			self.count = self.count - 1
			self.mruRing = removeFromMruRing( self.mruRing, mruEntry )
			self.mruMap[ key ] = nil
		end
	end,

	flush = function( self )
	
		-- FIXME [arahn 2012-06-13 via scouten]: Would be nice to call this
		-- for every open cache before we do leaks detection.
	
		self.mruMap = KSTableUtils.makeFullyWeak()
		self.mruRing = false
		self.dataMap = KSTableUtils.makeFullyWeak()
		self.count = 0

	end,
}


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

return KSMemoryCache
