#!/usr/bin/env python
# encoding: utf-8
"""
A base class for objects that are configurable.

Authors:

* Brian Granger
* Fernando Perez
* Min RK
"""

#-----------------------------------------------------------------------------
#  Copyright (C) 2008-2011  The IPython Development Team
#
#  Distributed under the terms of the BSD License.  The full license is in
#  the file COPYING, distributed as part of this software.
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

import datetime
from copy import deepcopy

from loader import Config
from IPython.utils.traitlets import HasTraits, Instance
from IPython.utils.text import indent, wrap_paragraphs


#-----------------------------------------------------------------------------
# Helper classes for Configurables
#-----------------------------------------------------------------------------


class ConfigurableError(Exception):
    pass


class MultipleInstanceError(ConfigurableError):
    pass

#-----------------------------------------------------------------------------
# Configurable implementation
#-----------------------------------------------------------------------------

class Configurable(HasTraits):

    config = Instance(Config,(),{})
    created = None

    def __init__(self, **kwargs):
        """Create a configurable given a config config.

        Parameters
        ----------
        config : Config
            If this is empty, default values are used. If config is a 
            :class:`Config` instance, it will be used to configure the
            instance.
        
        Notes
        -----
        Subclasses of Configurable must call the :meth:`__init__` method of
        :class:`Configurable` *before* doing anything else and using 
        :func:`super`::
        
            class MyConfigurable(Configurable):
                def __init__(self, config=None):
                    super(MyConfigurable, self).__init__(config)
                    # Then any other code you need to finish initialization.

        This ensures that instances will be configured properly.
        """
        config = kwargs.pop('config', None)
        if config is not None:
            # We used to deepcopy, but for now we are trying to just save
            # by reference.  This *could* have side effects as all components
            # will share config. In fact, I did find such a side effect in
            # _config_changed below. If a config attribute value was a mutable type
            # all instances of a component were getting the same copy, effectively
            # making that a class attribute.
            # self.config = deepcopy(config)
            self.config = config
        # This should go second so individual keyword arguments override 
        # the values in config.
        super(Configurable, self).__init__(**kwargs)
        self.created = datetime.datetime.now()

    #-------------------------------------------------------------------------
    # Static trait notifiations
    #-------------------------------------------------------------------------

    def _config_changed(self, name, old, new):
        """Update all the class traits having ``config=True`` as metadata.

        For any class trait with a ``config`` metadata attribute that is
        ``True``, we update the trait with the value of the corresponding
        config entry.
        """
        # Get all traits with a config metadata entry that is True
        traits = self.traits(config=True)

        # We auto-load config section for this class as well as any parent
        # classes that are Configurable subclasses.  This starts with Configurable
        # and works down the mro loading the config for each section.
        section_names = [cls.__name__ for cls in \
            reversed(self.__class__.__mro__) if 
            issubclass(cls, Configurable) and issubclass(self.__class__, cls)]

        for sname in section_names:
            # Don't do a blind getattr as that would cause the config to 
            # dynamically create the section with name self.__class__.__name__.
            if new._has_section(sname):
                my_config = new[sname]
                for k, v in traits.iteritems():
                    # Don't allow traitlets with config=True to start with
                    # uppercase.  Otherwise, they are confused with Config
                    # subsections.  But, developers shouldn't have uppercase
                    # attributes anyways! (PEP 6)
                    if k[0].upper()==k[0] and not k.startswith('_'):
                        raise ConfigurableError('Configurable traitlets with '
                        'config=True must start with a lowercase so they are '
                        'not confused with Config subsections: %s.%s' % \
                        (self.__class__.__name__, k))
                    try:
                        # Here we grab the value from the config
                        # If k has the naming convention of a config
                        # section, it will be auto created.
                        config_value = my_config[k]
                    except KeyError:
                        pass
                    else:
                        # print "Setting %s.%s from %s.%s=%r" % \
                        #     (self.__class__.__name__,k,sname,k,config_value)
                        # We have to do a deepcopy here if we don't deepcopy the entire
                        # config object. If we don't, a mutable config_value will be
                        # shared by all instances, effectively making it a class attribute.
                        setattr(self, k, deepcopy(config_value))

    @classmethod
    def class_get_help(cls):
        """Get the help string for this class in ReST format."""
        cls_traits = cls.class_traits(config=True)
        final_help = []
        final_help.append(u'%s options' % cls.__name__)
        final_help.append(len(final_help[0])*u'-')
        for k,v in cls.class_traits(config=True).iteritems():
            help = cls.class_get_trait_help(v)
            final_help.append(help)
        return '\n'.join(final_help)
    
    @classmethod
    def class_get_trait_help(cls, trait):
        """Get the help string for a single trait."""
        lines = []
        header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__)
        lines.append(header)
        try:
            dvr = repr(trait.get_default_value())
        except Exception:
            dvr = None # ignore defaults we can't construct
        if dvr is not None:
            if len(dvr) > 64:
                dvr = dvr[:61]+'...'
            lines.append(indent('Default: %s'%dvr, 4))
        if 'Enum' in trait.__class__.__name__:
            # include Enum choices
            lines.append(indent('Choices: %r'%(trait.values,)))
        
        help = trait.get_metadata('help')
        if help is not None:
            help = '\n'.join(wrap_paragraphs(help, 76))
            lines.append(indent(help, 4))
        return '\n'.join(lines)

    @classmethod
    def class_print_help(cls):
        """Get the help string for a single trait and print it."""
        print cls.class_get_help()

    @classmethod
    def class_config_section(cls):
        """Get the config class config section"""
        def c(s):
            """return a commented, wrapped block."""
            s = '\n\n'.join(wrap_paragraphs(s, 78))
            
            return '# ' + s.replace('\n', '\n# ')
        
        # section header
        breaker = '#' + '-'*78
        s = "# %s configuration"%cls.__name__
        lines = [breaker, s, breaker, '']
        # get the description trait
        desc = cls.class_traits().get('description')
        if desc:
            desc = desc.default_value
        else:
            # no description trait, use __doc__
            desc = getattr(cls, '__doc__', '')
        if desc:
            lines.append(c(desc))
            lines.append('')
        
        parents = []
        for parent in cls.mro():
            # only include parents that are not base classes
            # and are not the class itself
            if issubclass(parent, Configurable) and \
                    not parent in (Configurable, SingletonConfigurable, cls):
                parents.append(parent)
        
        if parents:
            pstr = ', '.join([ p.__name__ for p in parents ])
            lines.append(c('%s will inherit config from: %s'%(cls.__name__, pstr)))
            lines.append('')
        
        for name,trait in cls.class_traits(config=True).iteritems():
            help = trait.get_metadata('help') or ''
            lines.append(c(help))
            lines.append('# c.%s.%s = %r'%(cls.__name__, name, trait.get_default_value()))
            lines.append('')
        return '\n'.join(lines)
    
    

class SingletonConfigurable(Configurable):
    """A configurable that only allows one instance.

    This class is for classes that should only have one instance of itself
    or *any* subclass. To create and retrieve such a class use the
    :meth:`SingletonConfigurable.instance` method.
    """

    _instance = None
    
    @classmethod
    def _walk_mro(cls):
        """Walk the cls.mro() for parent classes that are also singletons
        
        For use in instance()
        """
        
        for subclass in cls.mro():
            if issubclass(cls, subclass) and \
                    issubclass(subclass, SingletonConfigurable) and \
                    subclass != SingletonConfigurable:
                yield subclass
    
    @classmethod
    def clear_instance(cls):
        """unset _instance for this class and singleton parents.
        """
        if not cls.initialized():
            return
        for subclass in cls._walk_mro():
            if isinstance(subclass._instance, cls):
                # only clear instances that are instances
                # of the calling class
                subclass._instance = None
            
    @classmethod
    def instance(cls, *args, **kwargs):
        """Returns a global instance of this class.

        This method create a new instance if none have previously been created
        and returns a previously created instance is one already exists.

        The arguments and keyword arguments passed to this method are passed
        on to the :meth:`__init__` method of the class upon instantiation.

        Examples
        --------

        Create a singleton class using instance, and retrieve it::

            >>> from IPython.config.configurable import SingletonConfigurable
            >>> class Foo(SingletonConfigurable): pass
            >>> foo = Foo.instance()
            >>> foo == Foo.instance()
            True

        Create a subclass that is retrived using the base class instance::

            >>> class Bar(SingletonConfigurable): pass
            >>> class Bam(Bar): pass
            >>> bam = Bam.instance()
            >>> bam == Bar.instance()
            True
        """
        # Create and save the instance
        if cls._instance is None:
            inst = cls(*args, **kwargs)
            # Now make sure that the instance will also be returned by
            # parent classes' _instance attribute.
            for subclass in cls._walk_mro():
                subclass._instance = inst
        
        if isinstance(cls._instance, cls):
            return cls._instance
        else:
            raise MultipleInstanceError(
                'Multiple incompatible subclass instances of '
                '%s are being created.' % cls.__name__
            )

    @classmethod
    def initialized(cls):
        """Has an instance been created?"""
        return hasattr(cls, "_instance") and cls._instance is not None


class LoggingConfigurable(Configurable):
    """A parent class for Configurables that log.
    
    Subclasses have a log trait, and the default behavior
    is to get the logger from the currently running Application
    via Application.instance().log.
    """
    
    log = Instance('logging.Logger')
    def _log_default(self):
        from IPython.config.application import Application
        return Application.instance().log
        
        
