/* $Id: xfce-startup-notification.c 21623 2006-05-11 08:53:05Z benny $
 * 
 * Copyright (C) 2004-2005 The Xfce Development Team
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library 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.
 *
 * Authors:
 *      Danny Milosavljevic <dannym@xfce.org>
 *      Jasper Huijsmans <jasper@xfce.org>
 *      Olivier Fourdan <fourdan@xfce.org>
 *      Benedikt Meurer <benny@xfce.org>
 *      
 * Startup notification based on gnome-desktop developed by 
 * Elliot Lee <sopwith@redhat.com> and Sid Vicious
 * 
 */
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#ifdef HAVE_STRING_H
#include <string.h>
#endif

#include <gtk/gtk.h>
#include "xfce-startup-notification.h"

#ifdef HAVE_LIBSTARTUP_NOTIFICATION
#define SN_API_NOT_YET_FROZEN
#include <libsn/sn.h>
#include <gdk/gdkx.h>
#define STARTUP_TIMEOUT (30 /* seconds */ * 1000)
#endif
#include <X11/Xlib.h>
#include <X11/Xatom.h>

#ifdef HAVE_LIBSTARTUP_NOTIFICATION

/*

because SnLauncherContext is not a GObject, weakrefs do not work.
Therefore sn_context_destroy_cb needs to be called MANUALLY after 
each sn_launcher_context_unref that will destroy the launcher context.

*/

static void  sn_context_destroy_cb(gpointer what);

/* Section 1: this stuff handles the timeouting */

typedef struct
{
    GSList *contexts;
    guint timeout_id;
}
StartupTimeoutData;

static StartupTimeoutData *startup_timeout_data = NULL;
static GHashTable* startup_context_hash = NULL; /* key = DESKTOP_STARTUP_ID, value = sn_context */

static gboolean atexit_registered = FALSE;

static void
sn_error_trap_push (SnDisplay * display, Display * xdisplay)
{
    gdk_error_trap_push ();
}

static void
sn_error_trap_pop (SnDisplay * display, Display * xdisplay)
{
    gdk_error_trap_pop ();
}

static char**
remove_desktop_startup_id(char const** envp)
{
    gchar **retval = NULL;
    int i;
    int j;
    
    int desktop_startup_id_len;
    for (i = 0; envp[i]; i++);

    retval = g_new (gchar *, i + 1);

    desktop_startup_id_len = strlen ("DESKTOP_STARTUP_ID");

    for (i = 0, j = 0; envp[i]; i++)
    {
	if (strncmp (envp[i], "DESKTOP_STARTUP_ID", desktop_startup_id_len) !=
	    0)
	{
	    retval[j] = g_strdup (envp[i]);
	    ++j;
	}
    }
    retval[j] = NULL;
    
    return retval;

}



static gboolean
startup_timeout (void *data)
{
    StartupTimeoutData *std = data;
    GSList *tmp;
    GTimeVal now;
    int min_timeout;

    min_timeout = STARTUP_TIMEOUT;

    g_get_current_time (&now);

    tmp = std->contexts;
    while (tmp != NULL)
    {
	SnLauncherContext *sn_context = tmp->data;
	GSList *next = tmp->next;
	long tv_sec, tv_usec;
	double elapsed;

	sn_launcher_context_get_last_active_time (sn_context, &tv_sec,
						  &tv_usec);

	elapsed =
	    ((((double) now.tv_sec - tv_sec) * G_USEC_PER_SEC +
	      (now.tv_usec - tv_usec))) / 1000.0;

	if (elapsed >= STARTUP_TIMEOUT)
	{
	    std->contexts = g_slist_remove (std->contexts, sn_context);
	    sn_launcher_context_complete (sn_context);
	    sn_launcher_context_unref (sn_context);
	    sn_context_destroy_cb (sn_context);
	}
	else
	{
	    min_timeout = MIN (min_timeout, (STARTUP_TIMEOUT - elapsed));
	}

	tmp = next;
    }

    if (std->contexts == NULL)
    {
	std->timeout_id = 0;
    }
    else
    {
	std->timeout_id = g_timeout_add (min_timeout, startup_timeout, std);
    }

    return FALSE;
}

static gboolean
sn_context_match_cb(gpointer key, gpointer value, gpointer search_value)
{
  return value == search_value;
}

static void 
sn_context_destroy_cb(gpointer what)
{
  if (startup_context_hash != NULL) {
    g_hash_table_foreach_remove (startup_context_hash, sn_context_match_cb, what);
  }
}

static void
init(void)
{
    if (startup_timeout_data == NULL)
    {
	startup_timeout_data = g_new (StartupTimeoutData, 1);
	startup_timeout_data->contexts = NULL;
	startup_timeout_data->timeout_id = 0;
    }
    if (startup_context_hash == NULL)
    {
	startup_context_hash = g_hash_table_new_full (g_str_hash, g_str_equal,
	                                              g_free, NULL);
    }
}

static void
remove_startup_timeout (SnLauncherContext* sn_context)
{
    if (startup_timeout_data == NULL) {
        return;
    }
    
    GSList* item = startup_timeout_data->contexts;
    while (item != NULL) {
      if (item->data == sn_context) {
        sn_launcher_context_unref (sn_context);
        startup_timeout_data->contexts = g_slist_remove_link (startup_timeout_data->contexts, item);
        break;
      } else {
        item = item->next;
      }
    }
}

static void
add_startup_timeout (SnLauncherContext * sn_context)
{
    sn_launcher_context_ref (sn_context);

    remove_startup_timeout (sn_context); /* just in case */

    startup_timeout_data->contexts =
	g_slist_prepend (startup_timeout_data->contexts, sn_context);

    if (startup_timeout_data->timeout_id == 0)
    {
	startup_timeout_data->timeout_id =
	    g_timeout_add (STARTUP_TIMEOUT, startup_timeout,
			   startup_timeout_data);
    }
}


static void
done (void)
{
    StartupTimeoutData *std = startup_timeout_data;

    if (!std)
    {
	/* No startup notification used, return silently */
	return;
    }

    g_slist_foreach (std->contexts, (GFunc) sn_launcher_context_unref, NULL);
    g_slist_free (std->contexts);

    if (std->timeout_id != 0)
    {
	g_source_remove (std->timeout_id);
	std->timeout_id = 0;
    }

    g_free (std);
    startup_timeout_data = NULL;
    
    if (startup_context_hash != NULL) {
        g_hash_table_destroy (startup_context_hash);
    }
    startup_context_hash = NULL;

    atexit_registered = FALSE;
}

/* End Section 1 */

/* Section 2: helpers solely to make xfce_startup_notification_cancel work */

static void
add_startup_context (SnLauncherContext* sn_context, char const* desktop_startup_id)
{
    /*g_object_weak_ref (G_OBJECT(sn_context), sn_context_destroy_cb, NULL);*/
    g_hash_table_insert (startup_context_hash, g_strdup (desktop_startup_id), sn_context);
}

/* End Section 2 */

/**
 * make_spawn_environment_for_sn_context
 *
 * adds startup notification data to environment 
 *
 * @id : the startup notification context id
 * @envp : the original environment, maybe containing a old DESKTOP_STARTUP_ID that it will get rid of
 *
 * Returns : the new environment that includes the startup notification data
 *
 */
static gchar **
make_spawn_environment_for_sn_context (const char* id, const char** envp)
{
    gchar **retval = NULL;
    int i, j;
    int desktop_startup_id_len;

    for (i = 0; envp[i]; i++)
      ;

    retval = g_new (gchar *, i + 2);

    desktop_startup_id_len = strlen ("DESKTOP_STARTUP_ID");

    for (i = 0, j = 0; envp[i]; i++)
    {
	if (strncmp (envp[i], "DESKTOP_STARTUP_ID", desktop_startup_id_len) !=
	    0)
	{
	    retval[j] = g_strdup (envp[i]);
	    ++j;
	}
    }

    retval[j] = g_strdup_printf ("DESKTOP_STARTUP_ID=%s", id);
    ++j;
    retval[j] = NULL;

    return retval;
}

static gint
get_active_workspace_number (GdkScreen *gscreen)
{
    static Atom _NET_CURRENT_DESKTOP = None;
    static Atom _WIN_WORKSPACE = None;
    GdkDisplay *gdpy = gdk_screen_get_display(gscreen);
    Display *dpy = GDK_DISPLAY_XDISPLAY(gdpy);
    GdkWindow *groot = gdk_screen_get_root_window(gscreen);
    Atom type_ret = None;
    int format_ret = 0;
    unsigned long nitems_ret = 0;
    unsigned long bytes_after_ret = 0;
    unsigned int *prop_ret = NULL;
    gint ws_num = 0;

    if (G_UNLIKELY (!_NET_CURRENT_DESKTOP))
    {
        _NET_CURRENT_DESKTOP = XInternAtom(dpy, "_NET_CURRENT_DESKTOP", False);
        _WIN_WORKSPACE = XInternAtom(dpy, "_WIN_WORKSPACE", False);
    }

    gdk_error_trap_push();
    
    if(XGetWindowProperty(dpy, GDK_WINDOW_XWINDOW(groot), _NET_CURRENT_DESKTOP,
            0, 32, False, XA_CARDINAL, &type_ret, &format_ret, &nitems_ret,
            &bytes_after_ret, (unsigned char **)&prop_ret) != Success)
    {
        if(XGetWindowProperty(dpy, GDK_WINDOW_XWINDOW(groot), _WIN_WORKSPACE,
            0, 32, False, XA_CARDINAL, &type_ret, &format_ret, &nitems_ret,
            &bytes_after_ret, (unsigned char **)&prop_ret) != Success)
        {
            return 0;
        }
    }
    
    gdk_error_trap_pop();
    
    if(type_ret == None || format_ret == 0 || !prop_ret) {
        if(prop_ret)
            XFree(prop_ret);
        return 0;
    }
    
    ws_num = *prop_ret;
    XFree(prop_ret);
    
    return ws_num;
}

/**
 * xfce_startup_notification_start
 *
 * adds startup notification information to the environment and returns the new environment.
 *
 * @screen : the screen to start on
 * @path : the binary path to register with the startup notification
 *
 * Returns: the new id
 *
 */
char*
xfce_startup_notification_start(GdkScreen* screen, char const* path)
{
    SnLauncherContext *sn_context = NULL;
    SnDisplay *sn_display = NULL;
    char* desktop_startup_id;
   
 
    if (!atexit_registered)
    {
        init ();
	g_atexit (done);
	atexit_registered = TRUE;
    }

    sn_display = sn_display_new (gdk_display, sn_error_trap_push,
                                 sn_error_trap_pop);

    if (sn_display != NULL)
    {
        sn_context = sn_launcher_context_new (sn_display,
					 gdk_screen_get_number (screen));
        if ((sn_context != NULL)
          && !sn_launcher_context_get_initiated (sn_context))
        {
            gint workspace = get_active_workspace_number (screen);
            sn_launcher_context_set_workspace (sn_context, workspace);
            sn_launcher_context_set_binary_name (sn_context, path);
            sn_launcher_context_initiate (sn_context,
					      g_get_prgname () ?
					      g_get_prgname () : "unknown",
					      path, 
					      gtk_get_current_event_time ());
					      

            desktop_startup_id = g_strdup (sn_launcher_context_get_startup_id (sn_context));
					      
            add_startup_timeout (sn_context); /* retains sn_context */
            add_startup_context (sn_context, desktop_startup_id); /* doesn't feel passionate about sn_context */
            sn_launcher_context_unref (sn_context);

            sn_display_unref (sn_display);
            
            return desktop_startup_id;
	}
	
	if (sn_context != NULL) {
            sn_launcher_context_unref (sn_context);
        }
	
	sn_display_unref (sn_display);
    }
    
    return NULL;
}

/**
 * xfce_startup_notification_cancel
 *
 * cancels the startup in progress, for example when the executable could not be started after all...
 *
 * @envp : the environment (returned by xfce_startup_notification_start)
 * 
 * Returns: the environment with the startup notification information removed.
 *
 */
void 
xfce_startup_notification_cancel(const char* id)
{
    SnLauncherContext *sn_context = NULL;
    char const* desktop_startup_id;
  
    desktop_startup_id = id;

    if (desktop_startup_id == NULL) { /* none there, fine */
      return;
    }
    
    if (startup_context_hash != NULL) { 
      /* check if the startup notification context is still around... */
      sn_context = g_hash_table_lookup (startup_context_hash, desktop_startup_id);
    } 

    if (sn_context != NULL) {
      sn_launcher_context_complete (sn_context); /* end sequence */
      
      remove_startup_timeout (sn_context);
      /* not refcounted by me so don't unref: sn_launcher_context_unref (sn_context);*/
      sn_context_destroy_cb (sn_context);
      
      /* not refcounted by me, so don't unref. */
    }
}

char** xfce_startup_notification_modify_environment(const char** envp, const char* id)
{
    return make_spawn_environment_for_sn_context (id, envp);
}

char** xfce_startup_notification_cleanup_environment(const char** envp)
{
    return remove_desktop_startup_id (envp);
}


#else /* not HAVE_LIBSTARTUP_NOTIFICATION */

char*
xfce_startup_notification_start(GdkScreen* screen, char const* path)
{
    return g_strdup ("no startup notification support");
}

void
xfce_startup_notification_cancel(const char* id)
{
}

char** xfce_startup_notification_modify_environment(const char** envp, const char* id)
{
    return g_strdupv ((gchar**)envp);
}

char** xfce_startup_notification_cleanup_environment(const char** envp)
{
    return g_strdupv ((gchar**)envp);
}

#endif /* not HAVE_LIBSTARTUP_NOTIFICATION */
