#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#if defined(_WIN32)
# include <gdk/gdkwin32.h>
# define HAVE_WIN32
#else
# include <gdk/gdkx.h>
# define HAVE_X
#endif
#include "guiutils.h"
#include "menubutton.h"

/*
 *	Map arrow image
 */
static char * map_arrow_xpm[] = {
"5 20 2 1",
"       c None",
".      c #000000",
"     ",
".....",
" ... ",
"  .  ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     "};


/*
 *	Menu Button Data Key:
 */
#define MENU_BUTTON_KEY		"menu_button_key"

/*
 *	Menu Button:
 */
typedef struct _menu_button_struct	menu_button_struct;
struct _menu_button_struct {

	GtkWidget	*toplevel,	/* GtkButton */
			*menu;		/* Popup GtkMenu */

	gint		map_type;	/* One of MENU_BUTTON_MAP_TYPE_* */

	gboolean	is_pressed,	/* Is button pressed? */
			menu_map_state;	/* Menu map state? */

	/* Pointer position when the toplevel GtkButton received the
	 * "pressed" signal, position relative to the toplevel GtkButton
	 */
	gint		press_x,
			press_y;
	gulong		last_press,
			last_release;

};
#define MENU_BUTTON(p)	((menu_button_struct *)(p))


static void MenuButtonDataDestroyCB(gpointer data);
static gint MenuButtonKeyCB(
	GtkWidget *widget, GdkEventKey *key, gpointer data
);
static gint MenuButtonMotionCB(
	GtkWidget *widget, GdkEventMotion *motion, gpointer data
);
static void MenuButtonMapPositionCB(
	GtkMenu *menu, gint *x, gint *y, gpointer data
);
static void MenuButtonDoMapMenu(menu_button_struct *mb, gulong t);
static void MenuButtonPressedCB(GtkWidget *widget, gpointer data);
static void MenuButtonReleasedCB(GtkWidget *widget, gpointer data);
static void MenuButtonMenuHideCB(GtkWidget *widget, gpointer data);

static GtkWidget *MenuButtonNewNexus(
	const gchar *label, guchar **icon_data,
	GtkWidget **menu_rtn, gint orientation
);
GtkWidget *MenuButtonNewH(
	const gchar *label, guchar **icon_data,
	GtkWidget **menu_rtn
);
GtkWidget *MenuButtonNewV(
	const gchar *label, guchar **icon_data,
	GtkWidget **menu_rtn
);
GtkWidget *MenuButtonNewPixmap(
	guchar **icon_data, GtkWidget **menu_rtn
);
GtkWidget *MenuButtonGetMenu(GtkWidget *w);
void MenuButtonSetMenu(GtkWidget *w, GtkMenu *menu);
void MenuButtonSetMapTrigger(
	GtkWidget *w, gint map_type
);


#define ATOI(s)         (((s) != NULL) ? atoi(s) : 0)
#define ATOL(s)         (((s) != NULL) ? atol(s) : 0)
#define ATOF(s)         (((s) != NULL) ? atof(s) : 0.0f)
#define STRDUP(s)       (((s) != NULL) ? g_strdup(s) : NULL)

#define MAX(a,b)        (((a) > (b)) ? (a) : (b))
#define MIN(a,b)        (((a) < (b)) ? (a) : (b))
#define CLIP(a,l,h)     (MIN(MAX((a),(l)),(h)))
#define STRLEN(s)       (((s) != NULL) ? strlen(s) : 0)
#define STRISEMPTY(s)   (((s) != NULL) ? (*(s) == '\0') : TRUE)


/*
 *	Menu Button data "destroy" signal callback.
 */
static void MenuButtonDataDestroyCB(gpointer data)
{
	menu_button_struct *mb = MENU_BUTTON(data);
	if(mb == NULL)
	    return;

	GTK_WIDGET_DESTROY(mb->menu)
	g_free(mb);
}


/*
 *	Menu Button "key_press_event" or "key_release_event" signal
 *	callback.
 */
static gint MenuButtonKeyCB(
	GtkWidget *widget, GdkEventKey *key, gpointer data
)
{
	gint status = FALSE;
	gboolean press;
	menu_button_struct *mb = MENU_BUTTON(data);
	if((key == NULL) || (mb == NULL))
	    return(FALSE);

#define SIGNAL_EMIT_STOP        {               \
 gtk_signal_emit_stop_by_name(                  \
  GTK_OBJECT(widget),                           \
  press ?                                       \
   "key_press_event" : "key_release_event"      \
 );                                             \
}

	press = (key->type == GDK_KEY_PRESS) ? TRUE : FALSE;

	switch(key->keyval)
	{
	  case GDK_Return:
	  case GDK_space:
	    /* Only respond to ENTER and SPACE key presses when the
	     * map_type is set to MENU_BUTTON_MAP_TYPE_PRESSED
	     */
	    if(mb->map_type == MENU_BUTTON_MAP_TYPE_PRESSED)
	    {
		if(press)
		{
		    MenuButtonDoMapMenu(mb, key->time);
		}
		SIGNAL_EMIT_STOP
		status = TRUE;
	    }
	    break;

	  case GDK_Down:
	  case GDK_KP_Down:
	    if(press)
	    {
		MenuButtonDoMapMenu(mb, key->time);
	    }
	    SIGNAL_EMIT_STOP
	    status = TRUE;
	    break;
	}

	return(status);
#undef SIGNAL_EMIT_STOP
}

/*
 *	Menu Button "motion_notify_event" signal callback.
 */
static gint MenuButtonMotionCB(
	GtkWidget *widget, GdkEventMotion *motion, gpointer data
)
{
	gint status = FALSE;
	menu_button_struct *mb = MENU_BUTTON(data);
	if((motion == NULL) || (mb == NULL))
	    return(status);

	status = TRUE;

	if(!mb->is_pressed)
	    return(status);

	/* if the map type is MENU_BUTTON_MAP_TYPE_PRESSED_DRAG then
	 * check if the motion has gone beyond the drag tolorance,
	 * if it has then the menu needs to be mapped
	 */
	if((mb->map_type == MENU_BUTTON_MAP_TYPE_PRESSED_DRAG) &&
	   !mb->menu_map_state
	)
	{
	    const gint	tolor = 2;
	    gint x, y, dx, dy;

	    /* Get pointer position */
	    if(motion->is_hint)
	    {
		GdkModifierType mask;
		gdk_window_get_pointer(
		    motion->window, &x, &y, &mask
		);
	    }
	    else
	    {
		x = (gint)motion->x;
		y = (gint)motion->y;
	    }

	    /* Calculate movement from original button press */
	    dx = x - mb->press_x;
	    dy = y - mb->press_y;

	    /* Moved enough to map menu? */
	    if((dx > tolor) || (dx < -tolor) ||
	       (dy > tolor) || (dy < -tolor)
	    )
		MenuButtonDoMapMenu(mb, GDK_CURRENT_TIME);
	}

	return(status);
}

/*
 *	Menu map position callback.
 */
static void MenuButtonMapPositionCB(
	GtkMenu *menu, gint *x, gint *y, gpointer data
)
{
	gint rx, ry, mx, my, mwidth, mheight, root_width, root_height;
	GdkWindow *root = GDK_ROOT_PARENT();
	const GtkAllocation *allocation;
	GtkWidget *w, *rel_widget = (GtkWidget *)data;
	if((menu == NULL) || (rel_widget == NULL) || (root == NULL))
	    return;

	/* Get position of relative widget, and then calculate the
	 * new menu mapping position
	 */
	w = rel_widget;
	allocation = &w->allocation;
	gdk_window_get_root_position(w->window, &rx, &ry);
	if(GTK_WIDGET_NO_WINDOW(w))
	{
	    rx += allocation->x;
	    ry += allocation->y;
	}
	mx = rx;
	my = ry + allocation->height;

	/* Get size of the menu */
	w = GTK_WIDGET(menu);
	allocation = &w->allocation;
	mwidth = allocation->width;
	mheight = allocation->height;

	/* Get size of the root window */
	gdk_window_get_size(root, &root_width, &root_height);

	/* Clip new menu mapping position to stay within the root window */
	if((mx + mwidth) > root_width)
	    mx = root_width - mwidth;
	if(mx < 0)
	    mx = 0;
	if((my + mheight) > root_height)
	    my = root_height - mheight;
	if(my < 0)
	    my = 0;

	/* Update return position */
	if(x != NULL)
	    *x = mx;
	if(y != NULL)
	    *y = my;
}

/*
 *	Ungrabs the button and holds it down, while mapping the menu.
 *
 *	The menu_map_state is not checked, however it will be set to
 *	TRUE.
 */
static void MenuButtonDoMapMenu(menu_button_struct *mb, gulong t)
{
	GtkWidget *w;


	/* Ungrab the button widget and hold it down by generating a
	 * "pressed" signal
	 */
	w = mb->toplevel;
	if(w != NULL)
	{
	    GtkButton *button = GTK_BUTTON(w);

	    /* Need to have the button ungrab */
	    if(GTK_WIDGET_HAS_GRAB(w))
		gtk_grab_remove(w);

	    /* For certain map types we need to flush GTK events since
	     * a quick button release needs to be detected in order to
	     * keep the menu mapped
	     */
	    switch(mb->map_type)
	    {
	      case MENU_BUTTON_MAP_TYPE_PRESSED:
		while(gtk_events_pending() > 0)
		    gtk_main_iteration();
		break;
	    }

	    /* Set up and send a "pressed" signal to the GtkButton to
	     * keep it pressed once the grab for the GtkButton has been
	     * removed and the GtkMenu has been mapped
	     */
	    button->in_button = 1;
	    button->button_down = 1;
	    gtk_signal_emit_by_name(GTK_OBJECT(w), "pressed");
	}


	/* Begin mapping menu */

	mb->menu_map_state = TRUE;	/* Mark menu as mapped */

	/* Map menu */
	w = mb->menu;
	if(w != NULL)
	    gtk_menu_popup(
		GTK_MENU(w), NULL, NULL,
		MenuButtonMapPositionCB, mb->toplevel,
		1,		/* Button Number */
		t		/* Time Stamp */
	    );
}

/*
 *	Menu Button GtkButton "pressed" signal callback.
 */
static void MenuButtonPressedCB(GtkWidget *widget, gpointer data)
{
	gint x, y;
	GdkModifierType mask;
	GdkWindow *window;
	GtkWidget *w;
	menu_button_struct *mb = MENU_BUTTON(data);
	if((widget == NULL) || (mb == NULL))
	    return;

	/* Already pressed? */
	if(mb->is_pressed)
	    return;

	w = mb->toplevel;
	if(w == NULL)
	    return;

	window = w->window;

	/* Get pointer position */
	gdk_window_get_pointer(window, &x, &y, &mask);

	/* Record button pressed */
	mb->is_pressed = TRUE;
	mb->press_x = x;
	mb->press_y = y;

	/* Handle menu mapping by map type */
	switch(mb->map_type)
	{
	  case MENU_BUTTON_MAP_TYPE_PRESSED:
	    MenuButtonDoMapMenu(mb, GDK_CURRENT_TIME);
	    break;

	  case MENU_BUTTON_MAP_TYPE_PRESSED_DRAG:
	    /* For map on press and drag, do not map the menu
	     * immediately, instead let the "motion_notify_event"
	     * signal callback map the menu
	     */
	    break;
	}
}

/*
 *	Menu Button GtkButton "released" signal callback.
 */
static void MenuButtonReleasedCB(GtkWidget *widget, gpointer data)
{
	menu_button_struct *mb = MENU_BUTTON(data);
	if((widget == NULL) || (mb == NULL))
	    return;

	if(mb->is_pressed)
	{
	    /* Record button released state */
	    mb->is_pressed = FALSE;
	    mb->press_x = 0;
	    mb->press_y = 0;

#if 0
	    /* If the menu was not mapped when the button is released
	     * and the map type is MENU_BUTTON_MAP_TYPE_PRESSED_DRAG
	     * then a clicked signal needs to be reported
	     */
/* Seems like since no manipulation of button states are done, a
 * "clicked" signal is already generated so we do not need this
 */

	    if(!mb->menu_map_state &&
	       (mb->map_type == MENU_BUTTON_MAP_TYPE_PRESSED_DRAG)
	    )
		gtk_signal_emit_by_name(GTK_OBJECT(widget), "clicked");
#endif
	}
}

/*
 *      Menu "hide" signal callback.
 */
static void MenuButtonMenuHideCB(GtkWidget *widget, gpointer data)
{
	GtkWidget *w;
	menu_button_struct *mb = MENU_BUTTON(data);
	if((widget == NULL) || (mb == NULL))
	    return;

	/* Release button if it was pressed */
	if(mb->is_pressed)
	{
	    w = mb->toplevel;
	    if(w != NULL)
	    {
		GtkButton *button = GTK_BUTTON(w);
		button->in_button = 0;
		gtk_signal_emit_by_name(GTK_OBJECT(w), "released");
	    }
	}

	/* Mark menu as no longer mapped, this needs to be set after
	 * button release reporting above so that the button "released"
	 * signal callback can detect if the menu was mapped
	 */
	mb->menu_map_state = FALSE;
}


/*
 *	Creates a new Menu Button.
 */
static GtkWidget *MenuButtonNewNexus(
	const gchar *label, guchar **icon_data,
	GtkWidget **menu_rtn, gint orientation
)
{
	GtkWidget *w, *w2, *parent;
	menu_button_struct *mb = MENU_BUTTON(g_malloc0(
	    sizeof(menu_button_struct)
	));

	/* Reset values */
	mb->map_type = MENU_BUTTON_MAP_TYPE_PRESSED;
	mb->is_pressed = FALSE;
	mb->menu_map_state = FALSE;
	mb->press_x = 0;
	mb->press_y = 0;

	/* Create button */
	switch(orientation)
	{
	  case 0:
	    mb->toplevel = w = GUIButtonPixmapLabelH(
		(guint8 **)icon_data, label, NULL
	    );
	    parent = GUIButtonGetMainBox(w);
	    w2 = gtk_pixmap_new_from_xpm_d(
		NULL, NULL, (guint8 **)map_arrow_xpm
	    );
	    gtk_box_pack_start(GTK_BOX(parent), w2, FALSE, FALSE, 0);
	    gtk_box_reorder_child(GTK_BOX(parent), w2, 1);
	    gtk_widget_show(w2);
	    break;
	  case 1:
	    mb->toplevel = w = GUIButtonPixmapLabelV(
		(guint8 **)icon_data, label, NULL
	    );
	    break;
	  default:
	    mb->toplevel = w = GUIButtonPixmap(
		(guint8 **)icon_data
	    );
	    break;
	}
	gtk_object_set_data_full(
	    GTK_OBJECT(w), MENU_BUTTON_KEY,
	    mb, MenuButtonDataDestroyCB
	);
	gtk_widget_add_events(
	    w,
	    GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK |
	    GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
	    GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK
	);
	gtk_signal_connect(
	    GTK_OBJECT(w), "key_press_event",
	    GTK_SIGNAL_FUNC(MenuButtonKeyCB), mb
	);
	gtk_signal_connect(
	    GTK_OBJECT(w), "key_release_event",
	    GTK_SIGNAL_FUNC(MenuButtonKeyCB), mb
	);
	gtk_signal_connect_after(
	    GTK_OBJECT(w), "pressed",
	    GTK_SIGNAL_FUNC(MenuButtonPressedCB), mb
	);
	gtk_signal_connect_after(
	    GTK_OBJECT(w), "released",
	    GTK_SIGNAL_FUNC(MenuButtonReleasedCB), mb
	);
	gtk_signal_connect_after(
	    GTK_OBJECT(w), "motion_notify_event",
	    GTK_SIGNAL_FUNC(MenuButtonMotionCB), mb
	);

	/* Create menu only if the menu_rtn is given */
	if(menu_rtn != NULL)
	{
	    mb->menu = w = GUIMenuCreate();
	    gtk_signal_connect(
		GTK_OBJECT(w), "hide",
		GTK_SIGNAL_FUNC(MenuButtonMenuHideCB), mb
	    );
	    *menu_rtn = w;
	}

	return(mb->toplevel);
}
GtkWidget *MenuButtonNewH(
	const gchar *label, guchar **icon_data,
	GtkWidget **menu_rtn
)
{
	return(MenuButtonNewNexus(label, icon_data, menu_rtn, 0));
}
GtkWidget *MenuButtonNewV(
	const gchar *label, guchar **icon_data,
	GtkWidget **menu_rtn
)
{
	return(MenuButtonNewNexus(label, icon_data, menu_rtn, 1));
}
GtkWidget *MenuButtonNewPixmap(
	guchar **icon_data, GtkWidget **menu_rtn
)
{
	return(MenuButtonNewNexus(NULL, icon_data, menu_rtn, 2));
}

/*
 *	Returns the Menu Button's GtkMenu.
 */
GtkWidget *MenuButtonGetMenu(GtkWidget *w)
{
	menu_button_struct *mb = MENU_BUTTON(
	    GTK_OBJECT_GET_DATA(w, MENU_BUTTON_KEY)
	);
	return((mb != NULL) ? mb->menu : NULL);
}

/*
 *	Sets the given GtkMenu as the Menu Button's new GtkMenu,
 *	the old GtkMenu (if any) will be destroyed.
 */
void MenuButtonSetMenu(GtkWidget *w, GtkMenu *menu)
{
	menu_button_struct *mb = MENU_BUTTON(
	    GTK_OBJECT_GET_DATA(w, MENU_BUTTON_KEY)
	);
	if(mb == NULL)
	    return;

	if(mb->menu != (GtkWidget *)menu)
	{
	    /* Destroy the old menu (if any) and set new menu */
	    GTK_WIDGET_DESTROY(mb->menu)
	    mb->menu = (GtkWidget *)menu;
	}
}

/*
 *	Sets the Menu Button's map trigger type.
 */
void MenuButtonSetMapTrigger(
	GtkWidget *w, gint map_type
)
{
	menu_button_struct *mb = MENU_BUTTON(
	    GTK_OBJECT_GET_DATA(w, MENU_BUTTON_KEY)
	);
	if(mb != NULL)
	    mb->map_type = map_type;
}
