class DataValidationError(ValueError):
    pass


class DataModel:

    def __init__(self, data):
        self.validate(data)
        self._data = data

    def __repr__(self):
        return "{0}({1!r})".format(type(self).__name__, self._data)

    def __eq__(self, other):
        if not isinstance(other, type(self)):
            raise TypeError(
                "cannot compare {0!r} with {1!r}".format(
                    type(self).__name__, type(other).__name__
                )
            )
        return self._data == other._data

    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, value):
        self._data[key] = value

    def __delitem__(self, key):
        del self._data[key]

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

    @classmethod
    def validate(cls, data):
        for k, v in cls.__SCHEMA__.items():
            if k not in data:
                raise DataValidationError(f"Missing required field: {k}")
            if not isinstance(data[k], v):
                raise DataValidationError(f"Invalid type for field {k}: {type(data[k])}")

        if hasattr(cls, "__OPTIONAL__"):
            for k, v in cls.__OPTIONAL__.items():
                if k in data and not isinstance(data[k], v):
                    raise DataValidationError(f"Invalid type for field {k}: {type(data[k])}")


class DataModelCollection(DataModel):
    """A homogeneous collection of data views.

    Subclasses are expected to assign a class attribute `item_class` to specify
    the type of items it contains. This class will be used to coerce return
    values when accessed. The item class should conform to the `DataModel`
    protocol.

    You should not instantiate an instance from this class, but from one of its
    subclasses instead.
    """

    item_class = None

    def __repr__(self):
        return "{0}({1!r})".format(type(self).__name__, self._data)

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

    def __getitem__(self, key):
        return self.item_class(self._data[key])

    def __setitem__(self, key, value):
        if isinstance(value, DataModel):
            value = value._data
        self._data[key] = value

    def __delitem__(self, key):
        del self._data[key]


class DataModelSequence(DataModelCollection):
    """A sequence of data views.

    Each entry is an instance of `item_class`.
    """

    @classmethod
    def validate(cls, data):
        for d in data:
            cls.item_class.validate(d)

    def __iter__(self):
        return (self.item_class(d) for d in self._data)

    def __getitem__(self, key):
        if isinstance(key, slice):
            return type(self)(self._data[key])
        return super().__getitem__(key)

    def append(self, value):
        if isinstance(value, DataModel):
            value = value._data
        self._data.append(value)


class DataModelMapping(DataModelCollection):
    """A mapping of data views.

    The keys are primitive values, while values are instances of `item_class`.
    """

    @classmethod
    def validate(cls, data):
        for d in data.values():
            cls.item_class.validate(d)

    def __iter__(self):
        return iter(self._data)

    def keys(self):
        return self._data.keys()

    def values(self):
        return [self[k] for k in self._data]

    def items(self):
        return [(k, self[k]) for k in self._data]
