Pods/BlocksKit/BlocksKit/DynamicDelegate/NSObject+A2BlockDelegate.m
//
//  NSObject+A2BlockDelegate.m
//  BlocksKit
//

#import <objc/message.h>
#import "NSObject+A2BlockDelegate.h"
#import "NSObject+A2DynamicDelegate.h"

#pragma mark - Declarations and macros

extern Protocol *a2_dataSourceProtocol(Class cls);
extern Protocol *a2_delegateProtocol(Class cls);

#pragma mark - Functions

static BOOL bk_object_isKindOfClass(id obj, Class testClass)
{
	BOOL isKindOfClass = NO;
	Class cls = object_getClass(obj);
	while (cls && !isKindOfClass) {
		isKindOfClass = (cls == testClass);
		cls = class_getSuperclass(cls);
	}

	return isKindOfClass;
}

static Protocol *a2_protocolForDelegatingObject(id obj, Protocol *protocol)
{
	NSString *protocolName = NSStringFromProtocol(protocol);
	if ([protocolName hasSuffix:@"Delegate"]) {
		Protocol *p = a2_delegateProtocol([obj class]);
		if (p) return p;
	} else if ([protocolName hasSuffix:@"DataSource"]) {
		Protocol *p = a2_dataSourceProtocol([obj class]);
		if (p) return p;
	}

	return protocol;
}

static inline BOOL isValidIMP(IMP impl) {
#if defined(__arm64__)
    if (impl == NULL || impl == _objc_msgForward) return NO;
#else
    if (impl == NULL || impl == _objc_msgForward || impl == (IMP)_objc_msgForward_stret) return NO;
#endif
    return YES;
}

static BOOL addMethodWithIMP(Class cls, SEL oldSel, SEL newSel, IMP newIMP, const char *types, BOOL aggressive) {
	if (!class_addMethod(cls, oldSel, newIMP, types)) {
		return NO;
	}

	// We just ended up implementing a method that doesn't exist
	// (-[NSURLConnection setDelegate:]) or overrode a superclass
	// version (-[UIImagePickerController setDelegate:]).
	IMP parentIMP = NULL;
	Class superclass = class_getSuperclass(cls);
	while (superclass && !isValidIMP(parentIMP)) {
		parentIMP = class_getMethodImplementation(superclass, oldSel);
		if (isValidIMP(parentIMP)) {
			break;
		} else {
			parentIMP = NULL;
		}

		superclass = class_getSuperclass(superclass);
	}

	if (parentIMP) {
		if (aggressive) {
			return class_addMethod(cls, newSel, parentIMP, types);
		}

		class_replaceMethod(cls, newSel, newIMP, types);
		class_replaceMethod(cls, oldSel, parentIMP, types);
	}

	return YES;
}

static BOOL swizzleWithIMP(Class cls, SEL oldSel, SEL newSel, IMP newIMP, const char *types, BOOL aggressive) {
    Method origMethod = class_getInstanceMethod(cls, oldSel);

	if (addMethodWithIMP(cls, oldSel, newSel, newIMP, types, aggressive)) {
		return YES;
	}

	// common case, actual swap
	BOOL ret = class_addMethod(cls, newSel, newIMP, types);
	Method newMethod = class_getInstanceMethod(cls, newSel);
	method_exchangeImplementations(origMethod, newMethod);
	return ret;
}

static SEL selectorWithPattern(const char *prefix, const char *key, const char *suffix) {
	size_t prefixLength = prefix ? strlen(prefix) : 0;
	size_t suffixLength = suffix ? strlen(suffix) : 0;

	char initial = key[0];
	if (prefixLength) initial = (char)toupper(initial);
	size_t initialLength = 1;

	const char *rest = key + initialLength;
	size_t restLength = strlen(rest);

	char selector[prefixLength + initialLength + restLength + suffixLength + 1];
	memcpy(selector, prefix, prefixLength);
	selector[prefixLength] = initial;
	memcpy(selector + prefixLength + initialLength, rest, restLength);
	memcpy(selector + prefixLength + initialLength + restLength, suffix, suffixLength);
	selector[prefixLength + initialLength + restLength + suffixLength] = '\0';

	return sel_registerName(selector);
}

static SEL getterForProperty(objc_property_t property, const char *name)
{
	if (property) {
		char *getterName = property_copyAttributeValue(property, "G");
		if (getterName) {
			SEL getter = sel_getUid(getterName);
			free(getterName);
			if (getter) return getter;
		}
	}

	const char *propertyName = property ? property_getName(property) : name;
	return sel_registerName(propertyName);
}

static SEL setterForProperty(objc_property_t property, const char *name)
{
	if (property) {
		char *setterName = property_copyAttributeValue(property, "S");
		if (setterName) {
			SEL setter = sel_getUid(setterName);
			free(setterName);
			if (setter) return setter;
		}
	}

	const char *propertyName = property ? property_getName(property) : name;
	return selectorWithPattern("set", propertyName, ":");
}

static inline SEL prefixedSelector(SEL original) {
	return selectorWithPattern("a2_", sel_getName(original), NULL);
}

#pragma mark -

typedef struct {
	SEL setter;
	SEL a2_setter;
	SEL getter;
} A2BlockDelegateInfo;

static NSUInteger A2BlockDelegateInfoSize(const void *__unused item) {
	return sizeof(A2BlockDelegateInfo);
}

static NSString *A2BlockDelegateInfoDescribe(const void *__unused item) {
	if (!item) { return nil; }
	const A2BlockDelegateInfo *info = item;
	return [NSString stringWithFormat:@"(setter: %s, getter: %s)", sel_getName(info->setter), sel_getName(info->getter)];
}

static inline A2DynamicDelegate *getDynamicDelegate(NSObject *delegatingObject, Protocol *protocol, const A2BlockDelegateInfo *info, BOOL ensuring) {
	A2DynamicDelegate *dynamicDelegate = [delegatingObject bk_dynamicDelegateForProtocol:a2_protocolForDelegatingObject(delegatingObject, protocol)];

	if (!info || !info->setter || !info->getter) {
		return dynamicDelegate;
	}

	if (!info->a2_setter && !info->setter) { return dynamicDelegate; }

	id (*getterDispatch)(id, SEL) = (id (*)(id, SEL)) objc_msgSend;
	id originalDelegate = getterDispatch(delegatingObject, info->getter);

	if (bk_object_isKindOfClass(originalDelegate, A2DynamicDelegate.class)) { return dynamicDelegate; }

	void (*setterDispatch)(id, SEL, id) = (void (*)(id, SEL, id)) objc_msgSend;
	setterDispatch(delegatingObject, info->a2_setter ?: info->setter, dynamicDelegate);

	return dynamicDelegate;
}

typedef A2DynamicDelegate *(^A2GetDynamicDelegateBlock)(NSObject *, BOOL);

@interface A2DynamicDelegate ()

@property (nonatomic, weak, readwrite) id realDelegate;

@end

#pragma mark -

@implementation NSObject (A2BlockDelegate)

#pragma mark Helpers

+ (NSMapTable *)bk_delegateInfoByProtocol:(BOOL)createIfNeeded
{
	NSMapTable *delegateInfo = objc_getAssociatedObject(self, _cmd);
	if (delegateInfo || !createIfNeeded) { return delegateInfo; }

	NSPointerFunctions *protocols = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsOpaqueMemory|NSPointerFunctionsObjectPointerPersonality];
	NSPointerFunctions *infoStruct = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsMallocMemory|NSPointerFunctionsStructPersonality|NSPointerFunctionsCopyIn];
	infoStruct.sizeFunction = A2BlockDelegateInfoSize;
	infoStruct.descriptionFunction = A2BlockDelegateInfoDescribe;

	delegateInfo = [[NSMapTable alloc] initWithKeyPointerFunctions:protocols valuePointerFunctions:infoStruct capacity:0];
	objc_setAssociatedObject(self, _cmd, delegateInfo, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

	return delegateInfo;
}

+ (const A2BlockDelegateInfo *)bk_delegateInfoForProtocol:(Protocol *)protocol
{
	A2BlockDelegateInfo *infoAsPtr = NULL;
	Class cls = self;
	while ((infoAsPtr == NULL || infoAsPtr->getter == NULL) && cls != nil && cls != NSObject.class) {
		NSMapTable *map = [cls bk_delegateInfoByProtocol:NO];
		infoAsPtr = (__bridge void *)[map objectForKey:protocol];
		cls = [cls superclass];
	}
	NSCAssert(infoAsPtr != NULL, @"Class %@ not assigned dynamic delegate for protocol %@", NSStringFromClass(self), NSStringFromProtocol(protocol));
	return infoAsPtr;
}

#pragma mark Linking block properties

+ (void)bk_linkDataSourceMethods:(NSDictionary *)dictionary
{
	[self bk_linkProtocol:a2_dataSourceProtocol(self) methods:dictionary];
}

+ (void)bk_linkDelegateMethods:(NSDictionary *)dictionary
{
	[self bk_linkProtocol:a2_delegateProtocol(self) methods:dictionary];
}

+ (void)bk_linkProtocol:(Protocol *)protocol methods:(NSDictionary *)dictionary
{
	[dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *propertyName, NSString *selectorName, BOOL *stop) {
		const char *name = propertyName.UTF8String;
		objc_property_t property = class_getProperty(self, name);
		NSCAssert(property, @"Property \"%@\" does not exist on class %s", propertyName, class_getName(self));

		char *dynamic = property_copyAttributeValue(property, "D");
		NSCAssert2(dynamic, @"Property \"%@\" on class %s must be backed with \"@dynamic\"", propertyName, class_getName(self));
		free(dynamic);

		char *copy = property_copyAttributeValue(property, "C");
		NSCAssert2(copy, @"Property \"%@\" on class %s must be defined with the \"copy\" attribute", propertyName, class_getName(self));
		free(copy);

		SEL selector = NSSelectorFromString(selectorName);
		SEL getter = getterForProperty(property, name);
		SEL setter = setterForProperty(property, name);

		if (class_respondsToSelector(self, setter) || class_respondsToSelector(self, getter)) { return; }

		const A2BlockDelegateInfo *info = [self bk_delegateInfoForProtocol:protocol];

		IMP getterImplementation = imp_implementationWithBlock(^(NSObject *delegatingObject) {
			A2DynamicDelegate *delegate = getDynamicDelegate(delegatingObject, protocol, info, NO);
			return [delegate blockImplementationForMethod:selector];
		});

		if (!class_addMethod(self, getter, getterImplementation, "@@:")) {
			NSCAssert(NO, @"Could not implement getter for \"%@\" property.", propertyName);
		}

		IMP setterImplementation = imp_implementationWithBlock(^(NSObject *delegatingObject, id block) {
			A2DynamicDelegate *delegate = getDynamicDelegate(delegatingObject, protocol, info, YES);
			[delegate implementMethod:selector withBlock:block];
		});

		if (!class_addMethod(self, setter, setterImplementation, "v@:@")) {
			NSCAssert(NO, @"Could not implement setter for \"%@\" property.", propertyName);
		}
	}];
}

#pragma mark Dynamic Delegate Replacement

+ (void)bk_registerDynamicDataSource
{
	[self bk_registerDynamicDelegateNamed:@"dataSource" forProtocol:a2_dataSourceProtocol(self)];
}
+ (void)bk_registerDynamicDelegate
{
	[self bk_registerDynamicDelegateNamed:@"delegate" forProtocol:a2_delegateProtocol(self)];
}

+ (void)bk_registerDynamicDataSourceNamed:(NSString *)dataSourceName
{
	[self bk_registerDynamicDelegateNamed:dataSourceName forProtocol:a2_dataSourceProtocol(self)];
}
+ (void)bk_registerDynamicDelegateNamed:(NSString *)delegateName
{
	[self bk_registerDynamicDelegateNamed:delegateName forProtocol:a2_delegateProtocol(self)];
}

+ (void)bk_registerDynamicDelegateNamed:(NSString *)delegateName forProtocol:(Protocol *)protocol
{
	NSMapTable *propertyMap = [self bk_delegateInfoByProtocol:YES];
	A2BlockDelegateInfo *infoAsPtr = (__bridge void *)[propertyMap objectForKey:protocol];
	if (infoAsPtr != NULL) { return; }

	const char *name = delegateName.UTF8String;
	objc_property_t property = class_getProperty(self, name);
	SEL setter = setterForProperty(property, name);
	SEL a2_setter = prefixedSelector(setter);
	SEL getter = getterForProperty(property, name);

	A2BlockDelegateInfo info = {
		setter, a2_setter, getter
	};

	[propertyMap setObject:(__bridge id)&info forKey:protocol];
	infoAsPtr = (__bridge void *)[propertyMap objectForKey:protocol];

	IMP setterImplementation = imp_implementationWithBlock(^(NSObject *delegatingObject, id delegate) {
		A2DynamicDelegate *dynamicDelegate = getDynamicDelegate(delegatingObject, protocol, infoAsPtr, YES);
		if ([delegate isEqual:dynamicDelegate]) {
			delegate = nil;
		}
		dynamicDelegate.realDelegate = delegate;
	});

	if (!swizzleWithIMP(self, setter, a2_setter, setterImplementation, "v@:@", YES)) {
		bzero(infoAsPtr, sizeof(A2BlockDelegateInfo));
		return;
	}

	if (![self instancesRespondToSelector:getter]) {
		IMP getterImplementation = imp_implementationWithBlock(^(NSObject *delegatingObject) {
			return [delegatingObject bk_dynamicDelegateForProtocol:a2_protocolForDelegatingObject(delegatingObject, protocol)];
		});

		addMethodWithIMP(self, getter, NULL, getterImplementation, "@@:", NO);
	}
}

- (id)bk_ensuredDynamicDelegate
{
	Protocol *protocol = a2_delegateProtocol(self.class);
	return [self bk_ensuredDynamicDelegateForProtocol:protocol];
}

- (id)bk_ensuredDynamicDelegateForProtocol:(Protocol *)protocol
{
	const A2BlockDelegateInfo *info = [self.class bk_delegateInfoForProtocol:protocol];
	return getDynamicDelegate(self, protocol, info, YES);
}

@end
