"""
Python <-> Objective-C bridge (PyObjC)

This module defines the core interfaces of the Python<->Objective-C bridge.
"""

__all__ = [
    "IBOutlet",
    "IBAction",
    "accessor",
    "Accessor",
    "typedAccessor",
    "callbackFor",
    "selectorFor",
    "synthesize",
    "namedselector",
    "typedSelector",
    "namedSelector",
    "instancemethod",
    "signature",
    "IBInspectable",
    "IB_DESIGNABLE",
    "callbackPointer",
]

import sys
import textwrap
import warnings

from objc._objc import (
    _C_ID,
    _C_NSBOOL,
    _C_SEL,
    _C_NSUInteger,
    _closurePointer,
    _makeClosure,
    ivar,
    selector,
)

from inspect import getfullargspec

_C_NSRange = [b"{_NSRange=II}", b"{_NSRange=QQ}"][sys.maxsize > 2**32]


#
# Interface builder support.
#
def IB_DESIGNABLE(cls):
    """
    Class decorator for annotating that a class can be used in Interface Builder.

    The decorator doesn't do anything.
    """
    return cls


def IBInspectable(prop):
    """
    Decorator for an Objective-C property to tell IB that the updated in IB.

    The decorator doesn't do anything
    """
    return prop


def IBOutlet(name=None):
    """
    Create an instance variable that can be used as an outlet in
    Interface Builder.
    """
    if name is None:
        return ivar(isOutlet=1)
    else:
        return ivar(name, isOutlet=1)


def IBAction(func):
    """
    Return an Objective-C method object that can be used as an action
    in Interface Builder.
    """
    if func is None:
        raise TypeError("IBAction argument must be a callable")
    return selector(func, signature=b"v@:@")


def instancemethod(func):
    if func is None:
        raise TypeError("instancemethod argument must be a callable")
    return selector(func, isClassMethod=False)


def accessor(func, typeSignature=b"@"):
    """
    Return an Objective-C method object that is conformant with key-value coding
    and key-value observing.
    """
    (
        args,
        varargs,
        varkw,
        defaults,
        kwonlyargs,
        _kwonlydefaults,
        _annotations,
    ) = getfullargspec(func)
    funcName = func.__name__
    maxArgs = len(args)
    minArgs = maxArgs - len(defaults or ())
    # implicit self
    selArgs = 1 + funcName.count("_")
    if varargs is not None or varkw is not None or kwonlyargs:
        raise TypeError(
            "%s can not be an accessor because it accepts varargs, varkw or kwonly"
            % (funcName,)
        )

    if not (minArgs <= selArgs <= maxArgs):
        if minArgs == maxArgs:
            raise TypeError(
                "%s expected to take %d args, but must accept %d "
                "from Objective-C (implicit self plus count of underscores)"
                % (funcName, maxArgs, selArgs)
            )
        else:
            raise TypeError(
                "%s expected to take between %d and %d args, but must accept %d "
                "from Objective-C (implicit self plus count of underscores)"
                % (funcName, minArgs, maxArgs, selArgs)
            )

    if selArgs == 3:
        if funcName.startswith("validate") and funcName.endswith("_error_"):
            return selector(func, signature=_C_NSBOOL + b"@:N^@o^@")

        if funcName.startswith("insertObject_in") and funcName.endswith("AtIndex_"):
            return selector(func, signature=b"v@:" + typeSignature + _C_NSUInteger)
        elif funcName.startswith("replaceObjectIn") and funcName.endswith(
            "AtIndex_withObject_"
        ):
            return selector(func, signature=b"v@:" + _C_NSUInteger + typeSignature)

        elif funcName.startswith("get") and funcName.endswith("_range_"):
            return selector(func, signature=b"v@:o^@" + _C_NSRange)

        elif funcName.startswith("insert") and funcName.endswith("_atIndexes_"):
            return selector(func, signature=b"v@:@@")

        elif funcName.startswith("replace") and "AtIndexes_with" in funcName:
            return selector(func, signature=b"v@:@@")

        # pass through to "too many arguments"

    elif selArgs == 2:
        if funcName.startswith("objectIn") and funcName.endswith("AtIndex_"):
            return selector(func, signature=typeSignature + b"@:" + _C_NSUInteger)
        elif funcName.startswith("removeObjectFrom") and funcName.endswith("AtIndex_"):
            return selector(func, signature=b"v@:" + _C_NSUInteger)
        elif funcName.startswith("remove") and funcName.endswith("AtIndexes_"):
            return selector(func, signature=b"v@:@")
        elif funcName.endswith("AtIndexes_"):
            return selector(func, signature=b"@@:@")
        elif funcName.startswith("memberOf"):
            return selector(func, signature=_C_NSBOOL + b"@:" + typeSignature)
        elif funcName.startswith("add") and funcName.endswith("Object_"):
            return selector(func, signature=b"v@:" + typeSignature)
        elif funcName.startswith("add"):
            return selector(func, signature=b"v@:@")
        elif funcName.startswith("intersect"):
            return selector(func, signature=b"v@:@")

        return selector(func, signature=b"v@:" + typeSignature)

    elif selArgs == 1:
        if funcName.startswith("countOf"):
            typeSignature = _C_NSUInteger
        elif funcName.startswith("enumerator"):
            typeSignature = b"@"

        return selector(func, signature=typeSignature + b"@:")

    raise TypeError(f"{funcName} not recognized as an accessor")


def typedSelector(signature):
    def _typedSelector(func):
        if func is None:
            raise TypeError("typedSelector() function argument must be a callable")
        if isinstance(func, classmethod):
            return selector(func.__func__, signature=signature, isClassMethod=True)

        return selector(func, signature=signature)

    return _typedSelector


def namedSelector(name, signature=None):
    """
    Decorator for overriding the Objective-C SEL for a method, usage:

        @namedSelector("foo:bar:")
        def foobar(self, foo, bar):
            return foo + bar
    """
    if signature is not None:

        def _namedselector(func):
            if func is None:
                raise TypeError("namedSelector argument must be a callable")
            if isinstance(func, classmethod):
                return selector(
                    func.__func__,
                    selector=name,
                    signature=signature,
                    isClassMethod=True,
                )
            return selector(func, selector=name, signature=signature)

    else:

        def _namedselector(func):
            if func is None:
                raise TypeError("namedSelector argument must be a callable")
            if isinstance(func, classmethod):
                return selector(
                    func.__func__,
                    selector=name,
                    isClassMethod=True,
                )
            return selector(func, selector=name)

    return _namedselector


def namedselector(name, signature=None):
    warnings.warn(
        "use objc.namedSelector instead of objc.namedselector",
        DeprecationWarning,
        stacklevel=2,
    )
    return namedSelector(name, signature)


def typedAccessor(typeSignature):
    """
    Decorator for creating a typed accessor, usage:

        @typedAccessor('i')
        def someIntegerAccessor(self):
            return self.someInteger

        @typedAccessor('i')
        def setSomeIntegerAccessor_(self, anInteger):
            self.someInteger = anInteger
    """

    def _typedAccessor(func):
        return accessor(func, typeSignature)

    return _typedAccessor


def Accessor(func):
    warnings.warn("Use objc.accessor instead of objc.Accessor", DeprecationWarning)
    return accessor(func)


#
# Callback support
#
def callbackFor(callable, argIndex=-1):  # noqa: A002
    """
    Decorator for converting a function into an object that can be used
    as a callback function for (Objective-)C API's that take such a beast
    as one of their arguments.

    Note that using this decorator for methods is unsupported and that this
    decorator is optional when the callback isn't stored by the called function

    Usage::

        @objc.callbackFor(NSArray.sortedArrayUsingFunction_context_)
        def compare(left, right, context):
            return 1
    """

    def addClosure(function):
        closure, meta = _makeClosure(function, callable, argIndex)
        function.pyobjc_closure = closure
        function.__metadata__ = lambda: meta
        return function

    return addClosure


def callbackPointer(closure):
    """
    Return a value for "closure" that can be passed to a function
    expecting a "void *" argument.
    """
    if not hasattr(closure, "pyobjc_closure"):
        raise ValueError("Object is not decorated with 'callbackFor'")

    return _closurePointer(closure.pyobjc_closure)


def selectorFor(callable, argIndex=-1):  # noqa: A002
    """
    Decorator that makes sure that the method has the right signature to be
    used as the selector argument to the specified method.

    Usage::

        @objc.selectorFor(NSApplication.beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_)  # noqa: B950
        def sheetDidEnd_returnCode_contextInfo_(self, sheet, returnCode, info):
            pass
    """
    if argIndex == -1:
        for arg in callable.__metadata__()["arguments"]:
            if arg["type"] == _C_SEL and "sel_of_type" in arg:
                signature = arg["sel_of_type"]
                break
        else:
            raise ValueError("Not a selector argument with type information")

    else:
        try:
            signature = callable.__metadata__()["arguments"][argIndex]["sel_of_type"]
        except (IndexError, KeyError):
            raise ValueError("Not a selector argument with type information")

    def addSignature(function):
        return selector(function, signature=signature)

    return addSignature


def synthesize(
    name, copy=False, readwrite=True, type=_C_ID, ivarName=None  # noqa: A002
):
    """
    Use this in a class dictionary to syntheze simple setting/setter methods.

    Note: this is only necessary to get proper behaviour when Key-Value coding
    is used and special features (like copying) are needed

    usage::

        class MyClass (NSObject):
            objc.synthesize('someTitle', copy=True)

    """
    if not name:
        raise ValueError("Empty property name")

    if ivarName is None:
        ivarName = "_" + name

    classDict = sys._getframe(1).f_locals

    setterName = f"set{name[0].upper()}{name[1:]}_"

    if copy:
        setter = textwrap.dedent(
            """
            def %(name)s(self, value):
                self.%(ivar)s = value.copy()
            """
            % {"name": setterName, "ivar": ivarName}
        )

    else:
        setter = textwrap.dedent(
            """
            def %(name)s(self, value):
                self.%(ivar)s = value
            """
            % {"name": setterName, "ivar": ivarName}
        )

    getter = textwrap.dedent(
        """
            def %(name)s(self):
                return self.%(ivar)s
            """
        % {"name": name, "ivar": ivarName}
    )

    if readwrite:
        exec(setter, globals(), classDict)

    exec(getter, globals(), classDict)

    classDict[ivarName] = ivar(type=type)


def signature(signature, **kw):
    """
    A Python method decorator that allows easy specification
    of Objective-C selectors.

    Usage::

        @objc.signature('i@:if')
        def methodWithX_andY_(self, x, y):
            return 0
    """
    warnings.warn(
        "Usage objc.typedSelector instead of objc.signature", DeprecationWarning
    )
    kw["signature"] = signature

    def makeSignature(func):
        return selector(func, **kw)

    return makeSignature
