# -*- coding: utf-8 -*-
import inspect
import os

try:
    from collections.abc import MutableMapping
except ImportError:
    from collections import MutableMapping


NO_DEFAULT = object()


# must not inherit from AttributeError, so not to mess with python's attribute-lookup flow
class EnvironmentVariableError(KeyError):
    pass


class TypedEnv(MutableMapping):
    """
    This object can be used in 'exploratory' mode:

        nv = TypedEnv()
        print(nv.HOME)

    It can also be used as a parser and validator of environment variables:

    class MyEnv(TypedEnv):
        username = TypedEnv.Str("USER", default='anonymous')
        path = TypedEnv.CSV("PATH", separator=":")
        tmp = TypedEnv.Str("TMP TEMP".split())  # support 'fallback' var-names

    nv = MyEnv()

    print(nv.username)

    for p in nv.path:
        print(p)

    try:
        print(p.tmp)
    except EnvironmentVariableError:
        print("TMP/TEMP is not defined")
    else:
        assert False
    """

    __slots__ = ["_env", "_defined_keys"]

    class _BaseVar(object):
        def __init__(self, name, default=NO_DEFAULT):
            self.names = tuple(name) if isinstance(name, (tuple, list)) else (name,)
            self.name = self.names[0]
            self.default = default

        def convert(self, value):
            return value

        def __get__(self, instance, owner):
            if not instance:
                return self
            try:
                return self.convert(instance._raw_get(*self.names))
            except EnvironmentVariableError:
                if self.default is NO_DEFAULT:
                    raise
                return self.default

        def __set__(self, instance, value):
            instance[self.name] = value

    class Str(_BaseVar):
        pass

    class Bool(_BaseVar):
        """
        Converts 'yes|true|1|no|false|0' to the appropriate boolean value.
        Case-insensitive. Throws a ``ValueError`` for any other value.
        """

        def convert(self, s):
            s = s.lower()
            if s not in ("yes", "no", "true", "false", "1", "0"):
                raise ValueError("Unrecognized boolean value: {!r}".format(s))
            return s in ("yes", "true", "1")

        def __set__(self, instance, value):
            instance[self.name] = "yes" if value else "no"

    class Int(_BaseVar):
        convert = staticmethod(int)

    class Float(_BaseVar):
        convert = staticmethod(float)

    class CSV(_BaseVar):
        """
        Comma-separated-strings get split using the ``separator`` (',' by default) into
        a list of objects of type ``type`` (``str`` by default).
        """

        def __init__(self, name, default=NO_DEFAULT, type=str, separator=","):
            super(TypedEnv.CSV, self).__init__(name, default=default)
            self.type = type
            self.separator = separator

        def __set__(self, instance, value):
            instance[self.name] = self.separator.join(map(str, value))

        def convert(self, value):
            return [self.type(v.strip()) for v in value.split(self.separator)]

    # =========

    def __init__(self, env=os.environ):
        self._env = env
        self._defined_keys = {k for (k, v) in inspect.getmembers(self.__class__) if isinstance(v, self._BaseVar)}

    def __iter__(self):
        return iter(dir(self))

    def __len__(self):
        return len(self._env)

    def __delitem__(self, name):
        del self._env[name]

    def __setitem__(self, name, value):
        self._env[name] = str(value)

    def _raw_get(self, *key_names):
        for key in key_names:
            value = self._env.get(key, NO_DEFAULT)
            if value is not NO_DEFAULT:
                return value
        else:
            raise EnvironmentVariableError(key_names[0])

    def __contains__(self, key):
        try:
            self._raw_get(key)
        except EnvironmentVariableError:
            return False
        else:
            return True

    def __getattr__(self, name):
        # if we're here then there was no descriptor defined
        try:
            return self._raw_get(name)
        except EnvironmentVariableError:
            raise AttributeError("{} has no attribute {!r}".format(self.__class__, name))

    def __getitem__(self, key):
        return getattr(self, key)  # delegate through the descriptors

    def get(self, key, default=None):
        try:
            return self[key]
        except EnvironmentVariableError:
            return default

    def __dir__(self):
        if self._defined_keys:
            # return only defined
            return sorted(self._defined_keys)
        # return whatever is in the environment (for convenience)
        members = set(self._env.keys())
        members.update(dir(self.__class__))
        return sorted(members)
