/*
 * 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 <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <Foundation/NSArray.h>
#include <Foundation/NSAutoreleasePool.h>
#include <Foundation/NSException.h>
#include <Foundation/NSLock.h>
#include <Foundation/NSObjCRuntime.h>
#include <Foundation/NSThread.h>
#include <GToolKit/GTK.h>
#include <GToolKit/GTKAction.h>
#include <GToolKit/GTKObject.h>
#include <gtk/gtksignal.h>
#ifdef HAVE_LIBGLADE
#include <glade/glade-xml.h>
#endif /* HAVE_LIBGLADE */

/*
 * Objects of this class can autorelease standard C pointers.
 */
@interface __GAutorelease : NSObject
{
    gpointer object;
    GFreeFunc function;
}
+ (gpointer) release:(gpointer) object with:(GFreeFunc) function;
@end

@implementation __GAutorelease

+ (gpointer) release:(gpointer) _object with:(GFreeFunc) _function
{
    __GAutorelease *obj = [[self new] autorelease];

    obj->object = _object;
    obj->function = _function;
    return _object;
}

- (void) dealloc
{
#ifdef DEBUG
    fprintf(stderr, "[__GAutorelease dealloc] object = %p\n", object);
#endif
    function(object);
    [super dealloc];
}
@end

/*
 * Convert UTF-8 C string to NSString object.
 */
NSString *gtoolkit_objc_string (const char *string)
{
    // GTK+ 1.2 does not support UTF-8, so use ASCII instead
    return string ? [NSString stringWithCString:string] : nil;
}

/*
 * Convert NSString object to UTF-8 C string.
 */
const char *gtoolkit_utf8_string (NSString *string)
{
    // GTK+ 1.2 does not support UTF-8, so use ASCII instead
    return [string cString];
}

/*
 * Convert NSArray to GList.
 */
GList *gtoolkit_array_to_list (NSArray *array)
{
    int index = [array count];
    GList *list = NULL;

    while (index--)
    {
	id obj = [array objectAtIndex:index];
	const void *data;

	data = [obj isKindOfClass:[GTK class]] ? [obj gtk]
	     : [obj isKindOfClass:[NSString class]] ? gtoolkit_utf8_string(obj)
//	     : [obj isKindOfClass:[NSData class]] ? [obj bytes]
	     : [obj isKindOfClass:[NSArray class]] ? gtoolkit_array_to_list(obj)
	     : (const void *) gtoolkit_utf8_string([obj description]);
	list = g_list_prepend(list, (gpointer) data);
    }

    // Hmm, gtk_list_insert_items() implicitly takes ownership of its second
    // argument, so we must not free this list here, which leads to a small
    // memory leak in all other cases...
#ifdef GLIST_AUTORELEASE
    return [__GAutorelease release:list with:(GFreeFunc) g_list_free];
#else
    return list;
#endif /* GLIST_AUTORELEASE */
}

static void add_to_array (gpointer data, NSMutableArray *array)
{
    // this will only work if data is some kind of GtkObject
    [array addObject:GTOOLKIT_OBJECT(data)];
}

/*
 * Convert GList of GTK+ objects to NSArray.
 */
NSArray *gtoolkit_list_to_array (GList *glist)
{
    NSMutableArray *array;

    array = [NSMutableArray arrayWithCapacity:g_list_length(glist)];
    g_list_foreach(glist, (GFunc) add_to_array, array);

    return array;
}

/*
 * Convert NSArray of NSString objects to NULL-terminated vector of strings.
 */
const char **gtoolkit_array_to_strvec (NSArray *array)
{
    int index, count = [array count];
    const char **vector;

    if (array == nil) return NULL;

    vector = g_new(const char *, count + 1);
    vector[count] = NULL;
    for (index = 0; index != count; ++index)
	vector[index] = gtoolkit_utf8_string([array objectAtIndex:index]);

    return [__GAutorelease release:vector with:g_free];
}

/*
 * Convert NULL-terminated vector of strings to NSArray of NSString objects.
 */
NSArray *gtoolkit_strvec_to_array (const char **vector)
{
    NSMutableArray *array = nil;

    if (vector)
    {
	array = [NSMutableArray arrayWithCapacity:4];
	while (*vector) [array addObject:gtoolkit_objc_string(*vector++)];
    }

    return array;
}

BOOL gtoolkit_debug;			// enable limited debug output
static NSLock *global_lock;		// lock for all global variables
static GHashTable *object_table;	// map gpointer to object

/*
 * Initialize some internal (non-objc) variables and data structures.
 * The GTKApplication class will call this function on startup.
 */
void gtoolkit_init (void)
{
#ifndef DEBUG
    if (getenv("GTOOLKIT_DEBUG"))
#endif
	gtoolkit_debug = YES;

    object_table = g_hash_table_new(g_direct_hash, g_direct_equal);
}

@implementation GTK

/*
 * Try to find the correct Objective-C class name for this object.
 */
static NSString *gtoolkit_class (GtkObject *gtk)
{
    NSString *result;
    const char *name;
    char *string;
    int pos = 0, copy = 0, uc = 0;		// uc: upper case

#ifdef HAVE_LIBGLADE
    if ((name = glade_get_widget_name((GtkWidget *) gtk)) &&
	(string = strstr(name, "::")))
	// TODO: check that this class matches the actual GTK+ class
	return [NSString stringWithCString:name length:string - name];
#endif /* HAVE_LIBGLADE */

    // guess Objective-C class name from the GTK+ class name
    name = gtk_type_name(GTK_OBJECT_TYPE(gtk));
    string = g_new(char, strlen(name) + 2);	// + '_' and '\0'

    if (islower(name[0])) uc = 1;		// gtkFoo -> GtkFoo
    else if (isupper(name[0]))
    {
	while (islower(name[++pos]));

	if (pos > 3) copy = pos;		// GnomeFoo -> Gnome_Foo
	else if (pos > 1) uc = pos;		// GtkFoo -> GTKFoo
	else
	{
	    while (name[pos] && !islower(name[pos])) ++pos;

	    if (name[pos]) copy = pos - 1;	// GTKFoo -> GTK_Foo
	}
    }

    strncpy(string, name, copy);
    for (pos = copy; pos < uc; ++pos) string[pos] = toupper(name[pos]);
    if (!uc) string[pos] = '_';
    strcpy(string + pos + !uc, name + pos);

#ifdef DEBUG
    fprintf(stderr, "(gtoolkit_class) %s -> %s\n", name, string);
#endif
    result = [NSString stringWithCString:string];
    g_free(string);
    return result;
}

/* Backwards compatibility function (TODO: obsolete) */
id (Gtk_to_Object) (gpointer gtk, const char *class)
{
    return Gtk_to_Object(gtk, class);
}

/*
 * Find Objective-C object for given gpointer, create one if necessary.
 * CLASS is the object's type name (use NULL to autodetect type name).
 */
id gtoolkit_object (gpointer gtk, const char *class)
{
    id result;

    if (gtk == NULL) return nil;

    if (global_lock) [global_lock lock];
    result = g_hash_table_lookup(object_table, gtk);
    if (global_lock) [global_lock unlock];

    if (result == nil)
    {
	// class names are limited to ASCII characters
	NSString *objc_class =
	    class ? [NSString stringWithCString:class] : gtoolkit_class(gtk);

	if ((result = [NSClassFromString(objc_class) alloc]) == nil)
	    [NSException raise:NSGenericException
		format:@"cannot find objc class `%@'", objc_class];

	[result setTag:-1];			// disable "show by default"
	result = [result initWithGtk:gtk];
    }

    return result;
}

/*
 * Notify the GToolKit runtime system that resource locking is necessary.
 * This method is intended only for use by the @GTKApplication class and
 * should never be called from user code.
 */
+ (void) taskNowMultiThreaded:(NSNotification *) event
{
    if (global_lock == nil) global_lock = [NSLock new];
}

#if 0
/*
 * Return a hash code for this object.
 */
- (unsigned) hash
{
    return (unsigned long) gtk / sizeof (gpointer);
}

/*
 * Compare two objects for equality.
 */
- (BOOL) isEqual:(id) anObject
{
    if ([anObject isKindOfClass:[GTK class]] == NO) return NO;
    return gtk == ((GTK *) anObject)->gtk;
}
#endif

 - (id) init
{
    [NSException raise :NSInvalidArgumentException	// avoid bug in objcdoc
	format:@"objects of type `%@' cannot be created with +new", isa];
    return nil;
}

/*
 * Initialize a new GTK object for the given gtk-object pointer /gtk/, which
 * must not be |NULL|. This is an internal method and should generally not
 * be called from user code, unless you really know what you are doing.<p>
 * Use the |GTOOLKIT_OBJECT| macro (or the |gtoolkit_object| function) to
 * create an Objective-C object from an existing gtk-object pointer.
 */
- (id) initWithGtk:(gpointer) _gtk
{
    [super init];

    [global_lock lock];
    g_hash_table_insert(object_table, *(gpointer *) &gtk = _gtk, self);
    [global_lock unlock];

    if (gtoolkit_debug)
	fprintf(stderr, "[%s init] at %p: gtk = %p\n",
		class_get_class_name(isa), self, gtk);	// GNU runtime

    return self;
}

 - (void) dealloc
{
    if (gtoolkit_debug)
	fprintf(stderr, "[%s dealloc] at %p: gtk = %p\n",
		class_get_class_name(isa), self, gtk);	// GNU runtime

    [global_lock lock];
    if (gtk) g_hash_table_remove(object_table, gtk);
    [global_lock unlock];
    [actions release];
    [super dealloc];
}

#ifdef DEBUG
/*
 * Return a textual description of the receiver.
 */
- (NSString *) description
{
    return [[super description] stringByAppendingFormat:@" gtk=%p", gtk];
}
#endif

/*
 * Return the internal gtk-object pointer. This may sometimes be useful to
 * access components of the underlying gtk object that do not have access
 * methods. The macro call |GTOOLKIT_OBJECT(|/gtk_pointer/|)| can perform
 * the reverse operation. Example:
 * <pre>
 * GTKButton *button = [GTKButton buttonWithLabel:label];
 * GtkButton *gtk_button = [button gtk];
 *
 * GTK_WIDGET_SET_FLAGS(gtk_button, GTK_CAN_DEFAULT);</pre>
 */
- (gpointer) gtk
{
    return gtk;
}

/*
 * Set the receiver's tag value to /tag/ (the initial tag value is |0|).
 */
- (void) setTag:(gint32) _tag
{
    tag = _tag;
}

/*
 * Return the receiver's tag value.
 * @see -setTag:
 */
- (gint32) tag
{
    return tag;
}

/*
 * Add a user defined /signal/ (without parameters) to the target class.
 * /flags/ may be either |0| or any valid combination of the signal flags
 * defined in the header file gtk/gtkenums.h.
 * The new signal can be emitted by sending @-emit:signal: to an object of
 * this class.
 */
+ (void) addUserSignal:(NSString *) signal flags:(GtkSignalRunType) flags
{
    gtk_object_class_user_signal_new(gtk_type_class([self getType]),
				     gtoolkit_utf8_string(signal), flags,
				     gtk_signal_default_marshaller,
				     GTK_TYPE_NONE, 0);
}

/*
 * Tell the receiver to emit the given /signal/ (without parameters), i.e.
 * invoke all actions connected to this signal. The /sender/ parameter is
 * ignored.
 * @see -stop:signal:
 */
- (void) emit:(id) sender signal:(NSString *) signal
{
    union { long l; double d; void *p; } result;

    gtk_signal_emit_by_name(gtk, gtoolkit_utf8_string(signal), &result);
}

/*
 * Stop the emission process for the given /signal/. Attempting to stop the
 * emission of a signal that isn't being emitted does nothing. See the GIMP
 * Toolkit documentation for details. The /sender/ parameter is ignored.
 * @see -emit:signal:
 */
- (void) stop:(id) sender signal:(NSString *) signal
{
    gtk_signal_emit_stop_by_name(gtk, gtoolkit_utf8_string(signal));
}

/*
 * This is the generic signal forwarder.
 */
static void forward_signal (gpointer gtk, GTKAction *action, guint n_args,
			    GtkArg *args)
{
    NSAutoreleasePool *pool = [NSAutoreleasePool new];

#ifdef DEBUG
    fprintf(stderr, "(forward_signal init) gtk = %p, action [%p %s %p]\n", gtk,
	    [action target], sel_get_name([action selector]), [action data]);
#endif
    [action performWithSender:GTOOLKIT_OBJECT(gtk) args:args count:n_args];
    [pool release];
#ifdef DEBUG
    fprintf(stderr, "(forward_signal exit) gtk = %p\n", gtk);
#endif
}

/*
 * Connect the /action/ (a target/selector pair) to the given /signal/ in
 * the receiver. The object will call the
 * @GTKAction#-performWithSender:args:count: method of the @GTKAction and
 * pass itself as the sending object when the given /signal/ is emitted.
 * Example:
 * <pre>
 * - (void) foo:(id) sender
 * ...
 * - (void) bar:(id) sender data:(myType *) data
 * ...
 *
 * [window connectSignal:@"destroy" withAction:
 *    [GTKAction actionWithTarget:anObject selector:@selector(foo:)]];
 *
 * [button connectSignal:@"clicked" withAction:
 *    [GTKAction actionWithTarget:anObject selector:@selector(bar:data:)
 *     data:\&myData]];</pre>
 */
- (void) connectSignal:(NSString *) signal withAction:(GTKAction *) action
{
    gtk_signal_connect_full(gtk, gtoolkit_utf8_string(signal), NULL,
		    (GtkCallbackMarshal) forward_signal, action, NULL, NO, NO);
    if (actions == nil) actions = [NSMutableArray new];
    [actions addObject:action];
}

/*
 * Connect the /target///selector/ pair to the given /signal/ in the
 * receiver. This method is equivalent to the call:
 * <pre>
 * [... connectSignal:/signal/ withAction:
 *    [GTKAction actionWithTarget:/target/ selector:/sel/]]</pre>
 * Use the @-connectSignal:withAction: method if you need a reference
 * to the GTKAction object (to block/unblock\/disconnect it later).
 */
- (void) connectSignal:(NSString *) signal withTarget:(id) target sel:(SEL) sel
{
    [self connectSignal:signal withAction:
	  [GTKAction actionWithTarget:target selector:sel]];
}

/*
 * Connect the /target///selector/ pair to the given /signal/ in the
 * receiver. This method is equivalent to the call:
 * <pre>
 * [... connectSignal:/signal/ withAction:
 *    [GTKAction actionWithTarget:/target/ selector:/sel/ data:/data/]]</pre>
 * Use the @-connectSignal:withAction: method if you need a reference
 * to the GTKAction object (to block/unblock\/disconnect it later).
 */
- (void) connectSignal:(NSString *) signal withTarget:(id) target sel:(SEL) sel
	 data:(const void *) data
{
    [self connectSignal:signal withAction:
	  [GTKAction actionWithTarget:target selector:sel data:data]];
}

/*
 * Similar to @-connectSignal:withAction: except the /action/ is connected
 * in the "after" slot. This allows a signal handler to be guaranteed to
 * run after other signal handlers connected to the same signal on the
 * same object and after the class function associated with the signal.
 * @see -connectSignal:withAction:
 */
- (void) connectSignal:(NSString *) signal withActionAfter:(GTKAction *) action
{
    gtk_signal_connect_full(gtk, gtoolkit_utf8_string(signal), NULL,
		    (GtkCallbackMarshal) forward_signal, action, NULL, NO, YES);
    if (actions == nil) actions = [NSMutableArray new];
    [actions addObject:action];
}

/*
 * Disconnect the /action/ from all signals in the receiver that it is
 * connected to. Multiple signal handlers may be disconnected with this call.
 */
- (void) disconnectAction:(GTKAction *) action
{
    gtk_signal_disconnect_by_data(gtk, action);
    [actions removeObject:action];
}

/*
 * Blocks the /action/ for all signals in the receiver that are connected
 * to it. Multiple signal handlers may be blocked with this call.
 */
- (void) blockAction:(GTKAction *) action
{
    gtk_signal_handler_block_by_data(gtk, action);
}

/*
 * Unblocks the /action/ for all signals in the receiver that are connected
 * to it. Multiple signal handlers may be unblocked with this call.
 */
- (void) unblockAction:(GTKAction *) action
{
    gtk_signal_handler_unblock_by_data(gtk, action);
}
@end
