/*-
 * Copyright (c) 2001 Jordan DeLong
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. 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.
 * 3. Neither the name of the author nor the names of contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
 */
#include "menu.h"

/* per-screen structure */
static struct menuscr {
	GC	menugc;
} *menuscr;

/* font to use */
#ifdef I18N
static XFontSet	menufont;
static XFontSetExtents *menufont_extents;
#else
static XFontStruct	*menufont	= NULL;
#endif

/* decoration group */
static dgroup_t		*menu_dgroup	= NULL;

/* pixmaps */
static pixmap_t		*submenu_bullet	= NULL;

/* stacking layer */
int menu_stacklayer;

/* XContext to get menus */
XContext menu_context;

/* initialize the menu system */
int menu_init(char *fontname, dgroup_t *dgroup, pixmap_t *bullet) {
	XGCValues gcvalues;
	int i, cnt;

	/* this is used to get menu_t's from windows */
	menu_context = XUniqueContext();

	/*
	 * get the font structure, fall back on a semi-decent
	 * font, and if we can't even get that just use 'fixed',
	 * which should always be there.  If we can't use 'fixed'
	 * we give up.
	 */
	if (fontname) {
#ifdef I18N
		menufont = XLoadQueryFontSet(display, fontname);
#else
		menufont = XLoadQueryFont(display, fontname);
#endif
		if (menufont)
			goto gotfont;
		PWARN("unable to get requested menu_font, trying default");
	}
	PWARN("using default font");
#ifdef I18N
	menufont = XLoadQueryFontSet(display,
		"-*-helvetica-medium-r-normal-*-12-*-*-*-*-*-*-*");
#else
	menufont = XLoadQueryFont(display,
		"-*-helvetica-medium-r-normal-*-12-*-*-*-*-*-*-*");
#endif
	if (menufont)
		goto gotfont;
	PWARN("failed to load default font; trying 'fixed' as last resort");
#ifdef I18N
	menufont = XLoadQueryFontSet(display, "fixed");
#else
	menufont = XLoadQueryFont(display, "fixed");
#endif
	if (menufont)
		goto gotfont;
	PWARN("failed to load font 'fixed', giving up on menus");
	return -1;
gotfont:

#ifdef I18N
        menufont_extents = XExtentsOfFontSet(menufont);
#endif
	/* get the per-screen structure */
	cnt = ScreenCount(display);
	menuscr = calloc(cnt, sizeof(struct menuscr));
	if (!menuscr)
		return -1;

	/* fill in the structure */
	for (i = 0; i < cnt; i++) {
		/*
		 * XXX: color needs to be in a parameter
		 */
		gcvalues.foreground = WhitePixel(display, i);
		gcvalues.background = BlackPixel(display, i);
#ifdef I18N	    
		menuscr[i].menugc = XCreateGC(display, RootWindow(display, i),
			GCForeground | GCBackground, &gcvalues);
#else	    
		gcvalues.font = menufont->fid;
		menuscr[i].menugc = XCreateGC(display, RootWindow(display, i),
			GCFont | GCForeground | GCBackground, &gcvalues);
#endif
	}

	menu_dgroup = dgroup;
	submenu_bullet = bullet;
	return 0;
}

/* shutdown the menu system */
void menu_shutdown() {
	int i, cnt;

	cnt = ScreenCount(display);
	if (menuscr) {
		for (i = 0; i < cnt; i++)
			if (menuscr[i].menugc)
				XFreeGC(display, menuscr[i].menugc);
		free(menuscr);
	}
	if (menufont)
#ifdef I18N
		XFreeFontSet(display, menufont);
#else
		XFreeFont(display, menufont);
#endif
}

/* create a menu */
menu_t *menu_create() {
	menu_t *menu;

	menu = calloc(1, sizeof(menu_t));
	if (!menu)
		return NULL;
	menu->forefather = menu;

	return menu;
}

/* add an entry to a menu, pos is the position to add it at */
menuent_t *menu_addent(menu_t *menu, int pos, int type, char *text, void *dat) {
	menuent_t *ent;
	menuent_t **tmp;
	menu_t **tmpsubmenus;
	int i;

	ent = calloc(1, sizeof(menuent_t));
	if (!ent)
		return NULL;
	ent->type = type;
	ent->text = text;

	/* get space for the new entry */
	tmp = realloc(menu->entries, (menu->entry_count + 1) * sizeof(menuent_t *));
	if (!tmp)
		goto free1;
	menu->entries = tmp;
	menu->entry_count++;

	/* submenus need some special attention */
	if (type == ET_SUBMENU) {
		tmpsubmenus = realloc(menu->submenus, (menu->submenu_count + 1) * sizeof(menu_t *));
		if (!tmpsubmenus)
			goto free2;
		menu->submenus = tmpsubmenus;
		menu->submenus[menu->submenu_count] = (menu_t *) dat;
		ent->dat.submenu = menu->submenu_count++;
		/* set the forefather pointer, and set it on all subwindows of this one */
		((menu_t *) dat)->forefather = menu->forefather;
		for (i = 0; i < ((menu_t *) dat)->submenu_count; i++)
			((menu_t *) dat)->submenus[i]->forefather = menu->forefather;
	} else
		ent->dat.dat = dat;

	/* move to make space if necessary, and put it in */
	if (pos == POS_LAST || pos >= menu->entry_count)
		pos = menu->entry_count - 1;
	else
		memmove(&menu->entries[pos + 1], &menu->entries[pos],
			(menu->entry_count - pos) * sizeof(menuent_t *));
	menu->entries[pos] = ent;

	return ent;

free2:
	menu->entry_count--;
free1:
	free(ent);
	return NULL;
}

/* dealloc a menu entry */
void menu_freeent(menuent_t *ent) {
	free(ent->text);

	/* free entry-type specific data */
	switch (ent->type) {
	case ET_RESTART:
	case ET_COMMAND:
		if (ent->dat.cmd)
			free(ent->dat.cmd);
		break;
	default:
		break;
	}

	free(ent);
}

/* resize a menu */
void menu_size(menu_t *menu) {
	screen_t *screen;
	int width, height;
	int strwidth, i;

	width = MENU_MINWIDTH;
	height = MENU_YBORDER * 2;
	for (i = 0; i < menu->entry_count; i++) {
#ifdef I18N
		height += menufont_extents->max_ink_extent.height;
		strwidth = XmbTextEscapement(menufont, menu->entries[i]->text, strlen(menu->entries[i]->text));
#else	    
		height += menufont->ascent + menufont->descent;
		strwidth = XTextWidth(menufont, menu->entries[i]->text, strlen(menu->entries[i]->text));
#endif	    
		if (submenu_bullet && menu->entries[i]->type == ET_SUBMENU)
			strwidth += submenu_bullet->width;
		if (width < strwidth)
			width = strwidth;
	}
	width += MENU_XBORDER * 2;

	/* fill in the client_t for each screen */
	TAILQ_FOREACH(screen, &screen_list, s_list) {
		menu->client[screen->num]->width = width;
		menu->client[screen->num]->height = height;
		client_sizeframe(menu->client[screen->num]);
	}
}

/* realize a menu (called from post-init, i.e. start) */
int menu_realize(menu_t *menu) {
	XSetWindowAttributes attr;
	clientflags_t flags;
	screen_t *screen;
	Window wnd;
	int i;

	menu->client = calloc(screen_count, sizeof(client_t *));
	if (!menu->client)
		return -1;

	menu->active_sub = calloc(screen_count, sizeof(menu_t *));
	if (!menu->active_sub)
		goto free;

	bzero(&flags, sizeof(clientflags_t));
	flags.internal = 1;
	flags.nofocus = 1;
	flags.noresize = 1;
	flags.noiconify = 1;
	flags.nodelete = 1;
	flags.sticky = 1;

	TAILQ_FOREACH(screen, &screen_list, s_list) {
		attr.background_pixel = BlackPixel(display, screen->num);
		wnd = XCreateWindow(display, RootWindow(display, screen->num),
			0, 0, 50, 50, 0, CopyFromParent, CopyFromParent,
			CopyFromParent, CWBackPixel, &attr);
		menu->client[screen->num] = client_add(screen, wnd,
			&flags, menu_dgroup);
		if (!menu->client[screen->num])
			return -1;

		/* set layer */
		menu->client[screen->num]->stacklayer = menu_stacklayer;

		XSaveContext(display, menu->client[screen->num]->frame,
			menu_context, (XPointer) menu);
		XSelectInput(display, menu->client[screen->num]->window,
			ExposureMask | ButtonPressMask | ButtonReleaseMask | EnterWindowMask);
		plugin_setcontext(plugin_this, menu->client[screen->num]->window);
		XMapWindow(display, menu->client[screen->num]->window);
	}

	menu_size(menu);

	/* realize the submenus */
	for (i = 0; i < menu->submenu_count; i++)
		menu_realize(menu->submenus[i]);

	return 0;

free:
	free(menu->client);
	return -1;
}

/* kill a menu, this may be before it's realized, so a lot is conditional */
void menu_delete(menu_t *menu) {
	int cnt, i;

	/* kill submenus */
	for (i = 0; i < menu->submenu_count; i++)
		menu_delete(menu->submenus[i]);
	if (menu->submenus)
		free(menu->submenus);

	/* get rid of the menu entries */
	for (i = 0; i < menu->entry_count; i++)
		if (menu->entries[i])
			menu_freeent(menu->entries[i]);
	if (menu->entries)
		free(menu->entries);

	/* free created clients */
	if (menu->client) {
		cnt = ScreenCount(display);
		for (i = 0; i < cnt; i++) {
			plugin_rmcontext(menu->client[i]->window);
			XDeleteContext(display, menu->client[i]->frame, menu_context);
			if (menu->client[i])
				client_rm(menu->client[i]);
		}
		free(menu->client);
	}

	/* free the active submenu pointers */
	if (menu->active_sub)
		free(menu->active_sub);

	free(menu);
}

/* draw a menu entry */
static void menu_drawent(menu_t *menu, client_t *client, int i, int y) {
	int tmpx, tmpy;

	if (submenu_bullet && menu->entries[i]->type == ET_SUBMENU) {
		tmpx = client->width - submenu_bullet->width;
#ifdef I18N
		tmpy = y + (((menufont_extents->max_ink_extent.height) / 2) - submenu_bullet->height / 2);
#else
		tmpy = y + (((menufont->ascent + menufont->descent) / 2) - submenu_bullet->height / 2);
#endif	    
	
		XSetClipMask(display, menuscr[client->screen->num].menugc,
			submenu_bullet->shapemasks[client->screen->num]);
		XSetClipOrigin(display, menuscr[client->screen->num].menugc, tmpx, tmpy);
		XCopyArea(display, submenu_bullet->pixmaps[client->screen->num], client->window,
			menuscr[client->screen->num].menugc, 0, 0, submenu_bullet->width,
			submenu_bullet->height, tmpx, tmpy);
		XSetClipMask(display, menuscr[client->screen->num].menugc, None);
	}
#ifdef I18N
	XmbDrawString(display, client->window, menufont, menuscr[client->screen->num].menugc,
		MENU_XBORDER, y + menufont_extents->max_logical_extent.height * 4 / 5, menu->entries[i]->text, strlen(menu->entries[i]->text));
#else    
	XDrawString(display, client->window, menuscr[client->screen->num].menugc,
		MENU_XBORDER, y + menufont->ascent, menu->entries[i]->text, strlen(menu->entries[i]->text));
#endif
}

/* draw a menu; entidx is for use during menu_interact */
void menu_expose(menu_t *menu, client_t *client, XExposeEvent *e) {
	int i, y, first, last;

	/*
	 * be slightly intelligent about what lines to draw; it's
	 * probably not worth the time to worry about the width
	 * of the line getting drawn...
	 */
	y = MENU_YBORDER;
	last = first = -1;
	for (i = 0; i < menu->entry_count; i++) {
#ifdef I18N    
		y += menufont_extents->max_ink_extent.height;
#else	    
		y += menufont->ascent + menufont->descent;
#endif
		if (y > e->y && first == -1)
			first = i - 1;
		if (y > e->y + e->height && last == -1)
			last = i;
	}

	/* now we draw the lines that need it */
	if (first < 0) first = 0;
	if (last == -1) last = menu->entry_count - 1;
#ifdef I18N
    	y = MENU_YBORDER + (menufont_extents->max_ink_extent.height) * first;
#else    
	y = MENU_YBORDER + (menufont->ascent + menufont->descent) * first;
#endif    
	for (i = first; i < last + 1; i++) {
		menu_drawent(menu, client, i, y);
#ifdef I18N	    
		y += menufont_extents->max_ink_extent.height;
#else	    
		y += menufont->ascent + menufont->descent;
#endif	    
	}
}

/* perform an action for a menu entry */
static void menu_action(menu_t *menu, menuent_t *ent, int snum) {
	switch (ent->type) {
	case ET_COMMAND:
		action_exec(menu->client[snum]->screen->num, ent->dat.cmd);
		break;
	case ET_RESTART:
		if (ent->dat.cmd)
			restart_bin = ent->dat.cmd;
		else
			restart_bin = binary_name;
		/* fall through */
	case ET_EXIT:
		restart_flag = 1;
		break;
	case ET_ABORT:
		/* this is useful for debug */
		if (!fork()) { abort(); exit(1); }
		break;
	}
}

/* open a menu */
static void menu_open(client_t *client, int x, int y) {
	client->x = x;
	client->y = y;
	XMoveWindow(display, client->frame, client->x, client->y);
	workspace_add_client(client->screen->desktop->current_space, client);
	desktop_add_client(client);
	XMapWindow(display, client->frame);
	stacking_raise(client);
	client->state = NormalState;
}

/* close a menu */
static void menu_close(menu_t *menu, client_t *client) {
	int snum = client->screen->num;

	/* close any open submenus */
	if (menu->active_sub[snum]) {
		menu_close(menu->active_sub[snum], menu->active_sub[snum]->client[snum]);
		menu->active_sub[snum] = NULL;
	}
	if (client->workspace) {
		desktop_rm_client(client);
		workspace_rm_client(client);
	}
	XUnmapWindow(display, client->frame);
	client->state = WithdrawnState;
}

/* draw (or erase) a highlight */
static void drawhighlight(client_t *client, int idx) {
#ifdef I18N    
	XFillRectangle(display, client->window, client->screen->xorgc, 2,
		idx * (menufont_extents->max_ink_extent.height) + MENU_YBORDER,
		client->width - 5, menufont_extents->max_ink_extent.height);
#else    
	XFillRectangle(display, client->window, client->screen->xorgc, 2,
		idx * (menufont->ascent + menufont->descent) + MENU_YBORDER,
		client->width - 5, menufont->ascent + menufont->descent);
#endif
}

/* open submenus or close them as neccessary while dragging across */
static void passopen(menu_t *menu, client_t *client, int entidx, int *oldentidx, int snum) {
	if (menu->entries[entidx]->type == ET_SUBMENU) {
		if (menu->active_sub[snum] != menu->submenus[menu->entries[entidx]->dat.submenu]) {
			if (menu->active_sub[snum])
				menu_close(menu, menu->active_sub[snum]->client[snum]);
			menu->active_sub[snum] = menu->submenus[menu->entries[entidx]->dat.submenu];
#ifdef I18N		    
			menu_open(menu->active_sub[snum]->client[snum],
				client->x + FULLWIDTH(client),
				client->y + MENU_YBORDER + (entidx * (menufont_extents->max_ink_extent.height)));
#else				  
			menu_open(menu->active_sub[snum]->client[snum],
				client->x + FULLWIDTH(client),
				client->y + MENU_YBORDER + (entidx * (menufont->ascent + menufont->descent)));
#endif				  
		}
		*oldentidx = -1;
	} else {
		drawhighlight(client, entidx);
		if (menu->active_sub[snum]) {
			menu_close(menu, menu->active_sub[snum]->client[snum]);
			menu->active_sub[snum] = NULL;
		}
		*oldentidx = entidx;
	}
}

/* main menu operation routine, e == NULL if not from a keypress */
static void menu_interact(menu_t *menu, client_t *client, XButtonEvent *butev) {
	XEvent event;
	XMotionEvent *e;
	menu_t *tmpmenu;
	Window lastsubwin, dumwin;
	int xpos, ypos, snum;
	int oldentidx, entidx, mask;
	int moving;

	/* grab the pointer */
	mask = ButtonReleaseMask | PointerMotionMask | ButtonMotionMask;
	if (XGrabPointer(display, client->screen->root, 0, mask, GrabModeAsync,
			GrabModeAsync, client->screen->root, None, CurrentTime) != GrabSuccess)
		return;
	stacking_raise(client);
	snum = client->screen->num;

	/* set the entry index var if we got a event structure */
	if (butev) {
#ifdef I18N	    
		oldentidx = entidx = (butev->y - MENU_YBORDER) 
			/ (menufont_extents->max_ink_extent.height);
#else	    
		oldentidx = entidx = (butev->y - MENU_YBORDER) 
			/ (menufont->ascent + menufont->descent);
#endif	    
		if (entidx >= 0 && entidx < menu->entry_count) {
			XSync(display, 0);
			while (XCheckMaskEvent(display, ExposureMask, &event))
				event_handle(&event);
			passopen(menu, client, entidx, &oldentidx, snum);
		} else {
			oldentidx = entidx = -1;
		}
	} else {
		oldentidx = entidx = -1;
	}

	moving = 0;
	lastsubwin = -1;
	mask |= ExposureMask;
	while (1) {
		XMaskEvent(display, mask, &event);

		switch (event.type) {
		case MotionNotify:
			e = &event.xmotion;
			moving = 1;

			/* see if we moved to another window */
			if (e->subwindow != lastsubwin && e->subwindow != client->frame) {
				lastsubwin = e->subwindow;
				if (oldentidx != -1)
					drawhighlight(client, oldentidx);
				if (e->subwindow == None
						|| XFindContext(display, e->subwindow, menu_context, (XPointer *) &tmpmenu) != 0) {
					oldentidx = entidx = -1;
					break;
				}
				if (menu->active_sub[snum] && menu->active_sub[snum]->client[snum]->frame != e->subwindow)
					menu_close(menu, menu->active_sub[snum]->client[snum]);
				menu = tmpmenu;
				client = menu->client[snum];
				oldentidx = -1;
			}

			/* get cordinates and erase old highlights, draw new if needed */
			XTranslateCoordinates(display, e->root, client->window, e->x_root, e->y_root,
				&xpos, &ypos, &dumwin);
			if (ypos >= client->height - MENU_YBORDER || ypos <= MENU_YBORDER || xpos <= MENU_XBORDER
					|| xpos >= client->width - MENU_XBORDER) {
				if (oldentidx != -1)
					drawhighlight(client, oldentidx);
				entidx = oldentidx = -1;
				break;
			}

#ifdef I18N
			entidx = (ypos - MENU_YBORDER) / (menufont_extents->max_ink_extent.height);
#else		    
			entidx = (ypos - MENU_YBORDER) / (menufont->ascent + menufont->descent);
#endif		    
			if (entidx != oldentidx) {
				if (oldentidx != -1)
					drawhighlight(client, oldentidx);
				passopen(menu, client, entidx, &oldentidx, snum);
			} else
				oldentidx = entidx;
			break;
		case ButtonRelease:
			XUngrabPointer(display, CurrentTime);
			if (oldentidx != -1)
				drawhighlight(client, oldentidx);
			if (entidx != -1) {
				if (menu->entries[entidx]->type == ET_SUBMENU)
					return;
				menu_action(menu, menu->entries[entidx], snum);

				/*
				 * for now, always close when doing an action.  at some point
				 * I may do break-off-able menus and such
				 */
				menu_close(menu->forefather, menu->forefather->client[snum]);
				return;
			}
			if (moving)
				menu_close(menu->forefather, menu->forefather->client[snum]);
			return;
		case Expose:
			event_handle(&event);
			break;
		}
	}
}

/* open or close a menu */
void menu_use(menu_t *menu, screen_t *screen) {
	Window dumwin;
	client_t *client;
	int dumint, x, y;

	client = menu->client[screen->num];

	if (client->state == WithdrawnState) {
		XQueryPointer(display, screen->root, &dumwin, &dumwin, &x, &y, &dumint, &dumint,
			&dumint);
		menu_open(client, x - client->width / 2, y);
		menu_interact(menu, client, NULL);
	} else {
		/* need an if it's moved thing */
		menu_close(menu, client);
	}
}

/* a click on a menu, go into interaction with the menu */
void menu_click(menu_t *menu, client_t *client, XButtonEvent *e) {
	menu_interact(menu, client, e);
}
