"""
Interpret packed binary data using named attributes
The namespace classes and metaclasses here allow variants
of classes to be generated by calling them with keyword arguments.
"""

from os import stat
from mmap import mmap, PROT_READ

def memmap(name):
    """
    memoryview over readonly mmap of file with specified name
    Allow empty files even if mmap rejects them
    """
    with open(name, 'rb') as file:
        try:
            return memoryview(mmap(file.fileno(), 0, access=PROT_READ))
        except ValueError:
            assert stat(file.fileno()).st_size == 0
            return memoryview(b'')

class CacheAttr(object):
    """
    Descriptor which replaces itself with the value it returns
    """
    def __init__(self, fget):
        self.fget = fget

    def __get__(self, instance, owner):
        if instance is None:
            return self
        value = self.fget(instance)
        setattr(instance, self.fget.__name__, value)
        return value

class ClassAttr(object):
    """
    A property descriptor for accessing attributes from a class
    This allows class attributes to be calculated from keyword values.
    """
    def __init__(self, fget):
        self.fget = fget

    def __get__(self, instance, owner):
        return self.fget(owner)

class MultiDict(dict):
    """
    A dict built from an iterator, with tuples as values
    Values from conflicting keys are grouped.
    """
    def __missing__(self, key):
        return ()

    def __init__(self, iterable):
        super().__init__()
        for key, value in iterable:
            self[key] = value

    def __setitem__(self, key, value):
        super().__setitem__(key, self[key] + (value,))

class AttrDict(MultiDict):
    """
    A dict built from an iterator, with an attribute shortcut
    Keys are required to all have the same type.
    Attribute lookups work only on singleton values.
    """

    def __init__(self, iterable):
        super().__init__(iterable)
        if self:
            self.type, = set(type(item) for item in self)

    def __getattr__(self, attr):
        if not self:
            raise AttributeError
        value, = self[getattr(self.type, attr)]
        return value

class LazyDict(dict):
    """
    A dict built on demand from an iterator
    Duplicate keys are disallowed
    """
    def __init__(self, iterable):
        super().__init__()
        self.iterator = iter(iterable)

    def __getitem__(self, item):
        if item in self:
            return super().__getitem__(item)
        raise KeyError(item)

    def __contains__(self, item):
        while not super().__contains__(item):
            try:
                (key, value) = next(self.iterator)
            except StopIteration:
                return False
            assert not super().__contains__(key), "Duplicate key"
            self[key] = value
        return True

    def get(self, item):
        item in self
        return super().get(item)

NULL = type(vars(dict))({})

class NameBase(object):
    """
    Base class for metaclass namespaces; subclasses inherit from specific collection types.
    Subclasses define the behavior of the attributes.
    """
    def __init__(self, __mapping__=NULL, __member__=(), __iterable__=(), **kwargs):
        self.__mapping__ = {**__mapping__, **kwargs}
        self.__member__ = __member__
        super().__init__(__iterable__)

    def __getattr__(self, name):
        try:
            return self.__mapping__[name]
        except KeyError:
            raise AttributeError

class NameSpace(NameBase, dict):
    """
    metaclass namespace
    The __mapping__ attribute tracks keywords mentioned in class declarations.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.pop('__classcell__', None)

    def __call__(self, new=False, **kwargs):
        if not new:
            kwargs = {key: value for key, value in kwargs.items()
                      if key in self.__mapping__ and kwargs[key] != self.__mapping__[key]}
        return type(self)(self.__mapping__, self.__member__, self,
                          **kwargs) if new or kwargs else self

class NameList(NameBase, list):
    """
    Sequence of classes with a mapping from name to index
    This tracks named.Struct members.
    Keyword arguments can be used to specify members.
    """
    def append(self, item, name):
        if name in self.__mapping__:
            raise KeyError
        self.__mapping__[name] = len(self)
        super().append(item)

    def __call__(self, new=False, **kwargs):
        elements = type(self)(__iterable__=(element(**kwargs) for element in self))
        for key, value in kwargs.items():
            if key in self.__mapping__ and isinstance(value, type):
                elements[self.__mapping__[key]] = value(**kwargs)
        new = new or not all(new is old for new, old in zip(elements, self))
        return type(self)(self.__mapping__, self.__member__, elements) if new else self

def keywords(bases):
    """
    Extract keywords from base classes with __namespace__ attributes
    """
    for base in bases:
        try:
            yield base.__namespace__.__mapping__
        except AttributeError:
            pass

class Meta(type):
    """
    metaclass for classes which derive variants via keyword values
    """
    __namespace__ = NameSpace
    __member__ = tuple
    @classmethod
    def __prepare__(mcs, name, bases, **kwargs):
        __mapping__ = {key: value for base in keywords(bases) for key, value in base.items()}
        try:
            return bases[0].__namespace__(new=True, **{**__mapping__, **kwargs})
        except (IndexError, AttributeError):
            return mcs.__namespace__(__mapping__, mcs.__member__(), **kwargs)

    def __new__(mcs, name, bases, namespace, **kwargs):
        return super().__new__(mcs, name, bases, namespace)

    def __init__(cls, name, bases, namespace, **kwargs):
        cls.__namespace__ = namespace
        super().__init__(name, bases, namespace)

    def __call__(cls, *args, **kwargs):
        namespace = cls.__namespace__(**kwargs)
        if namespace is not cls.__namespace__:
            cls = type(cls)(cls.__name__, (cls,), namespace, **kwargs)
        return super().__call__(*args) if args else cls

    def __getattr__(cls, name):
        return cls.__namespace__.__getattr__(name)
