from abc import ABCMeta, abstractmethod
from datetime import date, datetime
import ast

class ConfigSource(object):
    __metaclass__ = ABCMeta

    identifier = None
    """A string identifying this source, primarily used with
    :py:meth:`LayeredConfig.set`."""

    writable = False
    """Whether or not this source can accept changed configuration
    settings and store them in the same place as the original setting came
    from."""

    dirty = False
    """For writable sources, whether any parameter value in this source
    has been changed so that a call to :py:meth:`save` might be needed."""

    parent = None
    """The parent of this source, if this represents a nested
    configuration source, or None"""

    source = None
    """By convention, this should be your main connection handle, data
     access object, or other resource neededed to retrieve the
     settings."""

    @abstractmethod  # but subclasses should still call it through super()
    def __init__(self, **kwargs):
        """The constructor of the class should set up needed
        resources, such as opening and parsing a configuration file.

        It is a good idea to keep whatever connection handles, data
        access objects, or other resources needed to retrieve the
        settings, as unprocessed as possible. The methods that
        actually need the data (:py:meth:`has`, :py:meth:`get`,
        :py:meth:`subsection`, :py:meth:`subsections` and possibly
        :py:meth:`typed`) should use those resources directly instead
        of reading from cached locally stored copies.

        The constructor must call the superclass'  ``__init__`` method with all
        remaining keyword arguments, ie. ``super(MySource,
        self).__init__(**kwargs)``.

        """
        
        self.identifier = kwargs.get('identifier',
                                     self.__class__.__name__.lower())
        self.writable = kwargs.get('writable', False)
        self.parent = kwargs.get('parent')
        self.source = None

    @abstractmethod
    def has(self, key):
        """This method should return true if the parameter identified by
        ``key`` is present in this configuration source. It is up to
        each configuration source to define the semantics of what
        exactly "is present" means, but a guideline is that only real
        values should count as being present. If you only have some
        sort of placeholder or typing information for ``key`` this
        should probably not return True.

        Note that it is possible that a configuration source would
        return True for ``typed(some_key)`` and at the same time
        return False for ``has(some_key)``, if the source only carries
        typing information, not real values.

        """
        pass # pragma: no cover

    @abstractmethod
    def get(self, key):
        """Should return the actual value of the parameter identified by
        ``key``. If ``has(some_key)`` returns True, ``get(some_key)``
        should always succeed. If the configuration source does not
        include intrinsic typing information (ie. everything looks
        like a string) this method should return the string as-is,
        LayeredConfig is responsible for converting it to the correct
        type."""
        pass # pragma: no cover

    @abstractmethod
    def keys(self): pass  # pragma: no cover

    @abstractmethod
    def typed(self, key):
        """Should return True if this source contains typing information for
        ``key``, ie information about which data type this parameter
        should be. 

        For sources where everything is stored as a string, this
        should generally return False (no way of distinguishing an
        actual string from a date formatted as a string).
        """
        pass # pragma: no cover

    @abstractmethod
    def subsections(self):
        """Should return a list (or other iterator) of subsection keys, ie
        names that represent subsections of this configuration
        source. Not all configuration sources need to support
        subsections. In that case, this should just return an empty
        list.

        """
        pass  # pragma: no cover

    @abstractmethod
    def subsection(self, key):
        """Should return the subsection identified by ``key``, in the form of
        a new object of the same class, but initialized
        differently. Exactly how will depend on the source, but as a
        general rule the same resource handle used as ``self.source``
        should be passed to the new object. Often, the subsection key
        will need to be provided to the new object as well, so that
        :py:meth:`get` and other methods can use it to look in the
        correct place.

        As a general rule, the constructor should be called with a
        ``parent`` parameter set to ``self``.
        """
        pass  # pragma: no cover

    @abstractmethod
    def set(self, key, value):
        """Should set the parameter identified by ``key`` to the new value
        ``value``.

        This method should be prepared for any type of value, ie ints,
        lists, dates, bools... If the backend cannot handle the given
        type, it should convert to a str itself.

        Note that this does not mean that the changes should be
        persisted in the backend data, only in the existing objects
        view of the data (only when :py:meth:`save` is called, the
        changes should be persisted).
        """
        pass  # pragma: no cover

    def save(self):
        """Persist changed data to the backend. This generally means to update
        a loaded configuration file with all changed data, or similar.

        This method will only ever be called if :py:data:`writable` is
        True, and only if :py:data:`dirty` has been set to True.

        If your source is read-only, you don't have to implement this method.
        """
        pass 

    # @abstractmethod
    # should this be called "coerce", "cast" or something similar
    def typevalue(self, key, value):
        """Given a parameter identified by ``key`` and an untyped string,
        convert that string to the type that our version of key has.

        """

        def boolconvert(value):
            # not all bools should be converted, see test_typed_commandline
            if value == "True":
                return True
            elif value == "False":
                return False
            else:
                return value
            
        def listconvert(value):
            # this function might be called with both string
            # represenations of entire lists and simple (unquoted)
            # strings. String representations come in two flavours,
            # the (legacy/deprecated) python literal (eg "['foo',
            # 'bar']") and the simple (eg "foo, bar") The
            # ast.literal_eval handles the first case, and if the
            # value can't be parsed as a python expression, the second
            # way is attempted. If both fail, it is returned verbatim
            # (not wrapped in a list, for reasons)
            try:
                return ast.literal_eval(value)
            except (SyntaxError, ValueError):
                if "," in value:
                    return [x.strip() for x in value.split(",")]
                else:
                    return value

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

        def dateconvert(value):
            try:
                return datetime.strptime(value, "%Y-%m-%d").date()
            except ValueError:
                return datetime.strptime(value, "%Y-%m-%d").date()

        # self.get(key) should never fail
        default = self.get(key)
        if type(default) == type:
            # print("Using class for %s" % key)
            t = default
        else:
            # print("Using instance for %s" % key)
            t = type(default)

        if t == bool:
            t = boolconvert
        elif t == list:
            t = listconvert
        elif t == date:
            t = dateconvert
        elif t == datetime:
            t = datetimeconvert
        # print("Converting %r to %r" % (value,t(value)))
        return t(value)


