//
//  Target.m
//  MYUtilities
//
//  Created by Jens Alfke on 2/11/08.
//  Copyright 2008 Jens Alfke. All rights reserved.
//

#import "Target.h"


@implementation MYTarget


- (id) initWithReceiver: (id)receiver action: (SEL)action
{
    self = [super init];
    if( self ) {
        NSMethodSignature *sig = [receiver methodSignatureForSelector: action];
        CAssert(sig,@"%@<%p> does not respond to %@",[receiver class],receiver,NSStringFromSelector(action));
        CAssert(sig.numberOfArguments==3,
               @"-[%@ %@] can't be used as a target because it takes >1 param",
               [receiver class],NSStringFromSelector(action));
        CAssert(0==strcmp([sig getArgumentTypeAtIndex: 2],"@"),
               @"-[%@ %@] can't be used as a target because it takes a non-object param",
               [receiver class],NSStringFromSelector(action));
        NSInvocation *inv = [NSInvocation invocationWithMethodSignature: sig];
        inv.target = receiver;
        inv.selector = action;
        _invocations = [inv retain];
    }
    return self;
}

+ (MYTarget*) targetWithReceiver: (id)receiver action: (SEL)action
{
    return [[[self alloc] initWithReceiver: receiver action: action] autorelease];
}

- (void) dealloc
{
    [_invocations release];
    [super dealloc];
}


- (NSArray*) invocations
{
    NSMutableArray *invocations = $castIf(NSMutableArray,_invocations);
    if( ! invocations )
        invocations = [NSMutableArray arrayWithObject: _invocations];
    return invocations;
}


- (NSString*) description
{
    NSMutableString *desc = [NSMutableString stringWithFormat: @"%@{", self.class];
    BOOL first = YES;
    for( NSInvocation *inv in self.invocations ) {
        if( first )
            first = NO;
        else
            [desc appendString: @", "];
        [desc appendFormat: @"-[%@ %s]", [inv.target class], inv.selector];
    }
    [desc appendString: @"}"];
    return desc;
}


static BOOL equalInvocations( NSInvocation *a, NSInvocation *b )
{
    return a.target==b.target && a.selector==b.selector;
}


- (BOOL) isEqual: (MYTarget*)t
{
    if( ! [t isKindOfClass: [self class]] )
        return NO;
    if( [_invocations isKindOfClass: [NSInvocation class]] && [t->_invocations isKindOfClass: [NSInvocation class]] )
        return equalInvocations(_invocations,t->_invocations);
    NSArray *myInvocations = self.invocations, *itsInvocations = t.invocations;
    unsigned n = myInvocations.count;
    if( n != itsInvocations.count )
        return NO;
    for( int i=0; i<n; i++ )
        if( ! equalInvocations( [myInvocations objectAtIndex: i],
                                [itsInvocations objectAtIndex: i] ) )
            return NO;
    return YES;
}


- (void) addTarget: (MYTarget*)target
{
    setObj(&_invocations,[self invocations]);
    [_invocations addObjectsFromArray: target.invocations];
}


static id invokeSingleTarget( NSInvocation *invocation, id param )
{
    id result = nil;
    if( invocation && invocation.target ) {
        [invocation retain];
        @try{
            [invocation setArgument: &param atIndex: 2];
            [invocation invoke];
            
            NSMethodSignature *sig = invocation.methodSignature;
            NSUInteger returnLength = sig.methodReturnLength;
            if( returnLength==0 ) {
                result = nil; // void
            } else {
                const char *returnType = sig.methodReturnType;
                if( returnType[0]=='@' ) {
                    [invocation getReturnValue: &result];
                } else {
                    UInt8 returnBuffer[returnLength];
                    [invocation getReturnValue: &returnBuffer];
                    result = [NSValue valueWithBytes: &returnBuffer objCType: returnType];
                }
            }
        }@finally{
            [invocation release];
        }
    }
    return result;
}


- (id) invokeWithSender: (id)sender
{
    id result;
    NSMutableArray *invocations = $castIf(NSMutableArray,_invocations);
    if( invocations ) {
        result = nil;
        for( NSInvocation *invocation in invocations )
            result = invokeSingleTarget(invocation,sender);
    } else {
        result = invokeSingleTarget(_invocations,sender);
    }
    return result;
}


@end
