"""
Definition of the config-container type.
"""

import json

from .misc import Struct, merge_dicts, deep_getattr, deep_setattr


################################################################################

class ConfigContainerMixin:
    """
    A mixin-baseclass of the `ConfigContainer <#figura.container.ConfigContainer>`_ class.
    """

    INDENT_LEN = 2

    DEFAULT_JSON_DUMP_KWARGS = {
        'indent': INDENT_LEN,
        'ensure_ascii': False,
    }

    # ============================================================================================
    # attribute access
    # ============================================================================================

    def deep_getattr(self, attr_path, *args):
        """
        See deep_getattr_.

        .. _deep_getattr:
        """
        return deep_getattr(self, attr_path, *args)

    def deep_setattr(self, attr_path, value):
        """
        See deep_setattr_.

        .. _deep_setattr:
        """
        return deep_setattr(self, attr_path, value)

    def apply_overrides(self, overrides, **kwargs):
        """
        A convenience method, simply calling apply_overrides_to_config_.

        :note: this method modifies the config container in-place.

        .. _apply_overrides_to_config: #figura.override.apply_overrides_to_config
        """
        from figura.override import apply_overrides_to_config  # avoid circular import
        apply_overrides_to_config(self, overrides, **kwargs)

    # ============================================================================================
    # serialization
    # ============================================================================================

    def to_string(self, **kwargs):
        """
        A JSON-string representation of the config container.
        """
        return self.to_json(**kwargs)

    def __str__(self):
        return self.to_string()

    def __repr__(self):
        return '%s(%s)' % (
            self.__class__.__name__,
            super().__repr__())

    def to_json(self, **kwargs):
        """
        :param args+kwargs: extra args to pass to ``json.dumps``.
        :return: the json string representation of self.
        """
        kw = dict(self.DEFAULT_JSON_DUMP_KWARGS)
        kw.update(kwargs)
        return json.dumps(self, **kw)

    @classmethod
    def from_json(cls, json_str):
        """
        Create a ConfigContainer from a json string.
        """
        return cls.from_dict(json.loads(json_str))

    def to_dict(self):
        """
        deep-conversion of self to a dict.
        """
        return json.loads(self.to_json())

    @classmethod
    def from_dict(cls, x):
        """
        deep-conversion of a dict to a ConfigContainer
        """
        return get_config_container_from_dict(x, cls=cls)

    def to_python_string(self):
        """
        A python-string representation of the config container.
        """
        lines = self._to_python_lines(self, 0)
        return '\n'.join(lines)

    def _to_python_lines(self, x, indent_level=0):
        indent = ' ' * (self.INDENT_LEN * indent_level)
        lines = []
        for k, v in x.items():
            if isinstance(v, ConfigContainer):
                lines.append('%sclass %s:' % (indent, k))
                if len(v) > 0:
                    lines.extend(self._to_python_lines(v, indent_level + 1))
                else:
                    lines.append('%s%spass' % (indent, ' ' * self.INDENT_LEN))
            else:
                lines.append('%s%s = %r' % (indent, k, v))
        return lines


class ConfigContainer(Struct, ConfigContainerMixin):
    """
    A `Struct <#figura.misc.Struct>`_-like (recursive) container holding the configuration data.
    """

    DEFAULT_METADATA = Struct(
        is_override_set=False,
        is_opaque=False,
        is_opaque_override=False,
        doc=None,
        name=None,
        file=None,
        package=None,
    )

    def __init__(self, *args, **kwargs):
        metadata = kwargs.pop('metadata', None)
        super().__init__(*args, **kwargs)
        # not doing self._metadata=x because that results with self['_metadata']=x,
        # i.e. adding a new key to the container
        if metadata is not None:
            metadata = merge_dicts(self.DEFAULT_METADATA, metadata)
        else:
            metadata = Struct(self.DEFAULT_METADATA)
        self.__dict__['_metadata'] = metadata

    def get_metadata(self):
        return self._metadata

    def copy(self):
        return type(self)(self)

    @property
    def __doc__(self):
        return self._metadata.doc

    @property
    def __name__(self):
        return self._metadata.name

    @property
    def __file__(self):
        return self._metadata.file

    @property
    def __package__(self):
        return self._metadata.package

    def _add_module_metadata(self, module):
        metadata = self._metadata
        for attr in ['name', 'file', 'package']:
            try:
                metadata[attr] = getattr(module, '__%s__' % attr)
            except AttributeError:
                pass
        for attr in ['doc']:
            if metadata[attr]:
                continue
            try:
                metadata[attr] = getattr(module, '__%s__' % attr)
            except AttributeError:
                pass


################################################################################

def get_config_container_from_dict(x, cls=ConfigContainer):
    """
    Deep-conversion of a dict to a ConfigContainer.
    """
    if isinstance(x, dict):
        # convert recursively, and preserve dict type
        y = type(x)(
            (k, get_config_container_from_dict(v, cls=cls))
            for k, v in x.items()
        )
        return cls(y)
    else:
        # assume an atomic value
        return x


################################################################################
