"""Dependency injector config providers."""

import six

from dependency_injector.providers.base import Provider
from dependency_injector.utils import represent_provider
from dependency_injector.errors import Error


@six.python_2_unicode_compatible
class Config(Provider):
    """:py:class:`Config` provider provide dict values.

    :py:class:`Config` provider creates :py:class:`ChildConfig` objects for all
    undefined attribute calls. It makes possible to create deferred config
    value providers. It might be useful in cases where it is needed to
    define / pass some configuration in declarative manner, while
    configuration values will be loaded / updated in application's runtime.
    """

    __slots__ = ('value',)

    def __init__(self, value=None):
        """Initializer.

        :param value: Configuration dictionary.
        :type value: dict[str, object]
        """
        if not value:
            value = dict()
        self.value = value
        super(Config, self).__init__()

    def __getattr__(self, item):
        """Return instance of deferred config.

        :param item: Name of configuration option or section.
        :type item: str

        :rtype: :py:class:`ChildConfig`
        """
        return ChildConfig(parents=(item,), root_config=self)

    def _provide(self, paths=None):
        """Return provided instance.

        :param paths: Tuple of pieces of configuration option / section path.
        :type args: tuple[str]

        :rtype: object
        """
        value = self.value
        if paths:
            for path in paths:
                try:
                    value = value[path]
                except KeyError:
                    raise Error('Config key '
                                '"{0}" is undefined'.format('.'.join(paths)))
        return value

    def update_from(self, value):
        """Update current value from another one.

        :param value: Configuration dictionary.
        :type value: dict[str, object]

        :rtype: None
        """
        self.value.update(value)

    def __str__(self):
        """Return string representation of provider.

        :rtype: str
        """
        return represent_provider(provider=self, provides=self.value)

    __repr__ = __str__


@six.python_2_unicode_compatible
class ChildConfig(Provider):
    """:py:class:`ChildConfig` provider provides value from :py:class:`Config`.

    :py:class:`ChildConfig` provides value from the root config object
    according to the current path in the config tree.
    """

    __slots__ = ('parents', 'root_config')

    def __init__(self, parents, root_config):
        """Initializer.

        :param parents: Tuple of pieces of configuration option / section
            parent path.
        :type parents: tuple[str]

        :param root_config: Root configuration object.
        :type root_config: :py:class:`Config`
        """
        self.parents = parents
        self.root_config = root_config
        super(ChildConfig, self).__init__()

    def __getattr__(self, item):
        """Return instance of deferred config.

        :param item: Name of configuration option or section.
        :type item: str

        :rtype: :py:class:`ChildConfig`
        """
        return ChildConfig(parents=self.parents + (item,),
                           root_config=self.root_config)

    def _provide(self, *args, **kwargs):
        """Return provided instance.

        :param args: Tuple of context positional arguments.
        :type args: tuple[object]

        :param kwargs: Dictionary of context keyword arguments.
        :type kwargs: dict[str, object]

        :rtype: object
        """
        return self.root_config(self.parents)

    def __str__(self):
        """Return string representation of provider.

        :rtype: str
        """
        return represent_provider(provider=self,
                                  provides='.'.join(self.parents))

    __repr__ = __str__
