/*
 * Copyright (c) 2003-2004 Benedikt Meurer <benny@xfce.org>
 * Copyright (c) 2003-2004 Olivier Fourdan <fourdan@xfce.org>
 * All rights reserved.
 *
 * 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.
 *
 * http://www.freedesktop.org/standards/systemtray.html
 *
 * TODO:
 *
 *      - Tray icon hints (_NET_WM_NAME, WM_CLASS, _NET_WM_ICON)
 *
 * Credits : 
 *      - eggtraymanager.c Anders Carlsson <andersca@gnu.org>
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif
#ifdef HAVE_STRING_H
#include <string.h>
#endif

#include <X11/Xlib.h>

#include <gdk/gdk.h>
#include <gdk/gdkx.h>
#include <gtk/gtk.h>

#include <libxfcegui4/xfce-internals.h>
#include <libxfcegui4/xfce_marshal.h>
#include <libxfcegui4/xfce_systemtray.h>

/* signals provided by XfceSystemTray */
enum
{
    ICON_DOCKED,                /* tray icon docked */
    ICON_UNDOCKED,              /* tray icon undocked */
    SELECTION_CLEARED,          /* selection cleared (lost) */
    MESSAGE_NEW,                /* new balloon message */
    MESSAGE_CLEAR,              /* clear a balloon message */
    LAST_SIGNAL
};

/* store partial balloon messages */
struct message
{
    glong id;                   /* message id */
    Window window;
    gchar *buffer;              /* buffer space */
    gchar *head;                /* */
    glong left;                 /* chars left */
    glong timeout;              /* */
};

/* prototypes */
static void xfce_system_tray_init (XfceSystemTray *);
static void xfce_system_tray_class_init (XfceSystemTrayClass *);
static void xfce_system_tray_finalize (GObject *);
static void xfce_system_tray_dock_request (XClientMessageEvent *,
                                           XfceSystemTray *);
static gboolean xfce_system_tray_undock_request (GtkSocket *,
                                                 XfceSystemTray *);
static GdkFilterReturn
xfce_system_tray_data (XfceSystemTray *tray, GdkXEvent * xevent);
static GdkFilterReturn
xfce_system_tray_opcode (XfceSystemTray *tray, GdkXEvent * xevent);
static GdkFilterReturn
xfce_system_tray_filter (GdkXEvent *xev, GdkEvent *event, gpointer data);

/* */
static GObjectClass *parent_class;

/* */
static guint tray_signals[LAST_SIGNAL];

GQuark
xfce_system_tray_error_quark (void)
{
    static GQuark quark = 0;

    if (!quark)
    {
        quark = g_quark_from_static_string ("xfce-system-tray-error-quark");
    }

    return (quark);
}

GType
xfce_system_tray_get_type (void)
{
    static GType xfce_system_tray_type = 0;

    if (!xfce_system_tray_type)
    {
        static const GTypeInfo xfce_system_tray_info = {
            sizeof (XfceSystemTrayClass),
            NULL,		/* base_init */
            NULL,		/* base_finalize */
            (GClassInitFunc) xfce_system_tray_class_init,
            NULL,		/* class_finalize */
            NULL,		/* class_data */
            sizeof (XfceSystemTray),
            0,			/* n_preallocs */
            (GInstanceInitFunc) xfce_system_tray_init,
            NULL		/* value_table */
        };

        xfce_system_tray_type = g_type_register_static (G_TYPE_OBJECT,
                                                        "XfceSystemTray",
                                                        &xfce_system_tray_info,
                                                        0);
    }

    return (xfce_system_tray_type);
}

static void
xfce_system_tray_class_init (XfceSystemTrayClass * klass)
{
    GObjectClass *gobject_class;

    /* be sure to initialize the libraries i18n support first */
    _xfce_i18n_init ();

    parent_class = g_type_class_peek_parent (klass);

    gobject_class = G_OBJECT_CLASS (klass);
    gobject_class->finalize = xfce_system_tray_finalize;

    /*
     * create signals
     */
    tray_signals[ICON_DOCKED] = g_signal_new ("icon_docked",
                                              G_OBJECT_CLASS_TYPE (klass),
                                              G_SIGNAL_RUN_LAST,
                                              G_STRUCT_OFFSET
                                              (XfceSystemTrayClass,
                                               icon_docked), NULL, NULL,
                                              g_cclosure_marshal_VOID__OBJECT,
                                              G_TYPE_NONE, 1,
                                              GTK_TYPE_SOCKET);

    tray_signals[ICON_UNDOCKED] = g_signal_new ("icon_undocked",
                                                G_OBJECT_CLASS_TYPE (klass),
                                                G_SIGNAL_RUN_LAST,
                                                G_STRUCT_OFFSET
                                                (XfceSystemTrayClass,
                                                 icon_undocked), NULL, NULL,
                                                g_cclosure_marshal_VOID__OBJECT,
                                                G_TYPE_NONE, 1,
                                                GTK_TYPE_SOCKET);

    tray_signals[SELECTION_CLEARED] = g_signal_new ("selection_cleared",
                                                    G_OBJECT_CLASS_TYPE
                                                    (klass),
                                                    G_SIGNAL_RUN_LAST,
                                                    G_STRUCT_OFFSET
                                                    (XfceSystemTrayClass,
                                                     selection_cleared), NULL,
                                                    NULL,
                                                    g_cclosure_marshal_VOID__VOID,
                                                    G_TYPE_NONE, 0);

    tray_signals[MESSAGE_NEW] = g_signal_new ("message_new",
                                              G_OBJECT_CLASS_TYPE (klass),
                                              G_SIGNAL_RUN_LAST,
                                              G_STRUCT_OFFSET
                                              (XfceSystemTrayClass,
                                               message_new), NULL, NULL,
                                              p_xfce_marshal_VOID__OBJECT_LONG_UINT_STRING,
                                              G_TYPE_NONE, 4, GTK_TYPE_SOCKET,
                                              G_TYPE_UINT, G_TYPE_UINT,
                                              G_TYPE_STRING);

    tray_signals[MESSAGE_CLEAR] = g_signal_new ("message_clear",
                                                G_OBJECT_CLASS_TYPE (klass),
                                                G_SIGNAL_RUN_LAST,
                                                G_STRUCT_OFFSET
                                                (XfceSystemTrayClass,
                                                 message_clear), NULL, NULL,
                                                p_xfce_marshal_VOID__OBJECT_LONG,
                                                G_TYPE_NONE, 2,
                                                GTK_TYPE_SOCKET, G_TYPE_UINT);
}

static void
xfce_system_tray_init (XfceSystemTray * tray)
{
    tray->window = NULL;
    tray->sockets = g_hash_table_new (NULL, NULL);
}

static void
xfce_system_tray_finalize (GObject * object)
{
    XfceSystemTray *tray;

    tray = XFCE_SYSTEM_TRAY (object);

    xfce_system_tray_unregister (tray);

    g_list_free (tray->messages);
    g_hash_table_destroy (tray->sockets);

    G_OBJECT_CLASS (parent_class)->finalize (object);
}


/**
 * xfce_system_tray_new:
 * 
 * Creates a new #XfceSystemTray object.
 **/
XfceSystemTray *
xfce_system_tray_new (void)
{
    return (g_object_new (xfce_system_tray_get_type (), NULL));
}


/**
 * xfce_system_tray_register:
 * @tray    : an #XfceSystemTray object.
 * @xscreen : an X11 Screen object.
 * @err     : location to store a GError object to in case of an error.
 *
 * Registers a system @tray for a given @xscreen.
 *
 * Return value: TRUE on success, else FALSE.
 **/
gboolean
xfce_system_tray_register (XfceSystemTray * tray, Screen * xscreen,
                           GError ** err)
{
    XClientMessageEvent xev;
    char buffer[128];
    Display *display;
    Window xwindow;
    Window rootwin;
    guint32 timestamp;
    GtkWidget *window;
    GdkScreen *screen;

    display = DisplayOfScreen (xscreen);
    /*
     * Clients must use a window they created so that requestors can
     * route events to the owner of the selection.
     */
    screen =
        gdk_display_get_screen (gdk_x11_lookup_xdisplay (display),
                                XScreenNumberOfScreen (xscreen));
    window = gtk_invisible_new_for_screen (screen);
    gtk_widget_realize (window);
    gtk_widget_add_events (window,
                           GDK_PROPERTY_CHANGE_MASK | GDK_STRUCTURE_MASK);

    /*
     * Clients attempting to acquire a selection must set the time value
     * of the SetSelectionOwner request to the timestamp of the event
     * triggering the acquisition attempt, not to CurrentTime.
     */
    timestamp = gdk_x11_get_server_time (window->window);

    /*
     * On startup, the system tray must aquire a manager selection called
     * _NET_SYSTEM_TRAY_Sn, replacing n with the screen number the tray
     * wants to use.
     */
    (void) g_snprintf (buffer, sizeof (buffer), "_NET_SYSTEM_TRAY_S%d",
                       XScreenNumberOfScreen (xscreen));
    tray->atoms.selection = XInternAtom (display, buffer, False);

    XSetSelectionOwner (display, tray->atoms.selection,
                        GDK_WINDOW_XWINDOW (window->window), timestamp);

    /*
     * If the SetSelectionOwner request succeeds (not merely appears to
     * succeed), the client that issues it is recorded by the server as
     * being the owner of the selection for the time period starting at
     * the specified time.
     */
    xwindow = XGetSelectionOwner (display, tray->atoms.selection);

    if (xwindow == GDK_WINDOW_XWINDOW (window->window))
    {
        rootwin = RootWindowOfScreen (xscreen);

        memset (&xev, 0, sizeof (xev));

        xev.type = ClientMessage;
        xev.window = rootwin;
        xev.message_type = XInternAtom (display, "MANAGER", False);
        xev.format = 32;

        xev.data.l[0] = timestamp;
        xev.data.l[1] = tray->atoms.selection;
        xev.data.l[2] = xwindow;

        XSendEvent (display, rootwin, False, StructureNotifyMask,
                    (XEvent *) & xev);

        tray->atoms.message_data = XInternAtom (display,
                                                "_NET_SYSTEM_TRAY_MESSAGE_DATA",
                                                False);

        tray->atoms.opcode = XInternAtom (display,
                                          "_NET_SYSTEM_TRAY_OPCODE", False);

        tray->window = window;
        g_object_ref (G_OBJECT (tray->window));

        /* register window event filter */
        gdk_window_add_filter (window->window, xfce_system_tray_filter, tray);
        
        return (TRUE);
    }

    /*
     * A system tray that fails to get the selection or loses the selection
     * should assume that another system tray is running, and let the
     * selection owner handle tray issues.
     */
    if (err != NULL)
    {
        *err = g_error_new (XFCE_SYSTEM_TRAY_ERROR,
                            XFCE_SYSTEM_TRAY_ERROR_GET_SELECTION_FAILED,
                            _("Failed to acquire manager selection"));
    }
    else
    {
        g_warning (_("Failed to acquire manager selection"));
    }

    gtk_widget_destroy (window);

    return (FALSE);
}

/**
 * xfce_system_tray_unregister:
 * @tray  : an #XfceSystemTray object.
 *
 * Unregisters @tray - which has been registered prior to this - from an
 * X11 screen.
 **/
void
xfce_system_tray_unregister (XfceSystemTray * tray)
{
    guint32 timestamp;
    Display *display;
    Window xwindow;
    GtkWidget *tray_win;

    g_return_if_fail (XFCE_IS_SYSTEM_TRAY (tray));

    if (tray->window == NULL)
        return;


    tray_win = tray->window;
    gdk_error_trap_push ();
    display = GDK_WINDOW_XDISPLAY (tray->window->window);

    xwindow = XGetSelectionOwner (display, tray->atoms.selection);

    if (xwindow == GDK_WINDOW_XWINDOW (tray->window->window))
    {
        timestamp = gdk_x11_get_server_time (tray->window->window);
        XSetSelectionOwner (display, tray->atoms.selection, None, timestamp);
    }
    gdk_error_trap_pop ();

    /* remove window event filter */
    gdk_window_remove_filter (tray->window->window,
                              xfce_system_tray_filter, tray);
    tray->window = NULL;
    gtk_widget_destroy (tray_win);
}

gboolean
xfce_system_tray_check_running (Screen * xscreen)
{
    char buffer[128];
    Atom selection;

    (void) g_snprintf (buffer, sizeof (buffer), "_NET_SYSTEM_TRAY_S%d",
                       XScreenNumberOfScreen (xscreen));

    selection = XInternAtom (DisplayOfScreen (xscreen), buffer, False);

    if (XGetSelectionOwner (DisplayOfScreen (xscreen), selection) != None)
        return (TRUE);

    return (FALSE);
}

static void
xfce_system_tray_dock_request (XClientMessageEvent * xev,
                               XfceSystemTray * tray)
{
    GtkWidget *icon;
    GtkSocket *socket;
    Window *xwindow;
#ifdef ENABLE_KDE_SYSTRAY_PROXY
    GtkRequisition req;
#endif

    icon = GTK_WIDGET (g_hash_table_lookup (tray->sockets, GINT_TO_POINTER (xev->data.l[2])));
    if (icon)
    {
        /*
         * We used to simply ignore the request if a socket was already present,
         * but that seems to confuse some KDE apps like krusader.
         * Instead, remove the existing icon and let the process create a new one,
         * it seems it works just fine. Might not be totally standard, I don't 
         * know.
         */

        g_hash_table_remove (tray->sockets, GINT_TO_POINTER (xev->data.l[2]));
        gtk_widget_destroy (icon);
    }

    xwindow = g_new (Window, 1);
    *xwindow = (Window) xev->data.l[2];

    icon = gtk_socket_new ();
    socket = GTK_SOCKET (icon);
    g_object_set_data_full (G_OBJECT (icon), 
                            "xfce-tray-icon-xwindow",
                            xwindow, g_free);

    g_signal_emit (tray, tray_signals[ICON_DOCKED], 0, icon);

    /*
     * Check if the widget has been attached. If it has not been attached,
     * theres no need to add the socket.
     */
    if (GTK_IS_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (icon))))
    {
        /*
         * connect a callback to the socket, so we get notified when
         * the socket is unplugged.
         */
        g_signal_connect (icon, "plug_removed",
                          G_CALLBACK (xfce_system_tray_undock_request), tray);

        /*
         * register the XEMBED client window id for this socket
         */
        gtk_socket_add_id (GTK_SOCKET (icon), *xwindow);

        /*
         * add the socket to the list of known sockets
         */
        g_hash_table_insert (tray->sockets, GINT_TO_POINTER (xev->data.l[2]), GTK_SOCKET (icon));

#ifdef ENABLE_KDE_SYSTRAY_PROXY
        /*
         * Make sure the icons have a meaningfull size ...
         */ 
        req.width = req.height = 1;
        gtk_widget_size_request (GTK_WIDGET(socket), &req);
        if ((req.width < 16) || (req.height < 16))
        {
            gint nw = MAX (24, req.width);
            gint nh = MAX (24, req.height);
            g_warning (_("tray icon has requested a size of (%i x %i), resizing to (%i x %i)"), 
                        req.width, req.height, nw, nh);
            gtk_widget_set_size_request(icon, nw,  nh);
        }
#endif
        gtk_widget_show(icon);
    }
    else
    {
        g_warning (_("tray icon was not attached, destroying it"));

        /*
         * tray icon has not been attached, so no need to keep it
         * around any longer
         */
        gtk_widget_destroy (icon);
    }
}

static gboolean
xfce_system_tray_undock_request (GtkSocket * icon, XfceSystemTray * tray)
{
    Window *xwindow;

    xwindow = g_object_get_data (G_OBJECT (icon), "xfce-tray-icon-xwindow");

    /*
     * Remove the socket from the list of known sockets
     */
    g_hash_table_remove (tray->sockets, GINT_TO_POINTER (*xwindow));

    /*
     * Notify the tray frontend, that the icon is gone
     */
    g_signal_emit (tray, tray_signals[ICON_UNDOCKED], 0, icon);

    /*
     * notify the GtkSocket, that it is no longer needed and can be
     * destroyed
     */
    return (FALSE);
}

static GdkFilterReturn
xfce_system_tray_data (XfceSystemTray *tray, GdkXEvent * xevent)
{
    XClientMessageEvent *xev;
    struct message *message;
    GtkWidget *socket;
    glong length;
    GList *item;

    xev = (XClientMessageEvent *) xevent;

    for (item = tray->messages; item != NULL; item = item->next)
    {
        message = (struct message *) item->data;

        if (message->window != xev->window)
            continue;

        /* each message contains up to 20 chars */
        length = MIN (message->left, 20);
        memcpy (message->head, &(xev->data), length);
        message->left -= length;
        message->head += length;

        if (!message->left)
        {
            /*
             * Message completed, emit signal
             */

            socket = g_hash_table_lookup (tray->sockets,
                                          GINT_TO_POINTER (message->window));
            if (socket)
            {
               g_signal_emit (tray, tray_signals[MESSAGE_NEW], 0,
                              socket, message->id, message->timeout,
                              message->buffer);
            }
            
            tray->messages = g_list_remove_link (tray->messages, item);
            g_free (message->buffer);
            g_free (message);
        }

        break;
    }

    return (GDK_FILTER_REMOVE);
}

static GdkFilterReturn
xfce_system_tray_opcode (XfceSystemTray *tray, GdkXEvent * xevent)
{
    struct message *message;
    XClientMessageEvent *xev;
    GtkWidget *socket;

    xev = (XClientMessageEvent *) xevent;

    switch (xev->data.l[1])
    {
        case SYSTEM_TRAY_REQUEST_DOCK:
            xfce_system_tray_dock_request (xev, tray);
            return (GDK_FILTER_REMOVE);
            break;
        case SYSTEM_TRAY_BEGIN_MESSAGE:
            message = g_new0 (struct message, 1);
            message->timeout = xev->data.l[2];
            message->left = xev->data.l[3];
            message->id = xev->data.l[4];
            message->window = xev->window;
            message->buffer = g_new0 (gchar, message->left + 1);
            message->head = message->buffer;

            /* add message to list of pending messages */
            tray->messages = g_list_append (tray->messages, message);
            return (GDK_FILTER_REMOVE);
            break;
        case SYSTEM_TRAY_CANCEL_MESSAGE:
            socket = g_hash_table_lookup (tray->sockets,
                                          GINT_TO_POINTER (xev->window));
            if (socket)
            {
                g_signal_emit (tray, tray_signals[MESSAGE_CLEAR], 0, socket,
                               (glong) xev->data.l[2]);
            }
            return (GDK_FILTER_REMOVE);
            break;
        default:
            return (GDK_FILTER_CONTINUE);
            break;
    }

    return (GDK_FILTER_CONTINUE);
}

static GdkFilterReturn
xfce_system_tray_filter (GdkXEvent *xev, GdkEvent *event, gpointer data)
{
    XEvent *xevent = (GdkXEvent *)xev;
    XfceSystemTray *tray;

    tray = XFCE_SYSTEM_TRAY (data);

    if (xevent->type == ClientMessage)
    {
        if (xevent->xclient.message_type == tray->atoms.opcode)
        {
            return xfce_system_tray_opcode (tray, xev);
        }
        else if (xevent->xclient.message_type == tray->atoms.message_data)
        {
            return xfce_system_tray_data (tray, xevent);
        }
    }
    else if (xevent->type == SelectionClear)
    {
        g_signal_emit (tray, tray_signals[SELECTION_CLEARED], 0);
        xfce_system_tray_unregister (tray);
    }

    return GDK_FILTER_CONTINUE;
}


