# -*- coding: utf-8 -*-

import itertools
import logging
from datetime import datetime, date

try:
    from collections import OrderedDict
except ImportError:  # pragma: no cover
    # if on python 2.6
    from ordereddict import OrderedDict


class LayeredConfig(object):
    def __init__(self, *sources, **kwargs):
        """Creates a config object from one or more sources and provides
        unified access to a nested set of configuration
        parameters. The source of these parameters a config file
        (using .ini-file style syntax), command line parameters, and
        default settings embedded in code. Command line parameters
        override configuration file parameters, which in turn override
        default settings in code (hence **Layered** Config).

        Configuration parameters are accessed as regular object
        attributes, not dict-style key/value pairs.  Configuration
        parameter names should therefore be regular python
        identifiers, and preferrably avoid upper-case and "_" as well
        (i.e. only consist of the characters a-z and 0-9)

        Configuration parameter values can be typed (strings,
        integers, booleans, dates, lists...). Even though some sources
        lack typing information (eg in INI files, command-line
        parameters and enviroment variables, everything is a string),
        LayeredConfig will attempt to find typing information in other
        sources and convert data.

        :param \*sources: Initialized ConfigSource-derived objects
        :param cascade: If an attempt to get a non-existing parameter
                        on a sub (nested) configuration object should
                        attempt to get the parameter on the parent
                        config object. ``False`` by default,
        :type cascade: bool
        :param writable: Whether configuration values should be mutable.
                         ``True`` by default. This does not affect
                         :py:meth:`~Layeredconfig.set`.
        :type writable: bool

        """
        self._sources = sources
        self._subsections = OrderedDict()
        self._cascade = kwargs.get('cascade', False)
        self._writable = kwargs.get('writable', True)
        self._parent = None
        self._sectionkey = None

        # Each source may have any number of named subsections. We
        # create a LayeredConfig object for each name, and stuff all
        # matching subections from each of our sources in it.
        #
        # 1. find all names
        sectionkeys = []
        for src in self._sources:
            try:
                for k in src.subsections():
                    if k not in sectionkeys:
                        sectionkeys.append(k)
            except AttributeError:  # possibly others, or all
                # we couldn't get any subsections for source, perhaps
                # because it's an "empty" source. Well, that's ok.
                pass

        for k in sectionkeys:
            # 2. find all subsections in all of our sources
            s = []
            for src in self._sources:
                if k in list(src.subsections()):
                    s.append(src.subsection(k))
                else:
                    # create an "empty" subsection object. It's
                    # important that all the LayeredConfig objects in a
                    # tree have the exact same set of
                    # ConfigSource-derived types.
                    # print("creating empty %s" % src.__class__.__name__)
                    s.append(src.__class__(parent=src,
                                           identifier=src.identifier,
                                           writable=src.writable,
                                           empty=True,
                                           cascade=self._cascade))
            # 3. create a LayeredConfig object for the subsection
            c = self.__class__(*s,
                               cascade=self._cascade,
                               writable=self._writable)
            c._sectionkey = k
            c._parent = self
            self._subsections[k] = c

        # 4. give each source a chance to to some post-init setup.
        for src in self._sources:
            src.setup(self)

    @staticmethod
    def write(config):
        """Commits any pending modifications, ie save a configuration file if
        it has been marked "dirty" as a result of an normal
        assignment. The modifications are written to the first
        writable source in this config object.

        .. note::

           This is a static method, ie not a method on any object
           instance. This is because all attribute access on a
           LayeredConfig object is meant to retrieve configuration
           settings.

        :param config: The configuration object to save
        :type  config: layeredconfig.LayeredConfig

        """
        root = config
        while root._parent:
            root = root._parent

        for source in root._sources:
            if source.writable and source.dirty:
                source.save()

    @staticmethod
    def set(config, key, value, sourceid="defaults"):
        """Sets a value in this config object *without* marking any source
        dirty, and with exact control of exactly where to set the
        value. This is mostly useful for low-level trickery with
        config objects.

        :param config: The configuration object to set values on
        :param key: The parameter name
        :param value: The new value
        :param sourceid: The identifier for the underlying source that the
                         value should be set on.
        """
        for source in config._sources:
            if source.identifier == sourceid:
                source.set(key, value)
                # What if no source is found? We silently ignore...

    @staticmethod
    def get(config, key, default=None):
        """Gets a value from the config object, or return a default value if
        the parameter does not exist, like :py:meth:`dict.get` does.
        """

        if hasattr(config, key):
            return getattr(config, key)
        else:
            return default

    @staticmethod
    def dump(config):
        """Returns the entire content of the config object in a way that can
        be easily examined, compared or dumped to a string or file.

        :param config: The configuration object to dump
        :rtype: dict

        """
        def _dump(element):
            if not isinstance(element, config.__class__):
                return element

            section = dict()
            for key, subsection in element._subsections.items():
                section[key] = _dump(subsection)
            for key in element:
                section[key] = getattr(element, key)
            return section

        return _dump(config)

    # These are methods i'd like to implement next
    #
    #    @staticmethod
    #    def where(config, key):
    #        """returns the identifier of a source where a given key is found, or None."""
    #        pass
    #
    #    @staticmethod
    #    def load(config, d):
    #        """Recreates a dump()ed config object."""
    #        pass

    @staticmethod
    def datetimeconvert(value):
        """Convert the string *value* to a :py:class:`~datetime.datetime`
        object. *value* is assumed to be on the form "YYYY-MM-DD
        HH:MM:SS" (optionally ending with fractions of a second).

        """
        try:
            return datetime.strptime(value, "%Y-%m-%d %H:%M:%S.%f")
        except ValueError:
            return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")

    @staticmethod
    def dateconvert(value):
        """Convert the string *value* to a :py:class:`~datetime.date`
        object. *value* is assumed to be on the form "YYYY-MM-DD".

        """
        return datetime.strptime(value, "%Y-%m-%d").date()

    @staticmethod