/*-
 * Copyright (c) 2001, 2002 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"

/* list of clients */
clientlist_t	client_list = LIST_HEAD_INITIALIZER(&client_list);

/* currently focused client */
client_t	*client_focused;

/*
 * shutdown client stuff; unload all client_t's and reparent all clients
 * back to the root window.
 */
void client_shutdown() {
	client_t *client;

	/* remove all clients */
	while (!LIST_EMPTY(&client_list)) {
		client = LIST_FIRST(&client_list);
		if (!client->flags.internal) {
			XReparentWindow(display, client->window, client->screen->root,
				client->x, client->y);
			/*
			 * XXX: should save their initial border
			 * instead of just setting it back to 1.
			 */
			XSetWindowBorderWidth(display, client->window, 1);
		}
		client_rm(client);
	}
}

/*
 * XWMHints structure gets handled here; startup state and
 * if the client wants to be able to recieve input focus.
 */
static __inline void client_init_wmhints(client_t *client) {
	Atom type;
	u_long items, bytes;
	long *state;
	int fmt;

	/*
	 * start it up in the state it thinks it's in, or use the state hint
	 * in the XWMHints structure.
	 */
	XGetWindowProperty(display, client->window, WM_STATE, 0, 1, 0, WM_STATE,
		&type, &fmt, &items, &bytes, (u_char **) &state);
	if (type == WM_STATE) {
		client->state = *state;
		XFree(state);
	} else if (client->wmhints && client->wmhints->flags & StateHint)
		client->state = client->wmhints->initial_state;
	else
		client->state = WithdrawnState;

	/* flag noinput windows as nofocus */
	if (client->wmhints && client->wmhints->flags & InputHint)
		if (!client->wmhints->input)
			client->flags.nofocus = 1;
}

/* add decoration for a client during client_add */
static __inline void client_decorate(client_t *client, dgroup_t *dgroup) {
	XSetWindowAttributes attr;

	/*
	 * figure out what decoration group to use, unless
	 * a hints handler (from a plugin) set it already
	 */
	if (!client->dgroup) {
		if (dgroup)
			client->dgroup = dgroup;
		else if (client->flags.transient)
			client->dgroup = options.dgroup_trans;
		else if (client->flags.internal)
			client->dgroup = options.dgroup_internal;
		else
			client->dgroup = options.dgroup_default;
	}

	attr.override_redirect = 1;
	attr.background_pixel = BlackPixel(display, client->screen->num);
	client->frame = XCreateWindow(display, client->screen->root, client->x, client->y,
		FULLWIDTH(client), FULLHEIGHT(client), 0, CopyFromParent, CopyFromParent,
		CopyFromParent, CWOverrideRedirect | CWBackPixel, &attr);
	XSetWindowBorderWidth(display, client->window, 0);

	/* actual decor window creation (decor_decorate) posponed 'till later in add */
	decor_decorate(client);
	client_shape(client);
}

/* grab input on our decoration for the client */
static __inline void client_dograbs(client_t *client) {
	XSetWindowAttributes attr;
	long mask;

	mask = SubstructureNotifyMask | SubstructureRedirectMask;
	switch (options.focus) {
	case FOCUS_CLICK:
		if (!client->flags.nofocus)
			XGrabButton(display, AnyButton, AnyModifier, client->frame,
				1, ButtonPressMask, GrabModeSync, GrabModeAsync, None, None);
		else
			buttongrabhack(client->frame, AnyButton, options.mouse_modifier);
		break;
	case FOCUS_POINTER:
		mask |= LeaveWindowMask;
	case FOCUS_SLOPPY:
		mask |= EnterWindowMask;
		buttongrabhack(client->frame, AnyButton, options.mouse_modifier);
		break;
	}
	XSelectInput(display, client->frame, mask);
	XShapeSelectInput(display, client->window, ShapeNotifyMask);

	/* prevent us from recieving some unneccessary events */
	attr.do_not_propagate_mask = ButtonPressMask | ButtonReleaseMask;
	mask = CWDontPropagate;
	XChangeWindowAttributes(display, client->window, mask, &attr);
}

/*
 * add a client.
 * flags is the flags for the client, or NULL if we use defaults.
 * not all of the flags passed in will be honored: we decide here
 * if it's a transient, etc.  if plugin is not NULL and it's an internal
 * it means this should be associated with that plugin as an internal
 * window.  dgroup is the dgroup to use, otherwise we will try to figure
 * out for ourselves and use dgroups based on if it is a transient/internal,
 * and so forth.
 */
client_t *client_add(screen_t *screen, Window w,
		clientflags_t *flags, dgroup_t *dgroup) {
	XWindowAttributes win_attr;
	Window dumwin;
	client_t *client = NULL;
	long supplied;
#ifdef I18N
	XTextProperty text_prop;
	char **cl;
	int n;
#endif

	/*
	 * we need to do a grab so that it's not possible for the window
	 * we are about to manage to disappear halfway through the process
	 * of us preparing to manage it.
	 */
	XGrabServer(display);

	/* if we can't get window attributes, give up */
	if (!XGetWindowAttributes(display, w, &win_attr))
		goto done;

	/* get mem and set flags */
	client = calloc(1, sizeof(client_t));
	if (!client)
		goto done;
	if (flags)
		memcpy(&client->flags, flags, sizeof(clientflags_t));

	/* fill in client_t stuff */
	client->window = w;
	client->screen = screen;
	client->x = win_attr.x;
	client->y = win_attr.y;
	client->last_width = client->width = win_attr.width;
	client->last_height = client->height = win_attr.height;

	/* default stacking layer to normal */
	client->stacklayer = STACKLAYER_NORMAL;

	/* get PropertyChange information */
	if (!client->flags.internal)
		XSelectInput(display, client->window, PropertyChangeMask);

	/* get hints and do stuff that they require */
	client->wmhints = XGetWMHints(display, client->window);
	XGetWMNormalHints(display, client->window, &client->normalhints, &supplied);
	XGetClassHint(display, client->window, &client->classhint);
#ifdef I18N
	if (XGetWMName(display, client->window, &text_prop) != 0) {        
		if (text_prop.encoding == XA_STRING) {
			client->store_name = (char *)text_prop.value;
		} else {
			XmbTextPropertyToTextList(display, &text_prop, &cl, &n);
			if (cl) {
				client->store_name = strdup(cl[0]);
				XFreeStringList(cl);
			} else {
				client->store_name = strdup("NoName");
			}
		}
	} else {
		client->store_name = strdup("NoName");
	}
#else
	XFetchName(display, client->window, &client->store_name);
#endif
	if (XGetTransientForHint(display, client->window, &dumwin))
		client->flags.transient = 1;

	/* do window manager hints, and decorate */
	client_init_wmhints(client);
	plugin_init_hints(client);
	client_decorate(client, dgroup);

	/*
	 * put this client_t into client_context for this
	 * client's window and frame.
	 */
	XSaveContext(display, client->window, client_context, (XPointer) client);
	XSaveContext(display, client->frame, client_context, (XPointer) client);

	/* select input and then reparent the client to it's frame window */
	client_dograbs(client);
	XReparentWindow(display, client->window, client->frame,
		client->dgroup->left_space, client->dgroup->top_space);
	client_gravitate(client);

	/* lower the client in the frame window so decoration can cover it */
	XLowerWindow(display, client->window);

	/* link it up to the all-clients list */
	LIST_INSERT_HEAD(&client_list, client, c_list);

	/* save non internals */
	if (!client->flags.internal)
		XAddToSaveSet(display, client->window);

done:
	/* ungrab the server */
	XUngrabServer(display);

	return client;
}

/* remove a client */
void client_rm(client_t *client) {
	/*
	 * eliminate decoration windows and set dgroup to dgroup_empty
	 * before removing from the workspace/desktop because if this
	 * window is focused, removing it from the desktop will attempt
	 * to unfocus all of it's decoration windows (change their pixmaps
	 * to the nonfocus ones) which is a waste of time.
	 */
	decor_undecor(client);
	client->dgroup = &dgroup_empty;

	/* free stuff allocated by Xlib for this client */
	if (client->wmhints) 			XFree(client->wmhints);
	if (client->classhint.res_name) 	XFree(client->classhint.res_name);
	if (client->classhint.res_class)	XFree(client->classhint.res_class);
	if (client->store_name)			XFree(client->store_name);

	/* kill the frame window and remove entries in client_context */
	XDeleteContext(display, client->frame, client_context);
	XDeleteContext(display, client->window, client_context);
	XDestroyWindow(display, client->frame);

	/* remove from workspace and desktop */
	if (client->workspace) {
		desktop_rm_client(client);
		workspace_rm_client(client);
	}

	/* free managed client memory */
	LIST_REMOVE(client, c_list);
	free(client);
}

/* called by map_request and by manage_existing_windows (screen.c) */
void client_map(client_t *client, int allowplace) {
	/*
	 * no matter what state, we map it's
	 * top window
	 */
	XMapWindow(display, client->window);

	/*
	 * to prevent losing clients if you restart after
	 * changing desktop dims
	 */
	if (client->x + FULLWIDTH(client) < 0 || client->y + FULLHEIGHT(client) < 0
			|| client->x >= (client->screen->desktop->width
			- client->screen->desktop->viewx) * client->screen->width
			|| client->y >= (client->screen->desktop->height
			- client->screen->desktop->viewy) * client->screen->width) {
		client->y = client->x = 0;
		client_sizeframe(client);
	}

	/* put it into the state it wants to be in */
	switch (client->state) {
	case IconicState:
		/* client starts as an icon */
		client_setstate(client, IconicState);
		plugin_iconify_notify(client);
		break;
	case WithdrawnState:
	case NormalState:
	default:
		if (allowplace) {
			placement_place(client);
			client_sizeframe(client);
		} else {
			/*
			 * if placement is not allowed, it means that we are
			 * probably running on a window that existed before
			 * the window manager was started; this means the
			 * unmap_from_reparent flag will still be set.
			 * If the flag isn't set, we still do a birth
			 * animation.
			 */
			if (!client->flags.unmap_from_reparent)
				plugin_anim_birth(client);
		}

		/*
		 * add to the workspace appropriate for the client's
		 * position on the screen.
		 */
		workspace_add_bypos(client->screen->desktop, client);

		/* the window gets focus in map_notify */
		XMapWindow(display, client->frame);
		stacking_raise(client);
		client_setstate(client, NormalState);

		/*
		 * send configure to the client to make sure it
		 * knows where it is, and notify plugins.
		 */
		action_send_config(client);
		plugin_window_birth(client);
		break;
	}
}

/* shape a client */
void client_shape(client_t *client) {
	XRectangle *rect = NULL;
	XRectangle r1;
	int n, order;

	rect = XShapeGetRectangles(display, client->window, ShapeBounding, &n, &order);
	if (n > 1) {
		r1.x = 0;
		r1.y = 0;
		r1.width = client->width;
		r1.height = client->height;
		XShapeCombineRectangles(display, client->frame, ShapeBounding,
			client->dgroup->left_space, client->dgroup->top_space,
			&r1, 1, ShapeSubtract, Unsorted);
		XShapeCombineShape(display, client->frame, ShapeBounding,
			client->dgroup->left_space, client->dgroup->top_space,
			client->window, ShapeBounding, ShapeUnion);
		client->flags.shaped = 1;
	} else if (client->flags.shaped) {
		r1.x = 0;
		r1.y = 0;
		r1.width = client->width;
		r1.height = client->height;
		XShapeCombineRectangles(display, client->frame, ShapeBounding,
			client->dgroup->left_space, client->dgroup->top_space,
			&r1, 1, ShapeUnion, Unsorted);
		client->flags.shaped = 0;
	}

	XFree(rect);
}

/*
 * adjust client geometry according to the gravity settings
 * specified in the client normalhints.
 */
void client_gravitate(client_t *client) {
	static point_t gravoffs[] = {
		{  0,  0},	/* ForgetGravity */
		{ -1, -1},	/* NorthWestGravity */
		{  0, -1},	/* NorthGravity */
		{  1, -1},	/* NorthEastGravity */
		{ -1,  0},	/* WestGravity */
		{  0,  0},	/* CenterGravity */
		{  1,  0},	/* EastGravity */
		{ -1,  1},	/* SouthWestGravity */
		{  0,  1},	/* SouthGravity */
		{  1,  1},	/* SouthEastGravity */
		{  0,  0}	/* StaticGravity */
	};
	point_t *offs;

	/* make sure we can adjust this for gravity */
	if (!(client->normalhints.flags & PWinGravity))
		return;
	if (client->normalhints.win_gravity < 0
			|| client->normalhints.win_gravity > StaticGravity)
		return;

	/* adjust the geometry to correspond to gravity settings */
	offs = &gravoffs[client->normalhints.win_gravity];
	if (offs->x > 0)
		client->x -= DWIDTH(client);
	else if (offs->x == 0)
		client->x -= client->dgroup->left_space;
	if (offs->y > 0)
		client->y -= DHEIGHT(client);
	else if (offs->y == 0)
		client->y -= client->dgroup->top_space;
}

/* resize the decoration and such for a client: reshape, etc. */
void client_sizeframe(client_t *client) {
	dgroup_t *dgroup;

	if (client->width != client->last_width
			|| client->height != client->last_height) {
		dgroup = client->dgroup;
	
		XMoveResizeWindow(display, client->frame, client->x, client->y,
			FULLWIDTH(client), FULLHEIGHT(client));
		XResizeWindow(display, client->window, client->width, client->height);

		decor_shapesize(client);
		if (client->flags.shaped)
			client_shape(client);

		client->last_width = client->width;
		client->last_height = client->height;
	} else
		XMoveWindow(display, client->frame, client->x, client->y);
}

/* set the state for a window (our var and the WM_STATE) */
void client_setstate(client_t *client, int state) {
	u_long data[2];

	data[0] = state;
	data[1] = None;

	XChangeProperty(display, client->window, WM_STATE, WM_STATE, 32,
		PropModeReplace, (u_char *) data, 2);
	client->state = state;

	XSync(display, 0);
}
