/*      $Id$

        This program is free software; you can redistribute it and/or modify
        it under the terms of the GNU General Public License as published by
        the Free Software Foundation; either version 2, or (at your option)
        any later version.

        This program 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 General Public License for more details.

        You should have received a copy of the GNU General Public License
        along with this program; if not, write to the Free Software
        Foundation, Inc., Inc., 51 Franklin Street, Fifth Floor, Boston,
        MA 02110-1301, USA.


        Metacity - (c) 2001 Havoc Pennington
        xfwm4    - (c) 2002-2011 Olivier Fourdan

 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <X11/Xlib.h>
#include <glib.h>
#include <gdk/gdk.h>
#include <gdk/gdkx.h>
#include <gtk/gtk.h>
#include <unistd.h>
#include <string.h>
#include <libxfce4util/libxfce4util.h>

#include <common/xfwm-common.h>

#include "event_filter.h"
#include "menu.h"
#include "misc.h"

enum
{
    GRAB_DEVICE_POINTER,
    GRAB_DEVICE_KEYBOARD
};

static GtkWidget *menu_open = NULL;
static MenuItem menuitems[] = {
    {MENU_OP_MAXIMIZE,     N_("Ma_ximize")},
    {MENU_OP_UNMAXIMIZE,   N_("Unma_ximize")},
    {MENU_OP_MINIMIZE,     N_("Mi_nimize")},
    {MENU_OP_MINIMIZE_ALL, N_("Minimize _Other Windows")},
    {MENU_OP_UNMINIMIZE,   N_("S_how")},
    {MENU_OP_MOVE,         N_("_Move")},
    {MENU_OP_RESIZE,       N_("_Resize")},
    {0, NULL}, /* -------------------------------------------------------- */
    {MENU_OP_ABOVE,        N_("Always on _Top")},
    {MENU_OP_NORMAL,       N_("_Same as Other Windows")},
    {MENU_OP_BELOW,        N_("Always _Below Other Windows")},
    {MENU_OP_SHADE,        N_("Roll Window Up")},
    {MENU_OP_UNSHADE,      N_("Roll Window Down")},
    {MENU_OP_FULLSCREEN,   N_("_Fullscreen")},
    {MENU_OP_UNFULLSCREEN, N_("Leave _Fullscreen")},
    {MENU_OP_CONTEXT_HELP, N_("Context _Help")},
    {0, NULL}, /* -------------------------------------------------------- */
    {MENU_OP_STICK,        N_("Always on _Visible Workspace")},
    {MENU_OP_UNSTICK,      N_("Only _Visible on This Workspace")},
    {MENU_OP_WORKSPACES,   N_("Move to Another _Workspace")},
    {0, NULL}, /* -------------------------------------------------------- */
    {MENU_OP_DELETE,       N_("_Close")},
#if 0
    {0, NULL, NULL}, /* -------------------------------------------------------- */
    {MENU_OP_DESTROY,      N_("Destroy")},
    {0, NULL, NULL}, /* -------------------------------------------------------- */
#endif
    {MENU_OP_QUIT,         N_("_Quit")},
    {MENU_OP_RESTART,      N_("Restart")},
};

static eventFilterStatus
menu_filter (XfwmEvent *event, gpointer data)
{
    switch (event->meta.type)
    {
        case XFWM_EVENT_KEY:
        case XFWM_EVENT_BUTTON:
        case XFWM_EVENT_MOTION:
        case XFWM_EVENT_CROSSING:
            return EVENT_FILTER_STOP;
        case XFWM_EVENT_XEVENT:
            break;
    }
    return EVENT_FILTER_CONTINUE;
}


#if !GTK_CHECK_VERSION(3, 22, 0)
static void
popup_position_func (GtkMenu * menu, gint * x, gint * y, gboolean * push_in,
    gpointer user_data)
{
    GtkRequisition req;
    GdkPoint *pos;
    GdkScreen *screen;
    gint monitor_num;
    gint width;
    gint height;

    pos = user_data;

    gtk_widget_get_preferred_size (GTK_WIDGET (menu), NULL, &req);

    xfwm_get_screen_dimensions (&width, &height);

    if (pos->x >= 0)
    {
        *x = pos->x;
        *x = CLAMP (*x, 0, MAX (0, width - req.width));
    }
    else
    {
        *x = (width - req.width) / 2;
    }
    if (pos->x >= 0)
    {
        *y = pos->y;
        *y = CLAMP (*y, 0, MAX (0, height - req.height));
    }
    else
    {
        *y = (height - req.height) / 2;
    }

    screen = gtk_widget_get_screen (GTK_WIDGET (menu));
    monitor_num = gdk_screen_get_monitor_at_point (screen, *x, *y);
    gtk_menu_set_monitor (GTK_MENU (menu), monitor_num);

    g_free (user_data);
}
#endif

static gboolean
activate_cb (GtkWidget * menuitem, gpointer data)
{
    MenuData *menudata;

    g_return_val_if_fail (GTK_IS_WIDGET (menuitem), FALSE);
    TRACE ("entering");

    menu_open = NULL;
    menudata = data;

    eventFilterPop (menudata->menu->filter_setup);
    (*menudata->menu->func) (menudata->menu,
                             menudata->op,
                             menudata->menu->xid,
                             menudata->menu->data,
                             menudata->data);
    return (FALSE);
}

static gboolean
menu_closed (GtkMenu * widget, gpointer data)
{
    Menu *menu;

    TRACE ("entering");

    menu = data;
    menu_open = NULL;

    eventFilterPop (menu->filter_setup);
    (*menu->func) (menu, 0, menu->xid, menu->data, NULL);
    return (FALSE);
}

static GtkWidget *
menu_workspace (Menu * menu, MenuOp insensitive, gint ws, gint nws, gchar **wsn, gint wsn_items)
{
    GtkWidget *menu_widget;
    GtkWidget *menuitem;
    MenuData *menudata;
    gchar *name;
    gint i;

    menu_widget = gtk_menu_new ();
    gtk_menu_set_screen (GTK_MENU (menu->menu), menu->screen);

    for (i = 0; i < nws; i++)
    {
        if ((i < wsn_items) && wsn[i] && *(wsn[i]))
        {
            if (((i+1) < 10) && (strchr(wsn[i],'_')==NULL))
            {
                /* In the 1st 10, there is a name, but it doesn't have _ */
                name = g_strdup_printf ("_%i (%s)", i + 1, wsn[i]);
                menuitem = gtk_menu_item_new_with_mnemonic (name);
            }
            else
            {
                name = g_strdup_printf ("%i (%s)", i + 1, wsn[i]);
                menuitem = gtk_menu_item_new_with_label (name);
            }
        }
        else
        {
	    /* No workspace name */
            if ((i+1) < 10)
            {
		name = g_strdup_printf ("_%i", i + 1);
		menuitem = gtk_menu_item_new_with_mnemonic (name);
            }
            else
            {
		name = g_strdup_printf ("%i", i + 1);
		menuitem = gtk_menu_item_new_with_label (name);
            }
        }
        g_free (name);
        gtk_widget_set_sensitive (menuitem, !(insensitive & MENU_OP_WORKSPACES) && (i != ws));
        gtk_widget_show (menuitem);

        menudata = g_new0 (MenuData, 1);
        menudata->menu = menu;
        menudata->op = MENU_OP_WORKSPACES;
        menudata->data = GINT_TO_POINTER (i);
        menu_item_connect (menuitem, menudata);

        gtk_menu_shell_append (GTK_MENU_SHELL (menu_widget), menuitem);
    }

    return (menu_widget);
}

Menu *
menu_default (GdkScreen *gscr, Window xid, MenuOp ops, MenuOp insensitive, MenuFunc func,
    gint ws, gint nws, gchar **wsn, gint wsn_items, eventFilterSetup *filter_setup, gpointer data)
{
    GtkWidget *menuitem;
    GtkWidget *ws_menu;
    MenuData *menudata;
    Menu *menu;
    const gchar *label;
    int i;

    TRACE ("entering");

    menu = g_new0 (Menu, 1);
    menu->func = func;
    menu->filter_setup = filter_setup;
    menu->data = data;
    menu->ops = ops;
    menu->insensitive = insensitive;
    menu->screen = gscr;
    menu->xid = xid;
    menu->menu = gtk_menu_new ();
    gtk_menu_set_screen (GTK_MENU (menu->menu), menu->screen);

    i = 0;
    while (i < (int) (sizeof (menuitems) / sizeof (MenuItem)))
    {
        if ((ops & menuitems[i].op) || (menuitems[i].op == MENU_OP_SEPARATOR))
        {
            label = _(menuitems[i].label);
            ws_menu = NULL;
            switch (menuitems[i].op)
            {
                case MENU_OP_SEPARATOR:
                    menuitem = gtk_separator_menu_item_new ();
                    break;
                case MENU_OP_WORKSPACES:
                    menuitem = gtk_menu_item_new_with_mnemonic (label);
                    if (insensitive & menuitems[i].op)
                    {
                        gtk_widget_set_sensitive (menuitem, FALSE);
                    }
                    ws_menu = menu_workspace (menu, insensitive, ws, nws, wsn, wsn_items);
                    gtk_menu_item_set_submenu (GTK_MENU_ITEM (menuitem), ws_menu);
                    g_signal_connect (G_OBJECT (ws_menu), "selection-done", G_CALLBACK (menu_closed), menu);
                    break;
                default:
                    menuitem = gtk_menu_item_new_with_mnemonic (label);
                    if (insensitive & menuitems[i].op)
                    {
                        gtk_widget_set_sensitive (menuitem, FALSE);
                    }
                    menudata = g_new0 (MenuData, 1);
                    menudata->menu = menu;
                    menudata->op = menuitems[i].op;
                    menudata->data = data;
                    menu_item_connect (menuitem, menudata);
                    break;
            }
            gtk_menu_shell_append (GTK_MENU_SHELL (menu->menu), menuitem);
            gtk_widget_show (menuitem);
        }
        ++i;
    }
    g_signal_connect (G_OBJECT (menu->menu), "selection-done", G_CALLBACK (menu_closed), menu);

    return (menu);
}

static void
closure_notify (gpointer data, GClosure * closure)
{
    TRACE ("entering");
    
    if (data)
    {
        TRACE ("freeing data");
        g_free (data);
    }
}

GtkWidget *
menu_item_connect (GtkWidget * item, MenuData * item_data)
{
    g_return_val_if_fail (item != NULL, NULL);
    g_return_val_if_fail (GTK_IS_MENU_ITEM (item), NULL);
    TRACE ("entering");

    g_signal_connect_closure (G_OBJECT (item), "activate",
        g_cclosure_new (G_CALLBACK (activate_cb), item_data,
            (GClosureNotify) closure_notify), FALSE);
    return (item);
}

gboolean
menu_is_opened (void)
{
    TRACE ("entering");
    return (menu_open != NULL);
}

gboolean
menu_check_and_close (void)
{
    TRACE ("entering");
    if (menu_open)
    {
        TRACE ("emitting deactivate signal");
        g_signal_emit_by_name (G_OBJECT (menu_open), "deactivate");
        menu_open = NULL;
        return (TRUE);
    }
    return (FALSE);
}

static GdkDevice *
menu_get_device (GdkWindow *win, gint grab_device_type)
{
    GdkDisplay *display;
    GdkSeat *seat;

    display = gdk_window_get_display (win);
    seat = gdk_display_get_default_seat (display);

    switch (grab_device_type)
    {
        case GRAB_DEVICE_POINTER:
            return gdk_seat_get_pointer (seat);
        case GRAB_DEVICE_KEYBOARD:
            return gdk_seat_get_keyboard (seat);
        default:
            return NULL;
    }
}

static GdkGrabStatus
menu_device_grab (GdkWindow *win, gint grab_device_type, GdkEventMask event_mask, guint32 timestamp)
{
    GdkDevice *device;

    device = menu_get_device (win, grab_device_type);

    g_return_val_if_fail (device != NULL, GDK_GRAB_FAILED);

G_GNUC_BEGIN_IGNORE_DEPRECATIONS
    return gdk_device_grab (device, win, GDK_OWNERSHIP_NONE, TRUE, event_mask, NULL, timestamp);
G_GNUC_END_IGNORE_DEPRECATIONS
}

static void
menu_device_ungrab (GdkWindow *win, gint grab_device_type, guint32 timestamp)
{
    GdkDevice *device;

    device = menu_get_device (win, grab_device_type);

    g_return_if_fail (device != NULL);

G_GNUC_BEGIN_IGNORE_DEPRECATIONS
    gdk_device_ungrab (device, timestamp);
G_GNUC_END_IGNORE_DEPRECATIONS
}

static gboolean
grab_available (GdkWindow *win, guint32 timestamp)
{
    GdkEventMask pointer_mask;
    GdkEventMask keyboard_mask;
    GdkGrabStatus g1;
    GdkGrabStatus g2;
    gboolean grab_failed;
    gint i;

    TRACE ("entering");

    pointer_mask = GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
                   GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK |
                   GDK_POINTER_MOTION_MASK;
    keyboard_mask = GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK;
    g1 = menu_device_grab (win, GRAB_DEVICE_POINTER, pointer_mask, timestamp);
    g2 = menu_device_grab (win, GRAB_DEVICE_KEYBOARD, keyboard_mask, timestamp);
    grab_failed = FALSE;

    i = 0;
    while ((i++ < 100) && (grab_failed = ((g1 != GDK_GRAB_SUCCESS)
                || (g2 != GDK_GRAB_SUCCESS))))
    {
        TRACE ("grab not available yet, waiting... (%i)", i);
        g_usleep (100);
        if (g1 != GDK_GRAB_SUCCESS)
        {
            g1 = menu_device_grab (win, GRAB_DEVICE_POINTER, pointer_mask, timestamp);
        }
        if (g2 != GDK_GRAB_SUCCESS)
        {
            g2 = menu_device_grab (win, GRAB_DEVICE_KEYBOARD, keyboard_mask, timestamp);
        }
    }

    if (g1 == GDK_GRAB_SUCCESS)
    {
        menu_device_ungrab (win, GRAB_DEVICE_POINTER, timestamp);
    }
    if (g2 == GDK_GRAB_SUCCESS)
    {
        menu_device_ungrab (win, GRAB_DEVICE_KEYBOARD, timestamp);
    }

    return (!grab_failed);
}

#if GTK_CHECK_VERSION(3, 22, 0)
static GdkEvent *
menu_popup_event (Menu *menu, gint root_x, gint root_y, guint button, guint32 timestamp,
                  GdkWindow *window)
{
    GdkEvent *event;
    GdkSeat *seat;
    GdkDevice *device;

    TRACE ("entering");

    event = gtk_get_current_event ();

    if (event != NULL)
    {
        event = gdk_event_copy (event);
    }
    else
    {
        /* Create fake event since menu can be show without any events */

        seat = gdk_display_get_default_seat (gdk_window_get_display (window));
        device = gdk_seat_get_pointer (seat);

        event = gdk_event_new (GDK_BUTTON_PRESS);
        event->button.window = g_object_ref (window);
        event->button.time = timestamp;
        event->button.x = event->button.x_root = root_x;
        event->button.y = event->button.y_root = root_y;
        event->button.button = button;
        event->button.device = device;
        gdk_event_set_device (event, device);
    }

    return event;
}
#endif

gboolean
menu_popup (Menu *menu, gint root_x, gint root_y, guint button, guint32 timestamp)
{
    GdkPoint *pt;
    GdkWindow *window;
#if GTK_CHECK_VERSION(3, 22, 0)
    GdkEvent *event;
    GdkRectangle rectangle;
#endif

    g_return_val_if_fail (menu != NULL, FALSE);
    g_return_val_if_fail (GTK_IS_MENU (menu->menu), FALSE);
    TRACE ("entering");

    pt = g_new0 (GdkPoint, 1);
    pt->x = root_x;
    pt->y = root_y;

    window = gdk_screen_get_root_window (menu->screen);

    if (!menu_check_and_close ())
    {
        if (!grab_available (window, timestamp))
        {
            g_free (pt);
            TRACE ("cannot get grab on pointer/keyboard, cancel.");
            return FALSE;
        }
        TRACE ("opening new menu");
        menu_open = menu->menu;
        eventFilterPush (menu->filter_setup, menu_filter, NULL);

#if GTK_CHECK_VERSION(3, 22, 0)
        rectangle.x = root_x;
        rectangle.y = root_y;
        rectangle.width = 1;
        rectangle.height = 1;

        event = menu_popup_event (menu, root_x, root_y, button, timestamp, window);
        gtk_menu_popup_at_rect (GTK_MENU (menu->menu), window, &rectangle,
                                GDK_GRAVITY_NORTH_WEST, GDK_GRAVITY_NORTH_WEST, event);
        gdk_event_free (event);
#else
        gtk_menu_popup (GTK_MENU (menu->menu), NULL, NULL,
            popup_position_func, pt, 0, timestamp);
#endif

        if (g_object_get_data (G_OBJECT (menu->menu), "gtk-menu-transfer-window") == NULL)
        {
            myDisplayBeep (myDisplayGetDefault ());
            g_message (_("%s: GtkMenu failed to grab the pointer\n"), g_get_prgname ());
            gtk_menu_popdown (GTK_MENU (menu->menu));
            menu_open = NULL;
            eventFilterPop (menu->filter_setup);
            return FALSE;
        }
    }
    return TRUE;
}

void
menu_free (Menu * menu)
{
    g_return_if_fail (menu != NULL);
    g_return_if_fail (menu->menu != NULL);
    g_return_if_fail (GTK_IS_MENU (menu->menu));

    TRACE ("entering");

    gtk_widget_destroy (menu->menu);
    g_free (menu);
}
