/*-
 * 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"

/* font for titles */
#ifdef I18N
 XFontSet   titlefont;
#else
 XFontStruct	*titlefont;
#endif

/* list of decoration units */
static SLIST_HEAD(, decor) decor_list = SLIST_HEAD_INITIALIZER(&decor_list);

/* get our titlebar font */
void decor_init() {
	char *deffont = "fixed";
#ifdef I18N
	titlefont = XLoadQueryFontSet(display, options.title_font ? options.title_font : deffont);
#else
	titlefont = XLoadQueryFont(display, options.title_font ? options.title_font : deffont);
#endif
	if (!titlefont) {
		if (options.title_font) {
#ifdef I18N
			titlefont = XLoadQueryFontSet(display, deffont);
#else
			titlefont = XLoadQueryFont(display, deffont);
#endif
			if (!titlefont)
				errx(1, "unable to load fallback titlefont 'fixed'");
		} else
			errx(1, "unable to load titlefont 'fixed'");
	}
}

/* free memory used for decor stuff */
void decor_shutdown() {
	decor_t *decor;

	/* free the title font */
#ifdef I18N
	XFreeFontSet(display, titlefont);
#else
	XFreeFont(display, titlefont);
#endif

	/* free the list of decor_t */
	while (!SLIST_EMPTY(&decor_list)) {
		decor = SLIST_FIRST(&decor_list);
		SLIST_REMOVE_HEAD(&decor_list, d_list);
		free(decor->ident);
		free(decor);
	}
}

/* function to add a decoration */
void decor_add(decor_t *decor) {
	SLIST_INSERT_HEAD(&decor_list, decor, d_list);
}

/* find a decoration unit by name; used during rcfile parser */
decor_t *decor_ident(char *ident) {
	decor_t *decor;

	SLIST_FOREACH(decor, &decor_list, d_list)
		if (strcmp(decor->ident, ident) == 0)
			return decor;

	return NULL;
}


/* get pos of a decoration unit based on it's type/edge and modifiers */
static void decor_getdims(client_t *client, decor_t *decor, int *x, int *y,
		int *pwidth, int *pheight) {
	dgroup_t *dgroup;
	int width = 0, height = 0;
	int vert = 0;

	dgroup = client->dgroup;
	switch (decor->edge) {
	case DE_TOP:
		*y = dgroup->top_space - decor->pixmap->height;
		break;
	case DE_LEFT:
		*x = dgroup->left_space - decor->pixmap->width;
		vert = 1;
		break;
	case DE_RIGHT:
		*x = dgroup->left_space + client->width;
		vert = 1;
		break;
	case DE_BOTTOM:
		*y = dgroup->top_space + client->height;
		break;
	}

	switch (decor->type) {
	case DA_FULL:
		if (vert) {
			width = decor->pixmap->width;
			height = client->height;
			*y = dgroup->top_space;
		} else {
			width = dgroup->left_space + client->width + dgroup->right_space;
			height = decor->pixmap->height;
			*x = 0;
		}
		break;
	case DA_NEAR:
		if (vert)
			*y = dgroup->top_space;
		else
			*x = 0;
		break;
	case DA_FAR:
		if (vert)
			*y = dgroup->top_space + client->height - decor->pixmap->height;
		else
			*x = dgroup->left_space + client->width 
				+ dgroup->right_space - decor->pixmap->width;
		break;
	}

	if (!width || !height) {
		*pwidth = decor->pixmap->width;
		*pheight = decor->pixmap->height;
	} else {
		*pwidth = width;
		*pheight = height;
	}

	if (vert) {
		*y += decor->offset;
		*pheight += decor->lenmod;
		*y += client->height * decor->offsetmult;
		*pheight += client->height * decor->lenmult;

		*x += decor->soffset;
	} else {
		*x += decor->offset;
		*pwidth += decor->lenmod;
		*x += client->width * decor->offsetmult;
		*pwidth += client->width * decor->lenmult;

		*y += decor->soffset;
	}

	/* finally, make sure that the width/height are valid */
	if (*pwidth < 1) *pwidth = 1;
	if (*pheight < 1) *pheight = 1;
}

/* shape as a rect, not w/ mask combines (shapeunit) */
#define RECTSHAPE() do {									\
	rect[0].x = x; rect[0].y = y;								\
	rect[0].width = width;									\
	rect[0].height = height;								\
	XShapeCombineRectangles(display, client->frame, ShapeBounding, 0, 0, rect,		\
		1, ShapeUnion, Unsorted);							\
} while (0)

/* shape a decoration unit onto this client's frame */
static void decor_shapeunit(client_t *client, decor_t *decor, int idx) {
	XRectangle rect[1];
	int x, y, width, height;
	int num;

	decor_getdims(client, decor, &x, &y, &width, &height);
	XMoveResizeWindow(display, client->decorwin[idx], x, y, width ? width : 1, height ? height : 1);

	/* shape; if the width/height aren't the same as the decor we have a DA_FULL */
	if (width == decor->pixmap->width && height == decor->pixmap->height) {
		if (decor->flags.shaped)
			XShapeCombineMask(display, client->frame, ShapeBounding, x, y,
				decor->pixmap->shapemasks[client->screen->num], ShapeUnion);
		else
			RECTSHAPE();
	} else if (height != decor->pixmap->height) {
		if (decor->flags.shaped) {
			for (num = y; num <= y + height; num += decor->pixmap->height) {
				/* we also need to reshape the window itself for these */
				XShapeCombineMask(display, client->decorwin[idx], ShapeBounding, 0, num - y,
					decor->pixmap->shapemasks[client->screen->num], num == y ? ShapeSet : ShapeUnion);
				XShapeCombineMask(display, client->frame, ShapeBounding, x, num,
					decor->pixmap->shapemasks[client->screen->num], ShapeUnion);
			}
		} else {
			RECTSHAPE();
		}
	} else if (width != decor->pixmap->width) {
		if (decor->flags.shaped) {
			for (num = x; num <= x + width; num += decor->pixmap->width) {
				/* we also need to reshape the window itself for these */
				XShapeCombineMask(display, client->decorwin[idx], ShapeBounding, num - x, 0,
					decor->pixmap->shapemasks[client->screen->num], num == x ? ShapeSet : ShapeUnion);
				XShapeCombineMask(display, client->frame, ShapeBounding, num, y,
					decor->pixmap->shapemasks[client->screen->num], ShapeUnion);
			}
		} else {
			RECTSHAPE();
		}
	}
}

#undef RECTSHAPE

/* called on resizes and such to shape/size the client decor, etc */
void decor_shapesize(client_t *client) {
	XRectangle rect[4];
	dgroup_t *dgroup;
	int cnt = 0;
	int i;

	dgroup = client->dgroup;

	/* fill in old chops on right and bottom */
	if (client->width > client->last_width) {
		rect[cnt].x = dgroup->left_space + client->last_width;
		rect[cnt].y = 0;
		rect[cnt].width = client->width - client->last_width;
		rect[cnt].height = dgroup->top_space + client->height;
		cnt++;
	}
	if (client->height > client->last_height) {
		rect[cnt].x = 0;
		rect[cnt].y = dgroup->top_space + client->last_height;
		rect[cnt].width = dgroup->left_space + client->width;
		rect[cnt].height = client->height - client->last_height;
		cnt++;
	}
	if (cnt == 2) {
		rect[cnt].x = dgroup->left_space + client->last_width;
		rect[cnt].y = dgroup->top_space + client->last_height;
		rect[cnt].width = client->width - client->last_width;
		rect[cnt].height = client->height - client->last_height;
	}
	XShapeCombineRectangles(display, client->frame, ShapeBounding, 0, 0, rect,
		cnt, ShapeUnion, Unsorted);

	/* chop off the decoration area */
	rect[0].x = 0;
	rect[0].y = 0;
	rect[0].width = dgroup->left_space + client->width + dgroup->right_space;
	rect[0].height = dgroup->top_space;
	rect[1].x = 0;
	rect[1].y = dgroup->top_space;
	rect[1].width = dgroup->left_space;
	rect[1].height = client->height;
	rect[2].x = dgroup->left_space + client->width;
	rect[2].y = dgroup->top_space;
	rect[2].width = dgroup->right_space;
	rect[2].height = client->height;
	rect[3].x = 0;
	rect[3].y = dgroup->top_space + client->height;
	rect[3].width = dgroup->left_space + client->width + dgroup->right_space;
	rect[3].height = dgroup->bottom_space;
	XShapeCombineRectangles(display, client->frame, ShapeBounding, 0, 0, rect,
		4, ShapeSubtract, Unsorted);


	for (i = 0; i < client->dgroup->decor_count; i++)
		decor_shapeunit(client, client->dgroup->decor[i], i);
}

/* the initial shaping of a client's decoration, etc */
static void decor_firstshape(client_t *client) {
	XRectangle rect[4];
	dgroup_t *dgroup;
	int i;

	dgroup = client->dgroup;

	/* chop off the decoration area */
	rect[0].x = 0;
	rect[0].y = 0;
	rect[0].width = dgroup->left_space + client->width + dgroup->right_space;
	rect[0].height = dgroup->top_space;
	rect[1].x = 0;
	rect[1].y = dgroup->top_space;
	rect[1].width = dgroup->left_space;
	rect[1].height = client->height;
	rect[2].x = dgroup->left_space + client->width;
	rect[2].y = dgroup->top_space;
	rect[2].width = dgroup->right_space;
	rect[2].height = client->height;
	rect[3].x = 0;
	rect[3].y = dgroup->top_space + client->height;
	rect[3].width = dgroup->left_space + client->width + dgroup->right_space;
	rect[3].height = dgroup->bottom_space;
	XShapeCombineRectangles(display, client->frame, ShapeBounding, 0, 0, rect,
		4, ShapeSubtract, Unsorted);

	for (i = 0; i < client->dgroup->decor_count; i++)
		decor_shapeunit(client, client->dgroup->decor[i], i);
}

/* create a decoration window according to it's tyoe and edge */
static void decor_makewin(client_t *client, decor_t *decor, int idx) {
	XSetWindowAttributes attr;
	int mask = ButtonPressMask;
	int x, y;
	int width, height;

	decor_getdims(client, decor, &x, &y, &width, &height);

	attr.background_pixmap = decor->pixmap->pixmaps[client->screen->num];
	client->decorwin[idx] = XCreateWindow(display, client->frame, x, y,
		width, height, 0, CopyFromParent, CopyFromParent, CopyFromParent,
		CWBackPixmap, &attr);

	if (decor->flags.shaped)
		XShapeCombineMask(display, client->decorwin[idx], ShapeBounding, 0, 0,
			decor->pixmap->shapemasks[client->screen->num], ShapeSet);
	if (decor->flags.button)
		mask |= ButtonReleaseMask;
	if (client->dgroup->title == decor)
		mask |= ExposureMask;

	XSaveContext(display, client->decorwin[idx], client_context, (XPointer) client);
	XSaveContext(display, client->decorwin[idx], decor_context, (XPointer) decor);
	XSelectInput(display, client->decorwin[idx], mask);
	XMapWindow(display, client->decorwin[idx]);
}

/* called during client_add to add our decoration to a client */
void decor_decorate(client_t *client) {
	int i;

	/*
	 * alloc the amount of windows neccessary for this dgroup,
	 * if there are any.
	 */
	if (!client->dgroup->decor_count)
		return;

	client->decorwin = calloc(client->dgroup->decor_count, sizeof(Window));
	for (i = 0; i < client->dgroup->decor_count; i++)
		decor_makewin(client, client->dgroup->decor[i], i);

	decor_firstshape(client);
}

/* undecorate this client: kill the decor wins and free mem */
void decor_undecor(client_t *client) {
	int i;

	/*
	 * kill every decoration window we added, if we
	 * added any.
	 */
	if (!client->dgroup->decor_count)
		return;

	for (i = 0; i < client->dgroup->decor_count; i++) {
		XDeleteContext(display, client->decorwin[i], decor_context);
		XDeleteContext(display, client->decorwin[i], client_context);
		XDestroyWindow(display, client->decorwin[i]);
	}

	free(client->decorwin);
}

/* perform the operation specified by act */
static void decor_act(client_t *client, int act) {
	switch (act) {
	case ACT_NONE:
		break;
	case ACT_MOVE:
		action_move(client);
		break;
	case ACT_RESIZE:
		action_resize(client);
		break;
	case ACT_DELETE:
		if (!client->flags.nodelete)
			action_sendcmesg(client->window, WM_DELETE_WINDOW, CurrentTime);
		break;
	case ACT_ICONIFY:
		action_iconify(client);
		break;
	case ACT_ZOOM:
		action_zoom(client);
		break;
	}
}

/* handle a press on a button */
static void decor_buttonpress(client_t *client, decor_t *decor, int action) {
	XEvent event;
	int idx;

	/* get the decor index */
	for (idx = 0; idx < client->dgroup->decor_count; idx++)
		if (client->dgroup->decor[idx] == decor)
			break;

	/* change the pixmap */
	if (decor->pressedmap) {
		XSetWindowBackgroundPixmap(display, client->decorwin[idx],
			decor->pressedmap->pixmaps[client->screen->num]);
		XClearWindow(display, client->decorwin[idx]);
	}

	/* wait for button release */
	while (1) {
		XMaskEvent(display, ButtonReleaseMask, &event);

		if (event.type == ButtonRelease) {
			if (decor->pressedmap) {
				XSetWindowBackgroundPixmap(display, client->decorwin[idx],
					decor->focpixmap->pixmaps[client->screen->num]);
				XClearWindow(display, client->decorwin[idx]);
			}
			if (event.xbutton.x > 0 && event.xbutton.y > 0 && event.xbutton.x < decor->pixmap->width
					&& event.xbutton.y < decor->pixmap->height
					&& event.xbutton.root == client->screen->root)
				decor_act(client, action);
			return;
		}
	}
}

/* handle a press on a piece of decoration for a client */
void decor_handlepress(client_t *client, decor_t *decor, XButtonEvent *e) {
	int action;

	if (e->button == Button3)
		action = decor->rclick_action;
	else if (e->button == Button2)
		action = decor->mclick_action;
	else
		action = decor->lclick_action;

	if (decor->flags.button)
		decor_buttonpress(client, decor, action);
	else
		decor_act(client, action);
}

/* modify decoration pixmaps to indicate a client is focused */
void decor_focus(client_t *client) {
	int i;

	for (i = 0; i < client->dgroup->decor_count; i++) {
		if (client->dgroup->decor[i]->focpixmap != client->dgroup->decor[i]->pixmap) {
			XSetWindowBackgroundPixmap(display, client->decorwin[i],
				client->dgroup->decor[i]->focpixmap->pixmaps[client->screen->num]);
			XClearWindow(display, client->decorwin[i]);
			if (client->dgroup->title == client->dgroup->decor[i])
				decor_expose(client, client->dgroup->decor[i], NULL);
		}
	}
}

/* modify decoration pixmaps to indicate a client is unfocused (normal) */
void decor_unfocus(client_t *client) {
	int i;

	for (i = 0; i < client->dgroup->decor_count; i++) {
		if (client->dgroup->decor[i]->focpixmap != client->dgroup->decor[i]->pixmap) {
			XSetWindowBackgroundPixmap(display, client->decorwin[i],
				client->dgroup->decor[i]->pixmap->pixmaps[client->screen->num]);
			XClearWindow(display, client->decorwin[i]);
			if (client->dgroup->title == client->dgroup->decor[i])
				decor_expose(client, client->dgroup->decor[i], NULL);
		}
	}
}

/* handle exposes on decoration */
void decor_expose(client_t *client, decor_t *decor, XExposeEvent *e) {
	int idx;
#ifdef I18N
	XFontSetExtents *extent;

	extent = XExtentsOfFontSet(titlefont);
#endif   

	/* we are just drawing the whole thing, so we only use count == 0 */
	if (e && e->count > 0)
		return;

	/* get the decor index */
	for (idx = 0; idx < client->dgroup->decor_count; idx++)
		if (client->dgroup->decor[idx] == decor)
			break;

	/*
	 * since we're just drawing a line of text, optimizing which characters we
	 * actually need to draw is kinda more trouble than it's worth.
	 */
	if (client->store_name)
#ifdef I18N
		XmbDrawString(display, client->decorwin[idx], titlefont, client->screen->titlegc, client->dgroup->title_x,
			client->dgroup->title_y + (extent->max_logical_extent.height * 4 / 5), client->store_name, strlen(client->store_name));
#else
		XDrawString(display, client->decorwin[idx], client->screen->titlegc, client->dgroup->title_x,
			client->dgroup->title_y + titlefont->ascent, client->store_name, strlen(client->store_name));
#endif
}

/* if a window's title (store_name) changed, we hear about it here */
void decor_titlechange(client_t *client) {
	int i;

	for (i = 0; i < client->dgroup->decor_count; i++) {
		if (client->dgroup->decor[i] == client->dgroup->title) {
			XClearWindow(display, client->decorwin[i]);
			decor_expose(client, client->dgroup->decor[i], NULL);
			break;
		}
	}
}

