import collections.abc
import decimal
import enum
import inspect
import weakref
from ctypes import (
    CFUNCTYPE, POINTER, Array, Structure, Union, addressof, byref, c_bool, c_char_p, c_int, c_uint,
    c_uint8, c_ulong, c_void_p, cast, sizeof, string_at, py_object,
)

from .types import (
    compound_value_for_sequence, ctype_for_type, ctypes_for_method_encoding, encoding_for_ctype,
    register_ctype_for_type
)
from .runtime import (
    Class, SEL, add_ivar, add_method, ensure_bytes, get_class, get_ivar, libc, libobjc, objc_block, objc_id,
    objc_property_attribute_t, object_isClass, set_ivar, send_message, send_super,
)

__all__ = [
    'Block',
    'NSArray',
    'NSData',
    'NSDecimalNumber',
    'NSDictionary',
    'NSMutableArray',
    'NSMutableDictionary',
    'NSNumber',
    'NSObject',
    'NSObjectProtocol',
    'NSString',
    'ObjCBlock',
    'ObjCClass',
    'ObjCInstance',
    'ObjCMetaClass',
    'ObjCProtocol',
    'Protocol',
    'at',
    'for_objcclass',
    'get_type_for_objcclass_map',
    'ns_from_py',
    'objc_classmethod',
    'objc_const',
    'objc_ivar',
    'objc_method',
    'objc_property',
    'objc_rawmethod',
    'py_from_ns',
    'register_type_for_objcclass',
    'type_for_objcclass',
    'unregister_type_for_objcclass',
]

# Dictionary to keep references to Python objects which are stored in declared
# properties or dynamically created attributes of Objective-C objects. This ensures that
# the Python objects are not destroyed if they are otherwise no Python references left.
_keep_alive_objects = {}


def encoding_from_annotation(f, offset=1):
    argspec = inspect.getfullargspec(inspect.unwrap(f))

    encoding = [argspec.annotations.get('return', ObjCInstance), ObjCInstance, SEL]

    for varname in argspec.args[offset:]:
        encoding.append(argspec.annotations.get(varname, ObjCInstance))

    return encoding


class ObjCMethod(object):
    """An unbound Objective-C method. This is Rubicon's high-level equivalent of :class:`~rubicon.objc.runtime.Method`.

    :class:`ObjCMethod` objects normally don't need to be used directly. To call a method on an Objective-C object,
    you should use the method call syntax supported by :class:`ObjCInstance`,
    or the :func:`~rubicon.objc.runtime.send_message` function.

    .. note::

        This is *not* the same class as the one used for *bound* Objective-C methods,
        as returned from :meth:`ObjCInstance.__getattr__`. Currently, Rubicon doesn't provide any documented way
        to get an unbound :class:`ObjCMethod` object for an instance method of an :class:`ObjCClass`.
    """

    def __init__(self, method):
        """The constructor takes a :class:`~rubicon.objc.runtime.Method` object,
        whose information is used to create an :class:`ObjCMethod`.
        This can be used to call or introspect a :class:`~rubicon.objc.runtime.Method` pointer
        received from the Objective-C runtime.
        """

        self.selector = libobjc.method_getName(method)
        self.name = self.selector.name
        self.encoding = libobjc.method_getTypeEncoding(method)
        self.restype, *self.imp_argtypes = ctypes_for_method_encoding(self.encoding)
        assert self.imp_argtypes[:2] == [objc_id, SEL]
        self.method_argtypes = self.imp_argtypes[2:]

    def __repr__(self):
        return "<ObjCMethod: %s %s>" % (self.name, self.encoding)

    def __call__(self, receiver, *args, convert_args=True, convert_result=True):
        """Call the method on an object with the given arguments.

        The passed arguments are automatically converted to the expected argument types as needed:

        * :class:`enum.Enum` objects are replaced by their :attr:`~enum.Enum.value` before further conversion
        * For parameters that expect a block, Python callables are converted to :class:`Block`\\s
        * For parameters that expect an Objective-C object, Python objects are converted using :func:`ns_from_py`
        * For parameters that expect a C structure, Python sequences are converted
          using :func:`~rubicon.objc.types.compound_value_for_sequence`.
        * Finally, :mod:`ctypes` applies its normal function argument conversions.

        The above argument conversions (except those performed by :mod:`ctypes`) can be disabled
        by setting the ``convert_args`` keyword argument to ``False``.

        If the method returns an Objective-C object, it is automatically converted to an :class:`ObjCInstance`.
        This conversion can be disabled by setting the ``convert_result`` keyword argument to ``False``,
        in which case the object is returned as a raw :class:`~rubicon.objc.runtime.objc_id` value.

        The ``_cmd`` selector argument does *not* need to be passed in manually ---
        the method's :attr:`selector` is automatically added between the receiver and the method arguments.
        """

        if len(args) != len(self.method_argtypes):
            raise TypeError(
                'Method {} takes {} arguments, but got {} arguments'
                .format(self.name, len(args), len(self.method_argtypes))
            )

        if convert_args:
            converted_args = []
            for argtype, arg in zip(self.method_argtypes, args):
                if isinstance(arg, enum.Enum):
                    # Convert Python enum objects to their values
                    arg = arg.value

                if issubclass(argtype, objc_block):
                    if arg is None:
                        # allow for 'nil' block args, which some objc methods accept
                        arg = ns_from_py(arg)
                    elif (callable(arg)
                          and not isinstance(arg, Block)):  # <-- guard against someone someday making Block callable
                        # Note: We need to keep the temp. Block instance
                        # around at least until the objc method is called.
                        # _as_parameter_ is used in the actual ctypes marshalling below.
                        arg = Block(arg)
                    # ^ For blocks at this point either arg is a Block instance
                    # (making use of _as_parameter_), is None, or if it isn't either of
                    # those two, an ArgumentError will be raised below.
                elif issubclass(argtype, objc_id):
                    # Convert Python objects to Foundation objects
                    arg = ns_from_py(arg)
                elif isinstance(arg, collections.abc.Sequence) and issubclass(argtype, (Structure, Array)):
                    arg = compound_value_for_sequence(arg, argtype)

                converted_args.append(arg)
        else:
            converted_args = args

        result = send_message(
            receiver, self.selector, *converted_args,
            restype=self.restype, argtypes=self.method_argtypes,
        )

        if not convert_result:
            return result

        # Convert result to python type if it is a instance or class pointer.
        if self.restype is not None and issubclass(self.restype, objc_id):
            result = ObjCInstance(result)

        # Mark for release if we acquire ownership of an object. Do not autorelease here because
        # we might retain a Python reference while the Obj-C reference goes out of scope.
        if self.name.startswith((b'alloc', b'new', b'copy', b'mutableCopy')):
            result._needs_release = True

        return result


class ObjCPartialMethod(object):
    _sentinel = object()

    def __init__(self, name_start):
        super().__init__()

        self.name_start = name_start
        self.methods = {}  # Initialized in ObjCClass._load_methods

    def __repr__(self):
        return "{cls.__module__}.{cls.__qualname__}({self.name_start!r})".format(cls=type(self), self=self)

    def __call__(self, receiver, first_arg=_sentinel, **kwargs):
        if first_arg is ObjCPartialMethod._sentinel:
            if kwargs:
                raise TypeError("Missing first (positional) argument")

            args = []
            rest = frozenset()
        else:
            args = [first_arg]
            # Add "" to rest to indicate that the method takes arguments
            rest = frozenset(kwargs) | frozenset(("",))

        try:
            name, order = self.methods[rest]
        except KeyError:
            raise ValueError(
                "No method was found starting with {!r} and with keywords {}\nKnown keywords are:\n{}"
                .format(self.name_start, set(kwargs), "\n".join(repr(keywords) for keywords in self.methods))
            )

        meth = receiver.objc_class._cache_method(name)
        args += [kwargs[name] for name in order]
        return meth(receiver, *args)


class ObjCBoundMethod(object):
    """This represents an Objective-C method (an IMP) which has been bound
    to some id which will be passed as the first parameter to the method."""

    def __init__(self, method, receiver):
        """Initialize with a method and ObjCInstance or ObjCClass object."""
        self.method = method
        if type(receiver) == Class:
            self.receiver = cast(receiver, objc_id)
        else:
            self.receiver = receiver

    def __repr__(self):
        return '{cls.__module__}.{cls.__qualname__}({self.method}, {self.receiver})'.format(
            cls=type(self), self=self)

    def __call__(self, *args, **kwargs):
        """Call the method with the given arguments."""
        return self.method(self.receiver, *args, **kwargs)


def convert_method_arguments(encoding, args):
    """Used to convert Objective-C method arguments to Python values
    before passing them on to the Python-defined method.
    """
    new_args = []
    for e, a in zip(encoding[3:], args):
        if issubclass(e, (objc_id, ObjCInstance)):
            new_args.append(ObjCInstance(a))
        else:
            new_args.append(a)
    return new_args


class objc_method(object):
    """Exposes the decorated method as an Objective-C instance method in a custom class or protocol.

    In a custom Objective-C class, decorating a method with :func:`@objc_method <objc_method>`
    makes it available to Objective-C: a corresponding Objective-C method is created in the new Objective-C class,
    whose implementation calls the decorated Python method. The Python method receives all arguments
    (including ``self``) from the Objective-C method call, and its return value is passed back to Objective-C.

    In a custom Objective-C protocol, the behavior is similar, but the method body is ignored,
    since Objective-C protocol methods have no implementations. By convention, the method body in this case
    should be empty (``pass``). (Since the method is never called, you could put any other code there as well,
    but doing so is misleading and discouraged.)
    """

    def __init__(self, py_method):
        super().__init__()

        self.py_method = py_method
        self.encoding = encoding_from_annotation(py_method)

    def __call__(self, objc_self, objc_cmd, *args):
        py_self = ObjCInstance(objc_self)
        args = convert_method_arguments(self.encoding, args)
        result = self.py_method(py_self, *args)
        if self.encoding[0] is not None and issubclass(self.encoding[0], (objc_id, ObjCInstance)):
            result = ns_from_py(result)
            if result is not None:
                result = result.ptr
        if isinstance(result, c_void_p):
            return result.value
        else:
            return result

    def class_register(self, class_ptr, attr_name):
        name = attr_name.replace("_", ":")
        add_method(class_ptr, name, self, self.encoding)

    def protocol_register(self, proto_ptr, attr_name):
        name = attr_name.replace('_', ':')
        types = b''.join(encoding_for_ctype(ctype_for_type(tp)) for tp in self.encoding)
        libobjc.protocol_addMethodDescription(proto_ptr, SEL(name), types, True, True)


class objc_classmethod(object):
    """Exposes the decorated method as an Objective-C class method in a custom class or protocol.

    This decorator behaves exactly like :func:`@objc_method <objc_method>`, except that the decorated method
    becomes a class method, so it is exposed on the Objective-C class rather than its instances.
    """

    def __init__(self, py_method):
        super().__init__()

        self.py_method = py_method
        self.encoding = encoding_from_annotation(py_method)

    def __call__(self, objc_cls, objc_cmd, *args):
        py_cls = ObjCClass(objc_cls)
        args = convert_method_arguments(self.encoding, args)
        result = self.py_method(py_cls, *args)
        if self.encoding[0] is not None and issubclass(self.encoding[0], (objc_id, ObjCInstance)):
            result = ns_from_py(result)
            if result is not None:
                result = result.ptr
        if isinstance(result, c_void_p):
            return result.value
        else:
            return result

    def class_register(self, class_ptr, attr_name):
        name = attr_name.replace("_", ":")
        add_method(libobjc.object_getClass(class_ptr), name, self, self.encoding)

    def protocol_register(self, proto_ptr, attr_name):
        name = attr_name.replace('_', ':')
        types = b''.join(encoding_for_ctype(ctype_for_type(tp)) for tp in self.encoding)
        libobjc.protocol_addMethodDescription(proto_ptr, SEL(name), types, True, False)


class objc_ivar(object):
    """Defines an ivar in a custom Objective-C class.

    If you want to store additional data on a custom Objective-C class, it is recommended to use properties
    (:func:`objc_property`) instead of ivars. Properties are a more modern and high-level Objective-C feature,
    which automatically deal with reference counting for objects, and creation of getters and setters.

    The ivar type may be any :mod:`ctypes` type.

    Unlike properties, the contents of an ivar cannot be accessed or modified using Python attribute syntax.
    Instead, the :func:`get_ivar` and :func:`set_ivar` functions need to be used.
    """

    def __init__(self, vartype):
        self.vartype = vartype

    def class_register(self, class_ptr, attr_name):
        return add_ivar(class_ptr, attr_name, self.vartype)

    def protocol_register(self, proto_ptr, attr_name):
        raise TypeError('Objective-C protocols cannot have ivars')


class objc_property(object):
    """Defines a property in a custom Objective-C class or protocol.

    This class should be called in the body of an Objective-C subclass or protocol, for example:

    .. code-block:: python

        class MySubclass(NSObject):
            counter = objc_property(NSInteger)

    The property type may be any :mod:`ctypes` type, as well as any of the Python types
    accepted by :func:`~rubicon.objc.types.ctype_for_type`.

    Defining a property automatically defines a corresponding getter and setter.
    Following standard Objective-C naming conventions, for a property ``name`` the getter is called ``name``
    and the setter is called ``setName:``.

    In a custom Objective-C class, implementations for the getter and setter are also generated,
    which store the property's value in an ivar called ``_name``. If the property has an object type,
    the generated setter keeps the stored object retained, and releases it when it is replaced.

    In a custom Objective-C protocol, only the metadata for the property is generated.

    If ``weak`` is ``True``, the property will be created as a weak property. When assigning an object to it,
    the reference count of the object will not be increased. When the object is deallocated, the property
    value is set to None. Weak properties are only supported for Objective-C or Python object types.
    """

    def __init__(self, vartype=objc_id, weak=False):
        super().__init__()

        self.vartype = ctype_for_type(vartype)

        self.weak = weak

        self._is_py_object = issubclass(self.vartype, py_object)
        self._is_objc_object = issubclass(self.vartype, objc_id)

        # Weakly referenced Python objects are still stored in strong ivars.
        # Check here if we need a weak or strong ivar.
        self._ivar_weak = self.weak and not self._is_py_object

        if self.weak and not (self._is_py_object or self._is_objc_object):
            raise TypeError(
                "Incompatible type for ivar {!r}: Weak properties are only supported "
                "for Objective-C or Python object types".format(vartype)
            )

    def _get_property_attributes(self):
        attrs = [
            objc_property_attribute_t(b'T', encoding_for_ctype(self.vartype)),  # Type: vartype
        ]
        if self._is_objc_object:
            reference = b'W' if self.weak else b'&'
            attrs.append(objc_property_attribute_t(reference, b''))
        return (objc_property_attribute_t * len(attrs))(*attrs)

    def class_register(self, class_ptr, attr_name):

        ivar_name = '_' + attr_name

        add_ivar(class_ptr, ivar_name, self.vartype)

        # Implementation note:
        # 1. Objective-C objects are stored as strong or weak references in the ivar if the property was declared as
        #    strong or weak, respectively. In case of strong properties, we retain the object when storing it in the
        #    ivar and release it when the ivar is changed.
        # 2. Python objects are wrapped as `ctypes.py_object` which are then always stored as a strong reference in
        #    the ivar. Since this does not increase the reference count of the Python object itself, we keep a
        #    reference to it in `_keep_alive_objects`. For weak properties, we store a Python `wearef` to the object
        #    instead. This weakref is similarly kept alive.

        def _objc_getter(objc_self, _cmd):
            value = get_ivar(objc_self, ivar_name, weak=self._ivar_weak)

            # ctypes complains when a callback returns a "boxed" primitive type, so we have to manually unbox it.
            # If the data object has a value attribute and is not a structure or union, assume that it is
            # a primitive and unbox it.
            if not isinstance(value, (Structure, Union)):
                try:
                    value = value.value
                except AttributeError:
                    pass

            if self.weak and self._is_py_object:
                # Unpack the Python weakref.
                value = value()

            return value

        def _objc_setter(objc_self, _cmd, new_value):

            if self._is_py_object and self.weak:
                # Don't store the object itself but only a Python weakref.
                new_value = weakref.ref(new_value)

            if not isinstance(new_value, self.vartype):
                # If vartype is a primitive, then new_value may be unboxed. If that is the case, box it manually.
                new_value = self.vartype(new_value)

            if self._is_objc_object and not self.weak:
                # If vartype is objc_id, retrieve the old object stored in the ivar to release it later.
                old_value = get_ivar(objc_self, ivar_name, weak=self.weak)

                if new_value.value == old_value.value:
                    # Old and new value are the same, nothing to do.
                    return

            set_ivar(objc_self, ivar_name, new_value, weak=self._ivar_weak)

            # Perform reference management.

            if self._is_objc_object and not self.weak:
                if old_value:
                    # If the old value is a non-null Objective-C object, release it.
                    send_message(old_value, 'release', restype=None, argtypes=[])

                if new_value:
                    # Retain the object on the Objective-C side.
                    send_message(new_value, 'retain', restype=objc_id, argtypes=[])

            elif self._is_py_object:
                # Retain the Python object in dictionary, this replaces any previous entry for this property.
                _keep_alive_objects[(objc_self.value, self)] = new_value.value

        setter_name = 'set' + attr_name[0].upper() + attr_name[1:] + ':'

        add_method(
            class_ptr, attr_name, _objc_getter,
            [self.vartype, ObjCInstance, SEL],
        )
        add_method(
            class_ptr, setter_name, _objc_setter,
            [None, ObjCInstance, SEL, self.vartype],
        )

        attrs = self._get_property_attributes()
        libobjc.class_addProperty(class_ptr, ensure_bytes(attr_name), attrs, len(attrs))

    def dealloc_callback(self, objc_self, attr_name):

        ivar_name = '_' + attr_name

        if self._ivar_weak:
            # Clean up weak reference.
            set_ivar(objc_self, ivar_name, self.vartype(None), weak=True)
        elif self._is_objc_object:
            # If the old value is a non-null object, release it. There is no need to set the actual ivar to nil.
            old_value = get_ivar(objc_self, ivar_name, weak=self.weak)
            send_message(old_value, 'release', restype=None, argtypes=[])

        # Remove any Python objects that are kept alive.
        _keep_alive_objects.pop((objc_self.value, self), None)

    def protocol_register(self, proto_ptr, attr_name):
        attrs = self._get_property_attributes()
        libobjc.protocol_addProperty(proto_ptr, ensure_bytes(attr_name), attrs, len(attrs), True, True)


class objc_rawmethod(object):
    """Exposes the decorated method as an Objective-C instance method in a custom class,
    with fewer convenience features than :func:`objc_method`.

    This decorator behaves similarly to :func:`@objc_method <objc_method>`. However, unlike with :func:`objc_method`,
    no automatic conversions are performed (aside from those by :mod:`ctypes`).
    This means that all parameter and return types must be provided as :mod:`ctypes` types
    (no :func:`~rubicon.objc.types.ctype_for_type` conversion is performed), all arguments are passed in their raw form
    as received from :mod:`ctypes`, and the return value must be understood by :mod:`ctypes`.

    In addition, the implicit ``_cmd`` parameter is exposed to the Python method, which is not the case
    when using :func:`objc_method`. This means that the decorated Python method must always have
    an additional ``_cmd`` parameter after ``self``; if it is missing, there will be errors at runtime
    due to mismatched argument counts. Like ``self``, ``_cmd`` never needs to be annotated,
    and any annotations on it are ignored.
    """

    def __init__(self, py_method):
        super().__init__()

        self.py_method = py_method
        self.encoding = encoding_from_annotation(py_method, offset=2)

    def __call__(self, *args, **kwargs):
        return self.py_method(*args, **kwargs)

    def class_register(self, class_ptr, attr_name):
        name = attr_name.replace("_", ":")
        add_method(class_ptr, name, self, self.encoding)

    def protocol_register(self, proto_ptr, attr_name):
        raise TypeError('Protocols cannot have method implementations, use objc_method instead of objc_rawmethod')


_type_for_objcclass_map = {}


def type_for_objcclass(objcclass):
    """Look up the :class:`ObjCInstance` subclass used to represent instances of the given Objective-C class in Python.

    If the exact Objective-C class is not registered, each superclass is also checked,
    defaulting to :class:`ObjCInstance` if none of the classes in the superclass chain is registered.
    Afterwards, all searched superclasses are registered for the :class:`ObjCInstance` subclass that was found.
    (This speeds up future lookups, and ensures that previously computed mappings are not changed
    by unrelated registrations.)

    This method is mainly intended for internal use by Rubicon, but is exposed in the public API for completeness.
    """

    if isinstance(objcclass, ObjCClass):
        objcclass = objcclass.ptr

    superclass = objcclass
    traversed_classes = []
    pytype = ObjCInstance
    while superclass.value is not None:
        try:
            pytype = _type_for_objcclass_map[superclass.value]
        except KeyError:
            traversed_classes.append(superclass)
            superclass = libobjc.class_getSuperclass(superclass)
        else:
            break

    for cls in traversed_classes:
        register_type_for_objcclass(pytype, cls)

    return pytype


def register_type_for_objcclass(pytype, objcclass):
    """Register a conversion from an Objective-C class to an :class:`ObjCInstance` subclass.

    After a call of this function, when Rubicon wraps an Objective-C object that is an instance of ``objcclass``
    (or a subclass), the Python object will have the class ``pytype`` rather than :class:`ObjCInstance`.
    See :func:`type_for_objcclass` for a full description of the lookup process.

    .. warning::

        This function should only be called if no instances of ``objcclass`` (or a subclass)
        have been wrapped by Rubicon yet. If the function is called later, it will not fully take effect:
        the types of existing instances do not change, and mappings for subclasses of ``objcclass`` are not updated.
    """

    if isinstance(objcclass, ObjCClass):
        objcclass = objcclass.ptr

    _type_for_objcclass_map[objcclass.value] = pytype


def unregister_type_for_objcclass(objcclass):
    """Unregister a conversion from an Objective-C class to an :class:`ObjCInstance` subclass.

    .. warning::

        This function should only be called if no instances of ``objcclass`` (or a subclass)
        have been wrapped by Rubicon yet. If the function is called later, it will not fully take effect:
        the types of existing instances do not change, and mappings for subclasses of ``objcclass`` are not removed.
    """

    if isinstance(objcclass, ObjCClass):
        objcclass = objcclass.ptr

    del _type_for_objcclass_map[objcclass.value]


def get_type_for_objcclass_map():
    """Get a copy of all currently registered :class:`ObjCInstance` subclasses as a mapping.

    Keys are Objective-C class addresses as :class:`int`\\s.
    """

    return dict(_type_for_objcclass_map)


def for_objcclass(objcclass):
    """Decorator for registering a conversion from an Objective-C class to an :class:`ObjCInstance` subclass.

    This is equivalent to calling :func:`register_type_for_objcclass` on the decorated class.
    """

    def _for_objcclass(pytype):
        register_type_for_objcclass(pytype, objcclass)
        return pytype

    return _for_objcclass


class ObjCInstance(object):
    """Python wrapper for an Objective-C instance."""

    # Cache dictionary containing every currently existing ObjCInstance object,
    # with the key being the memory address (as an integer) of the Objective-C object that it wraps.
    # Because this is a weak value dictionary,
    # entries are automatically removed if the ObjCInstance is no longer referenced from Python.
    # (The object may still have references in Objective-C,
    # and a new ObjCInstance might be created for it if it is wrapped again later.)
    _cached_objects = weakref.WeakValueDictionary()

    @property
    def objc_class(self):
        """The Objective-C object's class, as an :class:`ObjCClass`."""

        # This property is used inside __getattr__, so any attribute accesses must be done through
        # super(...).__getattribute__ to prevent infinite recursion.
        try:
            return super(ObjCInstance, type(self)).__getattribute__(self, "_objc_class")
        except AttributeError:
            # This assumes that objects never change their class after they are seen by Rubicon.
            # Technically this might not always be true, because the Objective-C runtime provides a function
            # object_setClass that can change an object's class after creation, and some code also manipulates objects'
            # isa pointers directly (although the latter is no longer officially supported by Apple).
            # This is only extremely rarely done in practice though, and then usually only during object
            # creation/initialization, so it's basically safe to assume that an object's class will never change
            # after it's been wrapped in an ObjCInstance.
            super(ObjCInstance, type(self)).__setattr__(self, "_objc_class", ObjCClass(libobjc.object_getClass(self)))
            return super(ObjCInstance, type(self)).__getattribute__(self, "_objc_class")

    @staticmethod
    def _associated_attr_key_for_name(name):
        return SEL("rubicon.objc.py_attr.{}".format(name))

    def __new__(cls, object_ptr, _name=None, _bases=None, _ns=None):
        """The constructor accepts an :class:`~rubicon.objc.runtime.objc_id` or anything that can be cast to one,
        such as a :class:`~ctypes.c_void_p`, or an existing :class:`ObjCInstance`.

        :class:`ObjCInstance` objects are cached --- this means that for every Objective-C object
        there can be at most one :class:`ObjCInstance` object at any time. Rubicon will automatically create
        new :class:`ObjCInstance`\\s or return existing ones as needed.

        The returned object's Python class is not always exactly :class:`ObjCInstance`. For example,
        if the passed pointer refers to a class or a metaclass, an instance of :class:`ObjCClass`
        or :class:`ObjCMetaClass` is returned as appropriate. Additional custom :class:`ObjCInstance` subclasses
        may be defined and registered using :func:`register_type_for_objcclass`.
        Creating an :class:`ObjCInstance` from a ``nil`` pointer returns ``None``.

        Rubicon currently does not perform any automatic memory management on the Objective-C object
        wrapped in an :class:`ObjCInstance`. It is the user's responsibility to ``retain`` and ``release``
        wrapped objects as needed, like in Objective-C code without automatic reference counting.
        """

        # Make sure that object_ptr is wrapped in an objc_id.
        if not isinstance(object_ptr, objc_id):
            object_ptr = cast(object_ptr, objc_id)

        # If given a nil pointer, return None.
        if not object_ptr.value:
            return None

        # If an ObjCInstance already exists for the Objective-C object,
        # reuse it instead of creating a second ObjCInstance for the same object.
        try:
            return cls._cached_objects[object_ptr.value]
        except KeyError:
            pass

        # If the given pointer points to a class, return an ObjCClass instead (if we're not already creating one).
        if not issubclass(cls, ObjCClass) and object_isClass(object_ptr):
            return ObjCClass(object_ptr)

        # Otherwise, create a new ObjCInstance.
        if issubclass(cls, type):
            # Special case for ObjCClass to pass on the class name, bases and namespace to the type constructor.
            self = super().__new__(cls, _name, _bases, _ns)
        else:
            if isinstance(object_ptr, objc_block):
                cls = ObjCBlockInstance
            else:
                cls = type_for_objcclass(libobjc.object_getClass(object_ptr))
            self = super().__new__(cls)
        super(ObjCInstance, type(self)).__setattr__(self, "ptr", object_ptr)
        super(ObjCInstance, type(self)).__setattr__(self, "_as_parameter_", object_ptr)
        super(ObjCInstance, type(self)).__setattr__(self, "_needs_release", False)
        if isinstance(object_ptr, objc_block):
            super(ObjCInstance, type(self)).__setattr__(self, "block", ObjCBlock(object_ptr))
        # Store new object in the dictionary of cached objects, keyed
        # by the (integer) memory address pointed to by the object_ptr.
        cls._cached_objects[object_ptr.value] = self

        return self

    def release(self):
        """
        Manually decrement the reference count of the corresponding objc object

        The objc object is sent a dealloc message when its reference count reaches 0. Calling
        this method manually should not be necessary, unless the object was explicitly
        ``retain``\\ed before. Objects returned from ``.alloc().init...(...)`` and similar calls
        are released automatically by Rubicon when the corresponding Python object is deallocated.
        """
        self._needs_release = False
        send_message(self, "release", restype=objc_id, argtypes=[])

    def autorelease(self):
        """
        Decrements the receiver’s reference count at the end of the current autorelease pool block

        The objc object is sent a dealloc message when its reference count reaches 0. If called,
        the object will not be released when the Python object is deallocated.
        """
        self._needs_release = False
        result = send_message(self, "autorelease", restype=objc_id, argtypes=[])
        return ObjCInstance(result)

    def __del__(self):
        """
        Release the corresponding objc instance if we own it, i.e., if it was returned by
        by a method starting with 'alloc', 'new', 'copy', or 'mutableCopy' and it wasn't
        already explicitly released by calling :meth:`release` or :meth:`autorelease`.
        """

        if self._needs_release:
            send_message(self, "release", restype=objc_id, argtypes=[])

    def __str__(self):
        """Get a human-readable representation of ``self``.

        By default, ``self.description`` converted to a Python string is returned.
        If ``self.description`` is ``nil``, ``self.debugDescription`` converted to a Python is returned instead.
        If that is also ``nil``, ``repr(self)`` is returned as a fallback.
        """

        desc = self.description
        if desc is not None:
            return str(desc)

        desc = self.debugDescription
        if desc is not None:
            return str(desc)

        return repr(self)

    def __repr__(self):
        """Get a debugging representation of ``self``, which includes the Objective-C object's address, class,
        and ``debugDescription``.
        """

        return "<%s.%s %#x: %s at %#x: %s>" % (
            type(self).__module__,
            type(self).__qualname__,
            id(self),
            self.objc_class.name,
            self.ptr.value,
            self.debugDescription,
        )

    def __getattr__(self, name):
        """Allows accessing Objective-C properties and methods using Python attribute syntax.

        If ``self`` has a Python attribute with the given name, its value is returned.

        If there is an Objective-C property with the given name, its value is returned using its getter method.
        An attribute is considered a property if any of the following are true:

        * A property with the name is present on the class (i. e. declared using ``@property`` in the source code)
        * There is both a getter and setter method for the name
        * The name has been declared as a property using :meth:`ObjCClass.declare_property`

        Otherwise, a method matching the given name is looked up. :class:`ObjCInstance` understands two syntaxes
        for calling Objective-C methods:

        * "Flat" syntax: the Objective-C method name is spelled out in the attribute name,
          with all colons replaced with underscores, and all arguments are passed as positional arguments.
          For example, the Objective-C method call ``[self initWithWidth:w height:h]``
          translates to ``self.initWithWidth_height_(w, h)``.
        * "Interleaved" syntax: the Objective-C method name is split up between the attribute name
          and the keyword arguments passed to the returned method. For example, the Objective-C method call
          ``[self initWithRed:r green:g blue:b]`` translates to ``self.initWithRed(r, green=g, blue=b)``.

        The "interleaved" syntax is usually preferred, since it looks more similar to normal Objective-C syntax.
        However, the "flat" syntax is also fully supported. Certain method names require the "flat" syntax,
        for example if two arguments have the same label (e. g. ``performSelector:withObject:withObject:``),
        which is not supported by Python's keyword argument syntax.

        .. warning::

            The "interleaved" syntax currently ignores the ordering of its keyword arguments.
            However, in the interest of readability, the keyword arguments should always be passed
            in the same order as they appear in the method name.

            This also means that two methods whose names which differ only in the ordering of their keywords
            will conflict with each other, and can only be called reliably using "flat" syntax.

            As of Python 3.6, the order of keyword arguments passed to functions is preserved (:pep:`468`).
            In the future, once Rubicon requires Python 3.6 or newer, "interleaved" method calls
            will respect keyword argument order. This will fix the kind of conflict described above,
            but will also disallow specifying the keyword arguments out of order.
        """

        # Search for named instance method in the class object and if it
        # exists, return callable object with self as hidden argument.
        # Note: you should give self and not self.ptr as a parameter to
        # ObjCBoundMethod, so that it will be able to keep the ObjCInstance
        # alive for chained calls like MyClass.alloc().init() where the
        # object created by alloc() is not assigned to a variable.

        # If there's a property with this name; return the value directly.
        # If the name ends with _, we can shortcut this step, because it's
        # clear that we're dealing with a method call.
        if not name.endswith('_'):
            method = self.objc_class._cache_property_accessor(name)
            if method:
                return ObjCBoundMethod(method, self)()

        # See if there's a partial method starting with the given name,
        # either on self's class or any of the superclasses.
        cls = self.objc_class
        while cls is not None:
            # Load the class's methods if we haven't done so yet.
            if cls.methods_ptr is None:
                cls._load_methods()

            try:
                method = cls.partial_methods[name]
                break
            except KeyError:
                cls = cls.superclass
        else:
            method = None

        if method is None or set(method.methods) == {frozenset()}:
            # Find a method whose full name matches the given name if no partial method was found,
            # or the partial method can only resolve to a single method that takes no arguments.
            # The latter case avoids returning partial methods in cases where a regular method works just as well.
            method = self.objc_class._cache_method(name.replace("_", ":"))

        if method:
            return ObjCBoundMethod(method, self)

        # Check if the attribute name corresponds to an instance attribute defined at
        # runtime from Python. Return it if yes, raise an AttributeError otherwise.
        key = self._associated_attr_key_for_name(name)
        pyo_wrapper = libobjc.objc_getAssociatedObject(self, key)

        if pyo_wrapper.value is None:
            raise AttributeError('%s.%s %s has no attribute %s' % (
                type(self).__module__, type(self).__qualname__, self.objc_class.name, name)
            )
        address = get_ivar(pyo_wrapper, "wrapped_pointer")
        pyo = cast(address.value, py_object)

        return pyo.value

    def __setattr__(self, name, value):
        """Allows modifying Objective-C properties using Python syntax.

        If ``self`` has a Python attribute with the given name, it is set. Otherwise, the name should refer
        to an Objective-C property, whose setter method is called with ``value``.
        """

        if name in self.__dict__:
            # For attributes already in __dict__, use the default __setattr__.
            super(ObjCInstance, type(self)).__setattr__(self, name, value)
        else:
            method = self.objc_class._cache_property_mutator(name)
            if method:
                # Convert enums to their underlying values.
                if isinstance(value, enum.Enum):
                    value = value.value
                ObjCBoundMethod(method, self)(value)
            else:

                # Wrap the Python object in a WrappedPyObject instance.
                # A reference will be retained as long as the WrappedPyObject is alive.

                wrapper = send_message(
                    send_message(get_class('WrappedPyObject'), 'alloc',
                                 restype=objc_id, argtypes=[]),
                    'initWithObjectId:', id(value), restype=objc_id, argtypes=[objc_id]
                )

                # Set the Python value as an associated object. This will release
                # any previous wrapper object with the same key.
                key = self._associated_attr_key_for_name(name)
                libobjc.objc_setAssociatedObject(self, key, wrapper, 0x301)

                # Release the wrapper object, it will be retained by the association.
                send_message(wrapper, 'release', restype=objc_id, argtypes=[])

    def __delattr__(self, name):
        if name in self.__dict__:
            # For attributes already in __dict__, use the default __delattr__.
            super(ObjCInstance, type(self)).__delattr__(self, name)
        else:
            key = self._associated_attr_key_for_name(name)
            # Check for instance attributes defined at runtime.
            pyo_wrapper = libobjc.objc_getAssociatedObject(self, key)
            if pyo_wrapper.value is None:
                raise AttributeError('%s.%s %s has no attribute %s' % (
                    type(self).__module__, type(self).__qualname__, self.objc_class.name, name)
                )
            # If set, clear the instance attribute / associated object.
            libobjc.objc_setAssociatedObject(self, key, None, 0x301)


# The inheritance order is important here.
# type must come after ObjCInstance, so super() refers to ObjCInstance.
# This allows the ObjCInstance constructor to receive the class pointer
# as well as the name, bases, attrs arguments.
# The other way around this would not be possible, because then
# the type constructor would be called before ObjCInstance's, and there
# would be no opportunity to pass extra arguments.
class ObjCClass(ObjCInstance, type):
    """Python wrapper for an Objective-C class.

    :class:`ObjCClass` is a subclass of :class:`ObjCInstance` and supports the same syntaxes for calling methods
    and accessing properties.
    """

    @property
    def superclass(self):
        """The superclass of this class, or ``None`` if this is a root class (such as :class:`NSObject`)."""

        super_ptr = libobjc.class_getSuperclass(self)
        if super_ptr.value is None:
            return None
        else:
            return ObjCClass(super_ptr)

    @property
    def protocols(self):
        """The protocols adopted by this class."""

        out_count = c_uint()
        protocols_ptr = libobjc.class_copyProtocolList(self, byref(out_count))
        return tuple(ObjCProtocol(protocols_ptr[i]) for i in range(out_count.value))

    @classmethod
    def _new_from_name(cls, name):
        name = ensure_bytes(name)
        ptr = get_class(name)
        if ptr.value is None:
            raise NameError("ObjC Class '%s' couldn't be found." % name)

        return ptr, name

    @classmethod
    def _new_from_ptr(cls, ptr):
        ptr = cast(ptr, Class)
        if ptr.value is None:
            raise ValueError("Cannot create ObjCClass from nil pointer")
        elif not object_isClass(ptr):
            raise ValueError("Pointer {} ({:#x}) does not refer to a class".format(ptr, ptr.value))
        name = libobjc.class_getName(ptr)

        return ptr, name

    @classmethod
    def _new_from_class_statement(cls, name, bases, attrs, *, protocols):
        name = ensure_bytes(name)

        if get_class(name).value is not None:
            raise RuntimeError('An Objective-C class named {!r} already exists'.format(name))

        try:
            (superclass,) = bases
        except ValueError:
            raise ValueError('An Objective-C class must have exactly one base class, not {}'.format(len(bases)))

        # Check that the superclass is an ObjCClass.
        if not isinstance(superclass, ObjCClass):
            raise TypeError(
                'The superclass of an Objective-C class must be an ObjCClass, '
                'not a {cls.__module__}.{cls.__qualname__}'
                .format(cls=type(superclass))
            )

        # Check that all protocols are ObjCProtocols, and that there are no duplicates.
        for proto in protocols:
            if not isinstance(proto, ObjCProtocol):
                raise TypeError(
                    'The protocols list of an Objective-C class must contain ObjCProtocol objects, '
                    'not {cls.__module__}.{cls.__qualname__}'
                    .format(cls=type(proto))
                )
            elif protocols.count(proto) > 1:
                raise ValueError('Protocol {} is adopted more than once'.format(proto.name))

        # Create the ObjC class description
        ptr = libobjc.objc_allocateClassPair(superclass, name, 0)
        if ptr is None:
            raise RuntimeError('Class pair allocation failed')

        # Adopt all the protocols.
        for proto in protocols:
            if not libobjc.class_addProtocol(ptr, proto):
                raise RuntimeError('Failed to adopt protocol {}'.format(proto.name))

        # Register all methods, properties, ivars, etc.
        for attr_name, obj in attrs.items():
            if attr_name != "dealloc":
                try:
                    class_register = obj.class_register
                except AttributeError:
                    pass
                else:
                    class_register(ptr, attr_name)

        # Register any user-defined dealloc method. We treat dealloc differently to
        # inject our own cleanup code for properties, ivars, etc.

        user_dealloc = attrs.get("dealloc", None)

        def _new_delloc(objc_self, _cmd):

            # Invoke user-defined dealloc.
            if user_dealloc:
                user_dealloc(objc_self, _cmd)

            # Invoke dealloc callback of each attribute. Currently
            # defined for properties only.
            for attr_name, obj in attrs.items():
                try:
                    dealloc_callback = obj.dealloc_callback
                except AttributeError:
                    pass
                else:
                    dealloc_callback(objc_self, attr_name)

            # Invoke super dealloc.
            send_super(ptr, objc_self, "dealloc", restype=None, argtypes=[], _allow_dealloc=True)

        add_method(ptr, "dealloc", _new_delloc, [None, ObjCInstance, SEL])

        # Register the ObjC class
        libobjc.objc_registerClassPair(ptr)

        return ptr, name, attrs

    def __new__(cls, name_or_ptr, bases=None, attrs=None, *, protocols=()):
        """The constructor accepts either the name of an Objective-C class to look up
        (as :class:`str` or :class:`bytes`), or a pointer to an existing class object
        (in any form accepted by :class:`ObjCInstance`).

        If given a pointer, it must refer to an Objective-C class; pointers to other objects are not accepted.
        (Use :class:`ObjCInstance` to wrap a pointer that might also refer to other kinds of objects.)
        If the pointer refers to a metaclass, an instance of :class:`ObjCMetaClass` is returned instead.
        Creating an :class:`ObjCClass` from a ``Nil`` pointer returns ``None``.

        :class:`ObjCClass` can also be called like :class:`type`, with three arguments
        (name, bases list, namespace mapping). This form is called implicitly by Python's ``class`` syntax,
        and is used to create a new Objective-C class from Python (see :ref:`custom-classes-and-protocols`).
        The bases list must contain exactly one :class:`ObjCClass` to be extended by the new class.
        An optional ``protocols`` keyword argument is also accepted,
        which must be a sequence of :class:`ObjCProtocol`\\s for the new class to adopt.
        """

        if (bases is None) ^ (attrs is None):
            raise TypeError('ObjCClass arguments 2 and 3 must be given together')

        if bases is None and attrs is None:
            # A single argument provided. If it's a string, treat it as
            # a class name. Anything else treat as a class pointer.

            if protocols:
                raise ValueError('protocols kwarg is not allowed for the single-argument form of ObjCClass')

            attrs = {}

            if isinstance(name_or_ptr, (bytes, str)):
                ptr, name = cls._new_from_name(name_or_ptr)
            else:
                ptr, name = cls._new_from_ptr(name_or_ptr)
                if not issubclass(cls, ObjCMetaClass) and libobjc.class_isMetaClass(ptr):
                    return ObjCMetaClass(ptr)
        else:
            ptr, name, attrs = cls._new_from_class_statement(name_or_ptr, bases, attrs, protocols=protocols)

        objc_class_name = name.decode('utf-8')

        new_attrs = {
            'name': objc_class_name,
            'methods_ptr_count': c_uint(0),
            'methods_ptr': None,
            # Mapping of name -> method pointer
            'instance_method_ptrs': {},
            # Mapping of name -> instance method
            'instance_methods': {},
            # Mapping of name -> (accessor method, mutator method)
            'instance_properties': {},
            # Explicitly declared properties
            'forced_properties': set(),
            # Mapping of first keyword -> ObjCPartialMethod instances
            'partial_methods': {},
        }

        # On Python 3.6 and later, the class namespace may contain a __classcell__ attribute that must be passed on
        # to type.__new__. See https://docs.python.org/3/reference/datamodel.html#creating-the-class-object
        if '__classcell__' in attrs:
            new_attrs['__classcell__'] = attrs['__classcell__']

        # Create the class object. If there is already a cached instance for ptr,
        # it is returned and the additional arguments are ignored.
        # Logically this can only happen when creating an ObjCClass from an existing
        # name or pointer, not when creating a new class.
        # If there is no cached instance for ptr, a new one is created and cached.
        self = super().__new__(cls, ptr, objc_class_name, (ObjCInstance,), new_attrs)

        return self

    def __init__(self, *args, **kwargs):
        # Prevent kwargs from being passed on to type.__init__, which does not accept any kwargs in Python < 3.6.
        super().__init__(*args)

    def _cache_method(self, name):
        """Returns a python representation of the named instance method,
        either by looking it up in the cached list of methods or by searching
        for and creating a new method object."""

        supercls = self
        objc_method = None
        while supercls is not None:
            # Load the class's methods if we haven't done so yet.
            if supercls.methods_ptr is None:
                supercls._load_methods()

            try:
                objc_method = supercls.instance_methods[name]
                break
            except KeyError:
                pass

            try:
                objc_method = ObjCMethod(supercls.instance_method_ptrs[name])
                break
            except KeyError:
                pass

            supercls = supercls.superclass

        if objc_method is None:
            return None
        else:
            self.instance_methods[name] = objc_method
            return objc_method

    def _cache_property_methods(self, name):
        """Return the accessor and mutator for the named property.
        """
        if name.endswith('_'):
            # If the requested name ends with _, that's a marker that we're
            # dealing with a method call, not a property, so we can shortcut
            # the process.
            methods = None
        else:
            # Check 1: Does the class respond to the property?
            responds = libobjc.class_getProperty(self, name.encode('utf-8'))

            # Check 2: Does the class have an instance method to retrieve the given name
            accessor = self._cache_method(name)

            # Check 3: Is there a setName: method to set the property with the given name
            mutator = self._cache_method('set' + name[0].title() + name[1:] + ':')

            # Check 4: Is this a forced property on this class or a superclass?
            forced = False
            superclass = self
            while superclass is not None:
                if name in superclass.forced_properties:
                    forced = True
                    break
                superclass = superclass.superclass

            # If the class responds as a property, or it has both an accessor *and*
            # and mutator, then treat it as a property in Python.
            if responds or (accessor and mutator) or forced:
                methods = (accessor, mutator)
            else:
                methods = None
        return methods

    def _cache_property_accessor(self, name):
        """Returns a python representation of an accessor for the named
        property. Existence of a property is done by looking for the write
        selector (set<Name>:).
        """
        try:
            methods = self.instance_properties[name]
        except KeyError:
            methods = self._cache_property_methods(name)
            self.instance_properties[name] = methods
        if methods:
            return methods[0]
        return None

    def _cache_property_mutator(self, name):
        """Returns a python representation of an accessor for the named
        property. Existence of a property is done by looking for the write
        selector (set<Name>:).
        """
        try:
            methods = self.instance_properties[name]
        except KeyError:
            methods = self._cache_property_methods(name)
            self.instance_properties[name] = methods
        if methods:
            return methods[1]
        return None

    def declare_property(self, name):
        """Declare the instance method ``name`` to be a property getter.

        This causes the attribute named ``name`` on instances of this class to be treated as a property
        rather than a method --- accessing it returns the property's value, without requiring an explicit method call.
        See :class:`ObjCInstance.__getattr__` for a full description of how attribute access behaves for properties.

        Most properties do not need to be declared explicitly using this method, as they are detected automatically
        by :class:`ObjCInstance.__getattr__`. This method only needs to be used for properties that are read-only
        and don't have a ``@property`` declaration in the source code, because Rubicon cannot tell such properties
        apart from normal zero-argument methods.

        .. note::

            In the standard Apple SDKs, some properties are introduced as regular methods in one system version,
            and then declared as properties in a later system version. For example, the ``description`` method/property
            of :class:`NSObject` was declared as a regular method `up to OS X 10.9
            <https://github.com/phracker/MacOSX-SDKs/blob/9fc3ed0ad0345950ac25c28695b0427846eea966/MacOSX10.9.sdk/usr/include/objc/NSObject.h#L40>`__,
            but changed to a property `as of OS X 10.10
            <https://github.com/phracker/MacOSX-SDKs/blob/9fc3ed0ad0345950ac25c28695b0427846eea966/MacOSX10.10.sdk/usr/include/objc/NSObject.h#L43>`__.

            Such properties cause compatibility issues when accessed from Rubicon: ``obj.description()`` works on 10.9
            but is a :class:`TypeError` on 10.10, whereas ``obj.description`` works on 10.10
            but returns a method object on 10.9. To solve this issue, the property can be declared explicitly
            using ``NSObject.declare_property('description')``, so that it can always be accessed
            using ``obj.description``.
        """  # noqa: E501 # The links in the docstring above are too long to wrap to 120 chars.

        self.forced_properties.add(name)

    def declare_class_property(self, name):
        """Declare the class method ``name`` to be a property getter.

        This is equivalent to ``self.objc_class.declare_property(name)``.
        """

        self.objc_class.forced_properties.add(name)

    def __repr__(self):
        return "<%s.%s: %s at %#x>" % (
            type(self).__module__,
            type(self).__qualname__,
            self.name,
            self.ptr.value,
        )

    def __str__(self):
        return "{cls.__name__}({self.name!r})".format(cls=type(self), self=self)

    def __del__(self):
        libc.free(self.methods_ptr)

    def __instancecheck__(self, instance):
        """Check whether the given object is an instance of this class.

        If the given object is not an Objective-C object, ``False`` is returned.

        This method allows using :class:`ObjCClass`\\es as the second argument of :func:`isinstance`:
        ``isinstance(obj, NSString)`` is equivalent to ``obj.isKindOfClass(NSString)``.
        """

        if isinstance(instance, ObjCInstance):
            return bool(instance.isKindOfClass(self))
        else:
            return False

    def __subclasscheck__(self, subclass):
        """Check whether the given class is a subclass of this class.

        If the given object is not an Objective-C class, :class:`TypeError` is raised.

        This method allows using :class:`ObjCClass`\\es as the second argument of :func:`issubclass`:
        ``issubclass(cls, NSValue)`` is equivalent to ``obj.isSubclassOfClass(NSValue)``.
        """

        if isinstance(subclass, ObjCClass):
            return bool(subclass.isSubclassOfClass(self))
        else:
            raise TypeError(
                'issubclass(X, {self!r}) arg 1 must be an ObjCClass, not {tp.__module__}.{tp.__qualname__}'
                .format(self=self, tp=type(subclass))
            )

    def _load_methods(self):
        if self.methods_ptr is not None:
            raise RuntimeError("_load_methods cannot be called more than once")

        self.methods_ptr = libobjc.class_copyMethodList(self, byref(self.methods_ptr_count))

        if self.superclass is not None and self.superclass.methods_ptr is None:
            self.superclass._load_methods()

        for i in range(self.methods_ptr_count.value):
            method = self.methods_ptr[i]
            name = libobjc.method_getName(method).name.decode("utf-8")
            self.instance_method_ptrs[name] = method

            first, *rest = name.split(":")
            # Selectors end in a colon iff the method takes arguments.
            # Because of this, rest must either be empty (method takes no arguments)
            # or the last element must be an empty string (method takes arguments).
            assert not rest or rest[-1] == ""

            try:
                partial = self.partial_methods[first]
            except KeyError:
                if self.superclass is None:
                    super_partial = None
                else:
                    super_partial = self.superclass.partial_methods.get(first)

                partial = self.partial_methods[first] = ObjCPartialMethod(first)
                if super_partial is not None:
                    partial.methods.update(super_partial.methods)

            # order is rest without the dummy "" part
            order = rest[:-1]
            partial.methods[frozenset(rest)] = (name, order)


class ObjCMetaClass(ObjCClass):
    """Python wrapper for an Objective-C metaclass.

    :class:`ObjCMetaClass` is a subclass of :class:`ObjCClass` and supports almost exactly the same operations
    and methods. However, there is usually no need to look up a metaclass manually.
    The main reason why :class:`ObjCMetaClass` is a separate class is to differentiate it from :class:`ObjCClass`
    in the :func:`repr`. (Otherwise there would be no way to tell classes and metaclasses apart,
    since metaclasses are also classes, and have exactly the same name as their corresponding class.)
    """

    def __new__(cls, name_or_ptr):
        """The constructor accepts either the name of an Objective-C metaclass to look up
        (as :class:`str` or :class:`bytes`), or a pointer to an existing metaclass object
        (in any form accepted by :class:`ObjCInstance`).

        If given a pointer, it must refer to an Objective-C metaclass; pointers to other objects are not accepted.
        (Use :class:`ObjCInstance` to wrap a pointer that might also refer to other kinds of objects.)
        Creating an :class:`ObjCMetaClass` from a ``Nil`` pointer returns ``None``.
        """

        if isinstance(name_or_ptr, (bytes, str)):
            name = ensure_bytes(name_or_ptr)
            ptr = libobjc.objc_getMetaClass(name)
            if ptr.value is None:
                raise NameError("Objective-C metaclass {} not found".format(name))
        else:
            ptr = cast(name_or_ptr, Class)
            if ptr.value is None:
                raise ValueError("Cannot create ObjCMetaClass for nil pointer")
            elif not object_isClass(ptr) or not libobjc.class_isMetaClass(ptr):
                raise ValueError("Pointer {} ({:#x}) does not refer to a metaclass".format(ptr, ptr.value))

        return super().__new__(cls, ptr)


register_ctype_for_type(ObjCInstance, objc_id)
register_ctype_for_type(ObjCClass, Class)


NSObject = ObjCClass('NSObject')
NSObject.declare_property('debugDescription')
NSObject.declare_property('description')
NSNumber = ObjCClass('NSNumber')
NSDecimalNumber = ObjCClass('NSDecimalNumber')
NSString = ObjCClass('NSString')
NSString.declare_property('UTF8String')
NSData = ObjCClass('NSData')
NSArray = ObjCClass('NSArray')
NSMutableArray = ObjCClass('NSMutableArray')
NSDictionary = ObjCClass('NSDictionary')
NSMutableDictionary = ObjCClass('NSMutableDictionary')
Protocol = ObjCClass('Protocol')


def py_from_ns(nsobj):
    """Convert a Foundation object into an equivalent Python object if possible.

    Currently supported types:

    * :class:`~rubicon.objc.runtime.objc_id`: Wrapped in an :class:`ObjCInstance` and converted as below
    * :class:`NSString`: Converted to :class:`str`
    * :class:`NSData`: Converted to :class:`bytes`
    * :class:`NSDecimalNumber`: Converted to :class:`decimal.Decimal`
    * :class:`NSDictionary`: Converted to :class:`dict`, with all keys and values converted recursively
    * :class:`NSArray`: Converted to :class:`list`, with all elements converted recursively
    * :class:`NSNumber`: Converted to a :class:`bool`, :class:`int` or :class:`float` based on the type of its contents

    Other objects are returned unmodified as an :class:`ObjCInstance`.
    """

    if isinstance(nsobj, (objc_id, Class)):
        nsobj = ObjCInstance(nsobj)
    if not isinstance(nsobj, ObjCInstance):
        return nsobj

    if nsobj.isKindOfClass(NSDecimalNumber):
        return decimal.Decimal(str(nsobj.descriptionWithLocale(None)))
    elif nsobj.isKindOfClass(NSNumber):
        # Choose the property to access based on the type encoding. The actual conversion is done by ctypes.
        # Signed and unsigned integers are in separate cases to prevent overflow with unsigned long longs.
        objc_type = nsobj.objCType
        if objc_type == b'B':
            return nsobj.boolValue
        elif objc_type in b'csilq':
            return nsobj.longLongValue
        elif objc_type in b'CSILQ':
            return nsobj.unsignedLongLongValue
        elif objc_type in b'fd':
            return nsobj.doubleValue
        else:
            raise TypeError(
                'NSNumber containing unsupported type {!r} cannot be converted to a Python object'
                .format(objc_type)
            )
    elif nsobj.isKindOfClass(NSString):
        return str(nsobj)
    elif nsobj.isKindOfClass(NSData):
        # Despite the name, string_at converts the data at the address to a bytes object, not str.
        return string_at(send_message(nsobj, 'bytes', restype=POINTER(c_uint8), argtypes=[]), nsobj.length)
    elif nsobj.isKindOfClass(NSDictionary):
        return {py_from_ns(k): py_from_ns(v) for k, v in nsobj.items()}
    elif nsobj.isKindOfClass(NSArray):
        return [py_from_ns(o) for o in nsobj]
    else:
        return nsobj


def ns_from_py(pyobj):
    """Convert a Python object into an equivalent Foundation object. The returned object is autoreleased.

    This function is also available under the name :func:`at`, because its functionality is very similar to that of the
    Objective-C ``@`` operator and literals.

    Currently supported types:

    * ``None``, :class:`ObjCInstance`: Returned as-is
    * :class:`enum.Enum`: Replaced by their :attr:`~enum.Enum.value` and converted as below
    * :class:`str`: Converted to :class:`NSString`
    * :class:`bytes`: Converted to :class:`NSData`
    * :class:`decimal.Decimal`: Converted to :class:`NSDecimalNumber`
    * :class:`dict`: Converted to :class:`NSDictionary`, with all keys and values converted recursively
    * :class:`list`: Converted to :class:`NSArray`, with all elements converted recursively
    * :class:`bool`, :class:`int`, :class:`float`: Converted to :class:`NSNumber`

    Other types cause a :class:`TypeError`.
    """

    if isinstance(pyobj, enum.Enum):
        pyobj = pyobj.value

    # Many Objective-C method calls here use the convert_result=False kwarg to disable automatic conversion of
    # return values, because otherwise most of the Objective-C objects would be converted back to Python objects.
    if pyobj is None or isinstance(pyobj, ObjCInstance):
        return pyobj
    elif isinstance(pyobj, str):
        return ObjCInstance(NSString.stringWithUTF8String_(pyobj.encode('utf-8'), convert_result=False))
    elif isinstance(pyobj, bytes):
        return ObjCInstance(NSData.dataWithBytes(pyobj, length=len(pyobj)))
    elif isinstance(pyobj, decimal.Decimal):
        return ObjCInstance(NSDecimalNumber.decimalNumberWithString_(pyobj.to_eng_string(), convert_result=False))
    elif isinstance(pyobj, dict):
        dikt = NSMutableDictionary.dictionaryWithCapacity(len(pyobj))
        for k, v in pyobj.items():
            dikt.setObject(v, forKey=k)
        return dikt
    elif isinstance(pyobj, list):
        array = NSMutableArray.arrayWithCapacity(len(pyobj))
        for v in pyobj:
            array.addObject(v)
        return array
    elif isinstance(pyobj, bool):
        return ObjCInstance(NSNumber.numberWithBool_(pyobj, convert_result=False))
    elif isinstance(pyobj, int):
        return ObjCInstance(NSNumber.numberWithLong_(pyobj, convert_result=False))
    elif isinstance(pyobj, float):
        return ObjCInstance(NSNumber.numberWithDouble_(pyobj, convert_result=False))
    else:
        raise TypeError(
            "Don't know how to convert a {cls.__module__}.{cls.__qualname__} to a Foundation object"
            .format(cls=type(pyobj))
        )


at = ns_from_py


@for_objcclass(Protocol)
class ObjCProtocol(ObjCInstance):
    """Python wrapper for an Objective-C protocol."""

    @property
    def name(self):
        """The name of this protocol, as a :class:`str`."""

        return libobjc.protocol_getName(self).decode('utf-8')

    @property
    def protocols(self):
        """The protocols that this protocol extends."""

        out_count = c_uint()
        protocols_ptr = libobjc.protocol_copyProtocolList(self, byref(out_count))
        return tuple(ObjCProtocol(protocols_ptr[i]) for i in range(out_count.value))

    def __new__(cls, name_or_ptr, bases=None, ns=None):
        """The constructor accepts either the name of an Objective-C protocol to look up
        (as :class:`str` or :class:`bytes`), or a pointer to an existing protocol object
        (in any form accepted by :class:`ObjCInstance`).

        If given a pointer, it must refer to an Objective-C protocol; pointers to other objects are not accepted.
        (Use :class:`ObjCInstance` to wrap a pointer that might also refer to other kinds of objects.)
        Creating an :class:`ObjCProtocol` from a ``nil`` pointer returns ``None``.

        :class:`ObjCProtocol` can also be called like :class:`type`, with three arguments
        (name, bases list, namespace mapping). This form is called implicitly by Python's ``class`` syntax,
        and is used to create a new Objective-C protocol from Python (see :ref:`custom-classes-and-protocols`).
        The bases list can contain any number of :class:`ObjCProtocol` objects to be extended by the new protocol.
        """

        if (bases is None) ^ (ns is None):
            raise TypeError('ObjCProtocol arguments 2 and 3 must be given together')

        if bases is None and ns is None:
            if isinstance(name_or_ptr, (bytes, str)):
                name = ensure_bytes(name_or_ptr)
                ptr = libobjc.objc_getProtocol(name)
                if ptr.value is None:
                    raise NameError('Objective-C protocol {} not found'.format(name))
            else:
                ptr = cast(name_or_ptr, objc_id)
                if ptr.value is None:
                    raise ValueError('Cannot create ObjCProtocol for nil pointer')
                elif not send_message(ptr, 'isKindOfClass:', Protocol, restype=c_bool, argtypes=[objc_id]):
                    raise ValueError('Pointer {} ({:#x}) does not refer to a protocol'.format(ptr, ptr.value))
        else:
            name = ensure_bytes(name_or_ptr)

            if libobjc.objc_getProtocol(name).value is not None:
                raise RuntimeError('An Objective-C protocol named {!r} already exists'.format(name))

            # Check that all bases are protocols.
            for base in bases:
                if not isinstance(base, ObjCProtocol):
                    raise TypeError(
                        'An Objective-C protocol can only extend ObjCProtocol objects, '
                        'not {cls.__module__}.{cls.__qualname__}'
                        .format(cls=type(base))
                    )

            # Allocate the protocol object.
            ptr = libobjc.objc_allocateProtocol(name)
            if ptr is None:
                raise RuntimeError('Protocol allocation failed')

            # Adopt all the protocols.
            for proto in bases:
                libobjc.protocol_addProtocol(ptr, proto)

            # Register all methods and properties.
            for attr_name, obj in ns.items():
                if hasattr(obj, 'protocol_register'):
                    obj.protocol_register(ptr, attr_name)

            # Register the protocol object
            libobjc.objc_registerProtocol(ptr)

        return super().__new__(cls, ptr)

    def __repr__(self):
        return '<{cls.__module__}.{cls.__qualname__}: {self.name} at {self.ptr.value:#x}>'.format(
            cls=type(self), self=self)

    def __instancecheck__(self, instance):
        """Check whether the given object conforms to this protocol.

        If the given object is not an Objective-C object, ``False`` is returned.

        This method allows using :class:`ObjCProtocol`\\s as the second argument of :func:`isinstance`:
        ``isinstance(obj, NSCopying)`` is equivalent to ``obj.conformsToProtocol(NSCopying)``.
        """

        if isinstance(instance, ObjCInstance):
            return bool(instance.conformsToProtocol(self))
        else:
            return False

    def __subclasscheck__(self, subclass):
        """Check whether the given class or protocol conforms to this protocol.

        If the given object is not an Objective-C class or protocol, :class:`TypeError` is raised.

        This method allows using :class:`ObjCProtocol`\\s as the second argument of :func:`issubclass`:
        ``issubclass(cls, NSCopying)`` is equivalent to ``cls.conformsToProtocol(NSCopying)``,
        and ``issubclass(proto, NSCopying)`` is equivalent to ``protocol_conformsToProtocol(proto, NSCopying))``.
        """

        if isinstance(subclass, ObjCClass):
            return bool(subclass.conformsToProtocol(self))
        elif isinstance(subclass, ObjCProtocol):
            return bool(libobjc.protocol_conformsToProtocol(subclass, self))
        else:
            raise TypeError(
                'issubclass(X, {self!r}) arg 1 must be an ObjCClass or ObjCProtocol, '
                'not {tp.__module__}.{tp.__qualname__}'
                .format(self=self, tp=type(subclass))
            )


# Need to use a different name to avoid conflict with the NSObject class.
# NSObjectProtocol is also the name that Swift uses when importing the NSObject protocol.
NSObjectProtocol = ObjCProtocol('NSObject')


# When a Python object is assigned to a new ObjCInstance attribute, the Python
# object should be kept alive for the lifetime of the ObjCInstance. This is
# done by wrapping the Python object as a WrappedPyObject that increments
# the reference count during assignment and decrements it when the WrappedPyObject
# and the owning ObjCInstance are deallocated.
#
# The methods of the class defined below are decorated with
# rawmethod() instead of method() because WrappedPyObject
# are created inside of ObjCInstance's __new__ method and we have
# to be careful to not create another ObjCInstance here (which
# happens when the usual method decorator turns the self argument
# into an ObjCInstance), or else get trapped in an infinite recursion.

# Try to reuse an existing WrappedPyObject class.
# This allows reloading the module without having to restart
# the interpreter, although any changes to WrappedPyObject
# itself are only applied after a restart of course.

try:
    WrappedPyObject = ObjCClass("WrappedPyObject")
except NameError:
    class WrappedPyObject(NSObject):

        wrapped_pointer = objc_ivar(c_void_p)

        @objc_rawmethod
        def initWithObjectId_(self, cmd, address):
            self = send_message(self, 'init', restype=objc_id, argtypes=[])
            if self is not None:
                pyo = cast(address, py_object)
                _keep_alive_objects[(self.value, address.value)] = pyo.value
                set_ivar(self, 'wrapped_pointer', address)
            return self.value

        @objc_rawmethod
        def dealloc(self, cmd) -> None:
            address = get_ivar(self, 'wrapped_pointer')
            if address.value:
                del _keep_alive_objects[(self.value, address.value)]

        @objc_rawmethod
        def finalize(self, cmd) -> None:
            # Called instead of dealloc if using garbage collection.
            # (which would have to be explicitly started with
            # objc_startCollectorThread(), so probably not too much reason
            # to have this here, but I guess it can't hurt.)
            address = get_ivar(self, 'wrapped_pointer')
            if address.value:
                del _keep_alive_objects[(self.value, address.value)]
            send_super(__class__, self, 'finalize', restype=None, argtypes=[])


def objc_const(dll, name):
    """Create an :class:`ObjCInstance` from a global pointer variable in a :class:`~ctypes.CDLL`.

    This function is most commonly used to access constant object pointers defined by a library/framework, such as
    `NSCocoaErrorDomain <https://developer.apple.com/documentation/foundation/nscocoaerrordomain?language=objc>`__.
    """

    return ObjCInstance(objc_id.in_dll(dll, name))


_cfunc_type_block_invoke = CFUNCTYPE(c_void_p, c_void_p)
_cfunc_type_block_dispose = CFUNCTYPE(c_void_p, c_void_p)
_cfunc_type_block_copy = CFUNCTYPE(c_void_p, c_void_p, c_void_p)


class ObjCBlockStruct(Structure):
    _fields_ = [
        ('isa', c_void_p),
        ('flags', c_int),
        ('reserved', c_int),
        ('invoke', _cfunc_type_block_invoke),
        ('descriptor', c_void_p),
    ]


class BlockDescriptor(Structure):
    _fields_ = [
        ('reserved', c_ulong),
        ('size', c_ulong),
        ('copy_helper', _cfunc_type_block_copy),
        ('dispose_helper', _cfunc_type_block_dispose),
        ('signature', c_char_p),
    ]


class BlockLiteral(Structure):
    _fields_ = [
        ('isa', c_void_p),
        ('flags', c_int),
        ('reserved', c_int),
        ('invoke', c_void_p),  # NB: this must be c_void_p due to variadic nature
        ('descriptor', c_void_p)
    ]


def create_block_descriptor_struct(has_helpers, has_signature):
    descriptor_fields = [
        ('reserved', c_ulong),
        ('size', c_ulong),
    ]
    if has_helpers:
        descriptor_fields.extend([
            ('copy_helper', _cfunc_type_block_copy),
            ('dispose_helper', _cfunc_type_block_dispose),
        ])
    if has_signature:
        descriptor_fields.extend([
            ('signature', c_char_p),
        ])
    return type(
        'ObjCBlockDescriptor',
        (Structure, ),
        {'_fields_': descriptor_fields}
    )


def cast_block_descriptor(block):
    descriptor_struct = create_block_descriptor_struct(block.has_helpers, block.has_signature)
    return cast(block.struct.contents.descriptor, POINTER(descriptor_struct))


AUTO = object()


class BlockConsts:
    HAS_COPY_DISPOSE = 1 << 25
    HAS_CTOR = 1 << 26
    IS_GLOBAL = 1 << 28
    HAS_STRET = 1 << 29
    HAS_SIGNATURE = 1 << 30


class ObjCBlock:
    """Python wrapper for an Objective-C block object.

    This class is used to manually wrap an Objective-C block so that it can be called from Python. Usually Rubicon will
    do this automatically, if the block object was returned from an Objective-C method whose return type is declared
    to be a block type. If this automatic detection fails, for example if the method's return type is generic ``id``,
    Rubicon has no way to tell that the object in question is a block rather than a regular Objective-C object.
    In that case, the object needs to be manually wrapped using :class:`ObjCBlock`.
    """

    def __init__(self, pointer, restype=AUTO, *argtypes):
        """The constructor takes a block object, which can be either an :class:`ObjCInstance`, or a raw
        :class:`~rubicon.objc.runtime.objc_id` pointer.

        .. note::

            :class:`~rubicon.objc.runtime.objc_block` is also accepted, because it is a subclass of
            :class:`~rubicon.objc.runtime.objc_id`). Normally you do not need to make use of this,
            because in most cases Rubicon will automatically convert :class:`~rubicon.objc.runtime.objc_block`\\s
            to a callable object.

        In most cases, Rubicon can automatically determine the block's return type and parameter types.
        If a block object doesn't have return/parameter type information at runtime, Rubicon will raise an error when
        attempting to convert it. In that case, you need to explicitly pass the correct return type and parameter types
        to :class:`ObjCBlock` using the ``restype`` and ``argtypes`` parameters.
        """

        if isinstance(pointer, ObjCInstance):
            pointer = pointer.ptr
        self.pointer = pointer
        self.struct = cast(self.pointer, POINTER(ObjCBlockStruct))
        self.has_helpers = self.struct.contents.flags & BlockConsts.HAS_COPY_DISPOSE
        self.has_signature = self.struct.contents.flags & BlockConsts.HAS_SIGNATURE
        self.descriptor = cast_block_descriptor(self)
        self.signature = self.descriptor.contents.signature if self.has_signature else None
        if restype is AUTO:
            if argtypes:
                raise ValueError('Cannot use argtypes with restype AUTO')
            if not self.has_signature:
                raise ValueError('Cannot use AUTO types for blocks without signatures')
            restype, *argtypes = ctypes_for_method_encoding(self.signature)
        self.struct.contents.invoke.restype = ctype_for_type(restype)
        self.struct.contents.invoke.argtypes = (objc_id, ) + tuple(ctype_for_type(arg_type) for arg_type in argtypes)

    def __repr__(self):
        representation = '<ObjCBlock@{}'.format(hex(addressof(self.pointer)))
        if self.has_helpers:
            representation += ',has_helpers'
        if self.has_signature:
            representation += ',has_signature:' + self.signature
        representation += '>'
        return representation

    def __call__(self, *args):
        """Invoke the block object with the given arguments.

        The arguments and return value are converted from/to Python objects according to the default ``ctypes`` rules,
        based on the block's return and parameter types.
        """

        return self.struct.contents.invoke(self.pointer, *args)


class ObjCBlockInstance(ObjCInstance):
    def __call__(self, *args):
        return self.block(*args)


_NSConcreteStackBlock = (c_void_p * 32).in_dll(libc, "_NSConcreteStackBlock")


NOTHING = object()


class Block:
    """A wrapper that exposes a Python callable object to Objective-C as a block.

    .. note::

        :class:`Block` instances are currently *not* callable from Python, unlike :class:`ObjCBlock`.
    """

    _keep_alive_blocks_ = {}

    def __init__(self, func, restype=NOTHING, *argtypes):
        """The constructor accepts any Python callable object.

        If the callable has parameter and return type annotations, they are used as the block's parameter and return
        types. This allows using :class:`Block` as a decorator:

        .. code-block:: python

            @Block
            def the_block(arg: NSInteger) -> NSUInteger:
                return abs(arg)

        For callables without type annotations, the parameter and return types need to be passed to the :class:`Block`
        constructor in the ``restype`` and ``argtypes`` arguments:

        .. code-block:: python

            the_block = Block(abs, NSUInteger, NSInteger)
        """

        if not callable(func):
            raise TypeError('Blocks must be callable')

        self.func = func

        if restype is NOTHING:
            if argtypes:
                # This can't happen unless the caller does something hacky, but guard against it just in case.
                raise ValueError('Cannot pass argtypes without a restype')

            # No explicit restype/argtypes were passed into the constructor,
            # so try to extract them from the function's type annotations.

            try:
                signature = inspect.signature(func)
            except (TypeError, ValueError):
                raise ValueError(
                    'Could not retrieve function signature information - '
                    'please pass return and argument types directly into Block'
                )

            if signature.return_annotation == inspect.Signature.empty:
                raise ValueError(
                    'Function has no return type annotation - '
                    'please add one, or pass return and argument types directly into Block'
                )

            restype = signature.return_annotation
            argtypes = []
            for name, param in signature.parameters.items():
                if param.annotation == inspect.Parameter.empty:
                    raise ValueError(
                        'Function has no argument type annotation for parameter ' + repr(name)
                        + ' - please add one, or pass return and argument types directly into Block'
                    )

                argtypes.append(param.annotation)

        signature = tuple(ctype_for_type(tp) for tp in argtypes)

        restype = ctype_for_type(restype)

        self.cfunc_type = CFUNCTYPE(restype, c_void_p, *signature)

        self.literal = BlockLiteral()
        self.literal.isa = addressof(_NSConcreteStackBlock)
        self.literal.flags = BlockConsts.HAS_STRET | BlockConsts.HAS_SIGNATURE | BlockConsts.HAS_COPY_DISPOSE
        self.literal.reserved = 0
        self.cfunc_wrapper = self.cfunc_type(self.wrapper)
        self.literal.invoke = cast(self.cfunc_wrapper, c_void_p)
        self.descriptor = BlockDescriptor()
        self.descriptor.reserved = 0
        self.descriptor.size = sizeof(BlockLiteral)

        self.cfunc_copy_helper = _cfunc_type_block_copy(self.copy_helper)
        self.cfunc_dispose_helper = _cfunc_type_block_dispose(self.dispose_helper)
        self.descriptor.copy_helper = self.cfunc_copy_helper
        self.descriptor.dispose_helper = self.cfunc_dispose_helper

        self.descriptor.signature = encoding_for_ctype(restype) + b'@?' + b''.join(
            encoding_for_ctype(arg) for arg in signature
        )
        self.literal.descriptor = cast(byref(self.descriptor), c_void_p)
        self.block = cast(byref(self.literal), objc_block)
        self._as_parameter_ = self.block

    def wrapper(self, instance, *args):
        return self.func(*args)

    def dispose_helper(self, dst):
        Block._keep_alive_blocks_.pop(dst, None)

    def copy_helper(self, dst, src):
        # Update our keepalive table because objc just informed us that it
        # took ownership of a block/copied a block we are concerned with.
        # Note that sometime later we can expect calls to dispose_helper
        # for each of the 'dst' blocks objc told us about, but until then we
        # need to make sure the python code they reference stays in memory,
        # so basically put self in a class variable dictionary so it is
        # guaranteed to stay around until dispose_helper tells us they are all
        # gone.
        Block._keep_alive_blocks_[dst] = self
