require 'thread'
require 'iowa/AbstractCache'

module Iowa
	module Caches

		# Simple LRU cache for caching pages.  This cache is thread safe.

		class LRUCache < Iowa::AbstractCache

			def initialize(args)
				@cache = {}
				@queue = []
				@ttl_cache = {}
				@finalizers = []
				super(args)
			end

			# Check to see if the cache contains the given key.

			def include?(key)
				@cache.include?(key)
			end

			# Remove a key/value pair from the cache, by key.
		
			def delete(key)
				do_finalization(key,@cache[key])
				@cache.delete key
				@ttl_cache.delete key
			end

			# Return the element identified by the given key.

			def [](key)
				r = nil
				lock_cache do
					if @cache.has_key? key
						r = @cache[key]
						@queue.push @queue.delete(key)
						@ttl_cache[key] = Time.now.to_i
					end
				end
				r
			end
		
			def prune
				obj = @queue.shift
				delete obj
			end

			def attach(key, val)
				already_in_cache = include?(key)
				@cache[key] = val
				@ttl_cache[key] = Time.now.to_i
				if already_in_cache
					@queue.push @queue.delete(key)
				else
					@queue.push key
				end
			end
		
			# Set the element of the cache identified by the given key.  

			def []=(key, val)
				lock_cache do
					if @maxttl
						expiration = Time.now.to_i - @maxttl
						prune while !@queue.empty? && (@ttl_cache[@queue.first].to_i < expiration)
					end
					attach(key, val)
					prune if @queue.length > @max
				end
				@queue
			end

			# Allows one to set the maximum size of the cache queue.  If the queue
			# is currently larger than the size that it is being set to, elements
			# will be expired until the queue is at the maximum size.

			def size=(max)
				lock_cache do
					prune while @queue.length > max
					@max = max
				end
			end
			alias maxsize= size=

			# Return the maximum size of the cache.
			def maxsize
				@max
			end

			# Return the current size of the cache.
			def size
				@queue.length
			end

			# Allows one to set the maximum ttl.  If set to nil, there is no
			# TTL.  Setting the ttl will force a check against the new ttl,
			# expiring anything that it too old.

			def ttl=(newttl)
				@maxttl = newttl ? newttl.to_i : nil
				if @maxttl
					expiration = Time.now.to_i - @maxttl
					prune while !@queue.empty? && (@ttl_cache[@queue.first].to_i < expiration)
				end
			end

			# Returns the maximum TTL.
		
			def ttl
				@maxttl
			end

			# Return a copy of current set of keys to cache elements.
			def queue
				@queue.dup
			end
		
			# Adds a finalization method to the cache that will be called before
			# the object in the cache is expired.	
			def add_finalizer(*args,&block)
				@finalizers.push [block,args]
			end
			
			# Is called when an object in the cache is expired.  Iterates through
			# the defined finalization methods, if any.
			def do_finalization(key,obj)
					begin
						@finalizers.each do |f|
							f[0].call(key,obj,*f[1])
						end
					rescue Exception => e
						puts e, e.backtrace
						raise e
					end
			end

			def lock_cache
				unless @locked
					begin
						@locked = true
						@mutex.synchronize {yield}
					ensure
						@locked = false
					end
				else
					yield
				end
			end
		end
	end
end