/*
 * lwm, a window manager for X11
 * Copyright (C) 1997-2016 Elliott Hughes, James Carter
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <unistd.h>

#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>

#include "lwm.h"
#include "ewmh.h"

Client *current;
Client *last_focus = NULL;
static Client *clients;

static int popup_width;	/* The width of the size-feedback window. */

Edge interacting_edge;

static void sendClientMessage(Window, Atom, long, long);

Client *
client_head(void) {
	return clients;
}

void
setactive(Client *c, int on, long timestamp) {
	int inhibit;

	if (c == 0 || hidden(c))
		return;

	inhibit = !c->framed;

	if (!inhibit) {
		XMoveResizeWindow(dpy, c->parent,
			c->size.x, c->size.y - titleHeight(),
			c->size.width, c->size.height + titleHeight());
		XMoveWindow(dpy, c->window, border, border + titleHeight());
		sendConfigureNotify(c);
	}

	if (on && c->accepts_focus) {
		XSetInputFocus(dpy, c->window, RevertToPointerRoot, CurrentTime);
		if (c->proto & Ptakefocus)
			sendClientMessage(c->window, wm_protocols,
				wm_take_focus, timestamp);
		if (focus_mode == focus_click) {
			XUngrabButton(dpy, AnyButton, AnyModifier, c->window);
		}
		cmapfocus(c);
	}

	/* FIXME: is this sensible? */
	if (on && !c->accepts_focus) {
		XSetInputFocus(dpy, None, RevertToPointerRoot, CurrentTime);
	}

	if (!on && focus_mode == focus_click)
		XGrabButton(dpy, AnyButton, AnyModifier, c->window, False,
			ButtonPressMask | ButtonReleaseMask, GrabModeAsync,
			GrabModeSync, None, None);

	if (!inhibit)
		Client_DrawBorder(c, on);
}


void
Client_DrawBorder(Client *c, int active) {
	int quarter = (border + titleHeight()) / 4;

	if (c->parent == c->screen->root || c->parent == 0 ||
		c->framed == False || c->wstate.fullscreen == True)
		return;

	XSetWindowBackground(dpy, c->parent,
		active ? c->screen->black : c->screen->gray);
	XClearWindow(dpy, c->parent);

	/* Draw the ``box''. */
	if (active || focus_mode == focus_click) {
		XDrawRectangle(dpy, c->parent, c->screen->gc,
			quarter + 2, quarter, 2 * quarter, 2 * quarter);
	}

	/* Draw window title. */
	if (c->name != 0) {
#ifdef X_HAVE_UTF8_STRING
		if (c->name_utf8 == True)
			Xutf8DrawString(dpy, c->parent, font_set,
				c->screen->gc, border + 2 + (3 * quarter),
				2 + ascent(font_set_ext),
				c->name, c->namelen);
		else
#endif
			XmbDrawString(dpy, c->parent, font_set,
				c->screen->gc, border + 2 + (3 * quarter),
				2 + ascent(font_set_ext),
				c->name, c->namelen);
	}
}


Client *
Client_Get(Window w) {
	Client * c;
	
	if (w == 0 || (getScreenFromRoot(w) != 0))
		return 0;
	
	/* Search for the client corresponding to this window. */
	for (c = clients; c; c = c->next)
		if (c->window == w || c->parent == w)
			return c;
	
	/* Not found. */
	return 0;
}


Client *
Client_Add(Window w, Window root) {
	Client * c;

	if (w == 0 || w == root)
		return 0;

	/* Search for the client corresponding to this window. */
	for (c = clients; c != 0; c = c->next)
		if (c->window == w || c->parent == w)
			return c;

	c = calloc(1, sizeof *c);
	c->window = w;
	c->parent = root;
	c->framed = False;
	c->hidden = False;
	c->state = WithdrawnState;
	c->internal_state = INormal;
	c->cmap = None;
	c->name =  0;
	c->menu_name =  0;
	c->cursor = ENone;
	c->wtype = WTypeNone;
	c->wstate.skip_taskbar = False;
	c->wstate.skip_pager = False;
	c->wstate.fullscreen = False;
	c->wstate.above = False;
	c->wstate.below = False;
	c->strut.left = 0;
	c->strut.right = 0;
	c->strut.top = 0;
	c->strut.bottom = 0;
	c->ncmapwins = 0;
	c->cmapwins = 0;
	c->wmcmaps = 0;
	c->accepts_focus = 1;
	c->next = clients;

	/* Add to head of list of clients. */
	clients = c;
	return clients;
}


void
Client_Remove(Client *c) {
	Client * cc;
	ScreenInfo *screen = c->screen;
	
	if (c == 0)
		return;
	
	/* Remove the client from our client list. */
	if (c == clients)
		clients = c->next;
	for (cc = clients; cc && cc->next; cc = cc->next) {
		if (cc->next == c)
			cc->next = cc->next->next;
	}
	
	/* Remove it from the hidden list if it's hidden. */
	if (hidden(c)) {
		unhidec(c, 0);
		/* Al Smith points out that you also want to get rid of the menu
		 * so you can be sure that if you let go on an item, you always
		 * get the corresponding window. */
		if (mode == wm_menu_up) {
			XUnmapWindow(dpy, current_screen->popup);
			mode = wm_idle;
		}
	}
	
	/* A deleted window can no longer be the current window. */
	if (c == current || (current == NULL && c == last_focus)) {
		Client *focus = NULL;

		/* As pointed out by J. Han, if a window disappears while it's
		 * being reshaped you need to get rid of the size indicator. */
		if (c == current && mode == wm_reshaping) {
			XUnmapWindow(dpy, current_screen->popup);
			mode = wm_idle;
		}
		if (focus_mode == focus_click) {
			/* Try and find the window that this was a transient
			 * for, else focus on the top client. */
			if (c->trans != None) {
				focus = Client_Get(c->trans);
			}
			if (!focus) {
				Window dw1;
				Window dw2;
				Window *wins;
				unsigned int nwins;

				XQueryTree(dpy, c->screen->root, &dw1,
					&dw2, &wins, &nwins);
				while (nwins) {
					focus = Client_Get(wins[nwins -1]);
					if (focus) break;
					nwins--;
				}
				if (wins) XFree(wins);
			}
		}
		Client_Focus(focus, CurrentTime);
	}
	
	if (getScreenFromRoot(c->parent) == 0)
		XDestroyWindow(dpy, c->parent);
	
	if (c->ncmapwins != 0) {
		XFree(c->cmapwins);
		free(c->wmcmaps);
	}
	
	if (c->name != 0)
		free(c->name);
	if (c->menu_name != 0)
		free(c->name);
	
	free(c);

	ewmh_set_client_list(screen);
	ewmh_set_strut(screen);
}


void
Client_MakeSane(Client *c, Edge edge, int *x, int *y, int *dx, int *dy) {
	Bool	horizontal_ok = True;
	Bool	vertical_ok = True;

	if (edge != ENone) {
		/*
		 *	Make sure we're not making the window too small.
		 */
		if (*dx < c->size.min_width)
			horizontal_ok = False;
		if (*dy < c->size.min_height)
			vertical_ok = False;

		/*
		 * Make sure we're not making the window too large.
		 */
		if (c->size.flags & PMaxSize) {
			if (*dx > c->size.max_width)
				horizontal_ok = False;
			if (*dy > c->size.max_height)
				vertical_ok = False;
		}

		/*
		 * Make sure the window's width & height are multiples of
		 * the width & height increments (not including the base size).
		 */

		if (c->size.width_inc > 1) {
			int apparent_dx = *dx - 2 * border - c->size.base_width;
			int x_fix = apparent_dx % c->size.width_inc;

			switch (edge) {
			case ELeft:
			case ETopLeft:
			case EBottomLeft:
				*x += x_fix;
				/*FALLTHROUGH*/
			case ERight:
			case ETopRight:
			case EBottomRight:
				*dx -= x_fix;
				break;
			default: break;
			}
		}

		if (c->size.height_inc > 1) {
			int apparent_dy = *dy - 2 * border - c->size.base_height;
			int y_fix = apparent_dy % c->size.height_inc;

			switch (edge) {
			case ETop:
			case ETopLeft:
			case ETopRight:
				*y += y_fix;
				/*FALLTHROUGH*/
			case EBottom:
			case EBottomLeft:
			case EBottomRight:
				*dy -= y_fix;
				break;
			default: break;
			}
		}

		/*
		 * Check that we may change the client horizontally and vertically.
		 */

		if (c->size.width_inc == 0)
			horizontal_ok = False;
		if (c->size.height_inc == 0)
			vertical_ok = False;
	}

	/* Ensure that at least one border is not entirely within the
	 * reserved areas. Keeping clients completely within the
	 * the workarea is too restrictive, but this measure means they
	 * should always be accessible.
	 * Of course all of this is only applicable if the client doesn't
	 * set a strut itself.					jfc
	 */
	if (c->strut.left == 0 && c->strut.right == 0 &&
		c->strut.top == 0 && c->strut.bottom == 0) {
		if ((int)(*y + border) >=
			(int)(c->screen->display_height -
			c->screen->strut.bottom)) {
			*y = c->screen->display_height -
				c->screen->strut.bottom -border;
		}
		if ((int)(*y + c->size.height - border) <=
			(int)c->screen->strut.top) {
			*y = c->screen->strut.top + border - c->size.height;
		}
		if ((int)(*x + border) >=
			(int)(c->screen->display_width -
			c->screen->strut.right)) {
			*x = c->screen->display_width -
				c->screen->strut.right -border;
		}
		if ((int)(*x + c->size.width - border) <=
			(int)c->screen->strut.left) {
			*x = c->screen->strut.left + border - c->size.width;
		}
	}

	/*
	 * Introduce a resistance to the workarea edge, so that windows may
	 * be "thrown" to the edge of the workarea without precise mousing,
	 * as requested by MAD.
	 */
	if (*x < (int)c->screen->strut.left &&
		*x > ((int)c->screen->strut.left - EDGE_RESIST)) {
		*x =  (int)c->screen->strut.left;
	}
	if ((*x + c->size.width) >
		(int)(c->screen->display_width - c->screen->strut.right) &&
		(*x + c->size.width) <
		(int)(c->screen->display_width - c->screen->strut.right +			EDGE_RESIST)) {
		*x = (int)(c->screen->display_width - c->screen->strut.right -
			c->size.width);
	}
	if ((*y - titleHeight()) < (int)c->screen->strut.top &&
		(*y - titleHeight()) >
		((int)c->screen->strut.top - EDGE_RESIST)) {
		*y =  (int)c->screen->strut.top + titleHeight();
	}
	if ((*y + c->size.height) >
		(int)(c->screen->display_height - c->screen->strut.bottom) &&
		(*y + c->size.height) <
		(int)(c->screen->display_height - c->screen->strut.bottom +			EDGE_RESIST)) {
		*y = (int)(c->screen->display_height - c->screen->strut.bottom -
			c->size.height);
	}

	/*
	 * Update that part of the client information that we're happy with.
	 */
	if (interacting_edge != ENone) {
		if (horizontal_ok) {
			c->size.x = *x;
			c->size.width  = *dx;
		}
		if (vertical_ok) {
			c->size.y = *y;
			c->size.height = *dy;
		}
	} else {
		if (horizontal_ok)
			c->size.x = *x;
		if (vertical_ok)
			c->size.y = *y;
	}
}

void
Client_SizeFeedback(void) {
	int x, y;
	char buf[4*2 + 3 + 1];

	/* Make the popup 10% wider than the widest string it needs to show. */
	snprintf(buf, sizeof(buf), "%i x %i", current_screen->display_width,
		current_screen->display_height);
	popup_width = popupWidth(buf, strlen(buf));
	popup_width += popup_width/10;

	/* Put the popup in the right place to report on the window's size. */
	getMousePosition(&x, &y);
	XMoveResizeWindow(dpy, current_screen->popup, x + 8, y + 8,
		popup_width, popupHeight() + 1);
	XMapRaised(dpy, current_screen->popup);
	
	/*
	* Ensure that the popup contents get redrawn. Eventually, the function
	* size_expose will get called to do the actual redraw.
	*/
	XClearArea(dpy, current_screen->popup, 0, 0, 0, 0, True);
}

void
size_expose(void) {
	int width, height;
	char buf[4*2 + 3 + 1];
	
	width = current->size.width - 2*border;
	height = current->size.height - 2*border;

	/* This dance ensures that we report 80x24 for an xterm even when
	 * it has a scrollbar. */
	if (current->size.flags & (PMinSize|PBaseSize) && current->size.flags & PResizeInc) {
		if (current->size.flags & PBaseSize) {
			width -= current->size.base_width;
			height -= current->size.base_height;
		} else {
			width -= current->size.min_width;
			height -= current->size.min_height;
		}
	}

	if (current->size.width_inc != 0)
		width /= current->size.width_inc;
	if (current->size.height_inc != 0)
		height /= current->size.height_inc;

	snprintf(buf, sizeof(buf), "%i x %i", width, height);
	XmbDrawString(dpy, current_screen->popup, popup_font_set,
		current_screen->size_gc,
		(popup_width - popupWidth(buf, strlen(buf))) / 2,
		ascent(popup_font_set_ext) + 1, buf, strlen(buf));
}

static void
Client_OpaquePrimitive(Client *c, Edge edge) {
	Cursor cursor;
	int ox, oy;

	if (c == 0 /*|| c != current*/)
		return;

	/* Find out where we've got hold of the window. */
	getMousePosition(&ox, &oy);
	ox = c->size.x - ox;
	oy = c->size.y - oy;
	
	cursor = getEdgeCursor(edge);
	XChangeActivePointerGrab(dpy, ButtonMask | PointerMotionHintMask |
		ButtonMotionMask | OwnerGrabButtonMask, cursor, CurrentTime);
	
	/*
	 * Store some state so that we can get back into the main event
	 * dispatching thing.
	 */
	interacting_edge = edge;
	start_x = ox;
	start_y = oy;
	mode = wm_reshaping;
	ewmh_set_client_list(c->screen);
}

void
Client_Lower(Client *c)
{
	if (c == 0) return;

	XLowerWindow(dpy, c->window);
	if (c->framed) XLowerWindow(dpy, c->parent);
	ewmh_set_client_list(c->screen);
}

void
Client_Raise(Client *c)
{
	Client * trans;

	if (c == 0) return;

	if (c->framed) XRaiseWindow(dpy, c->parent);
	XRaiseWindow(dpy, c->window);

	for (trans = clients; trans != NULL; trans = trans->next) {
		if (trans->trans != c->window &&
			!(c->framed == True && trans->trans == c->parent))
			continue;
		if (trans->framed) XRaiseWindow(dpy, trans->parent);
		XRaiseWindow(dpy, trans->window);
	}
	
	ewmh_set_client_list(c->screen);
}

void
Client_Close(Client *c) {
	if (c == 0)
		return;

	/*
	 *	Terminate the client nicely if possible. Be brutal otherwise.
	 */
	if (c->proto & Pdelete) {
		sendClientMessage(c->window, wm_protocols, wm_delete, CurrentTime);
	} else {
		XKillClient(dpy, c->window);
	}
}

void
Client_SetState(Client *c, int state) {
	long	data[2];

	data[0] = (long) state;
	data[1] = (long) None;

	c->state = state;
	XChangeProperty(dpy, c->window, wm_state, wm_state, 32,
		PropModeReplace, (unsigned char *) data, 2);
	ewmh_set_state(c);
}

static void
sendClientMessage(Window w, Atom a, long data0, long data1) {
	XEvent	ev;
	long	mask;

	memset(&ev, 0, sizeof(ev));
	ev.xclient.type = ClientMessage;
	ev.xclient.window = w;
	ev.xclient.message_type = a;
	ev.xclient.format = 32;
	ev.xclient.data.l[0] = data0;
	ev.xclient.data.l[1] = data1;
	mask = (getScreenFromRoot(w) != 0) ? SubstructureRedirectMask : 0L;

	XSendEvent(dpy, w, False, mask, &ev);
}

extern void
Client_ResetAllCursors(void) {
	Client *c;
	XSetWindowAttributes attr;

	for (c = clients; c; c = c->next) {
		if (c->framed != True) continue;
		attr.cursor = c->screen->root_cursor;
		XChangeWindowAttributes(dpy, c->parent,
			CWCursor, &attr);
		c->cursor = ENone;
	}
}

extern void
Client_FreeAll(void) {
	Client *c;
	XWindowChanges wc;

	for (c = clients; c; c = c->next) {
		int not_mapped = !normal(c);

		/* elliott thinks leaving window unmapped causes the x server
		 * to lose them when the window manager quits. it doesn't
		 * happen to me with XFree86's Xnest, but unmapping the
		 * windows stops gtk window generating an extra window when
		 * the window manager quits.
		 * who is right? only time will tell....
		 */
		XUnmapWindow(dpy, c->parent);
		XUnmapWindow(dpy, c->window);
		/* Remap the window if it's hidden.  
		if (not_mapped) {
			XMapWindow(dpy, c->parent);
			XMapWindow(dpy, c->window);
		} */

		/* Reparent it, and then push it to the bottom if it was hidden. */
		XReparentWindow(dpy, c->window, c->screen->root, c->size.x, c->size.y);
		if (not_mapped)
			XLowerWindow(dpy, c->window);

		/* Give it back its initial border width. */
		wc.border_width = c->border;
		XConfigureWindow(dpy, c->window, CWBorderWidth, &wc);
	}
}

extern void
Client_ColourMap(XEvent *e) {
	int i;
	Client * c;

	for (c = clients; c; c = c->next) {
		for (i = 0; i < c->ncmapwins; i++) {
			if (c->cmapwins[i] == e->xcolormap.window) {
				c->wmcmaps[i] = e->xcolormap.colormap;
				if (c == current)
					cmapfocus(c);
				return;
			}
		}
	}
}

extern void
Client_ReshapeEdge(Client *c, Edge e) {
	Client_OpaquePrimitive(c, e);
}

extern void
Client_Move(Client *c) {
	Client_OpaquePrimitive(c, ENone);
}

extern int
hidden(Client *c) {
	return c->state == IconicState;
}

extern int
withdrawn(Client *c) {
	return c->state == WithdrawnState;
}

extern int
normal(Client *c) {
	return c->state == NormalState;
}

extern void
Client_EnterFullScreen(Client *c) {
	XWindowChanges fs;

	memcpy(&c->return_size, &c->size, sizeof(XSizeHints));
	if (c->framed) {
		c->size.x = fs.x = -border;
		c->size.y = fs.y = -border;
		c->size.width = fs.width =
			c->screen->display_width + 2 * border;
		c->size.height = fs.height =
			c->screen->display_height + 2 * border;
		XConfigureWindow(dpy, c->parent,
			CWX | CWY | CWWidth | CWHeight, &fs);

		fs.x = border;
		fs.y = border;
		fs.width = c->screen->display_width;
		fs.height = c->screen->display_height;
		XConfigureWindow(dpy, c->window,
			CWX | CWY | CWWidth | CWHeight, &fs);
		XRaiseWindow(dpy,c->parent);
	} else {
		c->size.x = c->size.y = fs.x = fs.y = 0;
		c->size.width = fs.width = c->screen->display_width;
		c->size.height = fs.height = c->screen->display_height;
		XConfigureWindow(dpy, c->window,
			CWX | CWY | CWWidth | CWHeight, &fs);
		XRaiseWindow(dpy,c->window);
	}
	sendConfigureNotify(c);
}

extern void
Client_ExitFullScreen(Client *c) {
	XWindowChanges fs;

	memcpy(&c->size, &c->return_size, sizeof(XSizeHints));
	if (c->framed == True) {
		fs.x = c->size.x;
		fs.y = c->size.y - titleHeight();
		fs.width = c->size.width;
		fs.height = c->size.height + titleHeight();
		XConfigureWindow(dpy, c->parent,
			CWX | CWY | CWWidth | CWHeight, &fs);

		fs.x = border;
		fs.y = border + titleHeight();
		fs.width = c->size.width -(2 * border);
		fs.height = c->size.height -(2 * border);
		XConfigureWindow(dpy, c->window,
			CWX | CWY | CWWidth | CWHeight, &fs);
	} else {
		fs.x = c->size.x;
		fs.y = c->size.y;
		fs.width = c->size.width;
		fs.height = c->size.height;
		XConfigureWindow(dpy, c->window,
			CWX | CWY | CWWidth | CWHeight, &fs);
	}
	sendConfigureNotify(c);
}

extern void
Client_Focus(Client *c, Time time) {
	if (current) {
		setactive(current, 0, 0L);
		XDeleteProperty(dpy, current->screen->root,
			ewmh_atom[_NET_ACTIVE_WINDOW]);
	}

	if (!c && current) {
		last_focus = current;
	} else {
		last_focus = NULL;
	}
	current = c;
	if (c) {
		setactive(current, 1, time);
		XChangeProperty(dpy, current->screen->root,
			ewmh_atom[_NET_ACTIVE_WINDOW],
			XA_WINDOW, 32, PropModeReplace,
			(unsigned char *)&current->window, 1);
	}

	if (focus_mode == focus_click)
		Client_Raise(c);
}

extern void
Client_Name(Client *c, const char *name, Bool is_utf8) {
	int tx;
	static const char dots[] = " [...] ";
	int cut;

	if (c->name) free(c->name);
	c->name = sdup((char *) name);
	c->namelen = strlen(c->name);
	c->name_utf8 = is_utf8;

	if (c->menu_name) free(c->menu_name);
	c->menu_name = 0;
	tx = titleWidth(popup_font_set, c);
	if (tx <= (c->screen->display_width - (c->screen->display_width / 10)))
		return;

	/* the menu entry for this client will not fit on the display
	 * (minus 10% for saftey), so produced a truncated version...
	 */
	cut = 5;
	do {
		if (c->menu_name) {
			free(c->menu_name);
			c->menu_name = 0;
		}
		if (cut >= (strlen(c->name) / 2)) break;
		c->menu_name = sdup(c->name);
		/* FIXME: this is not UTF-8 safe! */
		sprintf(&c->menu_name[(strlen(c->name) / 2) - cut], dots);
		strcat(c->menu_name,
			&c->name[(strlen(c->name) / 2) + cut]);
		c->menu_namelen = strlen(c->menu_name);
		cut++;
		tx = titleWidth(popup_font_set, c);
		if (!tx) break;
	} while (tx >
		(c->screen->display_width - (c->screen->display_width / 10)));
}
