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

/*
 * window placement X event loop, returns 1 if the
 * user moved the mouse, and 0 if not.
 */
static __inline int placeloop(client_t *client, long eventmask,
		point_t *saveptr, point_t *win, point_t *oldwin) {
	struct timeval tv;
	fd_set fdset;
	XEvent event;
	Window dumwin;
	int dumint, xfd;
	int mousing;

	/*
	 * obtain the pointer position; set the win and oldwin
	 * points to the top left corner of the client window.
	 */
	XQueryPointer(display, client->screen->root, &dumwin, &dumwin,
		&saveptr->x, &saveptr->y, &dumint, &dumint, &dumint);
	oldwin->x = win->x = client->x;
	oldwin->y = win->y = client->y;
	xfd = ConnectionNumber(display);
	mousing = 0;

	/*
	 * move the mouse cursor to the upper left corner of
	 * the window and draw the first winbox
	 */
	XWarpPointer(display, None, client->screen->root,
		0, 0, 1, 1, win->x, win->y);
	draw_winbox(client->screen, client, win->x, win->y,
		client->width, client->height);

	/* keyboard events can come from our keyboard grab */
	eventmask |= KeyPressMask | KeyReleaseMask;

	/* window placement event loop */
	while (1) {
		while (XCheckMaskEvent(display, eventmask, &event))
			switch (event.type) {
			case MotionNotify:
				mousing = 1;
				win->x = event.xmotion.x_root;
				win->y = event.xmotion.y_root;
				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);
				oldwin->x = win->x;
				oldwin->y = win->y;
				break;
			case KeyPress:
				event.xkey.window = client->window;
				event.xkey.subwindow = client->window;
				event.xkey.x = event.xkey.y = 0;
				XSendEvent(display, client->window, 0,
					KeyPressMask, &event);
				return mousing;
			case ButtonRelease:
				return 1;
			}

		/*
		 * wait for X events for a specified amount
		 * of time; if none are recieved, we're done.
		 */
		tv.tv_usec = options.interact_timeout;
		tv.tv_sec = 0;
		FD_ZERO(&fdset);
		FD_SET(xfd, &fdset);
		if (select(xfd + 1, &fdset, NULL, NULL,
				tv.tv_usec ? &tv : NULL) == 0)
			return mousing;
	}
}

/*
 * interact with the user for placement of a window; works similarly
 * to the window movement routine with the added provisions that the
 * operation can time out, keyboard presses terminate the operation,
 * and that it is always a nonopaque move.
 */
static void placement_interact(client_t *client) {
	point_t saveptr, win, oldwin;
	long eventmask;
	int mousing;

	/*
	 * grab the X pointer, keyboard and server for placement
	 * operation.
	 */
	eventmask = PointerMotionMask | ButtonMotionMask
		| ButtonReleaseMask;
	if (XGrabPointer(display, client->screen->root, 0, eventmask,
			GrabModeAsync, GrabModeAsync, client->screen->root,
			cursor_place, CurrentTime) != GrabSuccess)
		return;
	if (XGrabKeyboard(display, client->screen->root, 0,GrabModeAsync,
			GrabModeAsync, CurrentTime) != GrabSuccess)
		goto ungrab;
	XGrabServer(display);

	/* call the window placement event loop */
	mousing = placeloop(client, eventmask, &saveptr, &win, &oldwin);

	/*
	 * clean up window boxes, send a configure event
	 * if the client window was moved, and move the
	 * pointer.
	 */
	draw_winbox(client->screen, client, oldwin.x, oldwin.y,
		client->width, client->height);
	if (win.x != client->x || win.y != client->y) {
		client->x = win.x;
		client->y = win.y;
		action_send_config(client);
	}
	if (mousing)
		XWarpPointer(display, None, client->screen->root, 0, 0, 1, 1,
			client->x + (FULLWIDTH(client) / 2),
			client->y + (FULLHEIGHT(client) / 2));
	else
		XWarpPointer(display, None, client->screen->root,
			0, 0, 1, 1, saveptr.x, saveptr.y);

	/* release all grabs */
	XUngrabServer(display);
	XUngrabKeyboard(display, CurrentTime);
ungrab:
	XUngrabPointer(display, CurrentTime);
}

/* random placement */
static void placement_random(client_t *client) {
	client->x = random() % (client->screen->width - FULLWIDTH(client));
	client->y = random() % (client->screen->height - FULLHEIGHT(client));
	xinerama_correctloc(client);
}

/* place the client where the mouse cursor is */
static void placement_pointer(client_t *client) {
	Window dumwin;
	int dumint, mask;
	
	XQueryPointer(display, client->screen->root, &dumwin,
		&dumwin, &client->x, &client->y, &dumint, &dumint, &mask);
}

/*
 * calculate the total area of of other managed windows that
 * client would cover if it were placed at rclient.
 */
static __inline int area_covered(client_t *client, rect_t *rect,
		rect_t *rclient) {
	rect_t rtest;
	client_t *test;
	int area = 0;

	LIST_FOREACH(test, &client_list, c_list) {
		if (test == client)
			continue;
		if (test->screen != client->screen)
			continue;
		if (test->state != NormalState)
			continue;
		if (!test->workspace)
			continue;
		if (test->workspace->desktop != client->screen->desktop)
			continue;
		if (test->x + FULLWIDTH(test) < rect->x1)
			continue;
		if (test->y + FULLHEIGHT(test) < rect->y1)
			continue;
		if (test->x >= rect->x2 || test->y >= rect->y2)
			continue;

		/* this test client is valid, add it's intersection */
		rtest.x1 = test->x;
		rtest.y1 = test->y;
		rtest.x2 = test->x + FULLWIDTH(test);
		rtest.y2 = test->y + FULLHEIGHT(test);
		area += rect_intersection(&rtest, rclient);
	}

	return area;
}

/*
 * smart placement; the basic concept is that the
 * best place to put the window is the place
 * that obscures the least amount of other windows.
 */
static void placement_smart(client_t *client) {
	rect_t rect, rclient;
	point_t mid, low;
	int width, height;
	int sum, lowsum;
	int xwrap, ywrap;
	int x, y;
	int mon;

	/* set up some locals */
	width = FULLWIDTH(client);
	height = FULLHEIGHT(client);
	low.x = low.y = 0;
	lowsum = INT_MAX;

	/*
	 * try each xinerama screen in turn; if we are compiled
	 * without xinerama support or if the display doesn't
	 * have xinerama this just gives us the dimensions of
	 * the screen.
	 */
	mon = 0;
	while (xinerama_scrdims(client->screen, &mon, &rect)) {
		/*
		 * only can do this for things that are smaller
		 * than the size of the screen.
		 */
		if (width >= RECTWIDTH(&rect) || height >= RECTHEIGHT(&rect))
			continue;
		mid.x = rect.x1 + (RECTWIDTH(&rect) - width) / 2;
		mid.y = rect.y1 + (RECTHEIGHT(&rect) - height) / 2;

		/*
		 * calculate the total area of windows being covered
		 * at the potential positions in this screen; if it's
		 * lower than the currently lowest value, it becomes
		 * the new lowest sum.
		 */
		for (y = mid.y, ywrap = 0; !ywrap || y < mid.y; y += PLACETEST_YINC) {
			for (x = mid.x, xwrap = 0; !xwrap || x < mid.x; x += PLACETEST_XINC) {
				rclient.x1 = x;
				rclient.y1 = y;
				rclient.x2 = x + width;
				rclient.y2 = y + width;
				sum = area_covered(client, &rect, &rclient);
				if (sum < lowsum) {
					lowsum = sum;
					low.x = x;
					low.y = y;
				}

				if (!xwrap && x >= rect.x2 - width - PLACETEST_XINC) {
					x = rect.x1 - PLACETEST_XINC;
					xwrap = 1;
				}
			}

			if (!ywrap && y >= rect.y2 - height - PLACETEST_YINC) {
				y = rect.y1 - PLACETEST_YINC;
				ywrap = 1;
			}
		}
	}

	/*
	 * set the client position to the cordinates at which
	 * we found it would cover the least amount of other
	 * windows.
	 */
	client->x = low.x;
	client->y = low.y;
}

/* exported client placement function */
void placement_place(client_t *client) {
	int interact;

	if (!options.place_transients && client->flags.transient) {
		if (options.xinerama_correctloc)
			xinerama_correctloc(client);
		interact = 0;
	} else if (!options.place_nonzeros && (client->x || client->y)) {
		if (options.xinerama_correctloc)
			xinerama_correctloc(client);
		interact = 1;
	} else {
		interact = 1;

		switch (options.placement) {
		case PLACEMENT_NONE:					break;
		case PLACEMENT_RANDOM:	placement_random(client);	break;
		case PLACEMENT_POINTER:	placement_pointer(client);	break;
		case PLACEMENT_SMART:	placement_smart(client);	break;
		}
	}

	/* allow plugins to draw an animation */
	plugin_anim_birth(client);

	if (interact && options.place_interactive)
		placement_interact(client);
}
