__all__ = ('object_property', 'bool_property',
        'array_property', 'set_property', 'dict_property')

from objc import ivar, selector, _C_ID, _C_NSBOOL, _C_BOOL, NULL, _C_NSUInteger
from objc import lookUpClass
import collections
from copy import copy as copy_func
import sys

NSSet = lookUpClass('NSSet')
NSObject = lookUpClass('NSObject')

if sys.version_info[0] == 2:
    range = xrange

    def _str(value):
        return value

else: # pragma: no cover (py3k)
    long = int

    def _str(value):
        return value.decode('ascii')



def attrsetter(prop, name, copy):
    if copy:
        def func(self, value):
            if isinstance(value, NSObject):
                setattr(self, name, value.copy())
            else:
                setattr(self, name, copy_func(value))
    else:
        def func(self, value):
            setattr(self, name, value)
    return func

def attrgetter(name):
    def func(self):
        return getattr(self, name)
    return func

def _return_value(value):
    def func(self):
        return value

    return func

def _dynamic_getter(name):
    def getter(object):
        m = getattr(object.pyobjc_instanceMethods, name)
        return m()
    getter.__name__ = name
    return getter

def _dynamic_setter(name):
    def setter(object, value):
        m = getattr(object.pyobjc_instanceMethods, name)
        return m(value)
    setter.__name__ = name
    return setter

class object_property (object):
    def __init__(self, name=None,
            read_only=False, copy=False, dynamic=False,
            ivar=None, typestr=_C_ID, depends_on=None):
        self.__created = False
        self.__inherit = False
        self._name = name
        self._typestr = typestr
        self._ro = read_only
        self._copy = copy
        self._dynamic = dynamic
        self._ivar = ivar
        self._getter = None
        self._setter = None
        self._validate = None
        if depends_on is None:
            self._depends_on = None
        else:
            self._depends_on = set(depends_on)

        self.__getprop = None
        self.__setprop = None
        self.__parent = None

    def _clone(self):
        if self._depends_on is None:
            depends = None
        else:
            depends = self._depends_on.copy()

        v = type(self)(name=self._name,
                read_only=self._ro, copy=self._copy, dynamic=self._dynamic,
                ivar=self._ivar, typestr=self._typestr, depends_on=depends)
        v.__inherit = True

        v.__getprop = self.__getprop
        v.__setprop = self.__setprop
        v.__parent = self

        return v

    def __pyobjc_class_setup__(self, name, class_dict, instance_methods, class_methods):
        self.__created = True
        if self._name is None:
            self._name = name

        if self._ivar is not NULL:
            if self._ivar is None:
                ivname = '_' + self._name
            else:
                ivname = self._ivar

            if self.__parent is None:
                ivar_ref = ivar(name=ivname, type=self._typestr)
                class_dict[ivname] = ivar_ref

        if self._ro:
            self._setter = None

        else:
            setterName = b'set' + name[0].upper().encode('latin1') + name[1:].encode('latin1') + b':'
            signature = b'v@:' + self._typestr
            if self._setter is None:
                if self.__inherit:
                    pass

                elif self._dynamic:
                    dynSetterName = 'set' + name[0].upper() + name[1:] + '_'
                    self.__setprop = _dynamic_setter(dynSetterName)
                    instance_methods.add(setterName)

                else:

                    if self._ivar is NULL:
                        raise ValueError(
                            "Cannot create default setter for property "
                            "without ivar")

                    setprop = selector(
                        attrsetter(self._name, ivname, self._copy),
                        selector=setterName,
                        signature=signature
                    )
                    setprop.isHidden = True
                    instance_methods.add(setprop)

                    # Use dynamic setter to avoid problems when subclassing
                    self.__setprop = _dynamic_setter(_str(setterName))
            else:
                setprop = selector(
                    self._setter,
                    selector=setterName,
                    signature=signature
                )
                setprop.isHidden = True
                instance_methods.add(setprop)

                # Use dynamic setter to avoid problems when subclassing
                self.__setprop = _dynamic_setter(_str(setterName))

        if self._typestr in (_C_NSBOOL, _C_BOOL):
            getterName = b'is' + name[0].upper().encode('latin1') + name[1:].encode('latin1')
        else:
            getterName = self._name.encode('latin1')

        if self._getter is None:
            if self.__inherit:
                pass

            elif self._dynamic:
                if self._typestr in (_C_NSBOOL, _C_BOOL):
                    dynGetterName = 'is' + name[0].upper() + name[1:]
                else:
                    dynGetterName = self._name

                self.__getprop = _dynamic_getter(dynGetterName)
                instance_methods.add(getterName)

            else:
                if self._ivar is NULL:
                    raise ValueError(
                        "Cannot create default getter for property without ivar")

                self.__getprop = selector(
                        attrgetter(ivname),
                        selector=getterName,
                        signature=self._typestr + b'@:')
                self.__getprop.isHidden=True
                instance_methods.add(self.__getprop)

        else:
            self.__getprop = getprop = selector(
                    self._getter,
                    selector=getterName,
                    signature=self._typestr + b'@:')
            getprop.isHidden=True
            instance_methods.add(getprop)
            #self.__getprop = _dynamic_getter(getterName)

        if self._validate is not None:
            selName = b'validate' + self._name[0].upper().encode('latin') + self._name[1:].encode('latin') + b':error:'
            signature = _C_NSBOOL + b'@:N^@o^@'
            validate = selector(
                    self._validate,
                    selector=selName,
                    signature=signature)
            class_dict[validate.selector] = validate
            instance_methods.add(validate)

        if self._depends_on:
            if self.__parent is not None:
                if self.__parent._depends_on:
                    self._depends_on.update(self.__parent._depends_on.copy())

            self._depends_on = self._depends_on

            affecting = selector(
                    _return_value(NSSet.setWithArray_(list(self._depends_on))),
                    selector = b'keyPathsForValuesAffecting' + self._name[0].upper().encode('latin1') + self._name[1:].encode('latin1'),
                    signature = b'@@:',
                    isClassMethod=True)
            class_dict[affecting.selector] = affecting
            class_methods.add(affecting)

    def __get__(self, object, owner):
        if object is None:
            return self
        return self.__getprop(object)

    def __set__(self, object, value):
        if self.__setprop is None:
            raise ValueError("setting read-only property " + self._name)

        return self.__setprop(object, value)

    def __delete__(self, object):
        raise TypeError("cannot delete property " + self._name)

    def depends_on(self, keypath):
        if self._depends_on is None:
            self._depends_on = set()
        self._depends_on.add(keypath)

    def getter(self, function):
        if self.__created:
            v = self._clone()
            v._getter = function
            return v

        self._getter = function
        return self

    def setter(self, function):

        if self.__created:
            v = self._clone()
            v._ro = False
            v._setter = function
            return v

        if self._ro:
            raise ValueError("Defining settter for read-only property")

        self._setter = function
        return self

    def validate(self, function):
        if self._ro:
            raise ValueError("Defining validator for read-only property")

        if self.__created:
            v = self._clone()
            v._validate = function
            return v

        self._validate = function
        return self

class bool_property (object_property):
    def __init__(self, name=None,
            read_only=False, copy=False, dynamic=False,
            ivar=None, typestr=_C_NSBOOL):
        super(bool_property, self).__init__(
                name, read_only, copy, dynamic, ivar, typestr)




NSIndexSet = lookUpClass('NSIndexSet')
NSMutableIndexSet = lookUpClass('NSMutableIndexSet')
NSKeyValueChangeSetting = 1
NSKeyValueChangeInsertion = 2
NSKeyValueChangeRemoval = 3
NSKeyValueChangeReplacement = 4


# Helper function for (not) pickling array_proxy instances
# NOTE: Don't remove this function, it can be referenced from
# pickle files.
def _id(value):
    return value

# FIXME: split into two: array_proxy and mutable_array_proxy
class array_proxy (collections.MutableSequence):
    # XXX: The implemenation should be complete, but is currently not
    # tested.
    __slots__ = ('_name', '_parent', '__wrapped', '_ro')

    def __init__(self, name, parent, wrapped, read_only):
        self._name = name
        self._parent = parent
        self._ro = read_only
        self.__wrapped = wrapped

    @property
    def _wrapped(self):
        return self.__wrapped.__getvalue__(self._parent)

    def __indexSetForIndex(self, index):
        if isinstance(index, slice):
            result = NSMutableIndexSet.alloc().init()
            start, stop, step = index.indices(len(self._wrapped))
            for i in range(start, stop, step):
                result.addIndex_(i)

            return result

        elif isinstance(index, (int, long)):
            if index < 0:
                v = len(self) + index
                if v < 0:
                    raise IndexError(index)
                return NSIndexSet.alloc().initWithIndex_(v)

            else:
                return NSIndexSet.alloc().initWithIndex_(index)

        else:
            raise TypeError(index)



    def __repr__(self):
        return '<array proxy for property ' + self._name + ' ' + repr(self._wrapped) + '>'

    def __reduce__(self):
        # Ensure that the proxy itself doesn't get stored
        # in pickles.
        return _id, (self._wrapped,)

    def __getattr__(self, name):
        # Default: just defer to wrapped list
        return getattr(self._wrapped, name)

    def __len__(self):
        return self._wrapped.__len__()

    def __getitem__(self, index):
        return self._wrapped[index]

    def __setitem__(self, index, value):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        indexes = self.__indexSetForIndex(index)
        self._parent.willChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeSetting,
                indexes, self._name)
        try:
            self._wrapped[index] = value
        finally:
            self._parent.didChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeReplacement,
                indexes, self._name)

    def __delitem__(self, index):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        indexes = self.__indexSetForIndex(index)
        self._parent.willChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeRemoval,
                indexes, self._name)
        try:
            del self._wrapped[index]
        finally:
            self._parent.didChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeRemoval,
                indexes, self._name)

    def append(self, value):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        index = len(self)
        indexes = NSIndexSet.alloc().initWithIndex_(index)
        self._parent.willChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeInsertion,
                indexes, self._name)
        try:
            self._wrapped.append(value)
        finally:
            self._parent.didChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeInsertion,
                indexes, self._name)

    def insert(self, index, value):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        if isinstance(index, slice):
            raise TypeError("insert argument 1 is a slice")

        indexes = self.__indexSetForIndex(index)
        self._parent.willChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeInsertion,
                indexes, self._name)
        try:
            self._wrapped.insert(index, value)
        finally:
            self._parent.didChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeInsertion,
                indexes, self._name)

    def pop(self, index=-1):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        if isinstance(index, slice):
            raise TypeError("insert argument 1 is a slice")

        indexes = self.__indexSetForIndex(index)
        self._parent.willChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeRemoval,
                indexes, self._name)
        try:
            return self._wrapped.pop(index)
        finally:
            self._parent.didChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeRemoval,
                indexes, self._name)

    def extend(self, values):
        # XXX: This is suboptimal but correct
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        values = list(values)

        indexes = NSIndexSet.alloc().initWithIndexesInRange_((len(self), len(values)))

        self._parent.willChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeInsertion,
                indexes, self._name)
        try:
            self._wrapped.extend(values)

        finally:
            self._parent.didChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeInsertion,
                indexes, self._name)

    def __iadd__(self, values):
        self.extend(values)
        return self

    def __add__(self, values):
        return self._wrapped + values

    def __mul__(self, count):
        return self._wrapped * count

    def __imul__(self, count):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))
        if not isinstance(count, (int, long)):
            raise TypeError(count)

        indexes = NSIndexSet.alloc().initWithIndexesInRange_((len(self), len(self)*(count-1)))
        self._parent.willChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeInsertion,
                indexes, self._name)
        try:
            self._wrapped.__imul__(count)
        finally:
            self._parent.didChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeInsertion,
                indexes, self._name)

        return self


    def __eq__(self, other):
        if isinstance(other, array_proxy):
            return self._wrapped == other._wrapped

        else:
            return self._wrapped == other

    def __ne__(self, other):
        if isinstance(other, array_proxy):
            return self._wrapped != other._wrapped

        else:
            return self._wrapped != other

    def __lt__(self, other):
        if isinstance(other, array_proxy):
            return self._wrapped < other._wrapped

        else:
            return self._wrapped < other

    def __le__(self, other):
        if isinstance(other, array_proxy):
            return self._wrapped <= other._wrapped

        else:
            return self._wrapped <= other

    def __gt__(self, other):
        if isinstance(other, array_proxy):
            return self._wrapped > other._wrapped

        else:
            return self._wrapped > other

    def __ge__(self, other):
        if isinstance(other, array_proxy):
            return self._wrapped >= other._wrapped

        else:
            return self._wrapped >= other


    if sys.version_info[0] == 2:
        def __cmp__(self, other):
            if isinstance(other, array_proxy):
                return cmp(self._wrapped, other._wrapped)

            else:
                return cmp(self._wrapped, other)

    if sys.version_info[0] == 2:
        def sort(self, cmp=None, key=None, reverse=False):
            if self._ro:
                raise ValueError("Property '%s' is read-only"%(self._name,))

            indexes = NSIndexSet.alloc().initWithIndexesInRange_(
                    (0, len(self._wrapped)))
            self._parent.willChange_valuesAtIndexes_forKey_(
                    NSKeyValueChangeReplacement,
                    indexes, self._name)
            try:
                self._wrapped.sort(cmp=cmp, key=key, reverse=reverse)
            finally:
                self._parent.didChange_valuesAtIndexes_forKey_(
                    NSKeyValueChangeReplacement,
                    indexes, self._name)

    else: # pragma: no cover (py3k)
        def sort(self, key=None, reverse=False):
            if self._ro:
                raise ValueError("Property '%s' is read-only"%(self._name,))

            indexes = NSIndexSet.alloc().initWithIndexesInRange_(
                    (0, len(self._wrapped)))
            self._parent.willChange_valuesAtIndexes_forKey_(
                    NSKeyValueChangeReplacement,
                    indexes, self._name)
            try:
                self._wrapped.sort(key=key, reverse=reverse)
            finally:
                self._parent.didChange_valuesAtIndexes_forKey_(
                    NSKeyValueChangeReplacement,
                    indexes, self._name)

    def reverse(self):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        indexes = NSIndexSet.alloc().initWithIndexesInRange_(
                (0, len(self._wrapped)))
        self._parent.willChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeReplacement,
                indexes, self._name)
        try:
            self._wrapped.reverse()
        finally:
            self._parent.didChange_valuesAtIndexes_forKey_(
                NSKeyValueChangeReplacement,
                indexes, self._name)

def makeArrayAccessors(name):

    def countOf(self):
        return len(getattr(self, name))

    def objectIn(self, idx):
        return getattr(self, name)[idx]

    def insert(self, value, idx):
        getattr(self, name).insert(idx, value)

    def replace(self, idx, value):
        getattr(self, name)[idx] = value

    def remove(self, idx):
        del getattr(self, name)[idx]

    return countOf, objectIn, insert, remove, replace

class array_property (object_property):
    def __init__(self, name=None,
            read_only=False, copy=True, dynamic=False,
            ivar=None, depends_on=None):
        super(array_property, self).__init__(name,
                read_only=read_only,
                copy=copy, dynamic=dynamic,
                ivar=ivar, depends_on=depends_on)

    def __pyobjc_class_setup__(self, name, class_dict, instance_methods, class_methods):
        super(array_property, self).__pyobjc_class_setup__(name, class_dict, instance_methods, class_methods)


        # Insert (Mutable) Indexed Accessors
        # FIXME: should only do the mutable bits when we're actually a mutable property

        name = self._name
        Name = name[0].upper() + name[1:]

        countOf, objectIn, insert, remove, replace = makeArrayAccessors(self._name)

        countOf = selector(countOf,
                selector  = ('countOf%s'%(Name,)).encode('latin1'),
                signature = _C_NSUInteger + b'@:',
        )
        countOf.isHidden = True
        instance_methods.add(countOf)

        objectIn = selector(objectIn,
                selector  = ('objectIn%sAtIndex:'%(Name,)).encode('latin1'),
                signature = b'@@:' + _C_NSUInteger,
        )
        objectIn.isHidden = True
        instance_methods.add(objectIn)

        insert = selector(insert,
                selector  = ('insertObject:in%sAtIndex:'%(Name,)).encode('latin1'),
                signature = b'v@:@' + _C_NSUInteger,
        )
        insert.isHidden = True
        instance_methods.add(insert)

        remove = selector(remove,
                selector  = ('removeObjectFrom%sAtIndex:'%(Name,)).encode('latin1'),
                signature = b'v@:' + _C_NSUInteger,
        )
        remove.isHidden = True
        instance_methods.add(remove)

        replace = selector(replace,
                selector  = ('replaceObjectIn%sAtIndex:withObject:'%(Name,)).encode('latin1'),
                signature = b'v@:' + _C_NSUInteger + b'@',
        )
        replace.isHidden = True
        instance_methods.add(replace)


    def __set__(self, object, value):
        if isinstance(value, array_proxy):
            if value._name == self._name and value._parent is object:
                # attr.prop = attr.prop
                return

        if isinstance(value, array_proxy):
            value = list(value)

        super(array_property, self).__set__(object, value)

    def __get__(self, object, owner):
        v = object_property.__get__(self, object, owner)
        if v is None:
            v = list()
            object_property.__set__(self, object, v)
        return array_proxy(self._name, object, self, self._ro)

    def __getvalue__(self, object):
        v = object_property.__get__(self, object, None)
        if v is None: 
            v = list()
            object_property.__set__(self, object, v)
        return v


NSKeyValueUnionSetMutation = 1
NSKeyValueMinusSetMutation = 2
NSKeyValueIntersectSetMutation = 3
NSKeyValueSetSetMutation = 4


class set_proxy (collections.MutableSet):
    __slots__ = ('_name', '__wrapped', '_parent', '_ro')

    def __init__(self, name, parent, wrapped, read_only):
        self._name = name
        self._parent = parent
        self._ro = read_only
        self.__wrapped = wrapped

    def __repr__(self):
        return '<set proxy for property ' + self._name + ' ' + repr(self._wrapped) + '>'

    @property
    def _wrapped(self):
        return self.__wrapped.__getvalue__(self._parent)

    def __reduce__(self):
        # Ensure that the proxy itself doesn't get stored
        # in pickles.
        return _id, (self._wrapped,)

    def __getattr__(self, attr):
        return getattr(self._wrapped, attr)

    def __contains__(self, value):
        return self._wrapped.__contains__(value)

    def __iter__(self):
        return self._wrapped.__iter__()

    def __len__(self):
        return self._wrapped.__len__()


    def __eq__(self, other):
        if isinstance(other, set_proxy):
            return self._wrapped == other._wrapped

        else:
            return self._wrapped == other

    def __ne__(self, other):
        if isinstance(other, set_proxy):
            return self._wrapped != other._wrapped

        else:
            return self._wrapped != other

    def __lt__(self, other):
        if isinstance(other, set_proxy):
            return self._wrapped < other._wrapped

        else:
            return self._wrapped < other

    def __le__(self, other):
        if isinstance(other, set_proxy):
            return self._wrapped <= other._wrapped

        else:
            return self._wrapped <= other

    def __gt__(self, other):
        if isinstance(other, set_proxy):
            return self._wrapped > other._wrapped

        else:
            return self._wrapped > other

    def __ge__(self, other):
        if isinstance(other, set_proxy):
            return self._wrapped >= other._wrapped

        else:
            return self._wrapped >= other


    if sys.version_info[0] == 2:
        def __cmp__(self, other):
            raise TypeError('cannot compare sets using cmp()')

    def add(self, item):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueUnionSetMutation,
                set([item]),
        )
        try:
            self._wrapped.add(item)
        finally:
            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueUnionSetMutation,
                set([item]),
            )

    def clear(self):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        object = set(self._wrapped)
        self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueMinusSetMutation,
                object
        )
        try:
            self._wrapped.clear()
        finally:
            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueMinusSetMutation,
                object
            )

    def difference_update(self, *others):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        s = set()
        s.update(*others)
        self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueMinusSetMutation,
                s
        )
        try:
            self._wrapped.difference_update(s)

        finally:
            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueMinusSetMutation,
                s
            )


    def discard(self, item):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueMinusSetMutation,
                {item}
        )
        try:
            self._wrapped.discard(item)

        finally:
            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueMinusSetMutation,
                {item}
            )

    def intersection_update(self, other):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        other = set(other)

        self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueIntersectSetMutation,
                other
        )
        try:
            self._wrapped.intersection_update(other)

        finally:
            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueIntersectSetMutation,
                other
            )

    def pop(self):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        try:
            v = next(iter(self))
        except StopIteration:
            raise KeyError("Empty set")

        self.remove(v)
        return v

    def remove(self, item):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueMinusSetMutation,
                set([item])
        )
        try:
            self._wrapped.remove(item)

        finally:
            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueMinusSetMutation,
                set([item])
            )

    def symmetric_difference_update(self, other):
        # NOTE: This method does not call the corresponding method
        # of the wrapped set to ensure that we generate the right
        # notifications.
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        other = set(other)

        to_add = set()
        to_remove = set()
        for o in other:
            if o in self:
                to_remove.add(o)
            else:
                to_add.add(o)

        self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueMinusSetMutation,
                to_remove
        )
        try:
            self._wrapped.difference_update(to_remove)

        finally:
            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                    self._name,
                    NSKeyValueMinusSetMutation,
                    to_remove
            )

        self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueUnionSetMutation,
                to_add
        )
        try:
            self._wrapped.update(to_add)

        finally:
            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                    self._name,
                    NSKeyValueUnionSetMutation,
                    to_add
            )

    def update(self, *others):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        s = set()
        s.update(*others)

        self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueUnionSetMutation,
                s
        )
        try:
            self._wrapped.update(s)

        finally:
            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                self._name,
                NSKeyValueUnionSetMutation,
                s
            )

    def __or__(self, other):
        return self._wrapped | other

    def __and__(self, other):
        return self._wrapped & other

    def __xor__(self, other):
        return self._wrapped ^ other

    def __sub__(self, other):
        return self._wrapped - other

    def __ior__(self, other):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        self.update(other)
        return self

    def __isub__(self, other):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        self.difference_update(other)
        return self

    def __ixor__(self, other):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        self.symmetric_difference_update(other)
        return self

    def __iand__(self, other):
        if self._ro:
            raise ValueError("Property '%s' is read-only"%(self._name,))

        self.intersection_update(other)
        return self


def makeSetAccessors(name):
    def countOf(self):
        return len(getattr(self, name))

    def enumeratorOf(self):
        return iter(getattr(self, name))

    def memberOf(self, value):
        collection =  getattr(self, name)
        if value not in collection:
            return None

        for item in collection:
            if item == value:
                return item

    def add(self, value):
        getattr(self, name).add(value)

    def remove(self, value):
        getattr(self, name).discard(value)

    return countOf, enumeratorOf, memberOf, add, remove


class set_property (object_property):
    def __init__(self, name=None,
            read_only=False, copy=True, dynamic=False,
            ivar=None, depends_on=None):
        super(set_property, self).__init__(name,
                read_only=read_only,
                copy=copy, dynamic=dynamic,
                ivar=ivar, depends_on=depends_on)

    def __get__(self, object, owner):
        v = object_property.__get__(self, object, owner)
        if v is None:
            v = set()
            object_property.__set__(self, object, v)
        return set_proxy(self._name, object, self, self._ro)

    def __set__(self, object, value):
        if isinstance(value, set_proxy):
            if value._name == self._name and value._parent is object:
                # attr.prop = attr.prop
                return

        if isinstance(value, set_proxy):
            value = list(value)

        super(set_property, self).__set__(object, value)

    def __getvalue__(self, object):
        v = object_property.__get__(self, object, None)
        if v is None:
            v = set()
            object_property.__set__(self, object, v)
        return v

    def __pyobjc_class_setup__(self, name, class_dict, instance_methods, class_methods):
        super(set_property, self).__pyobjc_class_setup__(name, class_dict, instance_methods, class_methods)

        # (Mutable) Unordered Accessors
        # FIXME: should only do the mutable bits when we're actually a mutable property

        name = self._name
        Name = name[0].upper() + name[1:]

        countOf, enumeratorOf, memberOf, add, remove = makeSetAccessors(self._name)

        countOf = selector(countOf,
                selector  = ('countOf%s'%(Name,)).encode('latin1'),
                signature = _C_NSUInteger + b'@:',
        )
        countOf.isHidden = True
        instance_methods.add(countOf)

        enumeratorOf = selector(enumeratorOf,
                selector  = ('enumeratorOf%s'%(Name,)).encode('latin1'),
                signature = b'@@:',
        )
        enumeratorOf.isHidden = True
        instance_methods.add(enumeratorOf)

        memberOf = selector(memberOf,
                selector  = ('memberOf%s:'%(Name,)).encode('latin'),
                signature = b'@@:@',
        )
        memberOf.isHidden = True
        instance_methods.add(memberOf)

        add1 = selector(add,
                selector  = ('add%s:'%(Name,)).encode('latin'),
                signature = b'v@:@',
        )
        add1.isHidden = True
        instance_methods.add(add1)

        add2 = selector(add,
                selector  = ('add%sObject:'%(Name,)).encode('latin1'),
                signature = b'v@:@',
        )
        add2.isHidden = True
        instance_methods.add(add2)

        remove1 = selector(remove,
                selector  = ('remove%s:'%(Name,)).encode('latin1'),
                signature = b'v@:@',
        )
        remove1.isHidden = True
        instance_methods.add(remove1)

        remove2 = selector(remove,
                selector  = ('remove%sObject:'%(Name,)).encode('latin'),
                signature = b'v@:@',
        )
        remove2.isHidden = True
        instance_methods.add(remove2)


NSMutableDictionary = lookUpClass('NSMutableDictionary')

class dict_property (object_property):
    def __get__(self, object, owner):
        v = object_property.__get__(self, object, owner)
        if v is None:
            v = NSMutableDictionary.alloc().init()
            object_property.__set__(self, object, v)
        return object_property.__get__(self, object, owner)
