import string, sys, os
from types import DictType
from MiscUtils import AbstractError, NoDefault
from WebKit.ImportSpy import modloader
from Funcs import valueForString



class ConfigurationError(Exception):
    pass


class Configurable:
    """
    Configurable is an abstract superclass that provides configuration
    file functionality for subclasses.

    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):
        """ Returns 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):
        """ Returns the value of a particular setting in the configuration. """
        if default is NoDefault:
            return self.config()[name]
        else:
            return self.config().get(name, default)

    def hasSetting(self, name):
        return self.config().has_key(name)

    def defaultConfig(self):
        """ Returns a dictionary containing all the default values for the settings. This implementation returns {}. Subclasses should override. """
        return {}

    def configFilename(self):
        """ Returns the filename by which users can override the configuration. 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):
        """
        Returns the name of the configuration file (the portion
        before the '.config').  This is used on the command-line.
        """
        return os.path.splitext(os.path.basename(self.configFilename()))[0]

    def configReplacementValues(self):
        """
        Returns 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):
        """ Returns the user config overrides found in the optional config file, or {} if there is no such file. The config filename is taken from configFilename(). """
        try:
            filename = self.configFilename()
            if filename is None:
                return {}
            file = open(filename)
        except IOError:
            return {}
        else:
            contents = file.read()
            file.close()
            modloader.watchFile(filename)
            replacements = self.configReplacementValues()
            if replacements:
                try:
                    contents = contents % replacements
                except:
                    raise ConfigurationError, 'Unable to embed replacement text in %s.' % self.configFilename()

            try:
                config = eval(contents, {})
            except:
                raise ConfigurationError, 'Invalid configuration file, %s.' % self.configFilename()
            if type(config) is not DictType:
                raise ConfigurationError, 'Invalid type of configuration. Expecting dictionary, but got %s.'  % type(config)
            return config

    def printConfig(self, dest=None):
        """ Prints the configuration to the given destination, which defaults to 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(lambda key: len(key), keys))
        for key in keys:
            dest.write(string.ljust(key, width)+' = '+str(self.setting(key))+'\n')
        dest.write('\n')

    def commandLineConfig(self):
        """
        Settings that came from the command line (via
        addCommandLineSetting).
        """
        return _settings.get(self.configName(), {})

_settings = {}
def addCommandLineSetting(name, value):
    """
    Take a setting, like --AppServer.Verbose=0, and call
    addCommandLineSetting('AppServer.Verbose', '0'), and
    it will override any settings in AppServer.config
    """
    configName, settingName = string.split(name, '.', 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)