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

/*
 * because X11 treats the NumLock and ScrollLock as modifiers, the user's
 * keybindings wont be grabbed when the locks are on without grabbing for
 * those also.  the masks are available here.
 */
u_int numlock_mask, scroll_mask;

/* our list of keys that we bind */
static SLIST_HEAD(, keybind) keys_list = SLIST_HEAD_INITIALIZER(keys_list);

/* shutdown keys; free allocated memory and the like */
void keys_shutdown() {
	keybind_t *key, *next;

	/* free memory allocated in our list of keys */
	key = SLIST_FIRST(&keys_list);
	while (key) {
		/* some keybinding types have allocated memory */
		switch (key->action) {
		case KEY_COMMAND:
			free(key->dat.cmd);
			break;
		case KEY_SETVIEWPORT:
			free(key->dat.pt);
			break;
		}

		/* free the keybind_t */
		next = SLIST_NEXT(key, k_list);
		free(key);
		key = next;
	}
	SLIST_INIT(&keys_list);
}

/* add a key binding */
keybind_t *keys_add(int keycode, u_int modifiers, int action, void* dat) {
	keybind_t *key;

	key = malloc(sizeof(keybind_t));
	if (!key)
		return NULL;
	key->keycode = keycode;
	key->modifiers = modifiers;
	key->action = action;
	key->dat.generic = dat;
	SLIST_INSERT_HEAD(&keys_list, key, k_list);

	return key;
}

static __inline void gethackmods() {
	static int masks[8] = {
		ShiftMask, LockMask, ControlMask, Mod1Mask,
		Mod2Mask, Mod3Mask, Mod4Mask, Mod5Mask
	};
	XModifierKeymap *modmap;
	KeyCode ncode, scode;
	int i;

	/* determine modifier mappings for the below hack */
	modmap = XGetModifierMapping(display);
	if (modmap) {
		numlock_mask = scroll_mask = 0;
		ncode = XKeysymToKeycode(display, XK_Num_Lock);
		scode = XKeysymToKeycode(display, XK_Scroll_Lock);
		for (i = 0; i < 8 * modmap->max_keypermod; i++)
			if (ncode && modmap->modifiermap[i] == ncode)
				numlock_mask = masks[i / modmap->max_keypermod];
			else if (scode && modmap->modifiermap[i] == scode)
				scroll_mask = masks[i / modmap->max_keypermod];
		XFreeModifiermap(modmap);
	} else
		numlock_mask = scroll_mask = 0;
}

static __inline void grabhack(Window win, int keycode, u_int modifiers) {
	if (numlock_mask) {
		XGrabKey(display, keycode, modifiers | numlock_mask,
			win, 1, GrabModeAsync, GrabModeAsync);
		XGrabKey(display, keycode, modifiers | numlock_mask | LockMask,
			win, 1, GrabModeAsync, GrabModeAsync);
	}
	if (scroll_mask) {
		XGrabKey(display, keycode, modifiers | scroll_mask,
			win, 1, GrabModeAsync, GrabModeAsync);
		XGrabKey(display, keycode, modifiers | scroll_mask | LockMask,
			win, 1, GrabModeAsync, GrabModeAsync);
	}
	if (numlock_mask && scroll_mask) {
		XGrabKey(display, keycode, modifiers | numlock_mask | scroll_mask,
			win, 1, GrabModeAsync, GrabModeAsync);
		XGrabKey(display, keycode, modifiers | numlock_mask | scroll_mask | LockMask,
			win, 1, GrabModeAsync, GrabModeAsync);
	}
	XGrabKey(display, keycode, modifiers | LockMask, win, 1,
		GrabModeAsync, GrabModeAsync);
	XGrabKey(display, keycode, modifiers, win, 1,
		GrabModeAsync, GrabModeAsync);
}

void buttongrabhack(Window win, int button, u_int modifiers) {
	if (numlock_mask) {
		XGrabButton(display, AnyButton, modifiers | numlock_mask, win,
			1, ButtonPressMask, GrabModeSync, GrabModeAsync, None, None);
		XGrabButton(display, AnyButton, modifiers | numlock_mask | LockMask, win,
			1, ButtonPressMask, GrabModeSync, GrabModeAsync, None, None);
	}
	if (scroll_mask) {
		XGrabButton(display, AnyButton, modifiers | scroll_mask, win,
			1, ButtonPressMask, GrabModeSync, GrabModeAsync, None, None);
		XGrabButton(display, AnyButton, modifiers | scroll_mask | LockMask, win,
			1, ButtonPressMask, GrabModeSync, GrabModeAsync, None, None);
	}
	if (numlock_mask && scroll_mask) {
		XGrabButton(display, AnyButton, modifiers | numlock_mask | scroll_mask,
			win, 1, ButtonPressMask, GrabModeSync, GrabModeAsync,
			None, None);
		XGrabButton(display, AnyButton, modifiers | numlock_mask | scroll_mask | LockMask,
			win, 1, ButtonPressMask, GrabModeSync, GrabModeAsync,
			None, None);
	}
	XGrabButton(display, AnyButton, modifiers | LockMask, win, 1, ButtonPressMask,
		GrabModeSync, GrabModeAsync, None, None);
	XGrabButton(display, AnyButton, modifiers, win, 1, ButtonPressMask,
		GrabModeSync, GrabModeAsync, None, None);
}

/* do grabs on the root window of screen */
void keys_grab(screen_t *screen) {
	keybind_t *key;

	gethackmods();
	SLIST_FOREACH(key, &keys_list, k_list)
		grabhack(screen->root, key->keycode, key->modifiers);
}

/* move the viewport */
static void keys_moveport(screen_t *screen, int dir) {
	int vx, vy;

	switch (dir) {
	case MV_UP:		vx = 0; vy = -1;	break;
	case MV_DOWN:		vx = 0; vy = 1;		break;
	case MV_LEFT:		vx = -1; vy = 0;	break;
	case MV_RIGHT:		vx = 1; vy = 0;		break;
	case MV_UPRIGHT:	vx = 1; vy = -1;	break;
	case MV_DOWNRIGHT:	vx = 1; vy = 1;		break;
	case MV_DOWNLEFT:	vx = -1; vy = 1;	break;
	default:
	case MV_UPLEFT:		vx = -1; vy = -1;	break;
	}

	workspace_viewport_move(screen, screen->desktop, vx, vy);
}

/*
 * focus and raise a win, we don't use focus_setfocused because
 * when going across screens in the all or screen style cyclings
 * it would try to just set workspace->focused pointers.
 */
#define FOCUSRAISE(foc) do {						\
	focus_client((foc));						\
	stacking_raise(client_focused);					\
} while (0)

/* forward loop through screens, check if foc to focus foc, for cycle */
#define SCREEN_FLOOP(foc, currentscr) do {				\
	screen = TAILQ_NEXT((currentscr), s_list);			\
									\
	while (screen != (currentscr)) {				\
		if (!screen) {						\
			screen = TAILQ_FIRST(&screen_list);		\
			continue;					\
		}							\
									\
		if ((foc)) {						\
			FOCUSRAISE((foc));				\
			break;						\
		}							\
		screen = TAILQ_NEXT(screen, s_list);			\
	}								\
} while (0)

/* backward loop through screens, like floop */
#define SCREEN_BLOOP(foc, currentscr) do {				\
	screen = TAILQ_PREV((currentscr), screenlist, s_list);		\
									\
	while (screen != (currentscr)) {				\
		if (!screen) {						\
			screen = TAILQ_LAST(&screen_list, screenlist);	\
			continue;					\
		}							\
									\
		if ((foc)) {						\
			FOCUSRAISE((foc));				\
			break;						\
		}							\
									\
		screen = TAILQ_PREV(screen, screenlist, s_list);	\
	}								\
} while (0)

/* cycle screens */
static void keys_cyclescr(screen_t *keyscr, client_t *client, int cycletype) {
	screen_t *screen;
	screen_t *pointer_screen;
	Window pointer_root;
	Window dumwin;
	int dumint;

	/*
	 * screen-based cycling doesn't require visible client windows: it
	 * warps the pointer to new screens, and focused the focused window
	 * on the screen if there is one. The currently 'focused' screen
	 * is the one w/ the mouse pointer, because that is where keybinds
	 * will take effect.  So event if the focused window is on a different
	 * screen we treat the mouse pointer screen as the focused screen.
	 */
	XQueryPointer(display, keyscr->root, &pointer_root, &dumwin, &dumint, &dumint,
		&dumint, &dumint, &dumint);
	TAILQ_FOREACH(pointer_screen, &screen_list, s_list)
		if (pointer_screen->root == pointer_root)
			break;

	/*
	 * now we know which screen we're on; put it on the
	 * screen it needs to go to.
	 */
	switch (cycletype) {
	case CF_FSCR:
		screen = TAILQ_NEXT(pointer_screen, s_list);
		if (!screen)
			screen = TAILQ_FIRST(&screen_list);

		/* only refocus if this isn't the screen of the focused window */
		if (screen == pointer_screen)
			goto warp;
		if (screen->desktop->current_space->focused)
			if (!client || client->screen != screen)
				FOCUSRAISE(screen->desktop->current_space->focused);
	case CF_BSCR:
	default:
		screen = TAILQ_PREV(pointer_screen, screenlist, s_list);
		if (!screen)
			screen = TAILQ_LAST(&screen_list, screenlist);

		/* only refocus if this isn't the screen of the focused window */
		if (screen == pointer_screen)
			goto warp;
		if (screen->desktop->current_space->focused)
			if (!client || client->screen != screen)
				FOCUSRAISE(screen->desktop->current_space->focused);
	}

warp:
	/* put pointer in the middle of the screen */
	XWarpPointer(display, None, screen->root, 0, 0, 1, 1,
		screen->width / 2, screen->height / 2);
}

/*
 * cycle input focus, slightly complicated becase we support several cycling
 * types here (for multiscreen), see keys.h for info on the types.
 */
static void keys_cyclefoc(screen_t *keyscr, client_t *client, int cycletype) {
	screen_t *screen;

	/* screen based cycling; not strictly for just focus cycling */
	if (cycletype == CF_FSCR || cycletype == CF_BSCR) {
		keys_cyclescr(keyscr, client, cycletype);
		return;
	}

	/* if no window is focused, this is easier */
	if (!client) {
		if (cycletype % 2 == 0) {
			if (TAILQ_FIRST(&keyscr->desktop->current_space->w_foclist))
				FOCUSRAISE(TAILQ_FIRST(&keyscr->desktop->current_space->w_foclist));
			else if (cycletype == CF_FALL)
				SCREEN_FLOOP(TAILQ_FIRST(&screen->desktop->current_space->w_foclist),
					keyscr);
		} else {
			if (TAILQ_LAST(&keyscr->desktop->current_space->w_foclist, foclist))
				FOCUSRAISE(TAILQ_LAST(&keyscr->desktop->current_space->w_foclist, foclist));
			else if (cycletype == CF_BALL)
				SCREEN_BLOOP(TAILQ_LAST(&screen->desktop->current_space->w_foclist, foclist),
					keyscr);
		}
		return;
	}

	/* if the type is div by 2, it's a forward cycling type */
	if (cycletype % 2 == 0) {
		if (TAILQ_NEXT(client, c_focus)) {
			FOCUSRAISE(TAILQ_NEXT(client, c_focus));
		} else {
			if (cycletype == CF_FALL)
				SCREEN_FLOOP(TAILQ_FIRST(&screen->desktop->current_space->w_foclist),
					client->screen);
			else if (TAILQ_FIRST(&client->screen->desktop->current_space->w_foclist))
				FOCUSRAISE(TAILQ_FIRST(&client->screen->desktop->current_space->w_foclist));
		}
	} else {
		if (TAILQ_PREV(client, foclist, c_focus)) {
			FOCUSRAISE(TAILQ_PREV(client, foclist, c_focus));
		} else {
			if (cycletype == CF_BALL)
				SCREEN_BLOOP(TAILQ_LAST(&screen->desktop->current_space->w_foclist,
					foclist), client->screen);
			else if (TAILQ_LAST(&client->screen->desktop->current_space->w_foclist, foclist))
				FOCUSRAISE(TAILQ_LAST(&client->screen->desktop->current_space->w_foclist, foclist));
		}
	}
}

#undef SCREEN_BLOOP
#undef SCREEN_FLOOP
#undef FOCUSRAISE

/* perform an action for a key */
static void keys_action(screen_t *screen, keybind_t *key) {
	client_t *client = client_focused;

	switch (key->action) {
	case KEY_ICONIFY:
		if (client)
			action_iconify(client);
		break;
	case KEY_ZOOM:
		if (client)
			action_zoom(client);
		break;
	case KEY_SWITCHDESK:
		desktop_switch(screen, key->dat.num);
		break;
	case KEY_MOVEVIEWPORT:
		keys_moveport(screen, key->dat.dir);
		break;
	case KEY_SETVIEWPORT:
		workspace_viewport_move(screen, screen->desktop,
			key->dat.pt->x - screen->desktop->viewx,
			key->dat.pt->y - screen->desktop->viewy);
		break;
	case KEY_COMMAND:
		action_exec(screen->num, key->dat.cmd);
		break;
	case KEY_DELETE:
		if (client && !client->flags.nodelete)
			action_sendcmesg(client->window, WM_DELETE_WINDOW, CurrentTime);
		break;
	case KEY_CYCLEFOCUS:
		keys_cyclefoc(screen, client, key->dat.cycletype);
		break;
	case KEY_RAISE:
		if (client)
			stacking_raise(client);
		break;
	case KEY_LOWER:
		if (client)
			stacking_lower(client);
		break;
	case KEY_DGROUPSWITCH:
		if (client)
			dgroup_switch(client, NULL);
		break;
	case KEY_STICKY:
		if (client)
			client->flags.sticky = ~client->flags.sticky;
		break;
	case KEY_ABORT:
		if (!fork())
			abort();
		break;
	case KEY_RESTART:
		restart_bin = binary_name;
	case KEY_EXIT:
		restart_flag = 1;
		break;
	}
}

/* handles when we get keypresses from our grabs */
void keys_press(screen_t *screen, XKeyEvent *e) {
	keybind_t *key;

	SLIST_FOREACH(key, &keys_list, k_list)
		if (e->keycode == key->keycode
				&& (e->state & ~(numlock_mask |
				scroll_mask | LockMask)) == key->modifiers) {
			keys_action(screen, key);
			return;
		}
}
