"""Base class for database driver implementations"""
from basicproperty import propertied, common, basic
from basictypes import enumeration

DriverNameSet = enumeration.EnumerationSet.coerce( [
	# Each driver class should have a name attribute pointing to
	# a DriverName instance registered in this set.

	# add new drivers with calls to new( name, friendlyName, value )
	# add new default drivers to defaultdrivers.py module
])

class DriverName( enumeration.Enumeration ):
	"""Specifier for a driver-name enumeration value"""
	datatype = 'enumeration.DriverName'
	set = DriverNameSet

class DriverCapabilities( propertied.Propertied ):
	"""Object storing set of driver capability declarations

	At the moment, this is a fairly limited set of capability
	declarations, mostly because the system only supports
	a small number of features which are not considered
	important enough to be implemented across all supported
	databases.
	"""
	serial = common.BooleanProperty(
		"serial", """Whether we have native support for serial datatype

	If the serial datatype is not supported, we'll have to
	generate code to create a sequence, track the name of
	that sequence and potentially rewrite the defaultValue
	field of the schema.
	""",
		defaultValue = 0,
		setDefaultOnGet = 0,
	)
	inherits = common.BooleanProperty(
		"inherits", """Whether we support PostgreSQL-style inheritance on tables

	Inherits is an object-relational database feature that
	provides (multiple) inheritance for database tables. If
	it's available, we can model object hierarchies more
	naturally.  If not, we need to do (complex, slow) joins
	across multiple tables to load base-class and sub-class
	data.
	""",
		defaultValue = 0,
		setDefaultOnGet = 0,
	)
	queryUnicode = common.BooleanProperty(
		"queryUnicode", """Whether can accept Unicode for query strings

	If this is false, then sqlquery needs to translate queries into
	strings, which it will do using utf-8 encodings.
	""",
		defaultValue = 1,
		setDefaultOnGet = 0,
	)
	queryPyformat = common.BooleanProperty(
		"queryPyformat", """Whether can handle pyformat paramstyle, if not will use pyformatsupport""",
		defaultValue = 1,
	)
	schemaSupport = common.BooleanProperty(
		"schemaSupport", """Whether the database supports sub-namespace/sub-schemas

		AFAIK only PostgreSQL actually supports the feature
	""",
		defaultValue = 0,
		setDefaultOnGet = 0,
	)
	lastRowID = common.BooleanProperty(
		"lastRowID", """Whether the database supports last-row-id reporting""",
		defaultValue = True,
		setDefaultOnGet = 0,
	)

class DBDriver( propertied.Propertied):
	"""Base class for database driver implementations

	The DBDriver object is roughly analogous to the DB-API
	module.  In addition, it allows for resolving data types
	from native driver-data-type-declarations to basicproperty/
	basictypes/wxoo-style declarations.

	Subclasses will generally provide a considerable number of
	common query objects for use in introspection queries.
	
	Each DBDriver subclass is known by a specifier string which
	is a key in the DriverName enumeration.  The specifier
	strings are specified in the DBSpecifier object to select
	the appropriate DBDriver run-time class.

	Where appropriate, the DBDriver sub-class should
	allow for querying to determine "system" databases,
	such as ODBC named data sources.
		See: getSystemSpecifiers()
	"""
	capabilities = DriverCapabilities()
	name = basic.BasicProperty(
		'name', """DriverName enumeration identifying the driver in DBSpecifiers""",
		baseType = DriverName,
	)
	connections = common.WeakValueDictionaryProperty(
		"connections","""Mapping of specifier objects to live connections

	The DBDriver retains weak references to all active
	connections, to allow for connection sharing and/or
	properly shutting down all connections on exit.
	""",
		setDefaultOnGet = 1,
	)
	defaultConnectionClass = common.ClassByNameProperty(
		'defaultConnectionClass', """Default DBConnection sub-class to be used for this driver""",
		defaultValue = 'pytable.dbconnection.DBConnection',
	)
	friendlyName = common.StringProperty(
		"friendlyName", """Friendly name of the DBDriver type""",
		defaultValue = "",
	)
	paramstyle = common.StringProperty(
		"paramstyle", """DBAPI 2.0 parameter-style value""",
		defaultValue = "pyformat",
	)
	threadsafety = common.IntegerProperty(
		"paramstyle", """DBAPI 2.0 threadsafety value XXX should be an enumeration!""",
		defaultValue = 0,
	)
	apilevel = common.StringProperty(
		"apilevel", """DBAPI 2.0 apilevel value""",
		defaultValue = "2.0",
	)
	systemDBName = common.StringProperty(
		"systemDBName", """Default name for driver's system database (for listDatabases connections)""",
		defaultValue = "",
	)

	queries = {}
	userDescription = ""
	# properties which may be specified for a connection
	# file-based drivers likely will only have dsn in this set
	connectionProperties = ("dsn", "host","user","password", "database")
	fileBased = 0
	usesIntDescriptionTypes = True


	def copyErrorsFromModule( cls, source ):
		"""Method to copy Error names from source to the driver class"""
		for name in (
			'Warning','Error','InterfaceError',
			'DatabaseError','DataError','OperationalError',
			'IntegrityError', 'InternalError','ProgrammingError',
			'NotSupportedError',
		):
			setattr(cls,name,getattr(source,name,StandardError))
	copyErrorsFromModule = classmethod( copyErrorsFromModule )
	def connect(self, specifier = None, share=1, **namedarguments):
		"""Connect using this driver to the specified database

		specifier -- the DBSpecifier object encoding the connection
			information to be used in establishing the connection
		
			If specifier is None, then create a new DBSpecifier
			using the named arguments as arguments to the DBSpecifier
			constructor.  Note: drivers with driver-specific specifier
			classes may construct those specific classes instead of
			the base DBSpecifier class.

			If specifier is not None, but there are namedarguments,
			the specifier will be cloned (copied) with the passed
			namedarguments as parameters to the clone method.  The
			result will be that the named argument values will override
			the specifier object's values.

		share -- if true, the connection will be shared with any existing
			connection which matches the calculated final specifier
		"""
		specifier = self.unifySpecifier(specifier, **namedarguments)
		if share:
			for spec,conn in self.connections.iteritems():
				if specifier == spec and not conn.invalid:
					self.connections[specifier] = conn
					return conn
		connection = self.establishConnection(specifier)
		connection = self.wrapConnection( connection )
		self.connections[specifier] = connection
		connection.specifier = specifier
		return connection
	def reconnect( self, connection ):
		"""Reconnect the given DBConnection object"""
		spec = None
		for spec,conn in self.connections.iteritems():
			if conn is connection:
				del self.connections[ spec ]
				break
		# prefer use of the connection's specifier attribute
		# use our local copy otherwise
		spec = getattr( connection, 'specifier', spec )
		if not spec:
			raise ValueError( """Attempting to reconnect a connection which is neither known to the system nor provides a 'specifier' property: %s"""%(
				connection,
			))
		rawConnection = self.establishConnection( spec )
		connection.connection = rawConnection
		return connection
	def unifySpecifier(self, specifier, **namedarguments):
		"""Given specifier and named arguments create finalSpecifier

		specifier -- DBSpecifer or None
		named -- dictionary of attribute name to attribute value

		See DBDriver.connect for discussion of the semantics
		"""
		if specifier is None:
			specifier = self.createSpecifier( **namedarguments )
		elif namedarguments:
			specifier = specifier.clone( **namedarguments )
		return specifier
				
	
	def createSpecifier(self, **namedarguments):
		"""Create a new specifier object for this driver

		namedarguments -- applied to the constructor of the
			DBSpecifier

		Note:
			the drivername property will be set to our name
			property's value
		"""
		from pytable import dbspecifier
		namedarguments["drivername"] = self.name
		return dbspecifier.DBSpecifier( **namedarguments )
	def getSystemSpecifiers(self,*arguments,**namedarguments):
		"""Customization Point: return list of system specifiers

		For APIs which have system-registered data sources,
		such as ODBC, this method should be overwritten to provide
		system specifiers for the system-registered data sources.
		"""
		return []
	def wrapConnection( self, connection ):
		"""Wrap the connection with a driver-aware connection object"""
		return self.defaultConnectionClass(
			connection = connection,
			driver = self,
		)

	### Required customization points (abstract methods)
	def establishConnection(self, fullSpecifier):
		"""Abstract Method: Connect using the fully specified specifier passed

		fullSpecifier -- a specifier with all arguments unified
			and ready to be connected.  This specifier should
			include everything required to do the actual
			connection (including passwords or the like).

		All sub-classes must override this method!
		"""
		raise NotImplementedError("""DBDriver sub-class %s does not define an establishConnection method"""% (self.__class__.__name__,))

	_dataTypeRegistry = None
	def sqlToDataType( self, source ):
		"""Convert given SQL data-type to the desired data-type specifier

		source -- the source specifier, an SQL identifier

		returns a wxoo specifier or raises KeyError, only returns
		the first-registered data-type, so sub-classes that want
		to override the defaults need to prepend their dataTypeRegistry
		to the dbdriver.DBDriver.dataTypeRegistry list.
		"""
		if not self._dataTypeRegistry:
			self._dataTypeRegistry = {}
			for dbtype,wxootype,basetype in self.dataTypeRegistry:
				self._dataTypeRegistry.setdefault( dbtype, []).append( (wxootype,basetype) )
		source = source.lower()
		item = self._dataTypeRegistry.get(source, [('',object)])[0][0]
		if not item:
			raise KeyError( """Driver %r doesn't know how to calculate wxoo specifier for data-type %r"""%(
				self,source,
			))
		return item
	def sqlToBaseType( self, source ):
		"""Convert given SQL data-type to a property-base-type

		source -- the source specifier, an SQL identifier

		returns a base-type suitable for use in basicproperty
		properties to control the property's operation.
		"""
		if not self._dataTypeRegistry:
			self._dataTypeRegistry = {}
			for dbtype,wxootype,basetype in self.dataTypeRegistry:
				self._dataTypeRegistry.setdefault( dbtype, []).append( (wxootype,basetype) )
		source = source.lower()
		item = self._dataTypeRegistry.get(source, [('',object)])[0][1]
		if not item:
			raise KeyError( """Driver %r doesn't know how to calculate wxoo specifier for data-type %r"""%(
				self,source,
			))
		return item

	_localTypeRegistry = None
	def localToSQLType( self, source ):
		"""Convert a local numeric data-type to an SQL string data-type

		source -- the source specifier

		returns an sql data-type for the given local SQL type, the
		sub-class must provide the actual implementation.
		"""
		if not self._localTypeRegistry:
			self._localTypeRegistry = {}
			for key,value in self.localTypeRegistry:
				self._localTypeRegistry.setdefault( key, []).append( value )
		if self.usesIntDescriptionTypes:
			# everything save PyGreSQL, at the moment...
			source = int(source)
		item = self._localTypeRegistry.get( source, [''])[0]
		if not item:
			raise KeyError( """Driver %r doesn't know how to calculate SQL specifier for data-type %r"""%(
				self,source,
			))
		return item

			
	localTypeRegistry = [
		# sub-classes will have to create a table to override this...
	]
	dataTypeRegistry = [
		# partial registry of common SQL name -> wxoo name values...
		("float", 'float', float) ,
		("float4", 'float', float) ,
		("float8" , 'float', float) ,

		("int2", 'int', int) ,
		("int4", 'int', int) ,
		("int8", 'long', long) ,
		("integer", 'int', int) ,
		
		("bytea", 'str.long', str) ,
		("char", 'str', str) ,
		("text", 'str.long', str) ,
		("varchar", 'str', str) ,

		("bool", 'bool', 'basictypes.booleanfix.bool') ,

		("blob", 'str.locale', str) ,
		("bpchar", 'str.locale', str) ,

		("date" , 'datetime', 'mx.DateTime.DateTimeType') ,
		("interval" , 'datetimedelta', 'mx.DateTime.DateTimeDeltaType') ,
		("time", 'timeofday', 'basictypes.datemx_types.mxTimeOfDay') ,
		("timestamp", 'datetime', 'mx.DateTime.DateTimeType') ,
		("timestamptz", 'datetime', 'mx.DateTime.DateTimeType') , # missing zone info here

		# these 4 aren't mapped to anything useful yet
		("cash", 'decimal', int), 
		("money", 'decimal', int),
		("decimal", "decimal", int ),
		("numeric", 'decimal', int),
	]
