/*
Copyright (c) 2018 Raspberry Pi (Trading) Ltd.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the copyright holder nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <locale.h>
#include <stdlib.h>
#include <string.h>
#include <glib/gi18n.h>

#include <gio/gio.h>

#include "plugin.h"

#define ICON_BUTTON_TRIM 4

//#define AUTOHIDE      // experimental for now...

//#define DEBUG_ON
#ifdef DEBUG_ON
#define DEBUG(fmt,args...) g_message("ej: " fmt,##args)
#else
#define DEBUG
#endif

/* Plug-in global data */

typedef struct {

    GtkWidget *plugin;              /* Back pointer to the widget */
    LXPanel *panel;                 /* Back pointer to panel */
    GtkWidget *tray_icon;           /* Displayed image */
    config_setting_t *settings;     /* Plugin settings */
    GtkWidget *popup;               /* Popup message */
    GtkWidget *alignment;           /* Alignment object in popup message */
    GtkWidget *box;                 /* Vbox in popup message */
    GtkWidget *menu;                /* Popup menu */
    GtkWidget *empty;               /* Menuitem shown when no devices */
    GHashTable *devices;
    GSList *invalid_devices;
    GVolumeMonitor *monitor;
    gboolean autohide;
} EjecterPlugin;

/* Per-device data */

typedef struct {
    EjecterPlugin *plugin;
    GDrive *drive;
    GSList *volumes;
    GSList *mounts;
    GtkWidget *icon;
    GtkWidget *menuitem;
    GtkWidget *label;
    GtkWidget *eject;
    gboolean was_removed;
    gboolean was_ejected;
    gboolean was_mounted;
    uint volume_count;
    uint mount_count;
    char *name;
    char *description;
    char *drive_id;
} EjecterDevice;

/* Prototypes */

static void load_devices (EjecterPlugin *data);
static void verify_devices (EjecterPlugin *ej);
static void handle_volume_added (GtkWidget *widget, GVolume *volume, EjecterPlugin *data);
static void handle_mount_added (GtkWidget *widget, GMount *mount, EjecterPlugin *data);
static EjecterDevice *new_device (GDrive *drive, EjecterPlugin *plugin);
static void new_drive (EjecterPlugin *data, GDrive *drive);
static void new_volume (EjecterPlugin *data, GVolume *volume);
static void new_mount (EjecterPlugin *data, GMount *mount);
static void device_add_volume (EjecterDevice *dev, GVolume *volume);
static void device_add_mount (EjecterDevice *dev, GMount *mount); 
static void handle_mount_unmounted (GtkWidget *widget, gpointer ptr);
static void handle_volume_removed (GtkWidget *widget, gpointer ptr);
static void handle_drive_disconnected (GtkWidget *widget, gpointer ptr);
static void handle_eject_clicked (GtkWidget *widget, gpointer ptr);
static void device_remove (EjecterDevice *dev);
static void eject_done (GObject *source_object, GAsyncResult *res, gpointer ptr);
static void unmount_done (EjecterDevice *dev);
static void gtk_tooltip_window_style_set (EjecterPlugin *ej);
static void draw_round_rect (cairo_t *cr, gdouble aspect, gdouble x, gdouble y, gdouble corner_radius, gdouble width, gdouble height);
static void fill_background (GtkWidget *widget, cairo_t *cr, GdkColor *bg_color, GdkColor *border_color, guchar alpha);
static void update_shape (EjecterPlugin *ej);
static gboolean gtk_tooltip_paint_window (EjecterPlugin *ej);
static void ejecter_popup_set_position (GtkMenu *menu, gint *px, gint *py, gboolean *push_in, gpointer data);
static gboolean ejecter_mouse_out (GtkWidget * widget, GdkEventButton * event, EjecterPlugin *ej);
static void show_message (EjecterPlugin *ej, char *str1, char *str2);
static void hide_message (EjecterPlugin *ej);
static void update_icon (EjecterPlugin *ej);
static void show_menu (EjecterPlugin *ej);
static void hide_menu (EjecterPlugin *ej);
static void create_menuitem (EjecterDevice *d);
static void set_icon (LXPanel *p, GtkWidget *image, const char *icon, int size);

/* Debug functions */

static void dev_dump (EjecterDevice *dev)
{
    GSList *iter;
    printf ("\nDevice\n");
    if (dev->name) printf ("Device name %s\n", dev->name);
    else printf ("Null device name\n");
    if (dev->drive_id) printf ("Local drive ID %s\n", dev->drive_id);
    else printf ("Null local drive ID\n");
    if (dev->drive)
    {
        if (g_drive_get_identifier (dev->drive, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE)) printf ("Drive ID %s\n", g_drive_get_identifier (dev->drive, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE));
        else printf ("Drive exists; null drive ID\n");

        if (g_drive_get_name (dev->drive)) printf ("Drive name %s\n", g_drive_get_name (dev->drive));
        else printf ("Drive exists; null drive name\n");
    }
    else printf ("Null drive\n");
    printf ("Mounted %d Removed %d Ejected %d\n", dev->was_mounted, dev->was_removed, dev->was_ejected);
    printf ("Volumes %d Mounts %d \n", dev->volume_count, dev->mount_count);
    for (iter = dev->volumes; iter != NULL; iter = g_slist_next (iter))
        printf ("Volume name %s\n", g_volume_get_name (iter->data));
    for (iter = dev->mounts; iter != NULL; iter = g_slist_next (iter))
        printf ("Mount name %s\n", g_mount_get_name (iter->data));
    printf ("\n");
}

static void hash_dump (EjecterPlugin *data)
{
    printf ("\n%d entries in hash table\n", g_hash_table_size (data->devices));
    GList *iter, *keys = g_hash_table_get_keys (data->devices);
    for (iter = keys; iter != NULL; iter = g_list_next (iter))
    {
        printf ("Entry %s\n", iter->data);
        EjecterDevice *dev = g_hash_table_lookup (data->devices, iter->data);
        if (dev) dev_dump (dev);
        else printf ("No device matched\n");
    }
    printf ("\n");
}

/* Adding drives, volumes and mounts */

static void load_devices (EjecterPlugin *data)
{
    GList *iter, *vol_list = g_volume_monitor_get_volumes (data->monitor);

    for (iter = g_list_first (vol_list); iter != NULL; iter = g_list_next (iter))
    {
        GVolume *v = iter->data;
        GDrive *d = g_volume_get_drive (v);
        new_drive (data, d);
        new_volume (data, v);
        GMount *m = g_volume_get_mount (v);
        if (m != NULL) new_mount (data, m);
    }
}

static void verify_devices (EjecterPlugin *ej)
{
    // this is a brutal belt-and-braces sanity check for rogue devices that have failed to eject properly....
    GList *iter, *keys = g_hash_table_get_keys (ej->devices);
    for (iter = keys; iter != NULL; iter = g_list_next (iter))
    {
        EjecterDevice *dev = g_hash_table_lookup (ej->devices, iter->data);
        if (access (dev->drive_id, F_OK) == -1)
        {
            DEBUG ("Removing spurious table entry");
            g_hash_table_remove (ej->devices, dev->drive_id);
            dev->was_ejected = TRUE;
            device_remove (dev);
        }
    }
}

static void handle_volume_added (GtkWidget *widget, GVolume *volume, EjecterPlugin *data)
{
    new_volume (data, volume);
    if (data->menu && gtk_widget_get_visible (data->menu)) show_menu (data);
    update_icon (data);
}

static void handle_mount_added (GtkWidget *widget, GMount *mount, EjecterPlugin *data)
{
    new_mount (data, mount);
    if (data->menu && gtk_widget_get_visible (data->menu)) show_menu (data);
    update_icon (data);
}

static EjecterDevice *new_device (GDrive *drive, EjecterPlugin *plugin)
{
    EjecterDevice *d = g_malloc (sizeof (EjecterDevice));

    d->plugin = plugin;
    d->drive = drive;

    d->was_mounted = FALSE;
    d->was_removed = FALSE;
    d->was_ejected = FALSE;
    d->volumes = NULL;
    d->volume_count = 0;
    d->mounts = NULL;
    d->mount_count = 0;

    d->name = NULL;
    d->description = NULL;
    d->drive_id = NULL;

    g_signal_connect (drive, "disconnected", G_CALLBACK (handle_drive_disconnected), d);

    return d;
}

static void new_drive (EjecterPlugin *data, GDrive *drive)
{
    DEBUG ("NEW DRIVE");

    if (drive == NULL)
    {
        DEBUG ("Drive passed was null, skipping");
        return;
    }
    char *id = g_drive_get_identifier (drive, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE);
    if (id == NULL)
    {
        DEBUG ("No id");
        return;
    }
    if (g_slist_index (data->invalid_devices, id) != -1)
    {
        DEBUG ("Skipped drive (in invalid list)");
        return;
    }
    if (g_hash_table_lookup (data->devices, id) != NULL)
    {
        DEBUG ("Skipped drive (already in list)");
        return;
    }
    DEBUG ("Drive id: %s", id);
    
    EjecterDevice *d = new_device (drive, data);
    d->name = g_drive_get_name (drive);
    d->drive_id = id;
    g_hash_table_insert (data->devices, id, d);
}

static void new_volume (EjecterPlugin *data, GVolume *volume)
{
    DEBUG ("NEW VOLUME");

    if (volume == NULL)
    {
        DEBUG ("Volume was null, skipping");
        return;
    }
    GDrive *drive = g_volume_get_drive (volume);
    if (drive == NULL)
    {
        DEBUG ("Volume did not have a drive, skipping");
        return;
    }
    char *id = g_drive_get_identifier (drive, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE);
    if (id == NULL) return;
    if (g_slist_index (data->invalid_devices, id) != -1) return;
    
    DEBUG ("Drive id: %s", id);
    DEBUG ("Volume name: %s", g_volume_get_name (volume));
        
    EjecterDevice *dev = g_hash_table_lookup (data->devices, id);
    if (dev == NULL) 
    {
        new_drive (data, drive);
        dev = g_hash_table_lookup (data->devices, id);
        if (dev == NULL) return;
    }
    device_add_volume (dev, volume);
}

static void new_mount (EjecterPlugin *data, GMount *mount)
{
    DEBUG ("NEW MOUNT");

    if (mount == NULL)
    {
        DEBUG ("Mount was null, skipping");
        return;
    }
    GDrive *drive = g_mount_get_drive (mount);
    if (drive == NULL)
    {
        DEBUG ("Mount did not have a drive, skipping");
        return;
    }
    char *id = g_drive_get_identifier (drive, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE);
    if (id == NULL) return;
    
    DEBUG ("Mount id: %s", id);
    DEBUG ("Mount name: %s", g_mount_get_name (mount));
      
    EjecterDevice *dev = g_hash_table_lookup (data->devices, id);
    if (dev == NULL) return;
    device_add_mount (dev, mount);
}

static void device_add_volume (EjecterDevice *dev, GVolume *volume)
{
    dev->volumes = g_slist_prepend (dev->volumes, volume);
    dev->volume_count = g_slist_length (dev->volumes);

    g_signal_connect (volume, "removed", G_CALLBACK (handle_volume_removed), dev);
}

static void device_add_mount (EjecterDevice *dev, GMount *mount)
{
    GVolume *volume = g_mount_get_volume (mount);
    if (g_slist_index (dev->volumes, volume) < 0) device_add_volume (dev, volume);

    dev->mounts = g_slist_prepend (dev->mounts, mount);
    dev->mount_count = g_slist_length (dev->mounts);

    dev->was_mounted = TRUE;
    g_signal_connect (mount, "unmounted", G_CALLBACK (handle_mount_unmounted), dev);
}


/* Removing drives, volumes and mounts */

static void handle_mount_unmounted (GtkWidget *widget, gpointer ptr)
{
    GMount *mount = G_MOUNT (widget);
    EjecterDevice *dev = (EjecterDevice *) ptr;
    EjecterPlugin *data = dev->plugin;

    DEBUG ("DEVICE UNMOUNTED %s", dev->name);

    if (!dev) return;
    if (!mount) return;

    if (dev->mount_count && dev->mounts)
    {
        dev->mounts = g_slist_remove (dev->mounts, mount);
        dev->mount_count = g_slist_length (dev->mounts);
    }

    if (dev->mount_count == 0)
    {
        // if there are no mounts but was_ejected is FALSE, then a drive has been pulled 
        // without handle_drive_disconnected being called
        if (!dev->was_ejected)
        {
            dev->was_ejected = TRUE;
            device_remove (dev);
        }
        else
        {
            dev->was_mounted = FALSE;
            unmount_done (dev);
        }
    }

    if (data->menu && gtk_widget_get_visible (data->menu)) show_menu (data);
    update_icon (data);
}

static void handle_volume_removed (GtkWidget *widget, gpointer ptr)
{
    GVolume *volume = G_VOLUME (widget);
    EjecterDevice *dev = (EjecterDevice *) ptr;
    EjecterPlugin *data = dev->plugin;

    DEBUG ("VOLUME REMOVED %s", g_volume_get_name (volume));

    if (!dev) return;
    if (!volume) return;

    if (dev->volume_count && dev->volumes)
    {
        dev->volumes = g_slist_remove (dev->volumes, volume);
        dev->volume_count = g_slist_length (dev->volumes);
    }

    // if there are no volumes but was_ejected is FALSE, then a drive has been pulled 
    // without handle_drive_disconnected being called
    if (dev->volume_count == 0 && !dev->was_ejected)
    {
		dev->was_ejected = TRUE;
		device_remove (dev);
	}

    if (data->menu && gtk_widget_get_visible (data->menu)) show_menu (data);
    update_icon (data);
}

static void handle_drive_disconnected (GtkWidget *widget, gpointer ptr)
{
    EjecterDevice *dev = (EjecterDevice *) ptr;
    EjecterPlugin *data = dev->plugin;

	DEBUG ("DRIVE REMOVED %s", dev->name);

    // if nothing on the drive is mounted, no need to warn about failure to eject
    if (dev->mount_count == 0) dev->was_ejected = TRUE;
    device_remove (dev);

    if (data->menu && gtk_widget_get_visible (data->menu)) show_menu (data);
    update_icon (data);
}

static void handle_eject_clicked (GtkWidget *widget, gpointer ptr)
{
    EjecterDevice *d = (EjecterDevice *) ptr;
    GMountOperation *op = gtk_mount_operation_new (NULL);
    d->was_ejected = TRUE;
    if (g_drive_is_media_removable (d->drive) && g_drive_can_eject (d->drive))
    {
        DEBUG ("Eject: %s", g_drive_get_name (d->drive));
        g_drive_eject_with_operation (d->drive, G_MOUNT_UNMOUNT_NONE, op, NULL, eject_done, d);
    }
    else
    {
        DEBUG ("Unmount: %s", g_drive_get_name (d->drive));
        GList *iter;
        for (iter = g_drive_get_volumes (d->drive); iter != NULL; iter = g_list_next (iter))
        {
            GVolume *v = iter->data;
            GMount *m = g_volume_get_mount (v);
            g_mount_eject_with_operation (m, G_MOUNT_UNMOUNT_NONE, op, NULL, eject_done, d);
        }
    }
}

static void device_remove (EjecterDevice *dev)
{
    DEBUG ("REMOVED %s", dev->name);

    if (!dev) return;
    if (dev->was_removed) return;
    dev->was_removed = TRUE;

    EjecterPlugin *data = dev->plugin;

    // remove the device from the table of current devices
    if (dev->drive_id) g_hash_table_remove (data->devices, dev->drive_id);

    // disconnect all handlers
    g_signal_handlers_disconnect_matched (dev->drive, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, G_CALLBACK (handle_drive_disconnected), NULL);

    GSList *iter;
    for (iter = dev->mounts; iter != NULL; iter = g_slist_next (iter))
    {
        GMount *m = iter->data;
        g_signal_handlers_disconnect_matched (m, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, G_CALLBACK (handle_mount_unmounted), NULL);
    }
    for (iter = dev->volumes; iter != NULL; iter = g_slist_next (iter))
    {
        GVolume *v = iter->data;
        g_signal_handlers_disconnect_matched (v, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, G_CALLBACK (handle_volume_removed), NULL);
    }

    if (!dev->was_ejected)
    {
        char buffer[128];
		sprintf (buffer, _("%s was removed without ejecting"), dev->name);
        show_message (data, buffer, _("Please use menu to eject before removal"));
    }
    else hide_message (data);

    g_free (dev);
    dev = NULL;
}

static void eject_done (GObject *source_object, GAsyncResult *res, gpointer ptr)
{
    DEBUG ("EJECT COMPLETE");

    EjecterDevice *dev = (EjecterDevice *) ptr;

    // final backup check in case unmount calls didn't happen...
    if (!dev) return;

    dev->was_mounted = FALSE;
    if (dev->mount_count && dev->mounts)
    {
        g_slist_free (dev->mounts);
        dev->mounts = NULL;
        dev->mount_count = 0;
    }
    else return;

    DEBUG ("Backup eject called");
    unmount_done (dev);
}

static void unmount_done (EjecterDevice *dev)
{
    if (!dev->was_removed)
    {
        DEBUG ("UNMOUNTED %s", dev->name);

        char buffer[128];
        sprintf (buffer, _("%s has been ejected"), dev->name);
        show_message (dev->plugin, buffer, _("It is now safe to remove the device"));
    }
}

/* The functions below are a copy of those in GTK+2.0's gtktooltip.c, as for some reason, you cannot */
/* manually cause a tooltip to appear with a simple function call. I have no idea why not... */

static void on_screen_changed (GtkWidget *window)
{
	GdkScreen *screen;
	GdkColormap *cmap;

	screen = gtk_widget_get_screen (window);
	cmap = NULL;
	if (gdk_screen_is_composited (screen)) cmap = gdk_screen_get_rgba_colormap (screen);
	if (cmap == NULL) cmap = gdk_screen_get_rgb_colormap (screen);

	gtk_widget_set_colormap (window, cmap);
}

static void gtk_tooltip_window_style_set (EjecterPlugin *ej)
{
    gtk_alignment_set_padding (GTK_ALIGNMENT (ej->alignment), ej->popup->style->ythickness, ej->popup->style->ythickness,
        ej->popup->style->xthickness, ej->popup->style->xthickness);
    gtk_box_set_spacing (GTK_BOX (ej->box), ej->popup->style->xthickness);
    gtk_widget_queue_draw (ej->popup);
}

static void draw_round_rect (cairo_t *cr, gdouble aspect, gdouble x, gdouble y, gdouble corner_radius, gdouble width, gdouble height)
{
    gdouble radius = corner_radius / aspect;
    cairo_move_to (cr, x + radius, y);
    cairo_line_to (cr, x + width - radius, y);
    cairo_arc (cr, x + width - radius, y + radius, radius, -90.0f * G_PI / 180.0f, 0.0f * G_PI / 180.0f);
    cairo_line_to (cr, x + width, y + height - radius);
    cairo_arc (cr, x + width - radius, y + height - radius, radius, 0.0f * G_PI / 180.0f, 90.0f * G_PI / 180.0f);
    cairo_line_to (cr, x + radius, y + height);
    cairo_arc (cr, x + radius, y + height - radius, radius,  90.0f * G_PI / 180.0f, 180.0f * G_PI / 180.0f);
    cairo_line_to (cr, x, y + radius);
    cairo_arc (cr, x + radius, y + radius, radius, 180.0f * G_PI / 180.0f, 270.0f * G_PI / 180.0f);
    cairo_close_path (cr);
}

static void fill_background (GtkWidget *widget, cairo_t *cr, GdkColor *bg_color, GdkColor *border_color, guchar alpha)
{
    gint tooltip_radius;

    if (!gtk_widget_is_composited (widget)) alpha = 255;

    gtk_widget_style_get (widget, "tooltip-radius", &tooltip_radius,  NULL);

    cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
    cairo_paint (cr);
    cairo_set_operator (cr, CAIRO_OPERATOR_OVER);

    draw_round_rect (cr, 1.0, 0.5, 0.5, tooltip_radius, widget->allocation.width - 1,  widget->allocation.height - 1);

    cairo_set_source_rgba (cr, (float) bg_color->red / 65535.0, (float) bg_color->green / 65535.0, (float) bg_color->blue / 65535.0, (float) alpha / 255.0);
    cairo_fill_preserve (cr);

    cairo_set_source_rgba (cr, (float) border_color->red / 65535.0, (float) border_color->green / 65535.0, (float) border_color->blue / 65535.0, (float) alpha / 255.0);
    cairo_set_line_width (cr, 1.0);
    cairo_stroke (cr);
}

static void update_shape (EjecterPlugin *ej)
{
    GdkBitmap *mask;
    cairo_t *cr;
    gint width, height, tooltip_radius;

    gtk_widget_style_get (ej->popup, "tooltip-radius", &tooltip_radius, NULL);

    if (tooltip_radius == 0 || gtk_widget_is_composited (ej->popup))
    {
        gtk_widget_shape_combine_mask (ej->popup, NULL, 0, 0);
        return;
    }

    gtk_window_get_size (GTK_WINDOW (ej->popup), &width, &height);
    mask = (GdkBitmap *) gdk_pixmap_new (NULL, width, height, 1);
    cr = gdk_cairo_create (mask);

    fill_background (ej->popup, cr, &ej->popup->style->black, &ej->popup->style->black, 255);
    gtk_widget_shape_combine_mask (ej->popup, mask, 0, 0);

    cairo_destroy (cr);
    g_object_unref (mask);
}

static gboolean gtk_tooltip_paint_window (EjecterPlugin *ej)
{
    guchar tooltip_alpha;
    gint tooltip_radius;

    gtk_widget_style_get (ej->popup, "tooltip-alpha", &tooltip_alpha, "tooltip-radius", &tooltip_radius, NULL);

    if (tooltip_alpha != 255 || tooltip_radius != 0)
    {
        cairo_t *cr = gdk_cairo_create (ej->popup->window);
        fill_background (ej->popup, cr, &ej->popup->style->bg [GTK_STATE_NORMAL], &ej->popup->style->bg [GTK_STATE_SELECTED], tooltip_alpha);
        cairo_destroy (cr);
        update_shape (ej);
    }
    else gtk_paint_flat_box (ej->popup->style, ej->popup->window, GTK_STATE_NORMAL, GTK_SHADOW_OUT, NULL, ej->popup, "tooltip",
        0, 0, ej->popup->allocation.width, ej->popup->allocation.height);

    return FALSE;
}

/* Ejecter functions */

static void ejecter_popup_set_position (GtkMenu *menu, gint *px, gint *py, gboolean *push_in, gpointer data)
{
    EjecterPlugin *ej = (EjecterPlugin *) data;
    /* Determine the coordinates. */
    lxpanel_plugin_popup_set_position_helper (ej->panel, ej->plugin, GTK_WIDGET(menu), px, py);
    *push_in = TRUE;
}

static gboolean ejecter_mouse_out (GtkWidget * widget, GdkEventButton * event, EjecterPlugin *ej)
{
    gtk_widget_hide (ej->popup);
    gdk_pointer_ungrab (GDK_CURRENT_TIME);
    return FALSE;
}

static void show_message (EjecterPlugin *ej, char *str1, char *str2)
{
    GtkWidget *item;
    gint x, y;

    hide_menu (ej);
    hide_message (ej);

    ej->popup = gtk_window_new (GTK_WINDOW_POPUP);
    on_screen_changed (ej->popup);
    gtk_window_set_type_hint (GTK_WINDOW (ej->popup), GDK_WINDOW_TYPE_HINT_TOOLTIP);
    gtk_widget_set_app_paintable (ej->popup, TRUE);
    gtk_window_set_resizable (GTK_WINDOW (ej->popup), FALSE);
    gtk_widget_set_name (ej->popup, "gtk-tooltip");

    ej->alignment = gtk_alignment_new (0.5, 0.5, 1.0, 1.0);
    gtk_container_add (GTK_CONTAINER (ej->popup), ej->alignment);
    gtk_widget_show (ej->alignment);

    g_signal_connect_swapped (ej->popup, "style-set", G_CALLBACK (gtk_tooltip_window_style_set), ej);
    g_signal_connect_swapped (ej->popup, "expose-event", G_CALLBACK (gtk_tooltip_paint_window), ej);

    ej->box = gtk_vbox_new (FALSE, ej->popup->style->xthickness);
    gtk_container_add (GTK_CONTAINER (ej->alignment), ej->box);

    item = gtk_label_new (str1);
    gtk_box_pack_start (GTK_BOX (ej->box), item, FALSE, FALSE, 0);
    item = gtk_label_new (str2);
    gtk_box_pack_start (GTK_BOX (ej->box), item, FALSE, FALSE, 0);

    gtk_widget_show_all (ej->popup);
    gtk_widget_hide (ej->popup);
    lxpanel_plugin_popup_set_position_helper (ej->panel, ej->tray_icon, ej->popup, &x, &y);
    gdk_window_move (gtk_widget_get_window (ej->popup), x, y);
    gtk_window_present (GTK_WINDOW (ej->popup));
    gdk_pointer_grab (gtk_widget_get_window (ej->popup), TRUE, GDK_BUTTON_PRESS_MASK, NULL, NULL, GDK_CURRENT_TIME);
    g_signal_connect (G_OBJECT(ej->popup), "button-press-event", G_CALLBACK (ejecter_mouse_out), ej);
}

static void hide_message (EjecterPlugin *ej)
{
    if (ej->popup)
    {
		gtk_widget_destroy (ej->popup);
		ej->popup = NULL;
	}
}

static void update_icon (EjecterPlugin *ej)
{
    if (ej->autohide)
    {
        /* Loop through all devices, counting... */
        int count = 0;
        GList *iter, *keys = g_hash_table_get_keys (ej->devices);
        for (iter = keys; iter != NULL; iter = g_list_next (iter))
        {
            EjecterDevice *dev = g_hash_table_lookup (ej->devices, iter->data);
            if (dev)
            {
                gtk_widget_show_all (ej->plugin);
                return;
            }
        }
        gtk_widget_hide_all (ej->plugin);
    }
}

static void show_menu (EjecterPlugin *ej)
{
    hide_message (ej);
    hide_menu (ej);

    verify_devices (ej);
    ej->menu = gtk_menu_new ();

    /* Loop through all devices, creating menuitems for them */
    int count = 0;
    GList *iter, *keys = g_hash_table_get_keys (ej->devices);
    for (iter = keys; iter != NULL; iter = g_list_next (iter))
    {
        EjecterDevice *dev = g_hash_table_lookup (ej->devices, iter->data);
        if (dev)
        {
            create_menuitem (dev);
            gtk_menu_append (ej->menu, dev->menuitem);
            count++;
        }
    }

    /* If no devices were found, create the "empty" menuitem */
    if (!count)
    {
        ej->empty = gtk_menu_item_new_with_label (_("No ejectable devices"));
        gtk_widget_set_sensitive (ej->empty, FALSE);
        gtk_menu_append (ej->menu, ej->empty);
    }

    gtk_widget_show_all (ej->menu);
    gtk_menu_popup (GTK_MENU (ej->menu), NULL, NULL, ejecter_popup_set_position, ej, 1, gtk_get_current_event_time ());
}

static void hide_menu (EjecterPlugin *ej)
{
    if (ej->menu)
    {
		gtk_menu_popdown (GTK_MENU (ej->menu));
		gtk_widget_destroy (ej->menu);
		ej->menu = NULL;
	}
}

static void create_menuitem (EjecterDevice *d)
{
    char buffer[1024];
    GVolume *v;
    d->menuitem = gtk_image_menu_item_new ();
    gtk_image_menu_item_set_always_show_image (GTK_IMAGE_MENU_ITEM (d->menuitem), TRUE);

    GtkWidget *box = gtk_hbox_new (FALSE, 4);
    gtk_container_add (GTK_CONTAINER (d->menuitem), box);

    d->label = gtk_label_new (NULL);
    gtk_label_set_max_width_chars (GTK_LABEL (d->label), 40);
    gtk_label_set_ellipsize (GTK_LABEL (d->label), PANGO_ELLIPSIZE_END);
    if (d->volume_count == 0)
    {
        d->name = g_drive_get_name (d->drive);
        d->description = 0;
        sprintf (buffer, "%s", d->name);
        d->icon = gtk_image_new_from_gicon (g_drive_get_icon (d->drive), GTK_ICON_SIZE_BUTTON);
    }
    else if (d->volume_count == 1)
    {
        v = g_slist_nth_data (d->volumes, 0);
        d->name = g_drive_get_name (d->drive);
        d->description = g_volume_get_name (v);
        sprintf (buffer, "%s (%s)", d->name, d->description);
        d->icon = gtk_image_new_from_gicon (g_volume_get_icon (v), GTK_ICON_SIZE_BUTTON);
    }
    else
    {
        d->name = g_drive_get_name (d->drive);
        sprintf (buffer, "%s (", d->name);
        GSList *iter;
        gboolean first = TRUE;
        for (iter = d->volumes; iter != NULL; iter = g_slist_next (iter))
        {
            v = iter->data;
            if (first) first = FALSE;
            else strcat (buffer, ", ");
            if (g_volume_get_name (v)) strcat (buffer, g_volume_get_name (v));
        }
        strcat (buffer, ")");
        d->icon = gtk_image_new_from_gicon (g_volume_get_icon (v), GTK_ICON_SIZE_BUTTON);
    }
    gtk_label_set_text (GTK_LABEL (d->label), buffer);
    gtk_misc_set_alignment (GTK_MISC (d->label), 0.0, 0.5);
    gtk_box_pack_start (GTK_BOX (box), d->label, TRUE, TRUE, 0);

    d->eject = gtk_image_new ();
    set_icon (d->plugin->panel, d->eject, "media-eject", 16);
    gtk_box_pack_start (GTK_BOX (box), d->eject, FALSE, FALSE, 0);

    gtk_image_menu_item_set_always_show_image (GTK_IMAGE_MENU_ITEM (d->menuitem), TRUE);
    gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (d->menuitem), d->icon);
    gtk_widget_set_sensitive (d->menuitem, d->was_mounted);

    gtk_widget_show_all (d->menuitem);
    g_signal_connect (d->menuitem, "activate", G_CALLBACK (handle_eject_clicked), d);
}

static void set_icon (LXPanel *p, GtkWidget *image, const char *icon, int size)
{
    GdkPixbuf *pixbuf;
    if (size == 0) size = panel_get_icon_size (p) - ICON_BUTTON_TRIM;
    if (gtk_icon_theme_has_icon (panel_get_icon_theme (p), icon))
    {
        GtkIconInfo *info = gtk_icon_theme_lookup_icon (panel_get_icon_theme (p), icon, size, GTK_ICON_LOOKUP_FORCE_SIZE);
        pixbuf = gtk_icon_info_load_icon (info, NULL);
        gtk_icon_info_free (info);
        if (pixbuf != NULL)
        {
            gtk_image_set_from_pixbuf (GTK_IMAGE (image), pixbuf);
            g_object_unref (pixbuf);
            return;
        }
    }
    else
    {
        char path[256];
        sprintf (path, "%s/images/%s.png", PACKAGE_DATA_DIR, icon);
        pixbuf = gdk_pixbuf_new_from_file_at_scale (path, size, size, TRUE, NULL);
        if (pixbuf != NULL)
        {
            gtk_image_set_from_pixbuf (GTK_IMAGE (image), pixbuf);
            g_object_unref (pixbuf);
        }
    }
}

/* Handler for menu button click */
static gboolean ejecter_button_press_event (GtkWidget *widget, GdkEventButton *event, LXPanel *panel)
{
    EjecterPlugin * ej = lxpanel_plugin_get_data (widget);

#ifdef ENABLE_NLS
    textdomain ( GETTEXT_PACKAGE );
#endif
    /* Show or hide the popup menu on left-click */
    if (event->button == 1)
    {
        show_menu (ej);
        return TRUE;
    }
    else return FALSE;
}

/* Handler for system config changed message from panel */
static void ejecter_configuration_changed (LXPanel *panel, GtkWidget *p)
{
    EjecterPlugin * ej = lxpanel_plugin_get_data (p);

    set_icon (panel, ej->tray_icon, "media-eject", 0);
}

/* Plugin destructor. */
static void ejecter_destructor (gpointer user_data)
{
    EjecterPlugin * ej = (EjecterPlugin *) user_data;

    /* Deallocate memory */
    g_free (ej);
}

/* Plugin constructor. */
static GtkWidget *ejecter_constructor (LXPanel *panel, config_setting_t *settings)
{
    /* Allocate and initialize plugin context */
    EjecterPlugin * ej = g_new0 (EjecterPlugin, 1);
    GtkWidget *p;
    int val;
    
#ifdef ENABLE_NLS
    setlocale (LC_ALL, "");
    bindtextdomain ( GETTEXT_PACKAGE, PACKAGE_LOCALE_DIR );
    bind_textdomain_codeset ( GETTEXT_PACKAGE, "UTF-8" );
    textdomain ( GETTEXT_PACKAGE );
#endif

    ej->tray_icon = gtk_image_new ();
    set_icon (panel, ej->tray_icon, "media-eject", 0);
    gtk_widget_set_tooltip_text (ej->tray_icon, _("Select a drive in menu to eject safely"));
    gtk_widget_set_visible (ej->tray_icon, TRUE);

    /* Allocate top level widget and set into Plugin widget pointer. */
    ej->panel = panel;
    ej->plugin = p = gtk_button_new ();
    gtk_button_set_relief (GTK_BUTTON (ej->plugin), GTK_RELIEF_NONE);
    g_signal_connect (ej->plugin, "button-press-event", G_CALLBACK(ejecter_button_press_event), NULL);
    ej->settings = settings;
    lxpanel_plugin_set_data (p, ej, ejecter_destructor);
    gtk_widget_add_events (p, GDK_BUTTON_PRESS_MASK);

    /* Allocate icon as a child of top level */
    gtk_container_add (GTK_CONTAINER(p), ej->tray_icon);
    
    /* Initialise data structures */
    ej->monitor = g_volume_monitor_get ();

    ej->devices = g_hash_table_new (g_str_hash, g_str_equal);
    ej->invalid_devices = NULL;
    
    if (config_setting_lookup_int (settings, "AutoHide", &val))
    {
        if (val == 1) ej->autohide = TRUE;
        else ej->autohide = FALSE;
    }
    else ej->autohide = FALSE;

    /* Read in current devices */
    load_devices (ej);

    ej->popup = NULL;
    ej->menu = NULL;

    g_signal_connect (ej->monitor, "volume_added", G_CALLBACK (handle_volume_added), ej);
    g_signal_connect (ej->monitor, "mount_added", G_CALLBACK (handle_mount_added), ej);

    /* Show the widget, and return. */
    gtk_widget_show_all (p);
    update_icon (ej);
    return p;
}

static gboolean ejecter_apply_configuration (gpointer user_data)
{
    EjecterPlugin *ej = lxpanel_plugin_get_data ((GtkWidget *) user_data);

    config_group_set_int (ej->settings, "AutoHide", ej->autohide);
    if (ej->autohide) update_icon (ej);
    else  gtk_widget_show_all (ej->plugin);
}

static GtkWidget *ejecter_configure (LXPanel *panel, GtkWidget *p)
{
    EjecterPlugin *ej = lxpanel_plugin_get_data (p);
    return lxpanel_generic_config_dlg(_("Ejecter"), panel,
        ejecter_apply_configuration, p,
        _("Hide icon when no devices"), &ej->autohide, CONF_TYPE_BOOL,
        NULL);
}

FM_DEFINE_MODULE(lxpanel_gtk, ejecter)

/* Plugin descriptor. */
LXPanelPluginInit fm_module_init_lxpanel_gtk = {
    .name = N_("Ejecter"),
    .description = N_("Ejects mounted drives"),
    .new_instance = ejecter_constructor,
    .reconfigure = ejecter_configuration_changed,
    .button_press_event = ejecter_button_press_event,
    .config = ejecter_configure,
    .gettext_package = GETTEXT_PACKAGE
};
