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)