// TEST_CFLAGS -framework Foundation

#include "test.h"
#include <objc/runtime.h>
#import <Foundation/Foundation.h>

#include <Block_private.h>

#if !__has_feature(objc_arc)
#   define __bridge
#endif

#if __arm64__
    // stret supported, but is identical to non-stret
#   define STRET_SPECIAL 0
#else
    // stret supported and distinct from non-stret
#   define STRET_SPECIAL 1
#endif

typedef struct BigStruct {
    uintptr_t datums[200];
} BigStruct;

@interface Foo:NSObject
@end
@implementation Foo
- (BigStruct) methodThatReturnsBigStruct: (BigStruct) b
{
    return b;
}
@end

@interface Foo(bar)
- (int) boo: (int) a;
- (BigStruct) structThatIsBig: (BigStruct) b;
- (BigStruct) methodThatReturnsBigStruct: (BigStruct) b;
- (float) methodThatReturnsFloat: (float) aFloat;
@end

// This is void* instead of id to prevent interference from ARC.
typedef uintptr_t (*FuncPtr)(void *, SEL);
typedef BigStruct (*BigStructFuncPtr)(id, SEL, BigStruct);
typedef float (*FloatFuncPtr)(id, SEL, float);

BigStruct bigfunc(BigStruct a) {
    return a;
}

@interface Deallocator : NSObject @end
@implementation Deallocator
-(void) methodThatNobodyElseCalls1 { }
-(void) methodThatNobodyElseCalls2 { }
id retain_imp(Deallocator *self, SEL _cmd) {
    _objc_flush_caches([Deallocator class]);
    [self methodThatNobodyElseCalls1];
    struct objc_super sup = { self, [[Deallocator class] superclass] };
    return ((id(*)(struct objc_super *, SEL))objc_msgSendSuper)(&sup, _cmd);
}
void dealloc_imp(Deallocator *self, SEL _cmd) {
    _objc_flush_caches([Deallocator class]);
    [self methodThatNobodyElseCalls2];
    struct objc_super sup = { self, [[Deallocator class] superclass] };
    ((void(*)(struct objc_super *, SEL))objc_msgSendSuper)(&sup, _cmd);
}
+(void) load {
    class_addMethod(self, sel_registerName("retain"), (IMP)retain_imp, "");
    class_addMethod(self, sel_registerName("dealloc"), (IMP)dealloc_imp, "");
}
@end

/* Code copied from objc-block-trampolines.m to test Block innards */
typedef enum {
    ReturnValueInRegisterArgumentMode,
#if STRET_SPECIAL
    ReturnValueOnStackArgumentMode,
#endif
    
    ArgumentModeMax
} ArgumentMode;

static ArgumentMode _argumentModeForBlock(id block) {
    ArgumentMode aMode = ReturnValueInRegisterArgumentMode;
#if STRET_SPECIAL
    if ( _Block_use_stret((__bridge void *)block) )
        aMode = ReturnValueOnStackArgumentMode;
#else
    testassert(!_Block_use_stret((__bridge void *)block));
#endif
    
    return aMode;
}
/* End copied code */

int main () {
    // make sure the bits are in place
    int (^registerReturn)() = ^(){ return 42; };
    ArgumentMode aMode;
    
    aMode = _argumentModeForBlock(registerReturn);
    testassert(aMode == ReturnValueInRegisterArgumentMode);

    BigStruct (^stackReturn)() = ^() { BigStruct k = {{0}}; return k; };
    aMode = _argumentModeForBlock(stackReturn);
#if STRET_SPECIAL
    testassert(aMode == ReturnValueOnStackArgumentMode);
#else
    testassert(aMode == ReturnValueInRegisterArgumentMode);
#endif

    uintptr_t TEST_QUANTITY = is_guardmalloc() ? 1000 : 100000;
    FuncPtr funcArray[TEST_QUANTITY];

    for(uintptr_t i = 0; i < TEST_QUANTITY; i++) {
        uintptr_t (^block)(void *self) = ^uintptr_t(void *self) {
            testassert(i == (uintptr_t)self);
            return i;
        };
        block = (__bridge id)_Block_copy((__bridge void *)block);
        
        funcArray[i] =  (FuncPtr) imp_implementationWithBlock(block);
        
        testassert(block((void *)i) == i);
        
        id blockFromIMPResult = imp_getBlock((IMP)funcArray[i]);
        testassert(blockFromIMPResult == (id)block);
        
        _Block_release((__bridge void *)block);
    }
    
    for(uintptr_t i = 0; i < TEST_QUANTITY; i++) {
        uintptr_t result = funcArray[i]((void *)i, 0);
        testassert(i == result);
    }
    
    for(uintptr_t i = 0; i < TEST_QUANTITY; i = i + 3) {
	imp_removeBlock((IMP)funcArray[i]);
	id shouldBeNull = imp_getBlock((IMP)funcArray[i]);
	testassert(shouldBeNull == NULL);
    }
    
    for(uintptr_t i = 0; i < TEST_QUANTITY; i = i + 3) {
        uintptr_t j = i * i;
        
        uintptr_t (^block)(void *) = ^uintptr_t(void *self) {
            testassert(j == (uintptr_t)self);
            return j;
        };
        funcArray[i] =  (FuncPtr) imp_implementationWithBlock(block);
        
        testassert(block((void *)j) == j);
        testassert(funcArray[i]((void *)j, 0) == j);
    }
    
    for(uintptr_t i = 0; i < TEST_QUANTITY; i = i + 3) {
        uintptr_t j = i * i;
        uintptr_t result = funcArray[i]((void *)j, 0);
        testassert(j == result);
    }
    
    int (^implBlock)(id, int);
    
    implBlock = ^(id self __attribute__((unused)), int a){
        return -1 * a;
    };
    
    PUSH_POOL {
    
        IMP methodImp = imp_implementationWithBlock(implBlock);
    
        BOOL success = class_addMethod([Foo class], @selector(boo:), methodImp, "i@:i");
        testassert(success);

        Foo *f = [Foo new];
        int (*impF)(id self, SEL _cmd, int x) = (int(*)(id, SEL, int)) [Foo instanceMethodForSelector: @selector(boo:)];
        
        int x = impF(f, @selector(boo:), -42);
        
        testassert(x == 42);
        testassert([f boo: -42] == 42);
        
        BigStruct a;
        for (uintptr_t i = 0; i < 200; i++) {
            a.datums[i] = i;    
        }
        
        // slightly more straightforward here
        __block unsigned int state = 0;
        BigStruct (^structBlock)(id, BigStruct) = ^BigStruct(id self __attribute__((unused)), BigStruct c) {
            state++;
            return c;
        };
        BigStruct blockDirect = structBlock(nil, a);
        testassert(!memcmp(&a, &blockDirect, sizeof(BigStruct)));
        testassert(state==1);
        
        IMP bigStructIMP = imp_implementationWithBlock(structBlock);
        
        class_addMethod([Foo class], @selector(structThatIsBig:), bigStructIMP, "oh, type strings, how I hate thee. Fortunately, the runtime doesn't generally care.");
        
        BigStruct b;
        
        BigStructFuncPtr bFunc;
        
        b = bigfunc(a);
        testassert(!memcmp(&a, &b, sizeof(BigStruct)));
        b = bigfunc(a);
        testassert(!memcmp(&a, &b, sizeof(BigStruct)));
        
        bFunc = (BigStructFuncPtr) [Foo instanceMethodForSelector: @selector(methodThatReturnsBigStruct:)];
        
        b = bFunc(f, @selector(methodThatReturnsBigStruct:), a);
        testassert(!memcmp(&a, &b, sizeof(BigStruct)));
        
        b = [f methodThatReturnsBigStruct: a];
        testassert(!memcmp(&a, &b, sizeof(BigStruct)));
        
        bFunc = (BigStructFuncPtr) [Foo instanceMethodForSelector: @selector(structThatIsBig:)];
        
        b = bFunc(f, @selector(structThatIsBig:), a);
        testassert(!memcmp(&a, &b, sizeof(BigStruct)));
        testassert(state==2);
        
        b = [f structThatIsBig: a];
        testassert(!memcmp(&a, &b, sizeof(BigStruct)));
        testassert(state==3);
        
        
        IMP floatIMP = imp_implementationWithBlock(^float (id self __attribute__((unused)), float aFloat ) {
            return aFloat;
        });
        class_addMethod([Foo class], @selector(methodThatReturnsFloat:), floatIMP, "ooh.. type string unspecified again... oh noe... runtime might punish. not.");
        
        float e = (float)0.001;
        float retF = (float)[f methodThatReturnsFloat: 37.1212f];
        testassert( ((retF - e) < 37.1212) && ((retF + e) > 37.1212) );
        

#if !__has_feature(objc_arc)        
        // Make sure imp_implementationWithBlock() and imp_removeBlock() 
        // don't deadlock while calling Block_copy() and Block_release()
        Deallocator *dead = [[Deallocator alloc] init];
        IMP deadlockImp = imp_implementationWithBlock(^{ [dead self]; });
        [dead release];
        imp_removeBlock(deadlockImp);
#endif

    } POP_POOL;

    succeed(__FILE__);
}

