import sys, os
from types import DictType
from MiscUtils import AbstractError, NoDefault
from Funcs import valueForString


class ConfigurationError(Exception):
	pass


class Configurable:
	"""Abstract superclass for configuration file functionality.

	Subclasses should override:

		* defaultConfig()  to return a dictionary of default settings
		                   such as { 'Frequency': 5 }

		* configFilename() to return the filename by which users can
		                   override the configuration such as
		                   'Pinger.config'

	Subclasses typically use the setting() method, for example:

		time.sleep(self.setting('Frequency'))

	They might also use the printConfig() method, for example:

		self.printConfig()      # or
		self.printConfig(file)

	Users of your software can create a file with the same name as
	configFilename() and selectively override settings. The format of
	the file is a Python dictionary.

	Subclasses can also override userConfig() in order to obtain the
	user configuration settings from another source.

	"""


	## Init ##

	def __init__(self):
		self._config = None


	## Configuration

	def config(self):
		"""Return the configuration of the object as a dictionary.

		This is a combination of defaultConfig() and userConfig().
		This method caches the config.

		"""
		if self._config is None:
			self._config = self.defaultConfig()
			self._config.update(self.userConfig())
			self._config.update(self.commandLineConfig())
		return self._config

	def setting(self, name, default=NoDefault):
		"""Return the value of a particular setting in the configuration."""
		if default is NoDefault:
			try:
				return self.config()[name]
			except KeyError:
				raise KeyError, \
					'%s config keys are: %s' % (name, self.config().keys())
		else:
			return self.config().get(name, default)

	def setSetting(self, name, value):
		"""Set a particular configuration setting."""
		self.config()[name] = value

	def hasSetting(self, name):
		"""Check whether a configuration setting has been changed."""
		return self.config().has_key(name)

	def defaultConfig(self):
		"""Return a dictionary with all the default values for the settings.

		This implementation returns {}. Subclasses should override.

		"""
		return {}

	def configFilename(self):
		"""Return the full name of the user config file.

		Users can override the configuration by this config file.
		Subclasses must override to specify a name.
		Returning None is valid, in which case no user config file
		will be loaded.

		"""
		raise AbstractError, self.__class__

	def configName(self):
		"""Return the name of the configuration file without the extension.

		This is the portion of the config file name before the '.config'.
		This is used on the command-line.

		"""
		return os.path.splitext(os.path.basename(self.configFilename()))[0]

	def configReplacementValues(self):
		"""Return a dictionary for substitutions in the config file.

		This must be a dictionary suitable for use with "string % dict"
		that should be used on the text in the config file.
		If an empty dictionary (or None) is returned, then no substitution
		will be attempted.

		"""
		return {}

	def userConfig(self):
		"""Return the user config overrides.

		These settings can be found in the optional config file.
		Returns {} if there is no such file.

		The config filename is taken from configFilename().

		"""
		filename = self.configFilename()
		if not filename:
			return {}
		try:
			# open the config file in universal newline mode,
			# in case it has been edited on a different platform
			contents = open(filename, 'rU').read()
		except IOError, e:
			print 'WARNING: Config file', filename
			print '  not loaded: %s.' % e.strerror
			print
			return {}
		isDict = contents.lstrip().startswith('{')
		from WebKit.AppServer import globalAppServer
		if globalAppServer:
			globalAppServer._imp.watchFile(filename)
		replacements = self.configReplacementValues()
		if replacements and isDict:
			try:
				contents %= replacements
			except Exception:
				raise ConfigurationError, \
					'Unable to embed replacement text in %s.' % filename
		evalContext = replacements.copy()
		try:
			True, False
		except NameError: # Python < 2.3
			evalContext['True'] = 1
			evalContext['False'] = 0
		try:
			if isDict:
				config = eval(contents, evalContext)
			else:
				exec contents in evalContext
				config = evalContext
				for name in config.keys():
					if name.startswith('_'):
						del config[name]
		except Exception, e:
			raise ConfigurationError, \
				'Invalid configuration file, %s (%s).' % (filename, e)
		if type(config) is not DictType:
			raise ConfigurationError, 'Invalid type of configuration.' \
				' Expecting dictionary, but got %s.' % type(config)
		try:
			True, False
		except NameError: # Python < 2.3
			del evalContext['True']
			del evalContext['False']
		return config

	def printConfig(self, dest=None):
		"""Print the configuration to the given destination.

		The default destionation is stdout. A fixed with font is assumed
		for aligning the values to start at the same column.

		"""
		if dest is None:
			dest = sys.stdout
		keys = self.config().keys()
		keys.sort()
		width = max(map(len, keys))
		for key in keys:
			dest.write('%s = %s\n'
				% (key.ljust(width), str(self.setting(key))))
		dest.write('\n')

	def commandLineConfig(self):
		"""Return the settings that came from the command-line.

		These settings come via addCommandLineSetting().

		"""
		return _settings.get(self.configName(), {})


## Command line settings ##

_settings = {}

def addCommandLineSetting(name, value):
	"""Override the configuration with a command-line setting.

	Take a setting, like "AppServer.Verbose=0", and call
	addCommandLineSetting('AppServer.Verbose', '0'), and
	it will override any settings in AppServer.config

	"""
	configName, settingName = name.split('.', 1)
	value = valueForString(value)
	if not _settings.has_key(configName):
		_settings[configName] = {}
	_settings[configName][settingName] = value

def commandLineSetting(configName, settingName, default=NoDefault):
	"""Retrieve a command-line setting.

	You can use this with non-existent classes, like "Context.Root=/WK",
	and then fetch it back with commandLineSetting('Context', 'Root').

	"""
	if default is NoDefault:
		return _settings[configName][settingName]
	else:
		return _settings.get(configName, {}).get(settingName, default)
