Decorator to process a callback prototype.
    
    A callback prototype is a function whose signature includes all the values
    that will be passed by the callback API in question.
    
    The original function will be returned, with a ``prototype.adapt`` attribute
    which can be used to prepare third party callbacks.

def callback_prototype(prototype):
    """Decorator to process a callback prototype.
    
    A callback prototype is a function whose signature includes all the values
    that will be passed by the callback API in question.
    
    The original function will be returned, with a ``prototype.adapt`` attribute
    which can be used to prepare third party callbacks.
    """
    protosig = signature(prototype)
    positional, keyword = [], []
    for name, param in protosig.parameters.items():
        if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
            raise TypeError("*args/**kwargs not supported in prototypes")

        if (param.default is not Parameter.empty) \
            or (param.kind == Parameter.KEYWORD_ONLY):
            keyword.append(name)
        else:
            positional.append(name)
        
    kwargs = dict.fromkeys(keyword)
    def adapt(callback):
        """Introspect and prepare a third party callback."""
        sig = signature(callback)
        try:
            # XXX: callback can have extra optional parameters - OK?
            sig.bind(*positional, **kwargs)
            return callback
        except TypeError:
            pass
        
        # Match up arguments
        unmatched_pos = positional[:]
        unmatched_kw = kwargs.copy()
        unrecognised = []
        # TODO: unrecognised parameters with default values - OK?
        for name, param in sig.parameters.items():
            # print(name, param.kind) #DBG
            if param.kind == Parameter.POSITIONAL_ONLY:
                if len(unmatched_pos) > 0:
                    unmatched_pos.pop(0)
                else:
                    unrecognised.append(name)
            elif param.kind == Parameter.POSITIONAL_OR_KEYWORD:
                if (param.default is not Parameter.empty) and (name in unmatched_kw):
                    unmatched_kw.pop(name)
                elif len(unmatched_pos) > 0:
                    unmatched_pos.pop(0)    
                else:
                    unrecognised.append(name)
            elif param.kind == Parameter.VAR_POSITIONAL:
                unmatched_pos = []
            elif param.kind == Parameter.KEYWORD_ONLY:
                if name in unmatched_kw:
                    unmatched_kw.pop(name)
                else:
                    unrecognised.append(name)
            else:  # VAR_KEYWORD
                unmatched_kw = {}
        
            # print(unmatched_pos, unmatched_kw, unrecognised) #DBG
        
        if unrecognised:
            raise TypeError("Function {!r} had unmatched arguments: {}".format(callback, unrecognised))

        n_positional = len(positional) - len(unmatched_pos)

        @wraps(callback)
        def adapted(*args, **kwargs):
            """Wrapper for third party callbacks that discards excess arguments"""
#            print(args, kwargs)
            args = args[:n_positional]
            for name in unmatched_kw:
                # XXX: Could name not be in kwargs?
                kwargs.pop(name)
#            print(args, kwargs, unmatched_pos, cut_positional, unmatched_kw)
            return callback(*args, **kwargs)
        
        return adapted

    prototype.adapt = adapt
    return prototype