/*-
 * 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 "wm.h"

/* spawn a child */
pid_t action_exec(int screen, char *cmd) {
	char buff[256];
	char number[10];
	char *ptr;
	pid_t pid;

	if ((pid = fork()) == 0) {
		snprintf(buff, sizeof(buff), "DISPLAY=%s", XDisplayName(NULL));
		ptr = strchr(strchr(buff, ':'), '.');
		if (ptr) *ptr = '\0';
		snprintf(number, sizeof(number), "%i", screen);
		strncat(buff, ".", sizeof(buff) - strlen(buff));
		strncat(buff, number, sizeof(buff) - strlen(buff));
		putenv(buff);

		execl(_PATH_BSHELL, "sh", "-c", cmd, NULL);
		exit(1);
	}

	return pid;
}

/*
 * send a synthetic ConfigureNotify event to a client (as on a move).  the
 * ICCCM requires that these fake configurenotify events get sent in a
 * reparenting windowmanager whenever the windowmanager moves the client's
 * parent window (without also resizing the client window).
 */
void action_send_config(client_t *client) {
	XEvent event;

	event.type = ConfigureNotify;
	event.xconfigure.display = display;
	event.xconfigure.event = client->window;
	event.xconfigure.window = client->window;
	event.xconfigure.x = client->x + client->dgroup->left_space;
	event.xconfigure.y = client->y + client->dgroup->top_space;
	event.xconfigure.width = client->width;
	event.xconfigure.height = client->height;
	event.xconfigure.border_width = 0;
	event.xconfigure.above = client->frame;
	event.xconfigure.override_redirect = 0;
	XSendEvent(display, client->window, 0, StructureNotifyMask, &event);
}

/* send a ICCCM Client Message */
void action_sendcmesg(Window w, Atom a, Time timestamp) {
	XClientMessageEvent e;

	e.type = ClientMessage;
	e.window = w;
	e.message_type = WM_PROTOCOLS;
	e.format = 32;
	e.data.l[0] = a;
	e.data.l[1] = timestamp;
	XSendEvent(display, w, 0, 0, (XEvent *) &e);
}

/* pointer motion handling for window movement */
static __inline void movemotion(XMotionEvent *e, long eventmask, client_t *client,
		point_t *win, point_t *oldwin, point_t *ptr) {
	win->y += e->y_root - ptr->y;
	win->x += e->x_root - ptr->x;
	ptr->y = e->y_root;
	ptr->x = e->x_root;

	/* either move the window or redraw the winbox */
	if (!options.opaquemove) {
		draw_winbox(client->screen, client, oldwin->x,
			oldwin->y, client->width, client->height);
		draw_winbox(client->screen, client, win->x, win->y,
			client->width, client->height);
	} else
		XMoveWindow(display, client->frame, win->x, win->y);

	/* update oldwin point */
	oldwin->x = win->x;
	oldwin->y = win->y;
}

/* button release handling for window movement */
static __inline void moverelease(client_t *client, point_t *win,
		point_t *oldwin) {
	/*
	 * release the mouse pointer; in nonopaque mode, erase
	 * the current winbox and let go of the server.
	 */
	XUngrabPointer(display, CurrentTime);
	if (!options.opaquemove) {
		draw_winbox(client->screen, client, oldwin->x,
			oldwin->y, client->width, client->height);
		stacking_raise(client);
		XUngrabServer(display);
	}

	/*
	 * if the window position changed, move the window to
	 * it's final destination, send it a synthetic config
	 * and tell plugins that it moved.
	 */
	if (win->x != client->x || win->y != client->y) {
		client->x = win->x;
		client->y = win->y;
		XMoveWindow(display, client->frame, win->x, win->y);
		action_send_config(client);
		plugin_geometry_change(client);
	}
}

/* window movement X event loop */
static __inline void moveloop(client_t *client, long eventmask) {
	XEvent event;
	point_t win, oldwin, ptr;
	Window dumwin;
	int dumint;

	/*
	 * get the mouse pointer position, window position, and
	 * set old window position values to the current ones.
	 */
	XQueryPointer(display, client->screen->root, &dumwin, &dumwin,
		&ptr.x, &ptr.y, &dumint, &dumint, &dumint);
	oldwin.x = win.x = client->x;
	oldwin.y = win.y = client->y;

	/* in nonopaque mode, draw the first winbox now */
	if (!options.opaquemove)
		draw_winbox(client->screen, client, win.x, win.y,
			client->width, client->height);

	/* handle movement loop events */
	while (XMaskEvent(display, eventmask, &event), 1)
		switch (event.type) {
		case Expose:
			event_handle(&event);
			break;
		case MotionNotify:
			movemotion(&event.xmotion, eventmask, client,
				&win, &oldwin, &ptr);
			break;
		case ButtonRelease:
			moverelease(client, &win, &oldwin);
			return;
		}
}

/*
 * interactively move a window; handle both opaque and nonopaque
 * movement styles.  the client window in question becomes bound
 * to pointer movements until the pointer button is released,
 * at which point the window is left at that final destination
 * location and send a synthetic configure event.
 */
void action_move(client_t *client) {
	long eventmask;

	/*
	 * movement isn't allowed on nomove clients; ungrab
	 * the passive grab we hold on entry to this func and
	 * return
	 */
	if (client->flags.nomove) {
		XUngrabPointer(display, CurrentTime);
		return;
	}

	/* make grab for moving the window */
	eventmask = PointerMotionMask | ButtonMotionMask
		| ButtonReleaseMask;
	if (XGrabPointer(display, client->screen->root, 0, eventmask,
			GrabModeAsync, GrabModeAsync, client->screen->root,
			cursor_move, CurrentTime) != GrabSuccess)
		return;

	/*
	 * prepare according to opaque or nonopaque style move;
	 * opaque style requires that we handle exposures.
	 */
	if (options.opaquemove) {
		stacking_raise(client);
		eventmask |= ExposureMask;
	} else
		XGrabServer(display);

	/* call window movement loop */
	moveloop(client, eventmask);
}

/*
 * set width of client for resize, only move the bound
 * edge.
 */
static __inline void setwidth(client_t *client, int width,
		rect_t *win, int **dx, int **dy) {
	if (*dx == &win->x1)
		win->x1 = win->x2 - width - DWIDTH(client);
	else if (*dx == &win->x2)
		win->x2 = win->x1 + width + DWIDTH(client);
}

/*
 * set height of client for resize, only move the
 * bound edge.
 */
static __inline void setheight(client_t *client, int height,
		rect_t *win, int **dx, int **dy) {
	if (*dy == &win->y1)
		win->y1 = win->y2 - height - DHEIGHT(client);
	else if (*dy == &win->y2)
		win->y2 = win->y1 + height + DHEIGHT(client);
}

/*
 * check resizing against constraints specified by XNormalHints
 * given to us by the client.
 */
static __inline void resizelimit(client_t *client, rect_t *win,
		int **dx, int **dy) {
	int basew, baseh, minw, minh;

	/*
	 * get minimum and base height/width values for
	 * this client.  according to the ICCCM, if the
	 * base size is specified and minsize isn't, minsize
	 * should be base size; if base size is not specified
	 * and minsize is, use minsize for basesize.
	 */
	if (client->normalhints.flags & PBaseSize) {
		basew = client->normalhints.base_width;
		baseh = client->normalhints.base_height;
		if (client->normalhints.flags & PMinSize) {
			minw = client->normalhints.min_width;
			minh = client->normalhints.min_height;
		} else {
			minw = basew;
			minh = baseh;
		}
	} else if (client->normalhints.flags & PMinSize) {
		minw = basew = client->normalhints.min_width;
		minh = baseh = client->normalhints.min_height;
	} else
		baseh = basew = minw = minh = 1;

	/*
	 * keep resizing to increments as specified
	 * in the normal hints.
	 */
	if (client->normalhints.flags & PResizeInc) {
		int wid, hei;

		wid = win->x2 - win->x1 - DWIDTH(client) - basew;
		hei = win->y2 - win->y1 - DHEIGHT(client) - baseh;
		wid -= wid % client->normalhints.width_inc;
		hei -= hei % client->normalhints.height_inc;

		setwidth(client, wid + basew, win, dx, dy);
		setheight(client, hei + baseh, win, dx, dy);
	}

	/*
	 * if a maxmimum size was specified, make sure the client
	 * dimensions do not exceed it.
	 */
	if (client->normalhints.flags & PMaxSize) {
		if (win->x2 - win->x1 - DWIDTH(client) > client->normalhints.max_width)
			setwidth(client, client->normalhints.max_width, win, dx, dy);
		if (win->y2 - win->y1 - DHEIGHT(client) > client->normalhints.max_height)
			setheight(client, client->normalhints.max_height, win, dx, dy);
	}

	/*
	 * handle window minimum size; even if no minimum size hint
	 * was specified we enforce a minimum size of 1x1.
	 */
	if (win->x2 - win->x1 - DWIDTH(client) < minw)
		setwidth(client, minw, win, dx, dy);
	if (win->y2 - win->y1 - DHEIGHT(client) < minh)
		setheight(client, minh, win, dx, dy);
}

/* pointer motion handling for window resizing */
static __inline void resizemotion(XMotionEvent *e, client_t *client, rect_t *win,
		rect_t *oldwin, point_t *ptr, int **dx, int **dy, point_t *reloff) {
	ptr->x = e->x_root;
	ptr->y = e->y_root;

	/*
	 * rebind edges to the mouse in the x direction, if
	 * an edge was crossed.
	 */
	if (ptr->x > client->x + FULLWIDTH(client) && *dx != &win->x2) {
		win->x1 = client->x;
		*dx = &win->x2;
		reloff->x = reloff->y = 0;
	} else if (ptr->x < client->x && *dx != &win->x1) {
		win->x2 = client->x + FULLWIDTH(client);
		*dx = &win->x1;
		reloff->x = reloff->y = 0;
	}

	/*
	 * rebind edges to the mouse if edges were crossed
	 * in the y direction.
	 */
	if (ptr->y > client->y + FULLHEIGHT(client) && *dy != &win->y2) {
		win->y1 = client->y;
		*dy = &win->y2;
		reloff->x = reloff->y = 0;
	} else if (ptr->y < client->y && *dy != &win->y1) {
		win->y2 = client->y + FULLHEIGHT(client);
		*dy = &win->y1;
		reloff->x = reloff->y = 0;
	}

	/* move the bound edges */
	if (*dx)
		**dx = ptr->x + reloff->x;
	if (*dy)
		**dy = ptr->y + reloff->y;

	/* constrain based on normalhints */
	resizelimit(client, win, dx, dy);

	/*
	 * erase the old winbox and draw a new one if the
	 * window rectangle changed.
	 */
	if (memcmp(win, oldwin, sizeof(rect_t))) {
		draw_winbox(client->screen, client, oldwin->x1, oldwin->y1,
			oldwin->x2 - oldwin->x1 - DWIDTH(client),
			oldwin->y2 - oldwin->y1 - DHEIGHT(client));
		draw_winbox(client->screen, client, win->x1, win->y1,
			win->x2 - win->x1 - DWIDTH(client),
			win->y2 - win->y1 - DHEIGHT(client));
		memcpy(oldwin, win, sizeof(rect_t));
	}
}

/* button release handling for window resizing */
static __inline void resizerelease(client_t *client, rect_t *win,
		rect_t *oldwin) {
	/*
	 * erase the last window box, release the mouse pointer
	 * and release the server.
	 */
	draw_winbox(client->screen, client, oldwin->x1, oldwin->y1,
		oldwin->x2 - oldwin->x1 - DWIDTH(client),
		oldwin->y2 - oldwin->y1 - DHEIGHT(client));
	XUngrabPointer(display, CurrentTime);
	XUngrabServer(display);

	/*
	 * if the window geometry has changed, resize the client
	 * window and notify plugins.
	 */
	if (win->x1 != client->x || win->y1 != client->y 
			|| win->x2 != client->x + FULLWIDTH(client)
			|| win->y2 != client->y + FULLHEIGHT(client)) {
		client->x = win->x1;
		client->y = win->y1;
		client->width = win->x2 - win->x1 - DWIDTH(client);
		client->height = win->y2 - win->y1 - DHEIGHT(client);
		client_sizeframe(client);
		plugin_geometry_change(client);
	}
}

/* window resizing X event loop */
static __inline void resizeloop(client_t *client, long eventmask) {
	XEvent event;
	rect_t win, oldwin;
	point_t ptr, reloff;
	Window dumwin;
	int dumint;
	int *dx, *dy;

	/*
	 * get the mouse pointer position, window rectangle, and
	 * set old window rectangle values to the current ones.
	 */
	XQueryPointer(display, client->screen->root, &dumwin, &dumwin,
		&ptr.x, &ptr.y, &dumint, &dumint, &dumint);
	oldwin.x1 = win.x1 = client->x;
	oldwin.y1 = win.y1 = client->y;
	oldwin.x2 = win.x2 = win.x1 + FULLWIDTH(client);
	oldwin.y2 = win.y2 = win.y1 + FULLHEIGHT(client);

	/*
	 * in relative_resize, we start with dx and dy bound to
	 * the corner that the mouse is nearest to.  otherwise
	 * dx and dy start unbound
	 */
	if (options.relative_resize) {
		if (ptr.x - client->x < FULLWIDTH(client) / 2) {
			reloff.x = -(ptr.x - client->x);
			dx = &win.x1;
		} else {
			reloff.x = FULLWIDTH(client) - (ptr.x - client->x);
			dx = &win.x2;
		}
		if (ptr.y - client->y < FULLHEIGHT(client) / 2) {
			reloff.y = -(ptr.y - client->y);
			dy = &win.y1;
		} else {
			reloff.y = FULLHEIGHT(client) - (ptr.y - client->y);
			dy = &win.y2;
		}
	} else {
		reloff.x = reloff.y = 0;
		dx = dy = NULL;
	}

	/* draw winbox of the first window position */
	draw_winbox(client->screen, client, win.x1, win.y1,
		win.x2 - win.x1 - DWIDTH(client),
		win.y2 - win.y1 - DHEIGHT(client));

	/* handle resize loop events */
	while (XMaskEvent(display, eventmask, &event), 1)
		switch (event.type) {
		case MotionNotify:
			resizemotion(&event.xmotion, client, &win,
				&oldwin, &ptr, &dx, &dy, &reloff);
			break;
		case ButtonRelease:
			resizerelease(client, &win, &oldwin);
			return;
		}
}

/*
 * interactively resize a window.  when the mouse crosses a
 * particular edge, that edge becomes bound to mouse movements.
 * moving the mouse across an opposite edite will unbind the
 * currently bound edge in favor of the new one.  the user
 * may elect to use a relative resize option (options.relative_resize),
 * in which case the mouse pointer starts bound to the corner that
 * it is closest too, and maintains that binding till opposite edges
 * get crossed.
 *
 * during resizing it is also neccesary to make sure that the
 * dimensions of the window do not contradict limits specified
 * through the XNormalHints structure in the client_t.
 */
void action_resize(client_t *client) {
	long eventmask;

	/*
	 * resizing isn't allowed for noresize clients; ungrab
	 * the grab we have from earlier passive grab and
	 * return
	 */
	if (client->flags.noresize) {
		XUngrabPointer(display, CurrentTime);
		return;
	}

	/* grab for resizing the window */
	eventmask = ButtonReleaseMask | PointerMotionMask
		| ButtonMotionMask;
	if (XGrabPointer(display, client->screen->root, 0, eventmask,
			GrabModeAsync, GrabModeAsync, client->screen->root,
			cursor_move, CurrentTime) != GrabSuccess)
		return;

	/* prepare for resizing */
	XGrabServer(display);
	stacking_raise(client);

	/* call the window resizing loop */
	resizeloop(client, eventmask);
}

/* move a client window to the icon'd window list */
void action_iconify(client_t *client) {
	if (client->flags.noiconify)
		return;

	/* let plugs know, and then unmap it to iconic */
	XUnmapWindow(display, client->frame);
	client_setstate(client, IconicState);
	plugin_iconify_notify(client);

	/* get it out of the workspace */
	desktop_rm_client(client);
	workspace_rm_client(client);
}

/* restore an iconified client window */
void action_restore(client_t *client) {
	if (client->state != IconicState)
		return;

	/* set state to normal, and add it to the current workspace */
	client_setstate(client, NormalState);
	workspace_add_client(client->screen->desktop->current_space, client);
	desktop_add_client(client);
	plugin_restore_notify(client);
	XMapWindow(display, client->frame);
	stacking_raise(client);
	focus_setfocused(client);
} 

/* zoom (maximize) or unzoom a window */
void action_zoom(client_t *client) {
	int tmpint;

	/* zooming counts as both resizing and moving */
	if (client->flags.noresize || client->flags.nomove)
		return;

	/*
	 * when zooming a client we save it's position in the
	 * save_ members that match with geometry/dgroup
	 * information for the client in it's unzoomed state.
	 * when unzooming we save the zoomed state into the
	 * save variables so it will be accessable to plugins
	 * in plugin_unzoom_notify.
	 */
	if (client->flags.zoomed) {
		client->flags.zoomed = 0;
		swap(tmpint, client->save_x, client->x);
		swap(tmpint, client->save_y, client->y);
		swap(tmpint, client->save_width, client->width);
		swap(tmpint, client->save_height, client->height);
		if (options.fullscreen_zoom) {
			dgroup_t *dgrouptmp = client->dgroup;
			dgroup_switch(client, client->save_dgroup);
			client->save_dgroup = dgrouptmp;
		}
	} else {
		client->flags.zoomed = 1;
		client->save_x = client->x;
		client->save_y = client->y;
		client->save_width = client->width;
		client->save_height = client->height;
		if (options.fullscreen_zoom) {
			client->save_dgroup = client->dgroup;
			dgroup_switch(client, &dgroup_empty);
		}
		if (xinerama_zoom(client)) {
			client->x = 0;
			client->y = 0;
			client->width = client->screen->width - DWIDTH(client);
			client->height = client->screen->height - DHEIGHT(client);
		}
		stacking_raise(client);
		focus_setfocused(client);
	}

	/* notify plugins */
	if (client->flags.zoomed)
		plugin_zoom_notify(client);
	else
		plugin_unzoom_notify(client);

	/*
	 * It is neccesary to send a synth configure notify if we didn't
	 * resize the client, but only moved it, because it wont recieve
	 * a real one in that case.
	 */
	client_sizeframe(client);
	if (client->width == client->save_width && client->height == client->save_height)
		if (client->x != client->save_x || client->y != client->save_y)
			action_send_config(client);
}
