require 'thread'

module Rex

###
#
# This class implements a read/write lock synchronization
# primitive.  It is meant to allow for more efficient access to
# resources that are more often read from than written to and many
# times can have concurrent reader threads.  By allowing the reader
# threads to lock the resource concurrently rather than serially,
# a large performance boost can be seen.  Acquiring a write lock
# results in exclusive access to the resource and thereby prevents
# any read operations during the time that a write lock is acquired.
# Only one write lock may be acquired at a time.
#
###
class ReadWriteLock

	#
	# Initializes a reader/writer lock instance.
	#
	def initialize
		@read_sync_mutex  = Mutex.new
		@write_sync_mutex = Mutex.new
		@exclusive_mutex  = Mutex.new
		@readers          = 0
		@writer           = false
	end

	#
	# Acquires the read lock for the calling thread.
	#
	def lock_read
		read_sync_mutex.lock
		
		begin
			# If there are a non-zero number of readers and a 
			# writer is waiting to acquire the exclusive lock,
			# free up the sync mutex temporarily and lock/unlock
			# the exclusive lock.  This is to give the writer
			# thread a chance to acquire the lock and prevents
			# it from being constantly starved.
			if ((@readers > 0) and
			    (@writer))
				read_sync_mutex.unlock
				exclusive_mutex.lock
				exclusive_mutex.unlock
				read_sync_mutex.lock
			end

			# Increment the active reader count
			@readers += 1
			
			# If we now have just one reader, acquire the exclusive 
			# lock.  Track the thread owner so that we release the
			# lock from within the same thread context later on.
			if (@readers == 1)
				exclusive_mutex.lock
				
				@owner = Thread.current
			end
		ensure
			read_sync_mutex.unlock
		end
	end

	#
	# Releases the read lock for the calling thread.
	#
	def unlock_read
		read_sync_mutex.lock

		begin
			unlocked = false

			# Keep looping until we've lost this thread's reader
			# lock
			while (!unlocked)
				# If there are no more readers left after this one
				if (@readers - 1 == 0)
					# If the calling thread is the owner of the exclusive 
					# reader lock, then let's release that shit!
					if (Thread.current == @owner)
						@owner = nil

						exclusive_mutex.unlock
					end
				# If there is more than one reader left and this thread is
				# the owner of the exclusive lock, then keep looping so that
				# we can eventually unlock the exclusive mutex in this thread's
				# context
				elsif (Thread.current == @owner)
					read_sync_mutex.unlock

					next
				end

				# Unlocked!
				unlocked = true

				# Decrement the active reader count
				@readers -= 1
			end
		ensure
			read_sync_mutex.unlock
		end
	end

	#
	# Acquire the exclusive write lock.
	#
	def lock_write
		write_sync_mutex.lock

		begin
			@writer = true

			exclusive_mutex.lock
			
			@owner  = Thread.current
		ensure
			write_sync_mutex.unlock
		end
	end

	#
	# Release the exclusive write lock.
	#
	def unlock_write
		# If the caller is not the owner of the write lock, then someone is
		# doing something broken, let's let them know.
		if (Thread.current != @owner)
			raise RuntimeError, "Non-owner calling thread attempted to release write lock", caller
		end

		# Otherwise, release the exclusive write lock
		@writer = false

		exclusive_mutex.unlock
	end

	#
	# Synchronize a block for read access.
	#
	def synchronize_read
		lock_read
		begin
			yield
		ensure
			unlock_read
		end
	end

	#
	# Synchronize a block for write access.
	#
	def synchronize_write
		lock_write
		begin
			yield
		ensure
			unlock_write
		end
	end

protected

	attr_accessor :read_sync_mutex # :nodoc:
	attr_accessor :write_sync_mutex # :nodoc:
	attr_accessor :exclusive_mutex # :nodoc:

end

end
