import ZODB
from persistent.mapping import PersistentMapping
from persistent import Persistent
from BTrees.IOBTree import IOBTree
from BTrees.OOBTree import OOBTree
from BTrees.OIBTree import OIBTree
from MetadataService.InformationObject import InformationObject
from VolumeExpert import VolumeExpert
from MetadataService.Infospace import Infospace
#from Products.ZCatalog.Catalog import Catalog
#from Products import PluginIndexes
#from Products.TextIndexNG2 import TextIndexNG
from MetadataService.common import DateTimeType
import time
import logging

class VolumeList(PersistentMapping):

	def has_volume(self,guid):
		assert type(guid) is str and len(guid) > 0
		return self.has_key(guid)
	
	def get_volume(self,guid):
		return self[guid]
	
	def has_volume(self,guid):
		return self.has_key(guid)
	
	def store_volume(self,volume):
		if self.has_volume(volume.guid): self.remove_volume(volume.guid)
# 		volume._volume_list = self
		self[volume.guid] = volume
	
	def remove_volume(self,guid):
		if self.has_volume(guid):
			volume = self.get_volume(guid)
			volume.remove_all_files()
			del self[guid]
			
	def __init__(self):
		PersistentMapping.__init__(self)
		# initialize catalogs
# 		self.date_catalog = DateCatalog()
#		self.textindexng2_catalog = TextIndexNG2Catalog()
#		self.field_catalog = FieldCatalog()
#		self.path_catalog = PathCatalog()

	# this is called by the volume
	def _catalog_file(self,volume,file):
		cat_key = "|".join([volume.guid,file.path])
		all_involved_catalogs = {}
		for attrname,value in file.__dict__.iteritems():
			c = []
			if attrname[0:1] == "_": continue
			if type(value) is DateTimeType: c = [self.field_catalog]
# 			elif attrname is "volume": c = [self.field_catalog] # uncomment when fieldindex bug is fixed
			elif attrname is "contents": c = [self.textindexng2_catalog]
			elif attrname is "absolute_path": c = [self.path_catalog,self.field_catalog]
			elif attrname is "path": c = []
			elif type(value) is str: c = [self.field_catalog,self.textindexng2_catalog] #,self.textindexng2_catalog
			elif type(value) in (int,long): c = [self.field_catalog]
			for involved_catalog in c:
				all_involved_catalogs[str(involved_catalog)] = involved_catalog
				involved_catalog.make_index(attrname)
		for i in all_involved_catalogs.itervalues():
			t = time.time()
			i.catalogObject(object=file,uid=cat_key)
			logging.getLogger("Volume."+volume.guid).debug("Catalogued %s in %s in %s sec",file,i,time.time()-t)
			
	
	def _uncatalog_file(self,volume,cat_key):
		cat_key = "|".join([volume.guid,cat_key])
		catalogs = [self.field_catalog,self.path_catalog,self.textindexng2_catalog]#self.date_catalog,
		for i in catalogs:
			t = time.time()
			i.uncatalogObject(uid=cat_key)
			logging.getLogger("Volume."+volume.guid).debug("Uncatalogued %s from %s in %s",cat_key,i,time.time() -t)

	def search(self,expression): #FIX
		assert isinstance(expression,Expression)
		
		if isinstance(expression,Aggregation):
			resultset = None
			for e in expression.expressions:
				er = self.search(e)
				 # why bother, intersection with empty will always yield empty
				if isinstance(expression,And) and not er: return er
				logging.getLogger("Search").debug("%s results for expression %s",len(er),e)
				if resultset is None: resultset = er
				else:
					if isinstance(expression,Or): resultset.union_update(er)
					elif isinstance(expression,And):
						resultset.intersection_update(er)
						 # the intersection yielded an empty set, abort further operations
						if not resultset: return resultset
			if resultset is None: raise ValueError, "Aggregation expressions must contain at least one criterion expression"
			return resultset
			
		elif isinstance(expression,Criterion):
			
			# choose the right catalog based on query type
			opclass = expression.operator.__class__
			if opclass in [Equals,IsEqualOrLessThan,IsEqualOrGreaterThan]:
				right_catalog = self.field_catalog
			elif opclass in [IsBelow,IsOrIsBelow]:
				right_catalog = self.path_catalog
			elif opclass in [NL]:
				right_catalog = self.textindexng2_catalog
			else: raise NotImplementedError, "%s queries on %s not available"%(str(expression.operator),expression.attribute)
			
			#check that the chosen catalog to query has an index for that attribute and can, in fact, be queried for that attribute
			index_name = expression.attribute
			if not right_catalog.has_index(index_name):
				raise NotImplementedError, "no index on %s for attribute %s"%(right_catalog,index_name)
			
			# construct the proper query based on query type and attribute...
			if opclass is Equals: searchexp = {expression.attribute: { "query" : expression.value } }
			elif opclass is NL:
				searchexp = {expression.attribute: { "query" : expression.value} }
			elif opclass is IsEqualOrGreaterThan:
				searchexp = {expression.attribute: { "query" : expression.value,"range":"min"} }
			elif opclass is IsEqualOrLessThan:
				searchexp = {expression.attribute: { "query" : expression.value,"range":"max"} }
			elif opclass is IsOrIsBelow:
				searchexp = {expression.attribute: { "query" : expression.value } }
			else: raise NotImplementedError, "query type %s not implemented for %s"%(expression.operator,expression.attribute)
			
			
			# ...and query the catalog do the query and make a set out of the results
			logging.getLogger("Search").debug("Searching %s for catalog expression %s",right_catalog,searchexp)

			results = right_catalog.search(searchexp)
			resultset = [ right_catalog.paths[result.getRID()] for result in results ]
			return Set(resultset) # return a set of keys
		
		else: raise Exception, "What the hell?"

#class VolumeCatalog(Catalog):
#	
#	def __init__(self):
#		Catalog.__init__(self)
#		self.aq_parent = self
#	
#	def has_index(self,attrname):
#		return self.indexes.has_key(attrname)
#		
#	def make_index(self,attrname):
#		assert type(attrname) is str and len(attrname) > 0
#		if self.has_index(attrname): return
#		idx = self._get_index(attrname)
#		logging.getLogger("VolumeCatalog").debug(
#				"Making %s index for %s",str(idx),attrname)
#		self.addIndex(attrname,idx)
#	
#	def _get_index(self,attrname):
#		raise NotImplementedError

#class FieldCatalog(VolumeCatalog):
#	def _get_index(self,attrname): return PluginIndexes.FieldIndex.FieldIndex.FieldIndex(attrname)
#class PathCatalog(VolumeCatalog):
#	def _get_index(self,attrname): return PluginIndexes.PathIndex.PathIndex.PathIndex(attrname)
#import locale
#class TextIndexNG2Catalog(VolumeCatalog):
#	def _get_index(self,attrname):
#		options = { "default_encoding":locale.getpreferredencoding()}
#		return TextIndexNG.TextIndexNG(attrname,extra=options)
# class DateCatalog(VolumeCatalog):
# 	def _get_index(self,attrname): return PluginIndexes.DateIndex.DateIndex.DateIndex(attrname)


class FilesystemInfospace(Infospace):
	volumes = None
	
	def __init__(self):
		self.volumes = VolumeList()
	
	def __str__(self):
		return "filesystem infospace, containing volumes %s" % ", ".join([ str(a) for a in self.volumes.values() ])
		
		

class FileList(Persistent):
	
# 	volume = None
# 	catalog = None
# 	highest_file_id = None
	mapping = None
	
	def keys(self): return [a for a in self.mapping.keys()]
	
	def __init__(self):
# 		assert isinstance(volume,Volume)
# 		self.volume = volume
		self.mapping = OOBTree()
		self.mod_dates = OIBTree()
		
	def store_file(self,file):
		from File import File
		assert isinstance(file,File)
		self.__save_file(file)
	
	def __save_file(self,file): # private accesor for the catalog stuff
		self.mapping[file.path] = file
		self.mod_dates[file.path] = int(file.modification_date)
	
	def get_file(self,path):
		return self.mapping[path]

	def get_file_mod_date(self,path):
	# returns the file's mod date as an int.  optimized for performance.
		return self.mod_dates[path]

	def has_file(self,path):
		return self.mapping.has_key(path)
		
	def remove_file(self,path):
		del (self.mapping[path])
		del (self.mod_dates[path])

from sets import Set
#import ExtensionClass

#class Volume(InformationObject,ExtensionClass.Base):
class Volume(InformationObject):
	guid = None
	label = None
	filesystem_type = None
	removable = None
	fully_indexed = False # accesors for this? make it private?
	file_list = None
	mountpoint = None

	def __init__(self,guid,mountpoint,filesystem_type,label=None,removable=False):
		InformationObject.__init__(self)
		assert type(guid) is str and len(guid) > 0
		assert type(filesystem_type) is str and len(filesystem_type) > 0
		assert type(removable) is bool
		self.file_list = FileList()
		self.guid = guid
		self.label = label
		self.removable = removable
		self.filesystem_type = filesystem_type
		self.fully_indexed = False
		self.mountpoint = mountpoint
		
	
	def store_file(self,file):
		if self.has_file(file.path): self.remove_file(file.path)
		self.file_list.store_file(file)
		if hasattr(self,"_volume_list"): self._volume_list._catalog_file(self,file)
	
	def remove_file(self,path):
		if self.has_file(path):
			if hasattr(self,"_volume_list"): self._volume_list._uncatalog_file(self,path)
			self.file_list.remove_file(path)
		else: raise KeyError, "no such file %s in volume %s"%(path,self)
		
	def remove_all_files(self):
		all_files_paths =self.file_list.keys()
		for path in all_files_paths:
			logging.getLogger("Volume."+self.guid).debug("Removing %s",path)
			self.remove_file(path)
		
	def has_file(self,path): return self.file_list.has_file(path)
	
	def has_file_changed_since(self,path,time_arg):
		# receives a DateTime and compares it with the file's cached mod date
		stored_mod_date = self.file_list.get_file_mod_date(path)
		if type(time_arg) is not int: dtarg = int(time_arg)
# 		logging.getLogger("has_file_changed_since").debug("time_arg %s stored_mod_date %s, changed %s",time_arg,stored_mod_date,time_arg == stored_mod_date)
		# this is not strictly correct but we expect this to be better performant
		return time_arg == stored_mod_date
	
	def get_file(self,path): return self.file_list.get_file(path)
		
	def get_mountpoint(self):
		return self.mountpoint
	
	def update_mountpoint(self,mountpoint):
		self.mountpoint = mountpoint

	def is_mounted(self):
		return self.get_mountpoint() is not None
	
# 	def __hash__(self):
# 		akku = 0
# 		for char in self.guid:
# 			akku = akku + ord(char)
# 		return akku
		
	def __str__(self):
		mountpoint = self.get_mountpoint()
		if mountpoint: mountpoint = "mounted at %s"%mountpoint
		else: mountpoint = "unmounted"
		if self.label: label = " (labeled %s)"%self.label
		else: label = ""
		return "volume %s%s, %s"%(self.guid,label,mountpoint)
	
	def __repr__(self): return self.__str__()
