"""
This module implements a callback function that is used by the C code to
add Python special methods to Objective-C classes with a suitable interface.
"""

from objc._objc import (
    _block_call,
    _rescanClass,
    currentBundle,
    lookUpClass,
    options,
    registerMetaDataForSelector,
    selector,
)
import PyObjCTools.KeyValueCoding as kvc
from objc._new import make_generic_new, NEW_MAP
from objc._transform import _selectorToKeywords
import array
import copy
import ctypes
import socket

__all__ = (
    "addConvenienceForClass",
    "registerABCForClass",
    "registerUnavailableMethod",
    "registerNewKeywords",
    "registerNewKeywordsFromSelector",
)

CLASS_METHODS = {}
CLASS_ABC = {}


options._getKey = kvc.getKey
options._setKey = kvc.setKey
options._getKeyPath = kvc.getKeyPath
options._setKeyPath = kvc.setKeyPath
options._ArrayType = array.ArrayType
options._deepcopy = copy.deepcopy
options._c_void_p = ctypes.c_void_p
options._socket_error = socket.error
options._socket_gaierror = socket.gaierror

del kvc

for method in (
    b"alloc",
    b"copy",
    b"copyWithZone:",
    b"mutableCopy",
    b"mutableCopyWithZone:",
):
    registerMetaDataForSelector(
        b"NSObject", method, {"retval": {"already_retained": True}}
    )


def register(f):
    options._class_extender = f


@register
def add_convenience_methods(cls, type_dict):
    """
    Add additional methods to the type-dict of subclass 'name' of
    'super_class'.

    CLASS_METHODS is a global variable containing a mapping from
    class name to a list of Python method names and implementation.

    Matching entries from both mappings are added to the 'type_dict'.
    """

    # Only add the generic __new__ to pure ObjC classes,
    # __new__ will be added to Python subclasses by
    # ._transform.
    if not cls.__has_python_implementation__ and type(  # noqa: E721
        cls.__mro__[1].__new__
    ) != type(lambda: None):
        type_dict["__new__"] = make_generic_new(cls)

    for nm, value in CLASS_METHODS.get(cls.__name__, ()):
        type_dict[nm] = value

    try:
        for abc_class in CLASS_ABC[cls.__name__]:
            abc_class.register(cls)
        del CLASS_ABC[cls.__name__]
    except KeyError:
        pass


def register(f):
    options._make_bundleForClass = f


@register
def makeBundleForClass():
    cb = currentBundle()

    def bundleForClass(cls):
        return cb

    return selector(bundleForClass, isClassMethod=True)


def registerUnavailableMethod(classname, selector):
    """
    Mark *selector* as unavailable for *classname*.
    """
    if not isinstance(selector, bytes):
        raise TypeError("selector should by a bytes object")
    selname = selector.decode()

    # This adds None as a replacement value instead of
    # registering metadata because NS_UNAVAILABLE is
    # used to mark abstract base classes with concrete
    # public subclasses.
    # addConvenienceForClass(classname, ((selname.replace(":", "_"), None),))
    registerMetaDataForSelector(
        classname.encode(),
        selector,
        {"suggestion": f"{selector.decode()!r} is NS_UNAVAILABLE"},
    )

    if selname.startswith("init"):
        kw = _selectorToKeywords(selname)
        NEW_MAP.setdefault(classname, {})[kw] = None


def registerNewKeywordsFromSelector(classname, selector):
    """
    Register keywords calculated from 'selector' as passible
    keyword arguments for __new__ for the given class. The
    selector should be an 'init' method.
    """
    if not isinstance(selector, bytes):
        raise TypeError("selector should by a bytes object")
    selname = selector.decode()
    kw = _selectorToKeywords(selname)
    NEW_MAP.setdefault(classname, {})[kw] = selname.replace(":", "_")


def registerNewKeywords(classname, keywords, methodname):
    """
    Register the keyword tuple 'keywords' as a set of keyword
    arguments for __new__ for the given class that will result
    in the invocation of the given method.

    Method should be either an init method or a class method.
    """
    if not isinstance(keywords, tuple) or not all(isinstance(x, str) for x in keywords):
        raise TypeError("keywords must be tuple of strings")
    NEW_MAP.setdefault(classname, {})[keywords] = methodname


def registerABCForClass(classname, *abc_class):
    """
    Register *classname* with the *abc_class*-es when
    the class becomes available.
    """
    try:
        CLASS_ABC[classname] += tuple(abc_class)
    except KeyError:
        CLASS_ABC[classname] = tuple(abc_class)

    options._mapping_count += 1
    _rescanClass(classname)


def addConvenienceForClass(classname, methods):
    """
    Add the list with methods to the class with the specified name
    """
    try:
        CLASS_METHODS[classname] += tuple(methods)
    except KeyError:
        CLASS_METHODS[classname] = tuple(methods)

    options._mapping_count += 1
    _rescanClass(classname)


#
# Helper functions for converting data item to/from a representation
# that is usable inside Cocoa data structures.
#
# In particular:
#
# - Python "None" is stored as +[NSNull null] because Cocoa containers
#   won't store NULL as a value (and this transformation is undone when
#   retrieving data)
#
# - When a getter returns NULL in Cocoa the queried value is not present,
#   that's converted to an exception in Python.
#

_NULL = lookUpClass("NSNull").null()


def container_wrap(v):
    if v is None:
        return _NULL
    return v


def container_unwrap(v, exc_type, *exc_args):
    if v is None:
        raise exc_type(*exc_args)
    elif v is _NULL:
        return None
    return v


#
#
# Misc. small helpers
#
#

addConvenienceForClass("NSNull", (("__bool__", lambda self: False),))

addConvenienceForClass(
    "NSEnumerator",
    (
        ("__iter__", lambda self: self),
        ("__next__", lambda self: container_unwrap(self.nextObject(), StopIteration)),
    ),
)


def __call__(self, *args, **kwds):
    return _block_call(self, self.__block_signature__, args, kwds)


addConvenienceForClass("NSBlock", (("__call__", __call__),))
