import inspect
from .yamlizing_error import YamlizingError
class NODEFAULT:
    def __new__(cls):
        raise NotImplementedError
    def __init__(self):
        raise NotImplementedError
class _Attribute(object):
    __slots__ = ()
    def __repr__(self):
        rep = '<{}'.format(self.__class__.__name__)
        for attr_name in self.__class__.__slots__:
            attr = getattr(self, attr_name)
            if inspect.isclass(attr):
                attr = attr.__name__
            rep += ' {}:{}'.format(attr_name, attr)
        return rep + '>'
    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return False
        for attr_name in self.__class__.__slots__:
            if getattr(self, attr_name) != getattr(other, attr_name):
                return False
        return True
    def __hash__(self):
        return sum(hash(getattr(self, attr_name))
                   for attr_name in self.__class__.__slots__)
    def has_default(self, obj):
        raise NotImplementedError
    @property
    def is_required(self):
        raise NotImplementedError
    def ensure_type(self, data, node):
        raise NotImplementedError
    def to_yaml(self, obj, dumper, node_items):
        raise NotImplementedError
    def get_value(self, obj):
        raise NotImplementedError
    def set_value(self, obj, value):
        raise NotImplementedError
class Attribute(_Attribute):
    """
    Represents an attribute of a Python class, and a key/value pair in YAML.
    Attributes
    ----------
    name : str
        name of the attribute within the Python class
    key : str
        name of the attribute within the YAML representation
    type : type or ANY
        type of the attribute within the Python class. When ``ANY``, the type
        is a pass-through and whatever YAML determines it should be will be
        applied.
    default : value or NODEFAULT
        default value if not supplied in YAML. If ``default=NODEFAULT``, then
        the attribute must be supplied.
    storage_name : str
        ``'_yamlized_' + name``, stored as a separate attribute for speed.
    """
    __slots__ = ('_name', 'storage_name', 'key', 'type', 'default', 'fvalidator', 'doc')
    def __init__(self, name=None, key=None, type=NODEFAULT, default=NODEFAULT, validator=None,
                 doc=None):
        from yamlize.yamlizable import Dynamic, Typed
        # initialize _name for .name assignment
        self._name = None
        self.storage_name = None
        self.key = key
        self.name = name  # sets storage_name and key if applicable
        self.default = default
        self.fvalidator = validator
        self.doc = doc
        if type == NODEFAULT:
            self.type = Dynamic
        else:
            self.type = Typed(type)
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self, name):
        self._name = name
        if name is not None:
            self.storage_name = '_yamlized_' + name
        if self.key is None:
            self.key = name
    def has_default(self, obj):
        return not hasattr(obj, self.storage_name)
    @property
    def is_required(self):
        return self.default is NODEFAULT
    def ensure_type(self, data, node=None):
        if isinstance(data, self.type) or data == self.default:
            return data
        try:
            new_value = self.type(data)
        except BaseException:
            raise YamlizingError('Failed to coerce value `{}` to type `{}`'
                                 .format(data, self.type), node)
        if new_value != data:
            raise YamlizingError('Coerced `{}` to `{}`, but the new value `{}`'
                                 ' is not equal to old `{}`.'
                                 .format(type(data), type(new_value), new_value, data),
                                 node)
        return new_value
    def from_yaml(self, obj, loader, node, round_trip_data):
        try:
            # it is possible that we attempted to coerce None -> int, when None was the default
            value = self.type.from_yaml(loader, node, round_trip_data)
        except YamlizingError:
            if self.is_required:
                raise
            else:
                value = loader.construct_object(node, deep=True)
                if value != self.default:
                    raise
        try:
            self.set_value(obj, value)
        except Exception as ee:
            raise YamlizingError('Failed to assign attribute `{}` to `{}`, '
                                 'got: {}'
                                 .format(self.name, value, ee), node)
    def to_yaml(self, obj, dumper, node_items, round_trip_data):
        if self.has_default(obj):
            # short circuit, don't write out default data
            return
        data = self.get_value(obj)
        try:
            val_node = self.type.to_yaml(dumper, data, round_trip_data)
        except YamlizingError:
            if data == self.default:
                val_node = dumper.represent_data(data)
            else:
                raise
        key_node = dumper.represent_data(self.key)
        node_items.append((key_node, val_node))
    def get_value(self, obj):
        return self.__get__(obj)
    def set_value(self, obj, value):
        self.__set__(obj, value)
    def __get__(self, obj, owner=None):
        if obj is None:
            return self
        result = getattr(obj, self.storage_name, self.default)
        if result is NODEFAULT:
            raise YamlizingError('Attribute `{}` was not defined on `{}`'
                                 .format(self.name, obj))
        return result
    def __set__(self, obj, value):
        value = self.ensure_type(value)
        if self.fvalidator is not None:
            if self.fvalidator(obj, value) is False:
                raise ValueError('Cannot set `{}.{}` to invalid value `{}`'
                                 .format(obj.__class__.__name__, self.name, value))
        setattr(obj, self.storage_name, value)
    def __delete__(self, obj):
        delattr(obj, self.storage_name)
    def validator(self, fvalidator):
        return type(self)(self.name, self.key, self.type, self.default, fvalidator, self.doc)
class MapItem(_Attribute):
    """
    Represents a key of a dictionary, and a key/value pair in YAML.
    This should only be used temporarily.
    """
    __slots__ = ('key', 'key_type', 'val_type')
    def __init__(self, key, key_type, val_type):
        self.key = key
        self.key_type = key_type
        self.val_type = val_type
    def has_default(self, obj):
        return False
    @property
    def is_required(self):
        return False
    def to_yaml(self, obj, dumper, node_items, round_trip_data):
        data = self.get_value(obj)
        val_node = self.val_type.to_yaml(dumper, data, round_trip_data)
        key_node = self.key_type.to_yaml(dumper, self.key, round_trip_data)
        node_items.append((key_node, val_node))
    def get_value(self, obj):
        return obj[self.key]
    def set_value(self, obj, value):
        obj[self.key] = value
class KeyedListItem(_Attribute):
    """
    Represents a key of a dictionary, and a key/value pair in YAML.
    This should only be used temporarily.
    """
    __slots__ = ('key_attr', 'item_type', 'item_key')
    def __init__(self, key_attr, item_type, item_key):
        # HACK: key_attr is a descriptor, make it a tuple to trick python
        self.key_attr = (key_attr,)
        self.item_type = item_type
        self.item_key = item_key
    def has_default(self, obj):
        return False
    @property
    def is_required(self):
        return False
    def to_yaml(self, obj, dumper, node_items, round_trip_data):
        value = self.get_value(obj)
        # HACK: key_attr is a tuple of the Attribute descriptor
        key_node, val_node = self.item_type.to_yaml_key_val(
            dumper, value, self.key_attr[0], round_trip_data)
        node_items.append((key_node, val_node))
    def get_value(self, obj):
        return obj[self.item_key]
    def set_value(self, obj, value):
        obj[self.item_key] = value