/*
 * PSIP - A lightweight GTK GUI for pjsip
 * (C) James Budiono 2011
 * License: GNU GPL Version 3 or later, please see attached gpl-3.0.txt 
 * or http://www.gnu.org/copyleft/gpl.html
 * 
 * All the GUI stuff is here.
 * For all actual connections etc, see backend.
 */

#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <gdk/gdkkeysyms.h>
#include <osxcart/rtf.h>
#include <locale.h>
#include <libintl.h>

#include "psip.h"
#include "psip_icon.h"

#define PROGRESS_BAR_SPEED			250		/* move bar every 250ms - that is, 4 moves per second*/
#define REGISTRATION_REFRESH_DELAY	3		/* how many seconds after registration, before we refresh presence status */

#ifdef RELEASE
	#include "psip.glade.h"
	#include <zlib.h>
#else
	#define MAIN_GUI_GLADE "psip.glade"
	#define IM_GUI_GLADE "psip_im.glade"
#endif

psip_state_struct *psip_state;
GdkPixbuf *app_icon = NULL;
GtkFileFilter *filter_all;
GtkFileFilter *filter_wav;
GtkFileFilter *filter_rtf;




/* ====================== Thread-safe utility functions ===================== */

void lock_gdk() {
	if (g_thread_self() != psip_state->main_thread) {
		//g_print("locking\n");
		gdk_threads_enter();
	}
}

void unlock_gdk() {
	if (g_thread_self() != psip_state->main_thread ) {
		//g_print("unlocking\n");
		gdk_threads_leave();	
	}
}

/* === start notification command to make sound === */
GPid start_notification_sound_command (gchararray cmd) {
	gint argc;
	gchar **argv;
	GError *err;
	GPid pid;

	if (!strlen(cmd)) return 0;
	if (!g_shell_parse_argv (cmd, &argc, &argv, &err))
		return 0;
	if (!g_spawn_async (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &pid, &err) ) {
		g_strfreev(argv);
		return 0;
	}
	g_strfreev(argv);
	return pid;
}

/* === stop notification command === */
void stop_notification_sound_command (GPid pid) {
	if (pid) {
		int status;
		kill (pid, SIGTERM);
		waitpid (pid, &status, 0);
	}	
}

/* === get current time stamp - result must be g_free-d later === */
gchararray get_time_stamp() {
	gchararray outstr = g_malloc (200);
    time_t t;
    struct tm *tmp;
    
    t = time(NULL);
    tmp = localtime(&t);
    if (tmp == NULL) {
		g_free(outstr);
		return NULL;
    }
    if (strftime(outstr, 200, "%F %T ", tmp) == 0) {
		g_free(outstr);
		return NULL;
    }
	return outstr;
}




/* ====================== Non-thread-safe utility functions ===================== */
/* all functions are not thread-safe, caller must call lock_gdk() (or gdk_threads_enter) */

/* === push and pop status bar texts === */
void push_status (gchararray context, gchararray text) {
	guint id = gtk_statusbar_get_context_id (GTK_STATUSBAR (psip_state->main_statusbar), context);
	gtk_statusbar_push (GTK_STATUSBAR (psip_state->main_statusbar), id, text);
}

void pop_status (gchararray context) {
	gtk_statusbar_pop (GTK_STATUSBAR (psip_state->main_statusbar), 
						gtk_statusbar_get_context_id (GTK_STATUSBAR (psip_state->main_statusbar), context));
}

/* === create shared file filters === */
void create_file_filters() {
		filter_wav = gtk_file_filter_new ();
		gtk_file_filter_set_name (filter_wav, "WAV files");
		gtk_file_filter_add_mime_type (filter_wav, "audio/x-wav");

		filter_all = gtk_file_filter_new ();
		gtk_file_filter_set_name (filter_all, "All files");
		gtk_file_filter_add_pattern (filter_all, "*");

		filter_rtf = gtk_file_filter_new ();
		gtk_file_filter_set_name (filter_rtf, "RTF files");
		gtk_file_filter_add_mime_type (filter_rtf, "text/rtf");
		
		g_object_ref (filter_wav);
		g_object_ref (filter_all);	
		g_object_ref (filter_rtf);	
}

/* === get audio settings preference === */
void get_audio_preference_settings (audio_settings *as) {
	int i;
	gchararray s;
	
	as->input = as->output = NULL;
	for (i=0; i < MAX_AUDIO_DEVICES; i++) {
		if ( gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON(psip_state->audio_inputs[i])) ) {
			s = (gchararray) gtk_button_get_label (GTK_BUTTON(psip_state->audio_inputs[i]));
			if ( g_strcmp0 ("radiobutton", s) ) as->input = s;
			break;
		}
	}
	for (i=0; i < MAX_AUDIO_DEVICES; i++) {
		if ( gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON(psip_state->audio_outputs[i])) ) {
			s = (gchararray)  gtk_button_get_label ( GTK_BUTTON(psip_state->audio_outputs[i]));
			if ( g_strcmp0 ("radiobutton", s) ) as->output = s;
			break;
		}
	}	
}

/* === show error dialog === */
void show_error(gchararray s) {
	g_object_set (G_OBJECT (psip_state->error_dialog), "text", s, NULL);
	gtk_dialog_run (psip_state->error_dialog);
	gtk_widget_hide (GTK_WIDGET (psip_state->error_dialog));
}

/* === pulse progress bar until progress bar is hidden ===*/
gboolean pulse_progress_bar() {
	if (GTK_WIDGET_VISIBLE (psip_state->progress_window)) {
		gtk_progress_bar_pulse (GTK_PROGRESS_BAR (psip_state->progressbar));
		return TRUE;
	} else return FALSE;
}

/* === returns an adhoc quoted address, NULL is cancelled === */
//address must be freed with g_free
gchararray get_adhoc_address(gchararray title) {
	gchararray address, address_quoted;
	
	gtk_window_set_title (GTK_WINDOW (psip_state->adhoc_address_dialog), title);
	gtk_widget_grab_focus (psip_state->adhoc_address_field);
	gint result = gtk_dialog_run (psip_state->adhoc_address_dialog);
	if (result == GTK_RESPONSE_OK) {
		address = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->adhoc_address_field)));
		gtk_entry_set_text (psip_state->adhoc_address_field, address); 
		address_quoted = g_strdup_printf ("<%s>", address); g_free(address);
		return address_quoted;
	}
	return NULL;
}

/* === get the currently selected row from the view (buddylist or call_list) === 
 * Note: returned GtkIter must be freed using g_slice_free */
GtkTreeIter *view_get_selected(GtkTreeView *the_view, gboolean checkForParent) {
	GtkTreeIter iter, iter_root, *iter_copy;
	
	if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (the_view), NULL, &iter)) {
		if (checkForParent && 
				!gtk_tree_model_iter_parent ( gtk_tree_view_get_model (the_view), &iter_root, &iter))
				return NULL;
		iter_copy = g_slice_new(GtkTreeIter);
		*iter_copy = iter;
		return iter_copy;
	} 
	return NULL;
}



/* ================= PRESENCE utility functions ================= */

/* === set the online status of a buddy === */
void buddyview_set_online_status (GtkTreeIter *i, gboolean status, gchararray status_text) {
	GtkTreeIter iter_root, iter;
	gtk_tree_model_iter_parent (GTK_TREE_MODEL (psip_state->buddytree), &iter_root, i);			
	
	if (status == TRUE) {
		gtk_tree_store_set (psip_state->buddytree, i, BUDDY_ICON, "gtk-yes", BUDDY_STATUS, status, BUDDY_STATUS_TEXT, status_text, GTK_END_OF_LIST);
		
		//any one of them online - the group is online		
		gtk_tree_store_set (psip_state->buddytree, &iter_root, BUDDY_ICON, "gtk-yes", GTK_END_OF_LIST); 
	} else {
		gtk_tree_store_set (psip_state->buddytree, i, BUDDY_ICON, "gtk-no", BUDDY_STATUS, status, BUDDY_STATUS_TEXT, status_text, GTK_END_OF_LIST);
		
		//check whether any remaining are online, if not, change the group to offline too.
		gboolean ok = gtk_tree_model_iter_children( GTK_TREE_MODEL (psip_state->buddytree), &iter, &iter_root);
		gboolean overall_status = FALSE;
		do {
			gtk_tree_model_get (GTK_TREE_MODEL (psip_state->buddytree), &iter, BUDDY_STATUS, &status, GTK_END_OF_LIST);
			overall_status |= status;
			ok = gtk_tree_model_iter_next (GTK_TREE_MODEL (psip_state->buddytree), &iter);
		} while (ok);
		if (!overall_status) gtk_tree_store_set (psip_state->buddytree, &iter_root, BUDDY_ICON, "gtk-no", GTK_END_OF_LIST); 
	}
}

/* === add a new buddy === */
GtkTreeIter *find_category (GtkTreeIter *iter_root, gchararray category) {
	gchararray nick;
	GtkTreeIter *p = NULL;
	if (gtk_tree_model_get_iter_first ( GTK_TREE_MODEL(psip_state->buddytree), iter_root)) {
		do {
			gtk_tree_model_get (GTK_TREE_MODEL (psip_state->buddytree), iter_root, BUDDY_NICK, &nick, GTK_END_OF_LIST);
			if (!strcmp (nick, category)) { // found it
				p = iter_root;
				break;
			}
			g_free (nick);
		} while (gtk_tree_model_iter_next ( GTK_TREE_MODEL(psip_state->buddytree), iter_root));
	}
	if (p) return p;
	
	//not found - have to create a new one
	gtk_tree_store_append (psip_state->buddytree, iter_root, NULL);
	gtk_tree_store_set (psip_state->buddytree, iter_root, BUDDY_NICK, category, GTK_END_OF_LIST);			
	return iter_root;
}
void buddylist_add (gchararray nick, gchararray address, gchararray category) {	
	GtkTreeIter *i, iter_root;
	gint buddy_id = UNREGISTERED_BUDDY;

	i = g_slice_new(GtkTreeIter); //freed when buddy is deleted - see below.
	gtk_tree_store_append (psip_state->buddytree, i, find_category (&iter_root, category));
	buddy_id = backend_register_buddy (address, i);
	gtk_tree_store_set (psip_state->buddytree, i, BUDDY_NICK, nick, BUDDY_ADDRESS, address, BUDDY_STATUS, FALSE, BUDDY_ICON, "gtk-no", BUDDY_ID, buddy_id, GTK_END_OF_LIST);
}

//* === delete existing buddy === */
void buddylist_del (GtkTreeIter *iter) {
	GtkTreeIter iter_root;
	gint buddy_id;
	gpointer p = NULL;
	
	gtk_tree_model_iter_parent (GTK_TREE_MODEL (psip_state->buddytree), &iter_root, iter);
	
	gtk_tree_model_get (GTK_TREE_MODEL (psip_state->buddytree), iter, BUDDY_ID, &buddy_id, GTK_END_OF_LIST);
	if (buddy_id != UNREGISTERED_BUDDY) p = backend_deregister_buddy (buddy_id);
	gtk_tree_store_remove (psip_state->buddytree, iter);
	
	//if all the entries in this category has been deleted, delete the category as well
	if (!gtk_tree_model_iter_has_child (GTK_TREE_MODEL (psip_state->buddytree), &iter_root))
		gtk_tree_store_remove (psip_state->buddytree, &iter_root);
		
	if (p) g_slice_free(GtkTreeIter, p);	
}

/* === register all existing buddies to backend === */
void buddylist_register_all() {
	GtkTreeIter iter_root, iter, *iter_copy;
	gchararray address;
	gint buddy_id;
	
	if (gtk_tree_model_get_iter_first( GTK_TREE_MODEL (psip_state->buddytree), &iter_root)) {
		do {
			if (gtk_tree_model_iter_children (GTK_TREE_MODEL (psip_state->buddytree), &iter, &iter_root)) {
				do {
					gtk_tree_model_get (GTK_TREE_MODEL (psip_state->buddytree), &iter, BUDDY_ADDRESS, &address, GTK_END_OF_LIST);
					g_print("Connecting presence: %s\n", address);
								
					iter_copy = g_slice_new(GtkTreeIter);
					*iter_copy = iter;
					buddy_id = backend_register_buddy (address, iter_copy);
					gtk_tree_store_set (psip_state->buddytree, &iter, BUDDY_ID, buddy_id, GTK_END_OF_LIST);
					
					g_free(address);
				} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (psip_state->buddytree), &iter));
			}
		} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (psip_state->buddytree), &iter_root));
	}
}

/* === de-register all existing buddies to backend === */
/*
void buddylist_deregister_all() {
	GtkTreeIter iter_root, iter;
	gchararray address;
	gint buddy_id;
	gpointer p;

	if (gtk_tree_model_get_iter_first( GTK_TREE_MODEL (psip_state->buddytree), &iter_root)) {
		do {
			if (gtk_tree_model_iter_children (GTK_TREE_MODEL (psip_state->buddytree), &iter, &iter_root)) {	
				do {
					gtk_tree_model_get (GTK_TREE_MODEL (psip_state->buddytree), &iter, BUDDY_ADDRESS, &address, BUDDY_ID, &buddy_id, GTK_END_OF_LIST);
					g_print("Disconnecting presence: %s\n", address);
					
					p = backend_deregister_buddy (buddy_id);
					if (p) g_slice_free (GtkTreeIter, p);
					buddyview_set_online_status (&iter, FALSE, "");
					gtk_tree_store_set (psip_state->buddytree, &iter, BUDDY_ID, UNREGISTERED_BUDDY, GTK_END_OF_LIST);
					
					g_free(address);
				} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (psip_state->buddytree), &iter));
			}
		} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (psip_state->buddytree), &iter_root));
	}			
}
*/

/* === refresh buddy presence === */
gboolean buddylist_refresh_presence() {
	GtkTreeIter iter, iter_root;
	gchararray address;
	gint buddy_id;

	gboolean ok2 = gtk_tree_model_get_iter_first( GTK_TREE_MODEL (psip_state->buddytree), &iter_root);	
	if (ok2) {
		do {
			gboolean ok = gtk_tree_model_iter_children( GTK_TREE_MODEL (psip_state->buddytree), &iter, &iter_root);			
			if (ok) {
				do {
					gtk_tree_model_get (GTK_TREE_MODEL (psip_state->buddytree), &iter, BUDDY_ADDRESS, &address, BUDDY_ID, &buddy_id, GTK_END_OF_LIST);
					if (buddy_id != UNREGISTERED_BUDDY) {
						g_print("Refreshing: %s\n", address);
						backend_refresh_presence (buddy_id);
					}
					g_free(address);
					ok = gtk_tree_model_iter_next (GTK_TREE_MODEL (psip_state->buddytree), &iter);
				} while (ok);
			}
			ok2 = gtk_tree_model_iter_next (GTK_TREE_MODEL (psip_state->buddytree), &iter_root);
		} while (ok2);
	}
	return FALSE; // so that glib timeout will terminate
}





/* ================= IM Window utility functions =============== */

/* === create new im window === */
im_data *create_new_im_window(gchararray address) {
	GtkBuilder *builder;
	GError     *error = NULL;
	im_data *p;
		
	p = g_slice_new(im_data);
	p->address = address;
	p->is_typing = FALSE;
	g_hash_table_insert (psip_state->active_ims, address, p);
		
    /* Create new GtkBuilder object */
    builder = gtk_builder_new();
    /* Load UI from file. If error occurs, report it and quit application. */
#ifdef RELEASE
	if( ! gtk_builder_add_from_string( builder, IM_GUI_GLADE, -1, &error ) )
#else    
    if( ! gtk_builder_add_from_file( builder, IM_GUI_GLADE, &error ) )
#endif    
    {
        g_warning( "%s", error->message );
        return NULL;
    }
 
    /* Get UI components from builder */
    p->w = GTK_WINDOW ( gtk_builder_get_object( builder, "im_window" ) );
    p->msg = GTK_WIDGET( gtk_builder_get_object( builder, "message_text" ) );
    p->history = GTK_WIDGET( gtk_builder_get_object( builder, "history_text" ) );
    p->typing = GTK_WIDGET( gtk_builder_get_object( builder, "im_window_typing" ) );
    
    //create tags
    GtkTextBuffer *history_buffer = gtk_text_view_get_buffer ( GTK_TEXT_VIEW (p->history));
    p->local_prefix_tag = gtk_text_buffer_create_tag (history_buffer, "local_prefix", "weight", PANGO_WEIGHT_BOLD, "foreground", "#0000FF", NULL); 
    p->remote_prefix_tag = gtk_text_buffer_create_tag (history_buffer, "remote_prefix", "weight", PANGO_WEIGHT_BOLD, "foreground", "#FF0000", NULL); 
    
    /* Connect signals */
    gtk_builder_connect_signals( builder, p);
 
    /* Destroy builder, since we don't need it anymore */
    g_object_unref( G_OBJECT( builder ) );

	//and display our window
	gtk_window_set_title (p->w, address);
	gtk_widget_show (GTK_WIDGET (p->w));
    return p;
}

/* === close and destroy im window === */
void close_im_window(im_data *p) {
	//g_print("%s destroyed\n", p->address);
	g_hash_table_remove (psip_state->active_ims, p->address);
	g_free (p->address);
	gtk_widget_destroy ( GTK_WIDGET(p->w));
	g_slice_free(im_data, p);	
}

/* === insert text into im window's history field === */
void im_window_insert_history_text(im_data *p, gchararray from, gchararray text, GtkTextTag *tag) {
	GtkTextBuffer *history_buffer = gtk_text_view_get_buffer ( GTK_TEXT_VIEW (p->history));
	GtkTextIter end_iter; 
	gchararray when = get_time_stamp();
	gchararray ss = g_strdup_printf ("%s - %s: ", when, from);
	
	//append that to the history window
	gtk_text_buffer_get_end_iter (history_buffer, &end_iter);		
	gtk_text_buffer_insert_with_tags (history_buffer, &end_iter, ss, -1, tag, NULL);
	gtk_text_buffer_insert (history_buffer, &end_iter, text, -1);
	gtk_text_buffer_insert (history_buffer, &end_iter, "\n", -1);

	//scroll to end of newly inserted text
	gtk_text_buffer_place_cursor(history_buffer, &end_iter);
	gtk_text_view_scroll_to_mark( GTK_TEXT_VIEW (p->history),
		gtk_text_buffer_get_insert (history_buffer), 0.0, TRUE, 0.0, 1.0); 
	
	//flash window
	gtk_widget_grab_focus (GTK_WIDGET(p->msg));		
	gtk_window_set_urgency_hint (p->w, TRUE); /* this doesn't always work, but anyway ... */

	if (tag == p->local_prefix_tag) log_activity (g_strdup_printf("TEXT TO %s: %s", p->address, text ));
	else log_activity (g_strdup_printf("TEXT FROM %s: %s", from, text ));

	g_free(when); g_free(ss);
}





/* ================= Call Window utility functions =============== */
/* === add entry to active-call list (address & state must be g_malloc-ed, it will be freed here) === */
call_data *add_active_call (gchararray address, gchararray state, gint call_id) {
	call_data *p = g_slice_new (call_data);
	GtkTreeIter *iter = g_slice_new (GtkTreeIter);
	
	p->address = address;
	p->call_id = call_id;
	p->iter = iter;	
	p->level = 1.0;
	p->hold = FALSE;
	g_hash_table_insert (psip_state->active_calls, address, p);
	
	gtk_list_store_append (psip_state->call_list, iter);
	gtk_list_store_set (psip_state->call_list, iter, CALL_ADDRESS, address, CALL_STATUS, state, CALL_DATA, p, CALL_HOLD, FALSE, GTK_END_OF_LIST);
	gtk_tree_selection_select_iter (gtk_tree_view_get_selection (psip_state->call_listview), iter);
	gtk_window_present (psip_state->call_window);
	gtk_widget_show ( GTK_WIDGET(psip_state->call_window));
	
	g_free (state);
	return p;
}

/* === remove call from list === */
void remove_active_call (call_data *p) {
	g_hash_table_remove (psip_state->active_calls, p->address);
	
	gtk_list_store_remove (psip_state->call_list, p->iter);
	g_slice_free (GtkTreeIter,p->iter);
	g_free (p->address);
	g_slice_free (call_data, p);
}
/* === update call status === */
void update_call_state (call_data *p, gchararray state) {
	gtk_list_store_set (psip_state->call_list, p->iter, CALL_STATUS, state, GTK_END_OF_LIST);	
}

/* === add entry to call history === */
void add_call_history_entry (gchararray from, gchararray action) {
	GtkTreeIter iter;
	gchararray when = get_time_stamp();	
	gtk_list_store_append (psip_state->callhistorylist, &iter);
	gtk_list_store_set (psip_state->callhistorylist, &iter, CALL_HISTORY_WHEN, when, CALL_HISTORY_FROM, from, CALL_HISTORY_ACTION, action, GTK_END_OF_LIST);
	log_activity (g_strdup_printf("CALL %s FROM/TO %s", action, from ));
}

/* === send notification messages via IM to all active call parties === */
//text must be g_strdup-ed, it will be g-freed here
void notify_all_active_calls (gchararray text) {
	GtkTreeIter iter;
	gchararray address;
	gboolean ok = gtk_tree_model_get_iter_first( GTK_TREE_MODEL (psip_state->call_list), &iter);			
	if (ok) {
		do {
			gtk_tree_model_get (GTK_TREE_MODEL (psip_state->call_list), &iter, CALL_ADDRESS, &address, GTK_END_OF_LIST);
			//g_print("Sending notice to: %s: %s\n", address, text);
			backend_send_message (address, text);
			g_free(address);
			ok = gtk_tree_model_iter_next (GTK_TREE_MODEL (psip_state->call_list), &iter);
		} while (ok);
	}
	g_free(text);
}





/*====================== Callbacks - Main Window =======================*/

/* update the registration status on the button, also hide the progress window if shown */
void update_registration_status (gboolean registered) {
	gtk_widget_hide (GTK_WIDGET(psip_state->progress_window));
	gtk_widget_set_sensitive (GTK_WIDGET (psip_state->register_button), TRUE); 
	if (registered) {
		gtk_tool_button_set_label (GTK_TOOL_BUTTON(psip_state->register_button),"Registered");
		gtk_tool_button_set_stock_id(GTK_TOOL_BUTTON(psip_state->register_button), "gtk-yes");
		gtk_widget_set_tooltip_text (GTK_WIDGET(psip_state->register_button), "Click here to de-register.");
	} else {
		gtk_tool_button_set_label (GTK_TOOL_BUTTON(psip_state->register_button),"Unregistered");
		gtk_tool_button_set_stock_id (GTK_TOOL_BUTTON(psip_state->register_button), "gtk-no");
		gtk_widget_set_tooltip_text (GTK_WIDGET(psip_state->register_button), "Click here to register.");
	}
}

/*==== callback: registration state toggled - register or deregister ===*/
void on_registration_clicked (GtkToolButton *b, gpointer p) {
	if (!backend_is_registered()) {
		//start registration
		gtk_widget_set_sensitive (GTK_WIDGET (b), FALSE); 		
		gtk_progress_bar_set_text(GTK_PROGRESS_BAR (psip_state->progressbar), "Registering ...");
		gtk_widget_show (GTK_WIDGET (psip_state->progress_window));
		gdk_threads_add_timeout (PROGRESS_BAR_SPEED, pulse_progress_bar, NULL);
		backend_register();
		gdk_threads_add_timeout_seconds (REGISTRATION_REFRESH_DELAY, buddylist_refresh_presence, NULL);
	}
	else {
		update_registration_status (FALSE);
		backend_deregister();
	}
}

/*==== callback: remove a buddy - confirm and then delete ===*/
void on_remove_buddy_clicked (GtkToolButton *b, gpointer p) {
	GtkTreeIter *iter = view_get_selected (psip_state->buddylistview, TRUE);
	gchararray s, nick;
	
	if (iter) {
		gtk_tree_model_get (GTK_TREE_MODEL (psip_state->buddytree), iter, BUDDY_NICK, &nick, GTK_END_OF_LIST);
		s = g_strdup_printf("Removing <%s> from friend list.", nick);
		g_object_set (G_OBJECT (psip_state->confirm_dialog), "text", s, NULL);
		g_free(s); g_free(nick);
		
		//gtk_message_dialog_set_markup (psip_state->confirm_dialog, "<b>Removing a friend.</b>");
		if (gtk_dialog_run(psip_state->confirm_dialog) == GTK_RESPONSE_YES) {
			buddylist_del(iter);
		}
		gtk_widget_hide (GTK_WIDGET (psip_state->confirm_dialog));
		g_slice_free (GtkTreeIter,iter);
	} else show_error ("Choose a friend from the list first.");
}

/*==== callback: add a buddy ===*/
void on_add_buddy_clicked (GtkToolButton *b, gpointer p) {
	// clear fields from previous invocation
	gtk_entry_set_text (psip_state->addbuddy_nickname_field,"");	
	gtk_entry_set_text (psip_state->addbuddy_address_field,"");
	//gtk_entry_set_text (psip_state->addbuddy_category_field,""); //leave it as previous
	gtk_widget_grab_focus (GTK_WIDGET (psip_state->addbuddy_nickname_field));
	
try_again:	
	if (gtk_dialog_run(psip_state->addbuddy_dialog) == GTK_RESPONSE_OK) {
		gchararray nick = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->addbuddy_nickname_field)));
		gchararray address = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->addbuddy_address_field)));
		gchararray category = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->addbuddy_category_field)));
		//g_print ("Adding nick: %s address: %s\n", nick, address );
		if (strlen(nick) && strlen(address) && strlen(category)) {
			buddylist_add (nick, address, category);
			g_free(nick); g_free(address); g_free(category);			
		} else {
			show_error ("All fields must be filled-in.");
			g_free(nick); g_free(address); g_free(category);			
			goto try_again;
		}
	}
	gtk_widget_hide (GTK_WIDGET (psip_state->addbuddy_dialog));
}

/* === callback: edit buddy === */
void on_edit_buddy_clicked (GtkToolButton *b, gpointer p) {
	gchararray nick, address, category;
	GtkTreeIter iter_root, *i = view_get_selected (psip_state->buddylistview, TRUE);
	
	if (i) {
		// set fields from selected buddy
		gtk_tree_model_get (GTK_TREE_MODEL (psip_state->buddytree), i, BUDDY_NICK, &nick, BUDDY_ADDRESS, &address, GTK_END_OF_LIST);		
		gtk_entry_set_text (psip_state->addbuddy_nickname_field, nick);
		gtk_entry_set_text (psip_state->addbuddy_address_field, address);
		
		gtk_tree_model_iter_parent (GTK_TREE_MODEL (psip_state->buddytree), &iter_root, i);
		gtk_tree_model_get (GTK_TREE_MODEL (psip_state->buddytree), &iter_root, BUDDY_NICK, &category, GTK_END_OF_LIST);
		gtk_entry_set_text (psip_state->addbuddy_category_field, category);
		
		gtk_widget_grab_focus (GTK_WIDGET (psip_state->addbuddy_nickname_field));
		g_free(nick); g_free(address); g_free(category);

try_again:	
		if (gtk_dialog_run(psip_state->addbuddy_dialog) == GTK_RESPONSE_OK) {
			nick = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->addbuddy_nickname_field)));
			address = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->addbuddy_address_field)));
			category = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->addbuddy_category_field)));
			
			if (strlen(nick) && strlen(address) && strlen (category)) {
				buddylist_del (i);
				buddylist_add (nick, address, category);
				g_free(nick); g_free(address); g_free(category);
				
			} else {
				show_error ("All fields must be filled-in.");
				g_free(nick); g_free(address); g_free(category);
				goto try_again;
			}
		}
		gtk_widget_hide (GTK_WIDGET (psip_state->addbuddy_dialog));		
		g_slice_free (GtkTreeIter,i);		
	} else show_error ("Choose a friend from the list first.");		
}

/*==== callback: edit preferences ===*/
void on_preferences_clicked (GtkToolButton *b, gpointer p) {
	gchararray s;
	
	//prepare stuff before going in
	backend_prepare_preferences_dialog();
	
	//show the dialog
	gtk_dialog_run (psip_state->preferences_dialog);
			
	//strip whitespaces from all account entries
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->account_sip_url_field)));
	gtk_entry_set_text (psip_state->account_sip_url_field, s); g_free(s);
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->account_registrar_field)));
	gtk_entry_set_text (psip_state->account_registrar_field, s); g_free(s);
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->account_realm_field)));
	gtk_entry_set_text (psip_state->account_realm_field, s); g_free(s);
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->account_user_field)));
	gtk_entry_set_text (psip_state->account_user_field, s); g_free(s);
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->account_password_field)));
	gtk_entry_set_text (psip_state->account_password_field, s); g_free(s);
	
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->ring_command_field)));
	gtk_entry_set_text (psip_state->ring_command_field, s); g_free(s);	
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->help_command_field)));
	gtk_entry_set_text (psip_state->help_command_field, s); g_free(s);	
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->call_timeout_field)));
	gtk_entry_set_text (psip_state->call_timeout_field, s); g_free(s);
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->im_command_field)));
	gtk_entry_set_text (psip_state->im_command_field, s); g_free(s);	

	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->max_calls_field)));
	gtk_entry_set_text (psip_state->max_calls_field, s); g_free(s);	
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->sip_port_field)));
	gtk_entry_set_text (psip_state->sip_port_field, s); g_free(s);	
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->public_ip_field)));
	gtk_entry_set_text (psip_state->public_ip_field, s); g_free(s);	
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->stun_server_field)));
	gtk_entry_set_text (psip_state->stun_server_field, s); g_free(s);	
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->turn_server_field)));
	gtk_entry_set_text (psip_state->turn_server_field, s); g_free(s);	
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->turn_user_field)));
	gtk_entry_set_text (psip_state->turn_user_field, s); g_free(s);	
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->turn_password_field)));
	gtk_entry_set_text (psip_state->turn_password_field, s); g_free(s);	
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->turn_realm_field)));
	gtk_entry_set_text (psip_state->turn_realm_field, s); g_free(s);	
	
	s = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (psip_state->ring_wav_file_button));
	if (psip_state->ring_filename) g_free (psip_state->ring_filename); 
	psip_state->ring_filename = s;
		
	//process changes
	backend_reconfigure();	
}

/*==== callback: refresh presence ===*/
void on_refresh_presence_clicked (GtkToolButton *b, gpointer p) {
	buddylist_refresh_presence();
}

/*==== callback: help ===*/
void on_help_clicked (GtkToolButton *b, gpointer user_data) {
	GError *err = NULL;
	g_spawn_command_line_async (gtk_entry_get_text(psip_state->help_command_field), &err);
}

/* === callback: test ringtone === */
void on_test_ringtone_clicked (GtkToolButton *b, gpointer user_data) {
	gchararray s = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (psip_state->ring_wav_file_button));
	if (!s) return;
	
	if (psip_state->is_playing) backend_stop_playing();
	psip_state->is_playing = TRUE;
	backend_start_playing (s);
	
	GtkDialog *d = GTK_DIALOG (gtk_message_dialog_new (GTK_WINDOW(psip_state->preferences_dialog), 0, GTK_MESSAGE_OTHER, GTK_BUTTONS_CLOSE, 
								"Testing ringtone. Click \"Close\" when done."));
	gtk_window_set_title (GTK_WINDOW(d), "Ringtone Test");
	gtk_dialog_run (d);
	gtk_widget_destroy (GTK_WIDGET(d));
	
	g_free(s);
	psip_state->is_playing = FALSE;
	backend_stop_playing();
}

/*==== callback: set online status ===*/
void on_apply_online_status_clicked (GtkButton *b, gpointer p) {
	gchararray s;
	s = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->online_status_field)));
	gtk_entry_set_text (psip_state->online_status_field, s); g_free(s);		
	backend_state_set_online_status();
}






/*====================== Callbacks - Call Window & Call related functions =======================*/

/* === show/hide call history window === */
void on_open_call_history_clicked (GtkToolButton *b, gpointer p) {
	if (GTK_WIDGET_VISIBLE(psip_state->call_history_window)) {
		gtk_widget_hide (GTK_WIDGET(psip_state->call_history_window));
	} else {
		gtk_widget_show (GTK_WIDGET(psip_state->call_history_window));
	}
}

/* === show/hide call window === */
void on_open_call_window_clicked (GtkToolButton *b, gpointer p) {
	if (GTK_WIDGET_VISIBLE(psip_state->call_window)) {
		gtk_widget_hide (GTK_WIDGET(psip_state->call_window));
	} else {
		//gint rootx, rooty, width, height;
		//gtk_window_get_size (psip_state->main_window, &width, &height);
		//gtk_window_get_position (psip_state->main_window, &rootx, &rooty);
		//rootx += width;
		//gtk_window_move (psip_state->call_window, rootx, rooty);
		gtk_widget_show (GTK_WIDGET(psip_state->call_window));
	}
}

/* === utility function to make the call if address_quoted isn't already active === */
void make_the_call_unless_already_active (gchararray address_quoted) {
	call_data *p = g_hash_table_lookup (psip_state->active_calls, address_quoted);
	if (!p) {
		p = add_active_call (address_quoted, g_strdup("INITIATED"), INVALID_CALL_ID);
		add_call_history_entry (address_quoted, "OUTGOING");
		if ( !backend_make_call (p) ) remove_active_call (p);
	} else {
		g_free (address_quoted);
		gtk_widget_show ( GTK_WIDGET(psip_state->call_window));
		gtk_window_present (psip_state->call_window);
	}
}

/*==== callback: call a buddy ===*/
void on_call_buddy_clicked (GtkToolButton *b, gpointer p) {
	GtkTreeIter *iter;
	gchararray address_quoted;
		
	iter = view_get_selected(psip_state->buddylistview, TRUE);
	if (iter) {
		// valid selection, make the call
		gchararray address;
				
		gtk_tree_model_get (GTK_TREE_MODEL (psip_state->buddytree), iter, BUDDY_ADDRESS, &address, GTK_END_OF_LIST);
		address_quoted = g_strdup_printf("<%s>", address); g_free(address);
		make_the_call_unless_already_active (address_quoted);
		g_slice_free (GtkTreeIter,iter);
	} else {
		// non-valid selection, prompt for adhoc address
		address_quoted = get_adhoc_address("Make Call");
		if (address_quoted) make_the_call_unless_already_active (address_quoted);
	}
}

/* === callback: drop the selected call === */
void on_drop_call_clicked (GtkToolButton *b, gpointer not_used) {
	GtkTreeIter *iter = view_get_selected (psip_state->call_listview, FALSE);
	if (iter) {
		call_data *p;
		gtk_tree_model_get (GTK_TREE_MODEL (psip_state->call_list), iter, CALL_DATA, &p, GTK_END_OF_LIST);
		backend_drop_call (p);
		//remove_active_call (p);
		g_slice_free (GtkTreeIter, iter);	
	} else show_error ("Choose a call from the list first.");
}

/*==== callback: call an address not listed in buddy list ===*/
void on_adhoc_call_clicked (GtkButton *b, gpointer p) {
	gchararray address = g_strstrip (g_strdup (gtk_entry_get_text(psip_state->adhoc_call_field)));
	if (strlen(address)) {
		gchararray address_quoted = g_strdup_printf("<%s>", address); 
		make_the_call_unless_already_active (address_quoted);
	}
	g_free (address);
}

/* ==== callback: send dtmf digits === */
void on_send_dtmf(GtkButton *b) {
	GtkTreeIter *iter = view_get_selected (psip_state->call_listview, FALSE);
	if (iter) {
		call_data *p;
		gtk_tree_model_get (GTK_TREE_MODEL (psip_state->call_list), iter, CALL_DATA, &p, GTK_END_OF_LIST);
		g_print("sending digit: %s to call_id %d\n", gtk_button_get_label (b), p->call_id);
		backend_send_dtmf(p, (gchararray) gtk_button_get_label (b));
		g_slice_free (GtkTreeIter, iter);	
	} else show_error ("Choose a call from the list first.");
}

/* === callback: local mic volume control (0=mute local mic) === */
gboolean on_local_mic_volume_scaler_change_value (GtkRange *range, GtkScrollType scroll, gdouble value, gpointer user_data) {
	if (value < 0 ) value = 0;
	if (value > 1.0 ) value = 1.0;
	backend_adjust_local_mic_level (value);
	return FALSE;
}

/* === callback: remote speaker volume control (0=mute speaker) === */
gboolean on_remote_speaker_volume_scaler_change_value (GtkRange *range, GtkScrollType scroll, gdouble value, gpointer user_data) {
	if (value < 0 ) value = 0;
	if (value > 1.0 ) value = 1.0;
	
	GtkTreeIter *iter = view_get_selected (psip_state->call_listview, FALSE);
	if (iter) {
		call_data *p;
		gtk_tree_model_get (GTK_TREE_MODEL (psip_state->call_list), iter, CALL_DATA, &p, GTK_END_OF_LIST);
		backend_adjust_remote_speaker_level (p, value);
		g_slice_free (GtkTreeIter, iter);	
	} else show_error ("Choose a call from the list first.");
	return FALSE;	
}

/* === callback: selection of call_listview is changed === */
/* used to update display on various state - mainly, the remote speaker volume */
void on_call_listview_cursor_changed (GtkTreeView *treeview, gpointer user_data) {
	GtkTreeIter *iter = view_get_selected (psip_state->call_listview, FALSE);
	if (iter) {
		call_data *p;
		gtk_tree_model_get (GTK_TREE_MODEL (psip_state->call_list), iter, CALL_DATA, &p, GTK_END_OF_LIST);
		gtk_range_set_value (GTK_RANGE (psip_state->remote_speaker_volume_scaler), p->level);
		g_slice_free (GtkTreeIter, iter);	
	}
}

void on_hold_call_toggled (GtkCellRendererToggle *cell, gchar *path, gpointer user_data) {
	GtkTreeIter iter;
	if (gtk_tree_model_get_iter_from_string (GTK_TREE_MODEL (psip_state->call_list), &iter, path)) {
		call_data *p;
		gtk_tree_model_get (GTK_TREE_MODEL (psip_state->call_list), &iter, CALL_DATA, &p, GTK_END_OF_LIST);
		
		if (p->hold) {
			//currently held - now unhold
			gtk_list_store_set (psip_state->call_list, &iter, CALL_HOLD, FALSE, GTK_END_OF_LIST);
			p->hold = FALSE;
			backend_unhold_call (p);
		} else {
			//currently unheld - now hold
			gtk_list_store_set (psip_state->call_list, &iter, CALL_HOLD, TRUE, GTK_END_OF_LIST);
			p->hold = TRUE;	
			backend_hold_call (p);
		}
	}
}

/* === callback: record button activated === */
void on_record_call_toggled (GtkToggleToolButton *b, gpointer p) {
	if (gtk_toggle_tool_button_get_active(b) == TRUE && psip_state->is_recording == FALSE) {
		//start recording
		GtkWidget *dialog;
		dialog = gtk_file_chooser_dialog_new ("Save recorded conversation as",
							  psip_state->call_window,
							  GTK_FILE_CHOOSER_ACTION_SAVE,
							  GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
							  GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
							  NULL);
		gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter_wav);
		gtk_file_chooser_add_filter (GTK_FILE_CHOOSER(dialog), filter_all);
							  
		gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog), TRUE);
		if (!psip_state->recording_filename)
		  {
			gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), "/tmp");
			gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (dialog), "untitled.wav");
		  }
		else
		    gtk_file_chooser_set_filename (GTK_FILE_CHOOSER (dialog), psip_state->recording_filename);
		    
		if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
		  {
			if (psip_state->recording_filename) g_free (psip_state->recording_filename);
			psip_state->recording_filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
			psip_state->is_recording = backend_start_recording (psip_state->recording_filename);
			if (psip_state->is_recording) 
				notify_all_active_calls(g_strdup_printf("NOTICE: %s has started recording the conversation.",gtk_entry_get_text(psip_state->account_sip_url_field)));
		  } 
		gtk_widget_destroy (dialog);
		if (!psip_state->is_recording) gtk_toggle_tool_button_set_active (b, FALSE);
	}
	if (gtk_toggle_tool_button_get_active(b) == FALSE && psip_state->is_recording){
		backend_stop_recording();
		psip_state->is_recording = FALSE;
		notify_all_active_calls(g_strdup_printf("NOTICE: %s has stopped recording the conversation.",gtk_entry_get_text(psip_state->account_sip_url_field)));
	}
}

/* === callback: play button activated === */
void on_play_call_toggled (GtkToggleToolButton *b, gpointer p) {
	if (gtk_toggle_tool_button_get_active(b) == TRUE && psip_state->is_playing == FALSE) {
		//start playing
		GtkWidget *dialog;		
		dialog = gtk_file_chooser_dialog_new ("Open an audio file to play",
							  psip_state->call_window,
							  GTK_FILE_CHOOSER_ACTION_OPEN,
							  GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
							  GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
							  NULL);
		gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter_wav);
		gtk_file_chooser_add_filter (GTK_FILE_CHOOSER(dialog), filter_all);
		                                
		if (psip_state->playing_filename)  gtk_file_chooser_set_filename (GTK_FILE_CHOOSER (dialog), psip_state->playing_filename);
		if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
		  {
			if (psip_state->playing_filename) g_free (psip_state->playing_filename);
			psip_state->playing_filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
			psip_state->is_playing = backend_start_playing (psip_state->playing_filename);
		  }
		gtk_widget_destroy (dialog);		
		if (!psip_state->is_playing) gtk_toggle_tool_button_set_active (b, FALSE);
	}
	if (gtk_toggle_tool_button_get_active(b) == FALSE && psip_state->is_playing){
		backend_stop_playing();
		psip_state->is_playing = FALSE;
	}

}

/* === callback: clear history button === */
void on_call_history_clear_clicked (GtkButton *b, gpointer p) {
	gtk_list_store_clear (psip_state->callhistorylist);
}

/* === show call statistic === */
void on_call_statistic_clicked (GtkToolButton *b, gpointer p) {
	GtkTreeIter *iter = view_get_selected (psip_state->call_listview, FALSE);
	if (iter) {
		call_data *p;
		gchararray s, buffer = g_malloc (STATISTIC_BUFFER_SIZE);
		
		gtk_tree_model_get (GTK_TREE_MODEL (psip_state->call_list), iter, CALL_DATA, &p, GTK_END_OF_LIST);
		s = g_strdup_printf ("Call statistic for %s", p->address);
		if (backend_get_call_statistic(p, buffer)) {
			gtk_window_set_title (GTK_WINDOW(psip_state->call_statistic_dialog), s);
			g_free (s);
			s = g_markup_printf_escaped ("<span font=\"Mono\">%s</span>", buffer);
			gtk_label_set_markup (psip_state->call_statistic_detail, s);
			gtk_dialog_run (psip_state->call_statistic_dialog);			
		}		
		g_slice_free (GtkTreeIter, iter);
		g_free (buffer); g_free (s);
	} else show_error ("Choose a call from the list first.");
}






/*====================== Callbacks - IM Window & IM related functions =======================*/

/*==== callback: message a buddy - open new message window ===*/
void on_message_buddy_clicked (GtkToolButton *b, gpointer not_used) {
	GtkTreeIter *iter;
	gchararray address, address_quoted;
	
	iter = view_get_selected(psip_state->buddylistview, TRUE);
	if (iter) {
		// valid selection, send message to that address
		gtk_tree_model_get (GTK_TREE_MODEL (psip_state->buddytree), iter, BUDDY_ADDRESS, &address, GTK_END_OF_LIST);
		address_quoted = g_strdup_printf ("<%s>",address); g_free(address);
		im_data *p = g_hash_table_lookup(psip_state->active_ims, address_quoted);
		if (!p) { 
			//im window for this user doesn't exist yet, create one
			p = create_new_im_window(address_quoted);
			//g_print("new im window for %s\n", address_quoted);				
		} else {
			gtk_window_present (p->w);
			g_free (address_quoted);
		}
		g_slice_free (GtkTreeIter, iter);
	} else {
		// non-valid selection, prompt for adhoc address
		address_quoted = get_adhoc_address("Send Message");
		if (address_quoted) create_new_im_window(address_quoted);
	}
}

/*==== callback: im window destroyed ===*/
void on_im_window_close_clicked (GtkToolButton *b, gpointer user_data) {
	close_im_window ((im_data *) user_data);
}
gboolean on_im_window_delete_event (GtkWidget *w, GdkEvent  *e, gpointer user_data) {
	close_im_window ((im_data *) user_data);	
	return FALSE;
}

/* === callback: window "focused" (ie activated) === */
gboolean on_im_window_focus_in_event (GtkWidget *w, GdkEventFocus *e, gpointer p) {
	gtk_window_set_urgency_hint  (GTK_WINDOW(w), FALSE); // turn off "urgency" / flashing
	return FALSE;
}

/* === callback: send message === */
gboolean typing_indication_timeout (gpointer user_data);
void on_im_window_apply_clicked (GtkToolButton *unused, gpointer user_data) {
	im_data *p = (im_data *) user_data;
	GtkTextBuffer *msg_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (p->msg));
	GtkTextIter start, end;
	gchararray s;
	
	//get the text from the msg window
	gtk_text_buffer_get_bounds (msg_buffer, &start, &end);
	s = gtk_text_buffer_get_text (msg_buffer, &start, &end, FALSE);
	//g_print ("msg window content: %s\n", s);
	
	//append that to the history window
	im_window_insert_history_text (p, "You", s, p->local_prefix_tag);

	//and send it over the backend
	backend_send_message(p->address, s);
	
	//and clear msg window
	gtk_text_buffer_set_text (msg_buffer, "", -1);
	
	//and clear typing indication
	typing_indication_timeout (p);
}

/* === callback: typing indication timeout */
gboolean typing_indication_timeout (gpointer user_data) {
	im_data *p = (im_data *) user_data;	
	if (p->is_typing) {
		p->is_typing = FALSE;
		backend_typing_indicator (p->address, FALSE);
	}
	return FALSE;
}

/* === callback: capture Enter key for sending message & also used for typing indication === */
gboolean on_message_text_key_press_event (GtkWidget *widget, GdkEventKey* pKey, gpointer user_data){
#define CONTROL_KEYS ((GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK))
	im_data *p = (im_data *) user_data;	
	
	if (!p->is_typing) {
		p->is_typing = TRUE;
		backend_typing_indicator (p->address, TRUE);
		gdk_threads_add_timeout_seconds (DEFAULT_TYPING_TIMEOUT, typing_indication_timeout, p);
	}
	if (pKey->type == GDK_KEY_PRESS && 
		(pKey->keyval == GDK_Return || pKey->keyval == GDK_KP_Enter)  && 
		(pKey->state & CONTROL_KEYS) == 0) {
			on_im_window_apply_clicked (NULL, user_data);
		return TRUE;
	}
	return FALSE;
}

/* === callback: save message in im history window === */
void on_im_window_saveas_clicked (GtkButton *unused, gpointer user_data) {
	GtkWidget *dialog;
	gchararray filename;
	GError *err = NULL;
	im_data *p = (im_data *) user_data;	
	
	dialog = gtk_file_chooser_dialog_new ("Save Message As",
						  psip_state->call_window,
						  GTK_FILE_CHOOSER_ACTION_SAVE,
						  GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
						  GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
						  NULL);
	gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter_rtf);
	gtk_file_chooser_add_filter (GTK_FILE_CHOOSER(dialog), filter_all);					
	gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog), TRUE);
		
	if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) {
		filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
		if (filename) {
			if (!rtf_text_buffer_export (gtk_text_view_get_buffer (GTK_TEXT_VIEW (p->history)), filename, &err))
				show_error (err->message);
			g_free (filename);
		}
	} 
	gtk_widget_destroy (dialog);
}




/* ==================== Icon and Status Icon functions ======================== */

/* === callback: status icon clicked === */
void on_status_icon_activated(GtkStatusIcon *g, gpointer p) {
	if (GTK_WIDGET_VISIBLE(psip_state->main_window)) {
		gtk_widget_hide (GTK_WIDGET(psip_state->main_window));
	} else {
		gtk_window_deiconify (psip_state->main_window);		
		gtk_widget_show (GTK_WIDGET(psip_state->main_window));
	}	
}

/* === callback: main_window window event change === */
void on_main_window_state_event (GtkWidget *w, GdkEventWindowState *e, gpointer p) {
	if (!gtk_toggle_button_get_active(psip_state->minimise_tray_field)) return;
	
	//we just want to detect whether it's minimised	
	if ((e->new_window_state & GDK_WINDOW_STATE_ICONIFIED) == GDK_WINDOW_STATE_ICONIFIED) {
		gtk_widget_hide (GTK_WIDGET(psip_state->main_window));
	}
}

void setup_status_icon() {
	GtkStatusIcon *g = gtk_status_icon_new_from_pixbuf (app_icon);
	gtk_status_icon_set_tooltip (g, "psip");
	g_signal_connect(g, "activate", GTK_SIGNAL_FUNC (on_status_icon_activated), NULL);
	g_signal_connect(psip_state->main_window, "window-state-event", GTK_SIGNAL_FUNC (on_main_window_state_event), NULL);
}

void setup_window_icon() {
	GList *list;
	app_icon = gdk_pixbuf_new_from_inline (-1, psip_icon, FALSE, NULL);
	list = g_list_append(NULL, app_icon);
	gtk_window_set_default_icon_list (list);
	g_list_free (list);
	setup_status_icon();
}





/*========= main ======= */
int
main( int    argc,
      char **argv )
{
    GtkBuilder *builder;
    GError     *error = NULL;
 
	setlocale (LC_ALL, "");
	bindtextdomain ("psip", "/usr/share/locale");
	textdomain ("demo");
 
	g_thread_init(NULL);
	gdk_threads_init();
	
	/* allocate our data structure to hold applications states */
	psip_state = g_slice_new(psip_state_struct);
	psip_state->is_playing = FALSE;
	psip_state->is_recording = FALSE;
	psip_state->recording_filename = NULL;
	psip_state->playing_filename = NULL;
	psip_state->ring_filename = NULL;
	psip_state->activity_log_file = NULL;
	psip_state->main_thread = g_thread_self();
 
    /* Init GTK+ */
    gtk_init( &argc, &argv );
 
    /* Create new GtkBuilder object */
    builder = gtk_builder_new();
    
    /* Load main UI. */
#ifdef RELEASE
    /* gunzip and then load UI from memory. If error occurs, report it and quit application. */
	z_stream zs;
	char *glade_ui;
	
	zs.next_in = MAIN_GUI_GLADE;
	zs.avail_in = sizeof (MAIN_GUI_GLADE);
	
	zs.next_out = glade_ui = g_malloc (MAIN_GUI_GLADE_ORIGINAL_SIZE); 
	zs.avail_out = MAIN_GUI_GLADE_ORIGINAL_SIZE + 1; /* add one for null terminator */
	glade_ui [MAIN_GUI_GLADE_ORIGINAL_SIZE] = 0;
	
	zs.zalloc = Z_NULL;
	zs.zfree = Z_NULL;
	zs.opaque = NULL;
	
	inflateInit2 (&zs, 15+32); //15 = size of window, 32 = allow gzip + zip encoding
	int result = inflate (&zs, Z_FINISH);
	inflateEnd (&zs);
	
	if (result != Z_STREAM_END) {
		g_warning( "Unable to decompress UI.\n" );
		return (1);
	}

	if( ! gtk_builder_add_from_string( builder, glade_ui, -1, &error ) )
    {
        g_warning( "%s", error->message );
        return( 1 );
    }
    g_free (glade_ui);
#else
    /* Load UI from file. If error occurs, report it and quit application. */
    if( ! gtk_builder_add_from_file( builder, MAIN_GUI_GLADE, &error ) )
    {
        g_warning( "%s", error->message );
        return( 1 );
    }    
#endif

    /* Get main window pointer from UI */    
    psip_state->main_window = GTK_WINDOW( gtk_builder_get_object( builder, "main_window" ));

    /* get other objects we need from UI */
    psip_state->progress_window = GTK_WINDOW (gtk_builder_get_object( builder, "progress_window" ));
    psip_state->progressbar = GTK_WIDGET (gtk_builder_get_object( builder, "progress_bar" ));
    psip_state->register_button = GTK_WIDGET (gtk_builder_get_object( builder, "registration" ));
    psip_state->main_statusbar = GTK_WIDGET (gtk_builder_get_object( builder, "main_statusbar" ));
    psip_state->online_status_field = GTK_ENTRY (gtk_builder_get_object( builder, "online_status" ));
    
    psip_state->buddytree = GTK_TREE_STORE (gtk_builder_get_object( builder, "buddytree" ));    
    psip_state->buddylistview = GTK_TREE_VIEW (gtk_builder_get_object( builder, "buddylistview" ));
    
    psip_state->confirm_dialog = GTK_DIALOG (gtk_builder_get_object( builder, "confirm_dialog" ));
    psip_state->error_dialog = GTK_DIALOG (gtk_builder_get_object( builder, "error_dialog" ));
    
    psip_state->addbuddy_dialog = GTK_DIALOG (gtk_builder_get_object( builder, "addbuddy_dialog" ));
    psip_state->addbuddy_nickname_field = GTK_ENTRY (gtk_builder_get_object( builder, "addbuddy_nickname" ));
    psip_state->addbuddy_address_field = GTK_ENTRY (gtk_builder_get_object( builder, "addbuddy_address" ));
    psip_state->addbuddy_category_field = GTK_ENTRY (gtk_builder_get_object( builder, "addbuddy_category" ));
 
    psip_state->preferences_dialog = GTK_DIALOG (gtk_builder_get_object( builder, "preferences_dialog" ));
    psip_state->account_sip_url_field = GTK_ENTRY (gtk_builder_get_object( builder, "account_sip_url" ));  
    psip_state->account_registrar_field = GTK_ENTRY (gtk_builder_get_object( builder, "account_registrar" ));  
    psip_state->account_realm_field = GTK_ENTRY (gtk_builder_get_object( builder, "account_realm" ));
    psip_state->account_user_field = GTK_ENTRY (gtk_builder_get_object( builder, "account_user" ));  
    psip_state->account_password_field = GTK_ENTRY (gtk_builder_get_object( builder, "account_password" ));  
    psip_state->account_srtp_field = GTK_TOGGLE_BUTTON (gtk_builder_get_object( builder, "account_srtp" ));

    psip_state->loglevel_scaler = GTK_WIDGET (gtk_builder_get_object( builder, "loglevel_scaler" ));    
    psip_state->ring_command_field = GTK_ENTRY (gtk_builder_get_object( builder, "ring_command" ));    
    psip_state->help_command_field = GTK_ENTRY (gtk_builder_get_object( builder, "help_command" ));    
    psip_state->call_timeout_field = GTK_ENTRY (gtk_builder_get_object( builder, "call_timeout" ));    
    psip_state->minimise_tray_field = GTK_TOGGLE_BUTTON (gtk_builder_get_object( builder, "minimise_tray" ));
    psip_state->auto_register_field = GTK_TOGGLE_BUTTON (gtk_builder_get_object( builder, "auto_register" ));
    psip_state->ring_wav_file_button = GTK_FILE_CHOOSER_BUTTON (gtk_builder_get_object( builder, "ring_wav_file" ));
	psip_state->beep_on_im_field = GTK_TOGGLE_BUTTON (gtk_builder_get_object( builder, "beep_on_im" ));
	psip_state->im_command_field = GTK_ENTRY (gtk_builder_get_object( builder, "im_command" ));
	psip_state->activity_log_field = GTK_TOGGLE_BUTTON (gtk_builder_get_object( builder, "activity_log" ));
	
	psip_state->max_calls_field = GTK_ENTRY (gtk_builder_get_object( builder, "max_calls" ));    
    psip_state->sip_port_field = GTK_ENTRY (gtk_builder_get_object( builder, "sip_port" ));
    psip_state->public_ip_field = GTK_ENTRY (gtk_builder_get_object( builder, "public_ip" ));
    psip_state->stun_server_field = GTK_ENTRY (gtk_builder_get_object( builder, "stun_server" ));
	psip_state->disable_vad_field = GTK_TOGGLE_BUTTON (gtk_builder_get_object( builder, "disable_vad" ));
	psip_state->enable_ice_field = GTK_TOGGLE_BUTTON (gtk_builder_get_object( builder, "enable_ice" ));	
    psip_state->turn_server_field = GTK_ENTRY (gtk_builder_get_object( builder, "turn_server" )); 
    psip_state->turn_user_field = GTK_ENTRY (gtk_builder_get_object( builder, "turn_user" )); 
    psip_state->turn_password_field = GTK_ENTRY (gtk_builder_get_object( builder, "turn_password" )); 
    psip_state->turn_realm_field = GTK_ENTRY (gtk_builder_get_object( builder, "turn_realm" )); 

    create_file_filters();
    gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (psip_state->ring_wav_file_button), filter_wav);
    gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (psip_state->ring_wav_file_button), filter_all);

    psip_state->call_window = GTK_WINDOW (gtk_builder_get_object( builder, "call_window" ));
    psip_state->call_list = GTK_LIST_STORE (gtk_builder_get_object( builder, "call_list" ));
    psip_state->call_listview = GTK_TREE_VIEW (gtk_builder_get_object( builder, "call_listview" ));
    psip_state->local_mic_volume_scaler = GTK_WIDGET (gtk_builder_get_object( builder, "local_mic_volume_scaler" ));    
    psip_state->remote_speaker_volume_scaler = GTK_WIDGET (gtk_builder_get_object( builder, "remote_speaker_volume_scaler" ));
    gtk_range_set_value (GTK_RANGE (psip_state->local_mic_volume_scaler), 1.0);
    gtk_range_set_value (GTK_RANGE (psip_state->remote_speaker_volume_scaler), 1.0);
    psip_state->adhoc_call_field = GTK_ENTRY (gtk_builder_get_object( builder, "adhoc_call_entry" ));
    
    psip_state->call_history_window = GTK_WINDOW (gtk_builder_get_object( builder, "call_history_window" ));
	psip_state->callhistorylist = GTK_LIST_STORE (gtk_builder_get_object( builder, "callhistorylist" ));        

    psip_state->call_statistic_dialog = GTK_DIALOG (gtk_builder_get_object( builder, "statistic_dialog" ));
	psip_state->call_statistic_detail = GTK_LABEL (gtk_builder_get_object( builder, "statistic_detail" ));        
	
	psip_state->adhoc_address_dialog = GTK_DIALOG (gtk_builder_get_object( builder, "adhoc_address_dialog" ));	
	psip_state->adhoc_address_field = GTK_ENTRY (gtk_builder_get_object( builder, "adhoc_address" )); 
	
	setup_window_icon();
	
#ifdef DISABLE_RECORDER
	gtk_widget_destroy (GTK_WIDGET (gtk_builder_get_object( builder, "record_call" )));
#endif

	//get the radio buttons
	int i = 0;
	for (i = 0; i < MAX_AUDIO_DEVICES; i++) {
		gchararray s;
		s = g_strdup_printf("audio_input%d",i);
		psip_state->audio_inputs[i] = GTK_WIDGET (gtk_builder_get_object( builder, s ));  
		g_free(s);

		s = g_strdup_printf("audio_output%d",i);
		psip_state->audio_outputs[i] = GTK_WIDGET (gtk_builder_get_object( builder, s ));  
		g_free(s);		
	}
	
    /* Connect signals */
    gtk_builder_connect_signals( builder, NULL);
 
    /* Destroy builder, since we don't need it anymore */
    g_object_unref( G_OBJECT( builder ) );

	/* init system */
	psip_state->active_ims = g_hash_table_new (g_str_hash, g_str_equal);
	psip_state->active_calls = g_hash_table_new (g_str_hash, g_str_equal);
	load_config();
  
	if (backend_start() == TRUE) {
		/* Show window. All other widgets are automatically shown by GtkBuilder */
		gdk_threads_enter();
		
		buddylist_register_all();
		gtk_widget_show( GTK_WIDGET(psip_state->main_window) );

		if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON (psip_state->auto_register_field)))
			on_registration_clicked (GTK_TOOL_BUTTON(psip_state->register_button), NULL);		
	 
		/* Start main loop */
		gtk_main();
		
		/* done */
		//buddylist_deregister_all(); //seems unnecessary
		backend_stop();
		save_config();
		gdk_threads_leave();
	} else backend_stop();
	
	g_hash_table_destroy (psip_state->active_ims);
	g_hash_table_destroy (psip_state->active_calls);

	g_slice_free(psip_state_struct, psip_state);
	g_print( "\nDone.\n");
    return( 0 );
}

