from MetadataService.Service import Service
import os
from VolumeExpert import _VolumeExpert
from MetadataService.AdaptiveSleeper import AdaptiveSleeper
from MetadataService.ProducerConsumer import Full
from InfospaceObjects import Volume
import time

class VolumeCrawler(Service,AdaptiveSleeper):

	def __init__(self,volume,expert,produce_method):
		# Contract:
		assert isinstance(volume,Volume)
		assert isinstance(expert,_VolumeExpert)
		assert callable(produce_method)
		
		# Implementation:
		Service.__init__(self,public_interface = None)
		self.set_component_name("FilesystemAgent.VolumeCrawler."+volume.get_mountpoint().replace(".","_"))
		AdaptiveSleeper.__init__(self)
		self.log.debug("Initializing")
# 		self.dev_node = os.stat(volume.get_mountpoint()).st_dev
		self.volume = volume
		self.expert = expert
		self.produce = produce_method
		self.log.debug("Initialized")
		
	# start inotify code
	
	def start_inotify(self):
		"""Setup inotify notification."""
		try:
			import inotify as i
			self.inotify_mask = i.CLOSE_WRITE | i.CREATE_SUBDIR | i.DELETE_SELF | i.DELETE_FILE

			# we create the queue
			# and setup a callback to fill it
			self.inotify_watcher = i.Watcher()
			self.log.debug("Inotify enabled")

		except ImportError: self.log.info("This platform does not support inotify, disabled")
		
	def _process_inotify_event(self,event):
		"""This method acts on inotify events."""
		import inotify as i

		# Over here, we can trust that the events will only be
		# posted with a filename since the mask does not allow
		# for event types which may come without one
		# cross my heart hope to X-P
		# so if the file is not appropriate, we just return!
		# for those events that do not involve deleted files

		# FIXME do all events
		# implement the case "a file/dir was added/deleted to a dir, reindex the dir without incurring into multiple indexings for the directory for a cluster of added files
		if event & i.CLOSE_WRITE:
			# we just assume the file has been modified
			# FIXME check if is_appropriate() can be made immune to nonexistent files
			if not self.expert.is_appropriate(event.filename): return
			return ("FileChanged",event.filename,self.volume)
		elif event & i.CREATE_SUBDIR:
			# a subdir was created, store it
			if not self.expert.is_appropriate(event.filename): return
			self.inotify_watch(event.filename)
			return ("FileCreated",event.filename,self.volume)
		elif event & i.DELETE_SELF:
			# a directory was deleted, delete it and stop watching it as well.  We do not do this on subdir deletions, since the base dir for the crawl might be deleted: this is more complete.  Of course, this assumes the watched directory has no subdirectories =) which is naive but should be covered by the delete-on-search case
			# for the time being the event will be emitted regardless of whether it is appropriate or not, mainly because what's appropriate may change between startups
			self.inotify_ignore(event.filename)
			return ("FileDeleted",event.filename,self.volume)
		elif event & i.DELETE_FILE:
			# a file was deleted
			# for the time being the event will be emitted regardless of whether it is appropriate or not, mainly because what's appropriate may change between startups
			return ("FileDeleted",event.filename,self.volume)
		else: self.log.warning("Inotify: Unexpected %s",event)

	def vacate_inotify_queue(self):
		"""This delegate is run in a thread to vacate the inotify queue.
		Returns None when inotify was not detected.  Else it returns
		the number of vacated events."""

		if not hasattr(self,"inotify_watcher") or hasattr(self,"inotify_error"): return
		
		from Queue import Empty
		counter = 0
		while True:
			try: event = self.inotify_watcher.get_next_event(block=False)
			except Empty: break
			if self.killed(): break
			crawler_event = self._process_inotify_event(event)
			if event:
				self.log.debug("Vacated %s from inotify queue",str(crawler_event))
				self.add_to_queue(crawler_event)
				counter = counter + 1
		if counter: self.log.debug("Vacated %s events from inotify queue",counter)
		return counter

	def stop_inotify(self):
		# This stops the watcher altogether
		if hasattr(self,"inotify_watcher"): delattr(self,"inotify_watcher")
		self.log.debug("Inotify disabled")
	
	# FIXME signaling the indexer to delete a file is the additional responsibility of the process/service that found the file did not exist, e.g. the search() call in FilesystemAgent.
	
	def inotify_watch(self,filename):
		if hasattr(self,"inotify_watcher") and not hasattr(self,"inotify_error"):
			self.log.debug("Watching %s",filename)
			try: self.inotify_watcher.watch(filename,self.inotify_mask)
			except:
				self.log.error("Watching %s failed, marking inotify as unusable for this session",filename,exc_info=True)
				self.inotify_error = True
	
	def inotify_ignore(self,filename):
		if hasattr(self,"inotify_watcher"):
			self.log.debug("Ignoring %s",filename)
			self.inotify_watcher.ignore(filename)
	
	# end inotify code
	
	def run(self):
		try: self._run()
		except: self.log.error("Man, we're screwed",exc_info=True)
	
	def _run(self):
		self.log.info("Started")
		self.start_inotify()
		
		expert = self.expert
		# for the root
		
		start_folder = self.volume.get_mountpoint()
		self.add_to_queue(("FileFound",start_folder,self.volume))
		self.inotify_watch(start_folder)
		self.sleep()
		
		self.inotify_watch(start_folder)
		# and for the children
		for root, dirs, files in os.walk(self.volume.get_mountpoint()):  
			if self.killed(): break

			for name in dirs[:]:
				if self.killed(): break
				filename = os.path.join(root,name)
				if os.path.ismount(filename): # do not cross FS (volume) boundaries
					self.log.debug("Removing %s from walk list (mountpoint of other volume)",filename)
					dirs.remove(name)
				elif not expert.is_appropriate(filename):
					self.log.debug("Removing %s from walk list (inappropriate)",filename)
					dirs.remove(name)
				
			for name in files:
				self.vacate_inotify_queue()
				if self.killed(): break
				filename = os.path.join(root,name)
				if not expert.is_appropriate(filename):
					self.log.debug("Skipping %s (inappropriate)",filename)
					continue
				self.add_to_queue(("FileFound",filename,self.volume))
				self.sleep()
			
			for name in dirs:
				self.vacate_inotify_queue()
				if self.killed(): break
				filename = os.path.join(root,name)
				if os.access(filename,os.R_OK) == 1:
					self.add_to_queue(("FileFound",filename,self.volume))
					self.inotify_watch(filename)
				self.sleep()
		
		self.add_to_queue(("FullyIndexed",self.volume))
		
		msg = "Volume scan done"
		if hasattr(self,"inotify_watcher") and not hasattr(self,"inotify_error"):
			msg = msg + " - watching filesystem"
		self.log.info("%s",msg)

		while not self.killed():
			self.log.debug("Spin")
			if not self.vacate_inotify_queue():
				#No events were vacated, attempt to spin idly
				time.sleep(1)
		
		self.stop_inotify()
		self.log.info("Finished")

	def add_to_queue(self,event):
		assert type(event) is tuple
		while not self.killed():
			try:
				self.produce(event,1)
				return
			except Full: pass

