"""
Turfig configuration system.  It does not get any simpler.  My turf, your turf,
we configure our turf as we like it.

Turfig is a library for configuration files written in the form of Python
classes.  This basically adds support for declaration and validation of the
configuration variables and is a very flexible and simple configuration system
for any kind of thing.

This module provides a system of validation for configuration information
defined as class variables.  You write classes to define configuration settings,
all within glorious Python.  You put them in a file, something like this::

  class BaseConfig(Config):

     __vars__ = (
        ConfigVar('Extension', str, 'Extension to use.'),
        ConfigVar('Compression', bool, 'Enable compression.'),
     )

     # Set default values
     Extension = 'asp'
     Compression = False
     Encoding = 'latin-1'

And then maybe in another module, if desired::

  class HomeConfig(BaseConfig):

     # Override compression value.
     Compression = True

To validate a configuration, simply call the ``validate()`` function of this
module.  All the configuration variables defined in __vars__ will be
cross-checked against the attributes of the given object.

You can inherit and share the configuration data, they are just contained by
Python classes.  You can also instantiate those classes to do some
initializations dynamically in the constructor.

"""
__author__ = 'Martin Blais <blais@furius.ca>'

# stdlib imports
import sys, os, traceback, re, StringIO, textwrap, threading
from types import ClassType, TypeType
from os.path import exists

__all__ = ('validate', 'ConfigVar', 'ConfigError',
           'getdoc', 'getvalues', 'update_attribs')



def load_config_from_source(filename, options=None):
    """
    Load the given filename as Python, find a unique class as global level that
    is derived from Config, and instantiate, validate and return an instance.
    """

    # Load objects from source.
    cfg_classes = find_objects_in_source(filename, Config)
    if len(cfg_classes) != 1:
        raise ValueError("Invalid number of Config definitions in file '%s': %s" %
                         (filename, len(cfg_classes)))

    # Instantiate a configuration object.
    config_cls = cfg_classes[0]
    config_obj = config_cls(options or {})

    # Validate it and initialize the global state.
    validate(config_obj)

    return config_obj

def find_objects_in_source(filename, type_):
    "Load the source and find object of the given type."
    import imp
    cfg_modname = '__find__'
    cfg_module = imp.load_source(cfg_modname, filename)
    cfg_classes = []
    for obj in cfg_module.__dict__.itervalues():
        if (isinstance(obj, (ClassType, TypeType)) and
            issubclass(obj, type_) and
            obj.__module__ == cfg_modname):
            cfg_classes.append(obj)
    return cfg_classes

def load_config(cls_name, options=None):
    """
    Import the given class 'cls_name', instantiate the configuration object and
    validate it.
    """
    # Instantiate a configuration object.
    config_cls = import_object(cls_name)
    config_obj = config_cls(options or {})

    # Validate it and initialize the global state.
    validate(config_obj)

    return config_obj

def import_get_module(modname):
    """
    Import the given module name--which may contain dotted notation-- and return
    the top module.
    """
    exec 'import %s' % modname
    return eval(modname)

def import_object(objpath):
    """
    Given the path of an object, attempt to import it and return it.
    This progressively attempts to import all the dotted paths and return the
    contained object as soon as an import fails.
    """
    comps = objpath.split('.')
    modpath, objname = '.'.join(comps[:-1]), comps[-1]

    mod = import_get_module(modpath)
    return getattr(mod, objname)


def validate(obj):
    """
    Validate a configuration object.
    """
    # Get the configuration variables.
    config_vars = get_named_attribs(obj, '__vars__', True)

    seenvars = set()
    for cvar in config_vars:
        if cvar.name in seenvars:
            raise ConfigError(
                "Error: Duplicate variable description for '%s'." % cvar.name)
        seenvars.add(cvar.name)

        # Extract the configuration variables.
        if hasattr(obj, cvar.name):
            value = getattr(obj, cvar.name)

            # Check the type of the configuration variable.
            if not isinstance(value, cvar.vtype):
                raise ConfigError(
                    "Error: Invalid type for '%s': %s should be "
                    "of type %s" % (cvar.name, repr(value), cvar.vtype))

        # Check if the variable was required.
        elif cvar.required:
            raise ConfigError(
                "Error: Missing required configuration variable: '%s'." %
                cvar.name)

    # Check that there are no extraneous CapWorded variables.
    diffvars = (set(filter(re.compile('[A-Z][a-z0-9A-Z]+$').match, dir(obj))) -
                set([x.name for x in config_vars]))
    if diffvars:
        raise ConfigError(
            "Error: Extraneous variables: %s" % ', '.join(diffvars))



def getdoc(obj):
    """
    Return the documentation for the configuration variables on the given
    object.
    """
    # Get the configuration variables.
    config_vars = get_named_attribs(obj, '__vars__', True)

    s = StringIO.StringIO()
    for cvar in config_vars:
        print >> s, 'Variable: %s' % cvar.name
        print >> s, '    Value: %s' % repr(getattr(obj, cvar.name))
        print >> s, '    Type: %s' % (cvar.vtype,)
        print >> s, '    Doc'
        print >> s, textwrap.fill(cvar.doc,
                                  initial_indent='       ',
                                  subsequent_indent='       ')
        print >> s
    return s.getvalue()

def getvalues(obj):
    """
    Return the documentation for the configuration variables on the given
    object.
    """
    # Get the configuration variables.
    config_vars = get_named_attribs(obj, '__vars__', True)

    s = StringIO.StringIO()
    for cvar in config_vars:
        print >> s, '%s = %s' % (cvar.name, repr(getattr(obj, cvar.name)))
    return s.getvalue()





class Config(object):
    """
    Base class for config objects.
    """
    def __init__(self, options=None):
        self._options = options

    def get(self, vname, default=None):
        try:
            return getattr(self, vname)
        except AttributeError:
            return default

    def has_key(self, vname):
        return hasattr(self, vname)


class ConfigVar(object):
    """
    Declaration class for configuration variables.
    """
    def __init__(self, vname, vtype, vdoc, required=False):
        self.name = vname
        self.required = bool(required)

        if not isinstance(vtype, (type, tuple)):
            raise RuntimeError(
                "Error: Type for variable '%s' should be a "
                "type or tuple of types." % vname)

        if not isinstance(vdoc, (str, unicode)):
            raise RuntimeError(
                "Error: Documentation for variable '%s'.  "
                "should be a string or unicode." % vname)

        self.vtype = vtype
        self.doc = vdoc

    def __str__(self):
        return '<ConfigVar %s>' % self.name


class ConfigError(Exception):
    """
    Configuration errors.  Errors that occur during the configuration parsing.
    """



def update_attribs(obj, values):
    """
    Add the values in 'values' as attributes on 'obj'.
    This can be used to initialize an objects uninitialized values.
    """
    for key, value in values.iteritems():
        setattr(obj, key, value)

def get_named_attribs(obj, attribname, concat=False):
    """
    Fetches all the attributes of all the base classes with the given atribute
    name.  Returns a list of the all those attributes, concatenated together.

    If 'concat' is True, the value of those attributes are assumed to be
    sequences and are concatenated together to produce the result.
    """
    classes = get_classes(obj)

    allattribs = []
    for attrib in map(lambda x: x.__dict__.get('__vars__', None), classes):
        if not attrib:
            continue
        if concat:
            allattribs.extend(list(attrib))
        else:
            allattribs.append(attrib)
    return allattribs


def get_classes(obj):
    """
    Return all the classes that the class or object inherits from.
    """
    if not isinstance(obj, type):
        obj = obj.__class__
    allclasses, clsstack = [], [obj]
    while clsstack:
        thiscls = clsstack.pop()
        allclasses.append(thiscls)
        clsstack.extend(thiscls.__bases__)
    allclasses.reverse()
    return allclasses




try:
    from ranvier import LeafResource
    from xml.sax.saxutils import escape

    dump_config_tmpl = '''
<html>
  <head>
    <title>Configuration</title>
  </head>
  <body>
    <dl>
%s
    </dl>
  </body>
</html>
    '''

    var_tmpl = '''
<p>
<dt><b>%(name)s</b>: %(type)s</dt>
<dd>= <tt>%(value)s</tt></dd>
<dd><p><i>%(doc)s</i></p></dd>
    '''

    class DumpConfigResource(LeafResource):
        """
        Resource that dumps the current configuration parameters.
        """
        def __init__(self, config):
            self.config = config

        def handle(self, ctxt):
            ctxt.x.response.setContentType('text/html')
            s = StringIO.StringIO()



            # Get the configuration variables.
            config_vars = get_named_attribs(self.config, '__vars__', True)

            s = StringIO.StringIO()
            for cvar in config_vars:
                s.write(var_tmpl % {'name': escape(cvar.name),
                                    'type': escape(str(cvar.vtype)),
                                    'value': escape(repr(getattr(self.config,
                                                                 cvar.name))),
                                    'doc': escape(cvar.doc)})
            contents = s.getvalue()

            ctxt.x.response.write(dump_config_tmpl % contents)

except ImportError:
    pass # Skip it if we don't have Ranvier.
