/*
 * 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 <stdarg.h>
#include <stdio.h>
#include <Foundation/NSArray.h>
#include <Foundation/NSAutoreleasePool.h>
#include <Foundation/NSException.h>
#include <Foundation/NSLock.h>
#include <Foundation/NSNotification.h>
#include <Foundation/NSProcessInfo.h>
#include <Foundation/NSThread.h>
#include <Foundation/NSUtilities.h>
#include <GToolKit/GTKApplication.h>
#include <GToolKit/GTKButton.h>
#include <GToolKit/GTKDialog.h>
#include <gtk/gtkbox.h>
#include <gtk/gtklabel.h>
#include <gtk/gtksignal.h>

/*
 * Internal functions used by GTKRunMessageBox().
 */
static void msgbox_clicked (GtkWidget *widget)
{
    [GTKApp stopModalWithCode:[GTOOLKIT_OBJECT(widget) tag]];
}

static gint msgbox_delete (GtkWidget *widget, GdkEvent *event)
{
    msgbox_clicked(widget);
    return YES;
}

static GtkWidget *msgbox_insert (GtkWidget *box, NSString *label, int tag)
{
    GTKButton *button = [GTKButton buttonWithLabel:label];
    GtkButton *gtk_button = [button gtk];

    [button setTag:tag];
    gtk_box_pack_start((GtkBox *) box, (GtkWidget *) gtk_button, YES, NO, 0);
    gtk_misc_set_padding((GtkMisc *) gtk_button->child, 8, 0);
    GTK_WIDGET_SET_FLAGS(gtk_button, GTK_CAN_DEFAULT);
    gtk_signal_connect((GtkObject *) gtk_button, "clicked",
		       (GtkSignalFunc) msgbox_clicked, NULL);
    return (GtkWidget *) gtk_button;
}

/*
 * Display a modal message box or alert panel with up to 3 buttons and
 * a format string. Return value is n-1 if button number n is clicked.
 */
int GTKRunMessageBox (NSString *title, NSString *format, NSString *button1,
		      NSString *button2, NSString *button3, ...)
{
    GTKDialog *window = [GTKDialog dialog];
    GtkDialog *gtk_window = [window gtk];
    GtkWidget *hbox = gtk_window->action_area;	// use GtkHButtonBox here
    GtkWidget *vbox = gtk_window->vbox;
    GtkWidget *focus = NULL;
    GtkWidget *label;
    NSString *text;
    int result, tag = 0;
    va_list ap;

    if (button1) focus = msgbox_insert(hbox, button1, tag);
    if (button2) msgbox_insert(hbox, button2, ++tag);
    if (button3) msgbox_insert(hbox, button3, ++tag);
    if (focus) gtk_widget_grab_default(focus), gtk_widget_grab_focus(focus);
    if (title) gtk_window_set_title((GtkWindow *) gtk_window,
				    gtoolkit_utf8_string(title));

    va_start(ap, button3);
    text = [[NSString alloc] initWithFormat:format arguments:ap];
    label = gtk_label_new(gtoolkit_utf8_string(text));
    [text release];
    va_end(ap);

    [window setTag:tag];
    gtk_widget_show(label);
    gtk_misc_set_padding((GtkMisc *) label, 10, 10);
    gtk_label_set_justify((GtkLabel *) label, GTK_JUSTIFY_LEFT);
    gtk_box_pack_start_defaults((GtkBox *) vbox, label);
    gtk_window_set_position((GtkWindow *) gtk_window, GTK_WIN_POS_CENTER);

    gtk_signal_connect((GtkObject *) gtk_window, "delete_event",
		       (GtkSignalFunc) msgbox_delete, NULL);
    result = [GTKApp runModalForWindow:window];
    gtk_widget_destroy((GtkWidget *) gtk_window);
    return result;
}

extern void gtoolkit_init (void);	// initialize data structures

static NSLock *gtkapp_lock;		// lock for GTKApp instance variables

@implementation GTKApplication

NSString *GTKApplicationDidBecomeActiveNotification =
	@"GTKApplicationDidBecomeActive";
NSString *GTKApplicationDidFinishLaunchingNotification =
	@"GTKApplicationDidFinishLaunching";
NSString *GTKApplicationDidResignActiveNotification =
	@"GTKApplicationDidResignActive";
NSString *GTKApplicationWillFinishLaunchingNotification =
	@"GTKApplicationWillFinishLaunching";
NSString *GTKApplicationWillTerminateNotification =
	@"GTKApplicationWillTerminate";

GTKApplication *GTKApp;			// the application object

/*
 * Return the shared application object (as stored in the variable GTKApp).
 * This method will raise an NSGenericException if the application object
 * has not yet been created.
 * @see -initWithArgc:argv:
 */
+ (GTKApplication *) sharedApplication
{
    if (GTKApp == nil)
	[NSException raise:NSGenericException format:@"no application object"];
    return GTKApp;
}

 + (void) taskNowMultiThreaded:(NSNotification *) event
{
#ifdef DEBUG
    fprintf(stderr, "[GTKApplication taskNowMultiThreaded]\n");
#endif
    if (gtkapp_lock == nil) gtkapp_lock = [NSRecursiveLock new];
    [GTK taskNowMultiThreaded:event];
}

 - (id) init
{
    [NSException raise:NSInvalidArgumentException
	format:@"[GTKApp init]: need argc and argv"];
    return nil;
}

 - (void) windowDidBecomeMain:(NSNotification *) event
{
    GTKWindow *window = [event object];
    BOOL notify = NO;

#ifdef DEBUG
    fprintf(stderr, "[GTKApp windowDidBecomeMain] window = %p\n", window);
#endif
    if (window == nil) return;
    if (mainWindow == nil) notify = YES;
    mainWindow = window;
    if (notify)
	[center postNotificationName:GTKApplicationDidBecomeActiveNotification
		object:self];
}

 - (void) windowDidResignMain:(NSNotification *) event
{
    GTKWindow *window = [event object];
    BOOL notify = NO;

#ifdef DEBUG
    fprintf(stderr, "[GTKApp windowDidResignMain] window = %p\n", window);
#endif
    if (window == nil) return;
    if (mainWindow == window) notify = YES, mainWindow = nil;
    if (notify)
	[center postNotificationName:GTKApplicationDidResignActiveNotification
		object:self];
}

 - (void) windowWillClose:(NSNotification *) event
{
    GTKWindow *window = [event object];
    unsigned int count;
    id _delegate;

#ifdef DEBUG
    fprintf(stderr, "[GTKApp windowWillClose] window = %p\n", window);
#endif
    if (window == nil) return;
    if (window == mainWindow) [self windowDidResignMain:event];

    [gtkapp_lock lock];
    [windows removeObject:window];
    count = [windows count];
    _delegate = delegate;
    [gtkapp_lock unlock];

    if (count == 0 && (![_delegate respondsToSelector:
	    @selector(applicationShouldTerminateAfterLastWindowClosed:)] ||
	 [_delegate applicationShouldTerminateAfterLastWindowClosed:self]))
	[self terminate:self];
}

 - (void) windowWillOpen:(NSNotification *) event
{
    GTKWindow *window = [event object];

#ifdef DEBUG
    fprintf(stderr, "[GTKApp windowWillOpen] window = %p\n", window);
#endif
    if (window)
    {
	[gtkapp_lock lock];
	[windows addObject:window];
	[gtkapp_lock unlock];
    }
}

/*
 * Initialize a new GTKApplication object and store it in the global
 * variable GTKApp. Note that you cannot create more than one application
 * object in your program. This method will modify the program's command
 * line arguments as given by /argc/ and /argv/ and remove any options
 * related to GTK (such as |--display| or |--class|) from the argument
 * list.<p>
 * If the environment variable |GTOOLKIT_DEBUG| has been set, the
 * GTKApplication class will activate special debug code to trace
 * object allocation and deallocation calls.
 */
- (id) initWithArgc:(int *) argc argv:(char ***) argv
{
    if (GTKApp == nil)
    {
#ifdef HAVE_ENVIRON
	extern char **environ;		// get process environment
#else
#define environ	(*argv + *argc)		// (fake) empty environment
#endif

#ifdef DEBUG
	fprintf(stderr, "[GTKApp initWithArgc:argv:]\n");
#endif
	GTKApp = [super init];
	center = [NSNotificationCenter defaultCenter];
	windows = [NSMutableArray new];

	gtk_set_locale();
	gtk_init(argc, argv);
	gtoolkit_init();
	// TODO: this still does not work correctly on GNUstep systems
#if defined (LIB_FOUNDATION_LIBRARY) || defined (GNUSTEP_BASE_VERSION)
	[NSProcessInfo initializeWithArguments:*argv count:*argc
		       environment:environ];
#endif

	if ([NSThread isMultiThreaded])
	    [GTKApplication taskNowMultiThreaded:nil];
	else
	    [center addObserver:[GTKApplication class]
		    selector:@selector(taskNowMultiThreaded:)
		    name:NSWillBecomeMultiThreadedNotification object:nil];

	[center addObserver:self selector:@selector(windowDidBecomeMain:)
		name:GTKWindowDidBecomeMainNotification object:nil];
	[center addObserver:self selector:@selector(windowDidResignMain:)
		name:GTKWindowDidResignMainNotification object:nil];
	[center addObserver:self selector:@selector(windowWillClose:)
		name:GTKWindowWillCloseNotification object:nil];
	[center addObserver:self selector:@selector(windowWillOpen:)
		name:GTKWindowWillOpenNotification object:nil];
    }

    return GTKApp;
}

 - (void) dealloc
{
#ifdef DEBUG
    fprintf(stderr, "[GTKApp dealloc] (ignored)\n");
#endif
}

/*
 * This method is invoked automatically by the @-run method (see below)
 * just before the application's main event loop will be started. It
 * currently does nothing except for posting (in this order) the
 * GTKApplicationWillFinishLaunchingNotification and
 * GTKApplicationDidFinishLaunchingNotification to the default notification
 * center.
 */
- (void) finishLaunching
{
    NSAutoreleasePool *pool = [NSAutoreleasePool new];

#ifdef DEBUG
    fprintf(stderr, "[GTKApp finishLaunching]\n");
#endif
    [center postNotificationName:GTKApplicationWillFinishLaunchingNotification
	    object:self];

    // do something useful here...

    [center postNotificationName:GTKApplicationDidFinishLaunchingNotification
	    object:self];
    [pool release];
}

/*
 * Set the application object's delegate or unset it (if /delegate/ is
 * |nil|). If the delegate implements some of the methods described in
 * the protocol @proto(GTKApplicationDelegate), it will be notified by
 * the application object on the corresponding events.
 */
- (void) setDelegate:(id) object
{
    [gtkapp_lock lock];
    if (delegate) [center removeObserver:delegate name:nil object:self];
    delegate = object;

    if ([object respondsToSelector:@selector(applicationDidBecomeActive:)])
	[center addObserver:object
		selector:@selector(applicationDidBecomeActive:)
		name:GTKApplicationDidBecomeActiveNotification object:self];
    if ([object respondsToSelector:@selector(applicationDidFinishLaunching:)])
	[center addObserver:object
		selector:@selector(applicationDidFinishLaunching:)
		name:GTKApplicationDidFinishLaunchingNotification object:self];
    if ([object respondsToSelector:@selector(applicationDidResignActive:)])
	[center addObserver:object
		selector:@selector(applicationDidResignActive:)
		name:GTKApplicationDidResignActiveNotification object:self];
    if ([object respondsToSelector:@selector(applicationWillFinishLaunching:)])
	[center addObserver:object
		selector:@selector(applicationWillFinishLaunching:)
		name:GTKApplicationWillFinishLaunchingNotification object:self];
    if ([object respondsToSelector:@selector(applicationWillTerminate:)])
	[center addObserver:object
		selector:@selector(applicationWillTerminate:)
		name:GTKApplicationWillTerminateNotification object:self];
    [gtkapp_lock unlock];
}

/*
 * Return the application object's delegate.
 */
- (id) delegate
{
    return delegate;
}

/*
 * Start the application's main event loop.
 */
- (void) run
{
    // should the main loop catch exceptions (like NSApplication)?
    // it cannot be restartet if it was terminated by an exception
    [self finishLaunching];
    [self main];

    if ([NSThread isMultiThreaded]) [self exit:0];	// terminate safely
}

/*
 * Check whether the main event loop is running.
 */
- (BOOL) isRunning
{
    return [self mainLevel] > 0;
}

/*
 * Start a modal event loop for the given /window/, i.e. restrict all events
 * for this application to the specified window. This input mechanism should
 * only be used for panels or windows that require immediate user interaction.
 * Generally, it is best to avoid modal windows if possible.<p>
 * -runModalForWindow: does not return until the modal event loop is
 * terminated by a @-stopModalWithCode: message, and it returns
 * the status code passed as an argument to this method.
 * @see -stopModal, -stopModalWithCode:
 */
- (int) runModalForWindow:(GTKWindow *) window
{
#ifdef DEBUG
    fprintf(stderr, "[GTKApp runModalForWindow] window = %p\n", window);
#endif
    [window show];
    [window setModal:YES];
    [self main];
    [window setModal:NO];
#ifdef DEBUG
    fprintf(stderr, "[GTKApp runModalForWindow] (exit) code = %d\n", modalCode);
#endif
    return modalCode;
}

/*
 * Stop a running modal event loop with the code GTKRunStoppedResponse.
 */
- (void) stopModal
{
    [self stopModalWithCode:GTKRunStoppedResponse];
}

/*
 * Stop a running modal event loop with the given /code/.
 * @see -runModalForWindow:
 */
- (void) stopModalWithCode:(int) code
{
    modalCode = code;
    [self stop:self];
}

/*
 * Stop the application's main event loop (if it is running).
 * This will cause the @-run method to return (similar to @-terminate:),
 * but will not send any notifications. If a modal event loop is running,
 * it will be stopped instead of the main event loop.
 * The /sender/ parameter is ignored.
 */
- (void) stop:(id) sender
{
    if ([self isRunning]) [self mainQuit];
}

/*
 * Tell the application's main event loop to finish (if it is running).
 * This will cause the @-run method to return, which will normally end the
 * program. If a modal event loop is running, the program will be terminated
 * anyway. The /sender/ parameter is ignored.<p>
 * If the delegate implements the method:
 * <pre>
 * - (BOOL) applicationShouldTerminate:(GTKApplication *) sender</pre>
 *
 * this message is sent to the delegate to determine whether the application
 * should actually terminate. If it returns |YES|, this method will post
 * the GTKApplicationWillTerminateNotification to the default notification
 * center and terminate the event loop.
 */
- (void) terminate:(id) sender
{
    static BOOL terminated;
    volatile BOOL terminate = YES;	// avoid gcc warning

    [gtkapp_lock lock];
#ifdef DEBUG
    fprintf(stderr, "[GTKApp terminate] sender = %p\n", sender);
#endif
    NS_DURING
	if (terminated || ([delegate respondsToSelector:
		@selector(applicationShouldTerminate:)] &&
	     [delegate applicationShouldTerminate:self] == NO))
	    terminate = NO;
    NS_HANDLER
	[gtkapp_lock unlock];
	[localException raise];
    NS_ENDHANDLER

    if (terminate) terminated = YES;
    [gtkapp_lock unlock];
    if (terminate == NO) return;

    [center postNotificationName:GTKApplicationWillTerminateNotification
	    object:self];

    if ([self mainLevel] == 1) [self mainQuit];
    else [self exit:0];
}

/*
 * Report an exception to |stderr|. This method simply calls NSLog()
 * to print the /exception/'s name and reason.
 */
- (void) reportException:(NSException *) exception
{
    NSString *reason = [exception reason];

    if (reason) NSLog(@"%@: %@", [exception name], reason);
    else NSLog([exception name]);	// name must not contain '%'
}

/*
 * Check whether the application is active.
 */
- (BOOL) isActive
{
    return mainWindow != nil;
}

/*
 * Return the application's current main window (the /active/ window)
 * or |nil| if the application is not active (or has no windows).
 * @see -isActive
 */
- (GTKWindow *) mainWindow
{
    return mainWindow;
}

/*
 * Return the application's window list.
 */
- (NSArray *) windows
{
    NSArray *result;

    [gtkapp_lock lock];
    result = [NSArray arrayWithArray:windows];
    [gtkapp_lock unlock];
    return result;
}
@end
