
import sys as _sys

from collections import Container
from collections import Mapping
from collections import Hashable
from collections import MutableMapping

from frozendict import frozendict


def namedspace(typename, required_fields=(), optional_fields=(), mutable_fields=(), default_values=frozendict(),
               default_value_factories=frozendict()):
    """Builds a new namedspace class.

    >>> from itertools import count

    >>> Someclass = namedspace("Someclass", "id", optional_fields=("name", "description", "extra"),
    ...     mutable_fields=("description", "extra"), default_values={"description": "None available"},
    ...     default_value_factories={"id": lambda self, counter=count(start=1): counter.next(),
    ...         "name": lambda self: "Name for id={id}".format(id=self.id)})

    >>> sc1 = Someclass()

    >>> sc1.id
    1

    The value of 1 was automatically assigned by the
    default_value_factory for the 'id' field.

    >>> sc1.name
    'Name for id=1'

    This value was also generated by a default value factory, based on
    the value of another field.

    >>> sc1.description
    'None available'

    This value came from the default_values mapping.

    >>> sc1.description = "Some fancy description"
    >>> sc1.description
    'Some fancy description'

    The description field was set as a mutable field, which allows
    it to be modified.

    >>> del sc1.description

    Its value can also be deleted.

    >>> sc1.description
    'None available'

    Since its modified value was deleted, and it has a default value,
    it has reverted to its default value.

    >>> sc1.extra
    Traceback (most recent call last):
        <snip/>
    AttributeError: "Field 'extra' does not yet exist in this Someclass namedspace instance."

    The extra field is a valid field in this namedspace, but it has not
    yet been assigned a value and does not have a default. Since it one
    of the mutable fields, we can give it a value.

    >>> sc1.extra = "Extra cool value"
    >>> sc1.extra
    'Extra cool value'

    Only fields that have been declared as either required or optional
    are allowed.

    >>> sc1.some_other_field = "some other value"
    Traceback (most recent call last):
        <snip/>
    FieldNameError: "Field 'some_other_field' does not exist in Someclass namedspace."

    If we instantiate another instance, our id field will get the next counter value.
    >>> sc2 = Someclass()
    >>> sc2.id
    2

    Of course we can always supply all of the values at instantiation time.

    >>> sc3 = Someclass(id=1234, name="Foo", description="Bar", extra="Credit")

    All of the built in methods are "protected" so as to not pollute
    the public namespace of each instance.

    >>> sc3._field_names
    frozenset(['description', 'id', 'name', 'extra'])

    >>> sc3._field_values
    ('Bar', 1234, 'Foo', 'Credit')

    >>> sc3._field_items
    set([('description', 'Bar'), ('extra', 'Credit'), ('id', 1234), ('name', 'Foo')])

    """

    # Initialize the list of arguments that will get put into the
    # doc string of the generated class
    arg_list_items = []

    #
    # Validate parameters
    #
    for arg_name in ("required_fields", "optional_fields", "mutable_fields"):
        arg_value = locals()[arg_name]

        if isinstance(arg_value, basestring):
            arg_value = (arg_value,)
            exec "{arg_name} = arg_value".format(arg_name=arg_name)
        elif not isinstance(arg_value, Container):
            raise ValueError("Value for argument '{arg_name}' must be a string or container of strings.".format(
                    arg_name=arg_name))

        for field_name in arg_value:
            if not isinstance(field_name, basestring):
                raise ValueError("Items of container argument '{arg_name}' must be strings.".format(arg_name=arg_name))

        if len(arg_value) != len(frozenset(arg_value)):
            raise ValueError("Value for argument '{arg_name}' contains duplicate fields.".format(
                    arg_name=arg_name))

        arg_list_items.append("{arg_name}={arg_value!r}".format(arg_name=arg_name, arg_value=tuple(arg_value)))

        exec "{arg_name} = frozenset(arg_value)".format(arg_name=arg_name)

    all_fields = required_fields.union(optional_fields)

    for field_name in mutable_fields:
        if field_name not in all_fields:
            raise ValueError("Mutable field '{field_name}' is not a required or optional field.".format(
                    field_name=field_name))

    for arg_name in ("default_values", "default_value_factories"):
        arg_value = locals()[arg_name]
        if not isinstance(arg_value, Mapping):
            raise ValueError("Value for argument '{arg_name}' must be a mapping.".format(arg_name=arg_name))

        default_field_names = frozenset(arg_value.iterkeys())
        if not default_field_names.issubset(all_fields):
            bad_default_field_names = default_field_names - all_fields
            raise ValueError("Value for argument '{arg_name}' contains invalid field(s) '{field_names}'.".format(
                    arg_name=arg_name, field_names=", ".join(bad_default_field_names)))

        arg_list_items.append("{arg_name}={arg_value!r}".format(arg_name=arg_name, arg_value=dict(arg_value)))

        exec "{arg_name} = frozendict(arg_value)".format(arg_name=arg_name)

    for field_name, factory in default_value_factories.iteritems():
        if not callable(factory):
            raise ValueError("Default value factory for '{field_name}' is not callable.".format(field_name=field_name))

    # Fill-in the class template
    class_definition = _class_template.format(
        typename=typename,
        arg_list=", ".join(arg_list_items),
        )

    # Execute the template string in a temporary namespace and support
    # tracing utilities by setting a value for frame.f_globals['__name__']
    namespace = dict(
        __name__='namedspace_{typename}'.format(typename=typename),
        all_fields=all_fields,
        required_fields=required_fields,
        mutable_fields=mutable_fields,
        default_values=default_values,
        default_value_factories=default_value_factories,
        Hashable=Hashable,
        MutableMapping=MutableMapping,
        )

    #
    # Code from here down copied verbatim from namedtuple
    #
    try:
        exec class_definition in namespace
    except SyntaxError as e:
        raise SyntaxError(e.message + ':\n' + class_definition)
    result = namespace[typename]

    # For pickling to work, the __module__ variable needs to be set to the frame
    # where the named tuple is created.  Bypass this step in enviroments where
    # sys._getframe is not defined (Jython for example) or sys._getframe is not
    # defined for arguments greater than 0 (IronPython).
    try:
        result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')
    except (AttributeError, ValueError):
        pass

    return result

_class_template = """\
class {typename}(object):
    "{typename}({arg_list})"

    class FieldNameError(AttributeError, KeyError): pass
    class ReadOnlyNamedspaceError(TypeError): pass
    class ReadOnlyFieldError(TypeError): pass
    class MutableNamedspaceError(TypeError): pass

    _all_fields = all_fields
    _required_fields = required_fields
    _mutable_fields = mutable_fields
    _default_values = default_values
    _default_value_factories = default_value_factories

    def __init__(self, **kwargs):
        self._field_value_storage = dict()

        for field_name, field_value in kwargs.iteritems():
            if field_name in self._all_fields:
                self._field_value_storage[field_name] = field_value
            else:
                raise ValueError("field '{{field_name}} does not exist in the {typename} namedspace.".format(
                        field_name=field_name))

        for field_name in self._all_fields:
            try:
                field_value = self._get_value(field_name)
            except self.FieldNameError:
                field_value = None

            if field_value in (None, "") and field_name in self._required_fields:
                raise ValueError("A value for field '{{field_name}}' is required.".format(field_name=field_name))
            elif not field_name in self._field_value_storage:
                self._field_value_storage[field_name] = field_value

    def __repr__(self):
        'Return a nicely formatted representation string'
        return '{typename}({{items!r}})'.format(items=self.as_dict())

    #
    # Generic value access methods
    #
    def _get_value(self, field_name):
        if not field_name in self._all_fields:
            raise self.FieldNameError("Field '{{field_name}}' does not exist in the {typename} namedspace.".format(
                    field_name=field_name))

        field_value = self._field_value_storage.get(field_name)
        if field_value is None:
            field_value = self._default_values.get(field_name)
            if field_value is None:
                factory = self._default_value_factories.get(field_name)
                if factory is None:
                    raise self.FieldNameError("Field '{{field_name}}' does not yet exist in this {typename}"
                            " namedspace instance.".format(field_name=field_name))
                else:
                    field_value = factory(self)

        return field_value

    def _validate_field_mutability(self, field_name):
        if not field_name in self._all_fields:
            raise self.FieldNameError("Field '{{field_name}}' does not exist in {typename} namedspace.".format(
                    field_name=field_name))
        if not field_name in self._mutable_fields:
            if self._mutable_fields:
                raise self.ReadOnlyFieldError("Field '{{field_name}}' of {typename} namedspace is read-only.".format(
                        field_name=field_name))
            else:
                raise self.ReadOnlyNamedspaceError("{typename} namedspace is read-only.")

    def _set_value(self, field_name, field_value):
        self._validate_field_mutability(field_name)
        self._field_value_storage[field_name] = field_value

    def _del_value(self, field_name):
        self._validate_field_mutability(field_name)
        del self._field_value_storage[field_name]

    #
    # Namedspace API
    #
    @property
    def _field_names(self):
        return self._all_fields

    @property
    def _field_names_iter(self):
        return iter(self._all_fields)

    @property
    def _field_values_iter(self):
        for field_name in self._all_fields:
            yield getattr(self, field_name, None)

    @property
    def _field_values(self):
        return tuple(self._field_values_iter)

    @property
    def _field_items_iter(self):
        for field_name in self._all_fields:
            yield (field_name, getattr(self, field_name, None))

    @property
    def _field_items(self):
        set_type = set if self._mutable_fields else frozenset
        return set_type(self._field_items_iter)

    @property
    def _as_dict(self):
        'Return a the namedspace values as a new dictionary.'
        return dict(self._field_items_iter)

    #
    # Attribute API
    #
    def __getattr__(self, attr_name):
        try:
            return self._get_value(attr_name)
        except self.FieldNameError as e:
            raise AttributeError(str(e))

    def __setattr__(self, attr_name, attr_value):
        if attr_name == "_field_value_storage":
            return super({typename}, self).__setattr__(attr_name, attr_value)
        else:
            return self._set_value(attr_name, attr_value)

    def __delattr__(self, attr_name):
        return self._del_value(attr_name)

    #
    # Hashable API
    #
    def __hash__(self):
        if self._mutable_fields:
            raise self.MutableNamedspaceError("Mutable {typename} namedspace instance is not hashable.")
        else:
            return hash(self._field_values)

    #
    # MutableMapping API
    #
    def __contains__(self, name):
        return name in self._field_names

    def __iter__(self):
        return self._field_names_iter

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

    def __getitem__(self, item_name):
        try:
            return self._get_value(item_name)
        except self.FieldNameError as e:
            raise KeyError(str(e))

    def __setitem__(self, item_name, item_value):
        return self._set_value(item_name, item_value)

    def __delitem__(self, item_name):
        return self._del_value(item_name)


Hashable.register({typename})
MutableMapping.register({typename})
"""
