/*
 * GToolKit - Objective-C interface to the GIMP Toolkit
 * Copyright (c) 1998, 1999, 2000  Elmar Ludwig
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307, USA.
 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <GToolKit/GTK.h>
#include <GToolKit/GTKAction.h>

@implementation GTKAction

/*
 * Create an autoreleased GTKAction that will send /selector/ to /target/.
 * @see -performWithSender:args:count:
 */
+ (id) actionWithTarget:(id) _target selector:(SEL) _selector
{
    return [[[self alloc] initWithTarget:_target selector:_selector]
	    autorelease];
}

/*
 * Create an autoreleased GTKAction that will send /selector/ to /target/.
 * /data/ will be passed as an additional argument to the method.
 * Note that /data/ is not retained by default (if it is an object).
 */
+ (id) actionWithTarget:(id) _target selector:(SEL) _selector
       data:(const void *) _data
{
    return [[[self alloc] initWithTarget:_target selector:_selector data:_data]
	    autorelease];
}

/*
 * Initialize a new GTKAction without /target/ and /selector/.
 * @see -setTarget:selector:
 */
- (id) init
{
    return [self initWithTarget:nil selector:0];
}

/*
 * Initialize a new GTKAction that will send /selector/ to /target/.
 * @see -performWithSender:args:count:
 */
- (id) initWithTarget:(id) _target selector:(SEL) _selector
{
    return [self initWithTarget:_target selector:_selector data:nil];
}

/*
 * Initialize a new GTKAction that will send /selector/ to /target/.
 * /data/ will be passed as an additional argument to the method.
 * Note that /data/ is not retained by default (if it is an object).
 */
- (id) initWithTarget:(id) _target selector:(SEL) _selector
       data:(const void *) _data
{
    [[super init] setTarget:_target selector:_selector];
    [self setData:_data];
    return self;
}

 - (void) dealloc
{
    if (retained) [(id) data release];
    [super dealloc];
}

/*
 * Set the receiver's target and/or selector. If /target/ is |nil| or
 * /selector/ is |0|, the previous target or selector value is not changed.
 */
- (void) setTarget:(id) _target selector:(SEL) _selector
{
    if (_target || _selector == 0) target = _target;
    if (_selector) selector = _selector;

    if (selector)
    {
	if ([target respondsToSelector:selector])
	    method = [target methodForSelector:selector];
	else
	    [target doesNotRecognizeSelector:selector];
    }
}

/*
 * Set the receiver's user data.
 */
- (void) setData:(const void *) _data
{
    if (retained) [(id) _data retain], [(id) data release];
    data = (void *) _data;
}

/*
 * Tell the receiver to retain its user data. Use this only if the data
 * really is some kind of NSObject (or |nil|).
 */
- (void) retainData
{
    if (retained == NO) [(id) data retain], retained = YES;
}

/*
 * Return the current target of this action.
 */
- (id) target
{
    return target;
}

/*
 * Return the current selector of this action.
 */
- (SEL) selector
{
    return selector;
}

/*
 * Return the current user data of this action.
 */
- (void *) data
{
    return retained ? [[(id) data retain] autorelease] : data;
}

/*
 * Return whether the user data should be retained.
 */
- (BOOL) dataRetained
{
    return retained;
}

/*
 * Send the current selector to the target. The action will pass the
 * /sender/ object, /count/ arguments from the argument array starting at
 * /args/ and the user data (if it has been set) to the method. The return
 * value will be stored in args[count], which must contain the correct type
 * information for the return value.<p>
 * If the selector's return type is |void| and /count/ is |0|, args may be
 * |NULL|.<p>
 * Note:!! The current implementation will only pass up to 6 arguments from
 * the /args/ array. If it contains more values, the argument list will be
 * truncated.
 * @see GTK#-connectSignal:withAction: in class @GTK
 */
- (void) performWithSender:(id) sender args:(GtkArg *) args
	 count:(unsigned) count
{
#define MAXARGS	6
    int (*func)(id, SEL, ...) = (int (*)(id, SEL, ...)) method;
    struct
    {
	gboolean is_int;	// is this type "compatible" to int?
	union
	{
	    int ival;		// integer value of the argument
	    void *pval;		// pointer value of the argument
	} arg;
    } arg[MAXARGS];		// callback argument vector
    GtkType type;		// argument/return value type info
    int result;
    int index;

    // this code currently does not use the NSInvocation class because:
    //	 - NSInvocation is implemented only on a few systems (yet)
    //	 - NSInvocation is relatively slow

    // allow up to MAXARGS arguments of type INT, LONG, FLOAT or POINTER
    for (index = 0; index != MAXARGS; ++index)
    {
	if (index == count)
	{
	    arg[index].is_int = NO;
	    arg[index].arg.pval = data;
	    break;
	}

	type = args[index].type;
#ifdef DEBUG
	if (type == GTK_TYPE_NONE || type == GTK_TYPE_INVALID)
	    g_warning("cannot pass GTK_TYPE_NONE as argument");
#endif
#if SIZEOF_LONG != SIZEOF_INT && SIZEOF_LONG != SIZEOF_VOID_P
	if (type == GTK_TYPE_LONG || type == GTK_TYPE_ULONG)
	    g_warning("cannot pass GTK_TYPE_LONG on this machine");
#endif
#if SIZEOF_FLOAT != SIZEOF_INT && SIZEOF_FLOAT != SIZEOF_VOID_P
	if (type == GTK_TYPE_FLOAT)
	    g_warning("cannot pass GTK_TYPE_FLOAT on this machine");
#endif
	switch (type)
	{
	    case GTK_TYPE_INT:   case GTK_TYPE_UINT:  case GTK_TYPE_ENUM:
#if SIZEOF_INT == SIZEOF_LONG
	    case GTK_TYPE_LONG:  case GTK_TYPE_ULONG:
#endif
	    case GTK_TYPE_BOOL:  case GTK_TYPE_FLAGS:
#if SIZEOF_INT == SIZEOF_FLOAT
	    case GTK_TYPE_FLOAT:
#endif
		arg[index].is_int = YES;
		arg[index].arg.ival = GTK_VALUE_INT(args[index]);
		break;
	    case GTK_TYPE_STRING:
		arg[index].is_int = NO;
		arg[index].arg.pval =
		    gtoolkit_objc_string(GTK_VALUE_STRING(args[index]));
		break;
	    default:
		arg[index].is_int = NO;
		arg[index].arg.pval = gtk_type_is_a(type, GTK_TYPE_OBJECT) ?
		    GTOOLKIT_OBJECT(GTK_VALUE_OBJECT(args[index])) :
		    GTK_VALUE_POINTER(args[index]);
	}
    }

#define CALL_0()	  (arg[0].is_int ? CALL_1(          ival) \
					 : CALL_1(          pval))
#define CALL_1(a)	  (arg[1].is_int ? CALL_2(a,        ival) \
					 : CALL_2(a,        pval))
#define CALL_2(a,b)	  (arg[2].is_int ? CALL_3(a,b,      ival) \
					 : CALL_3(a,b,      pval))
#define CALL_3(a,b,c)	  (arg[3].is_int ? CALL_4(a,b,c,    ival) \
					 : CALL_4(a,b,c,    pval))
#define CALL_4(a,b,c,d)	  (arg[4].is_int ? CALL_5(a,b,c,d,  ival) \
					 : CALL_5(a,b,c,d,  pval))
#define CALL_5(a,b,c,d,e) (arg[5].is_int ? CALL_6(a,b,c,d,e,ival) \
					 : CALL_6(a,b,c,d,e,pval))
#define CALL_6(a,b,c,d,e,f) \
	func(target, selector, sender, arg[0].arg.a, arg[1].arg.b, \
	     arg[2].arg.c, arg[3].arg.d, arg[4].arg.e, arg[5].arg.f, data)

    // method is always called with the maximum number of arguments,
    // this is ok because Objective-C uses the C calling convention
    if (retained) [[(id) data retain] autorelease];
#if SIZEOF_INT == SIZEOF_VOID_P
    result = CALL_6(pval, pval, pval, pval, pval, pval);
#else
    result = CALL_0();
#endif

    // return type may be VOID or INT (or "compatible")
    type = args ? args[count].type : GTK_TYPE_NONE;
#ifdef DEBUG
    if (type == GTK_TYPE_INVALID)
	g_warning("cannot return GTK_TYPE_INVALID");
#endif
#if SIZEOF_LONG != SIZEOF_INT
    if (type == GTK_TYPE_LONG || type == GTK_TYPE_ULONG)
	g_warning("cannot return GTK_TYPE_LONG on this machine");
#endif
#if SIZEOF_FLOAT != SIZEOF_INT
    if (type == GTK_TYPE_FLOAT)
	g_warning("cannot return GTK_TYPE_FLOAT on this machine");
#endif
    if (type != GTK_TYPE_NONE)
	*GTK_RETLOC_INT(args[count]) = result;
#undef MAXARGS
}
@end
