import logging
import sys
try:
    from typing import List, Dict, Any, Optional
except ImportError:
    pass

logger = logging.getLogger(__name__)

# Tricking mypy to think `_omit`'s type is NoneType
# To make us not add things like `Union[Optional[str], OmitType]`
NoneType = type(None)
_omit = None  # type: NoneType
_omit = object()  # type: ignore


# Don't add any additionalProperties to objects. Useful for completeness testing
STRICT = False

def _str_to_class(cls, typ_str):
    if isinstance(typ_str, str):
        return getattr(sys.modules[cls.__module__], typ_str)
    return typ_str


def _property_from_json(cls, data, breadcrumb, name, py_name, typ_str, required, nullable):
    if not required and name not in data:
        return _omit
    try:
        obj = data[name]
    except KeyError as e:
        raise ValueError('KeyError in {}: {}'.format(breadcrumb, e))
    if nullable and obj is None:
        return obj
    typ = _str_to_class(cls, typ_str)
    if issubclass(typ, CrdObject) or issubclass(typ, CrdObjectList):
        return typ.from_json(obj, breadcrumb + '.' + name)
    return obj


class CrdObject(object):
    _properties = []  # type: List

    def __init__(self, **kwargs):
        for prop in self._properties:
            setattr(self, prop[1], kwargs.pop(prop[1]))
        if kwargs:
            raise TypeError(
                '{} got unexpected arguments {}'.format(self.__class__.__name__, kwargs.keys()))
        self._additionalProperties = {}  # type: Dict[str, Any]

    def _property_impl(self, name):
        obj = getattr(self, '_' + name)
        if obj is _omit:
            raise AttributeError(name + ' not found')
        return obj

    def _property_to_json(self, name, py_name, typ_str, required, nullable):
        obj = getattr(self, '_' + py_name)
        typ = _str_to_class(self.__class__, typ_str)
        if issubclass(typ, CrdObject) or issubclass(typ, CrdObjectList):
            if nullable and obj is None:
                return obj
            if not required and obj is _omit:
                return obj
            return obj.to_json()
        else:
            return obj

    def to_json(self):
        # type: () -> Dict[str, Any]
        res = {p[0]: self._property_to_json(*p) for p in self._properties}
        res.update(self._additionalProperties)
        return {k: v for k, v in res.items() if v is not _omit}

    @classmethod
    def from_json(cls, data, breadcrumb=''):
        try:
            sanitized = {
                p[1]: _property_from_json(cls, data, breadcrumb, *p) for p in cls._properties
            }
            extra = {k:v for k,v in data.items() if k not in sanitized}
            ret = cls(**sanitized)
            ret._additionalProperties = {} if STRICT else extra
            return ret
        except (TypeError, AttributeError, KeyError):
            logger.exception(breadcrumb)
            raise


class CrdClass(CrdObject):
    @classmethod
    def from_json(cls, data, breadcrumb=''):
        kind = data['kind']
        if kind != cls.__name__:
            raise ValueError("kind mismatch: {} != {}".format(kind, cls.__name__))
        return super(CrdClass, cls).from_json(data, breadcrumb)

    def to_json(self):
        ret = super(CrdClass, self).to_json()
        ret['kind'] = self.__class__.__name__
        return ret


class CrdObjectList(list):
    # Py3: Replace `Any` with `TypeVar('T_CrdObject', bound='CrdObject')`
    _items_type = None  # type: Optional[Any]

    def to_json(self):
        # type: () -> List
        if self._items_type is None:
            return self
        if issubclass(self._items_type, CrdObject) or issubclass(self._items_type, CrdObjectList):
            return [e.to_json() for e in self]
        return list(self)


    @classmethod
    def from_json(cls, data, breadcrumb=''):
        if cls._items_type is None:
            return cls(data)
        if issubclass(cls._items_type, CrdObject) or issubclass(cls._items_type, CrdObjectList):
            return cls(cls._items_type.from_json(e, breadcrumb + '[{}]'.format(i)) for i, e in enumerate(data))
        return cls(data)

    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, repr(list(self)))

