"""Module containnig a memory memory manager which provides a sliding window on a number of memory mapped files"""
import os
import sys
import mmap

from mmap import mmap, ACCESS_READ
try:
	from mmap import ALLOCATIONGRANULARITY
except ImportError:
	# in python pre 2.6, the ALLOCATIONGRANULARITY does not exist as it is mainly
	# useful for aligning the offset. The offset argument doesn't exist there though
	from mmap import PAGESIZE as ALLOCATIONGRANULARITY
#END handle pythons missing quality assurance

from sys import getrefcount

__all__ = [	"align_to_mmap", "is_64_bit",
			"MapWindow", "MapRegion", "MapRegionList", "ALLOCATIONGRANULARITY"]

#{ Utilities

def align_to_mmap(num, round_up):
	"""
	Align the given integer number to the closest page offset, which usually is 4096 bytes.
	
	:param round_up: if True, the next higher multiple of page size is used, otherwise
		the lower page_size will be used (i.e. if True, 1 becomes 4096, otherwise it becomes 0)
	:return: num rounded to closest page"""
	res = (num / ALLOCATIONGRANULARITY) * ALLOCATIONGRANULARITY;
	if round_up and (res != num):
		res += ALLOCATIONGRANULARITY
	#END handle size
	return res;
	
def is_64_bit():
	""":return: True if the system is 64 bit. Otherwise it can be assumed to be 32 bit"""
	return sys.maxint > (1<<32) - 1

#}END utilities


#{ Utility Classes 

class MapWindow(object):
	"""Utility type which is used to snap windows towards each other, and to adjust their size"""
	__slots__ = (
				'ofs',		# offset into the file in bytes
				'size'				# size of the window in bytes
				)

	def __init__(self, offset, size):
		self.ofs = offset
		self.size = size

	def __repr__(self):
		return "MapWindow(%i, %i)" % (self.ofs, self.size) 

	@classmethod
	def from_region(cls, region):
		""":return: new window from a region"""
		return cls(region._b, region.size())

	def ofs_end(self):
		return self.ofs + self.size

	def align(self):
		"""Assures the previous window area is contained in the new one"""
		nofs = align_to_mmap(self.ofs, 0)
		self.size += self.ofs - nofs	# keep size constant
		self.ofs = nofs
		self.size = align_to_mmap(self.size, 1)

	def extend_left_to(self, window, max_size):
		"""Adjust the offset to start where the given window on our left ends if possible, 
		but don't make yourself larger than max_size.
		The resize will assure that the new window still contains the old window area"""
		rofs = self.ofs - window.ofs_end()
		nsize = rofs + self.size
		rofs -= nsize - min(nsize, max_size)
		self.ofs = self.ofs - rofs
		self.size += rofs

	def extend_right_to(self, window, max_size):
		"""Adjust the size to make our window end where the right window begins, but don't
		get larger than max_size"""
		self.size = min(self.size + (window.ofs - self.ofs_end()), max_size)


class MapRegion(object):
	"""Defines a mapped region of memory, aligned to pagesizes
	
	**Note:** deallocates used region automatically on destruction"""
	__slots__ = [
					'_b'	, 	# beginning of mapping
					'_mf',	# mapped memory chunk (as returned by mmap)
					'_uc',	# total amount of usages
					'_size', # cached size of our memory map
					'__weakref__'
				]
	_need_compat_layer = sys.version_info[1] < 6
	
	if _need_compat_layer:
		__slots__.append('_mfb')		# mapped memory buffer to provide offset
	#END handle additional slot
	
	#{ Configuration
	# Used for testing only. If True, all data will be loaded into memory at once.
	# This makes sure no file handles will remain open.
	_test_read_into_memory = False
	#} END configuration
	
	
	def __init__(self, path_or_fd, ofs, size, flags = 0):
		"""Initialize a region, allocate the memory map
		:param path_or_fd: path to the file to map, or the opened file descriptor
		:param ofs: **aligned** offset into the file to be mapped 
		:param size: if size is larger then the file on disk, the whole file will be
			allocated the the size automatically adjusted
		:param flags: additional flags to be given when opening the file. 
		:raise Exception: if no memory can be allocated"""
		self._b = ofs
		self._size = 0
		self._uc = 0
		
		if isinstance(path_or_fd, int):
			fd = path_or_fd
		else:
			fd = os.open(path_or_fd, os.O_RDONLY|getattr(os, 'O_BINARY', 0)|flags)
		#END handle fd
		
		try:
			kwargs = dict(access=ACCESS_READ, offset=ofs)
			corrected_size = size
			sizeofs = ofs
			if self._need_compat_layer:
				del(kwargs['offset'])
				corrected_size += ofs
				sizeofs = 0
			# END handle python not supporting offset ! Arg
			
			# have to correct size, otherwise (instead of the c version) it will 
			# bark that the size is too large ... many extra file accesses because
			# if this ... argh !
			actual_size = min(os.fstat(fd).st_size - sizeofs, corrected_size)
			if self._test_read_into_memory:
				self._mf = self._read_into_memory(fd, ofs, actual_size)
			else:
				self._mf = mmap(fd, actual_size, **kwargs)
			#END handle memory mode
			
			self._size = len(self._mf)
			
			if self._need_compat_layer:
				self._mfb = buffer(self._mf, ofs, self._size)
			#END handle buffer wrapping
		finally:
			if isinstance(path_or_fd, basestring):
				os.close(fd)
			#END only close it if we opened it
		#END close file handle
		
	def _read_into_memory(self, fd, offset, size):
		""":return: string data as read from the given file descriptor, offset and size """
		os.lseek(fd, offset, os.SEEK_SET)
		mf = ''
		bytes_todo = size
		while bytes_todo:
			chunk = 1024*1024
			d = os.read(fd, chunk)
			bytes_todo -= len(d)
			mf += d
		#END loop copy items
		return mf
		
	def __repr__(self):
		return "MapRegion<%i, %i>" % (self._b, self.size())
		
	#{ Interface

	def buffer(self):
		""":return: a buffer containing the memory"""
		return self._mf
	
	def map(self):
		""":return: a memory map containing the memory"""
		return self._mf
		
	def ofs_begin(self):
		""":return: absolute byte offset to the first byte of the mapping"""
		return self._b
		
	def size(self):
		""":return: total size of the mapped region in bytes"""
		return self._size
		
	def ofs_end(self):
		""":return: Absolute offset to one byte beyond the mapping into the file"""
		return self._b + self._size
		
	def includes_ofs(self, ofs):
		""":return: True if the given offset can be read in our mapped region"""
		return self._b <= ofs < self._b + self._size
		
	def client_count(self):
		""":return: number of clients currently using this region"""
		# -1: self on stack, -1 self in this method, -1 self in getrefcount
		return getrefcount(self)-3
		
	def usage_count(self):
		""":return: amount of usages so far"""
		return self._uc
		
	def increment_usage_count(self):
		"""Adjust the usage count by the given positive or negative offset"""
		self._uc += 1
		
	# re-define all methods which need offset adjustments in compatibility mode
	if _need_compat_layer:
		def size(self):
			return self._size - self._b
			
		def ofs_end(self):
			# always the size - we are as large as it gets
			return self._size
			
		def buffer(self):
			return self._mfb
			
		def includes_ofs(self, ofs):
			return self._b <= ofs < self._size
	#END handle compat layer
	
	#} END interface
	
	
class MapRegionList(list):
	"""List of MapRegion instances associating a path with a list of regions."""
	__slots__ = (
				'_path_or_fd', 	# path or file descriptor which is mapped by all our regions
				'_file_size'		# total size of the file we map
				)
	
	def __new__(cls, path):
		return super(MapRegionList, cls).__new__(cls)
	
	def __init__(self, path_or_fd):
		self._path_or_fd = path_or_fd
		self._file_size = None
		
	def client_count(self):
		""":return: amount of clients which hold a reference to this instance"""
		return getrefcount(self)-3
		
	def path_or_fd(self):
		""":return: path or file descriptor we are attached to"""
		return self._path_or_fd
		
	def file_size(self):
		""":return: size of file we manager"""
		if self._file_size is None:
			if isinstance(self._path_or_fd, basestring):
				self._file_size = os.stat(self._path_or_fd).st_size
			else:
				self._file_size = os.fstat(self._path_or_fd).st_size
			#END handle path type
		#END update file size
		return self._file_size
	
#} END utilty classes
