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

/* our handler table */
typedef void (*event_handler_t)(XEvent *event);
static event_handler_t handlers[LASTEvent];

/* handler for DestroyNotify events */
static void event_destroy_notify(XEvent *event) {
	XDestroyWindowEvent *e = &event->xdestroywindow;
	client_t *client;

	if (XFindContext(display, e->window, client_context, (XPointer *) &client))
		return;
	if (e->window != client->window)
		return;

	if (!client->flags.internal)
		XRemoveFromSaveSet(display, client->window);
	plugin_window_death(client);
	client_rm(client);
}

/* handler for ConfigureRequest */
static void event_configure_request(XEvent *event) {
	XConfigureRequestEvent *e = &event->xconfigurerequest;
	XWindowChanges wc;
	client_t *client;

	/* now we only update the structure if the window has one */
	if (XFindContext(display, e->window, client_context, (XPointer *) &client))
		client = NULL;

	/*
	 * we need to respond to ConfigureRequests of windows that
	 * haven't yet been mapped, and thus don't have client_t structs
	 * in our list.
	 */
	if (!client) {
		wc.x = e->x;
		wc.y = e->y;
		wc.width = e->width;
		wc.height = e->height;
		wc.border_width = e->border_width;
		wc.stack_mode = e->detail;
		XConfigureWindow(display, e->window, e->value_mask, &wc);
		return;
	}

	/* the event should be for the client's window */
	assert(e->window == client->window);

	/*
	 * now for managed windows we do stuff to our managed
	 * client structure.
	 */
	if (e->value_mask & CWX)
		client->x = e->x;
	if (e->value_mask & CWY)
		client->y = e->y;
	if (e->value_mask & CWWidth)
		client->width = e->width;
	if (e->value_mask & CWHeight)
		client->height = e->height;

	/*
	 * when a client moves itself, it may need
	 * to change workspaces
	 */
	client_gravitate(client);
	client_sizeframe(client);
	action_send_config(client);
	workspace_add_bypos(client->screen->desktop, client);
	plugin_geometry_change(client);
}

/* handler for MapRequest events */
static void event_map_request(XEvent *event) {
	XMapRequestEvent *e = &event->xmaprequest;
	screen_t *screen;
	client_t *client;

	/*
	 * for some reason some broken clients (netscape) seem to do more
	 * than one map request on top window..., so make sure this window
	 * isn't already managed.
	 */
	if (!XFindContext(display, e->window, client_context, (XPointer *) &client))
		return;
	if (XFindContext(display, e->parent, root_context, (XPointer *) &screen))
		return;

	/*
	 * give plugins a chance to handle this window exclusively,
	 * if plugin_map_request returns PLUGIN_USING we don't managed
	 * in with client_add.
	 */
	if (plugin_map_request(screen, e))
		return;
	client = client_add(screen, e->window, NULL, NULL);
	if (!client)
		return;
	client_map(client, 1);
}

/* handler for MapNotify events */
static void event_map_notify(XEvent *event) {
	XMapEvent *e = &event->xmap;
	client_t *client;

	/*
	 * look for the client context we created for it in event_map_request;
	 * and only continue if this is a notify from the client->window; if
	 * it's for the frame we don't care here.
	 */
	if (XFindContext(display, e->window, client_context, (XPointer *) &client))
		return;
	if (e->window != client->window)
		return;

	/* give focus to new windows (if the options say to) */
	if (options.focus_new && client->workspace)
		focus_setfocused(client);
}

/* handler for UnmapNotify events */
static void event_unmap_notify(XEvent *event) {
	XUnmapEvent *e = &event->xunmap;
	client_t *client;

	/*
	 * handle unmap notify events on managed windows; we only are
	 * interested in the unmapnotify events generated when the
	 * client window (not the frame) unmaps; because that signals
	 * that the client should be unmanaged.  the only exception
	 * is when we get unmap events from a reparent (see below).
	 */
	if (XFindContext(display, e->window, client_context, (XPointer *) &client))
		return;
	if (e->window != client->window)
		return;

	/*
	 * see if the event came from reparenting
	 * (see screen.c, manage_existing_windows)
	 */
	if (client->flags.unmap_from_reparent) {
		client_map(client, 0);
		client->flags.unmap_from_reparent = 0;
		return;
	}

	/*
	 * get rid of our client structure, reparent it to the root,
	 * and remove from our saveset
	 */
	if (client->state == NormalState)
		XUnmapWindow(display, client->frame);
	plugin_window_death(client);
	client_setstate(client, WithdrawnState);
	if (!client->flags.internal)
		XRemoveFromSaveSet(display, client->window);
	XReparentWindow(display, client->window, client->screen->root, client->x, client->y);
	client_rm(client);
}

/* handler for ButtonPress events */
static void event_button_press(XEvent *event) {
	XButtonEvent *e = &event->xbutton;
	client_t *client;
	screen_t *screen;
	decor_t *decor;

	/*
	 * if the click doesn't have a client context, check if it is
	 * a root window click; in which case notify plugins.
	 */
	if (XFindContext(display, e->window, client_context, (XPointer *) &client)) {
		if (!XFindContext(display, e->window, root_context, (XPointer *) &screen))
			plugin_root_button(screen, e);
		return;
	}

	/*
	 * handle button presses on client frame window (for mouse_modifier
	 * grabs), the client window to do focusing in click focus mode,
	 * or client decoration.
	 */
	if (e->window == client->frame) {
		if (options.focus == FOCUS_CLICK)
			focus_setfocused(client);

		/*
		 * either do move/resize for mouse_modifier grabs, or in
		 * click focus mode we will get events for initial clicks
		 * in the click->frame to do a raise.
		 *
		 * XXX: the buttons that do which for mouse_modifier should
		 * be configurable.
		 */
		if ((e->state & ~(numlock_mask | scroll_mask | LockMask)) == options.mouse_modifier) {
			if (e->button == Button3)
				action_resize(client);
			else
				action_move(client);
		} else {
			XAllowEvents(display, ReplayPointer, CurrentTime);
			stacking_raise(client);
		}
	} else if (!XFindContext(display, e->window, decor_context, (XPointer *) &decor)) {
		if (options.focus == FOCUS_CLICK)
			focus_setfocused(client);
		decor_handlepress(client, decor, e);
	}
}

/* handler for ButtonRelease events */
static void event_button_release(XEvent *event) {
	XButtonEvent *e = &event->xbutton;
	screen_t *screen;

	/*
	 * the only clicks we are concerned with are clicks on
	 * a managed root window.
	 */
	if (XFindContext(display, e->window, root_context, (XPointer *) &screen))
		return;
	plugin_root_button(screen, e);

	/*
	 * switch workspace when clicking on the edges of the
	 * screen.
	 *
	 * XXX: this should likely be removed at some point...
	 */
	if (e->button == Button1) {
		if (e->y_root == 0) {
			if (workspace_viewport_move(screen, screen->desktop, 0, -1))
				XWarpPointer(display, None, screen->root, 0, 0, 1, 1,
					e->x_root, screen->height - 1);
		} else if (e->y_root == screen->height - 1) {
			if (workspace_viewport_move(screen, screen->desktop, 0, 1))
				XWarpPointer(display, None, screen->root, 0, 0, 1, 1,
					e->x_root, 0);
		} else if (e->x_root == 0) {
			if (workspace_viewport_move(screen, screen->desktop, -1, 0))
				XWarpPointer(display, None, screen->root, 0, 0, 1, 1,
					screen->width - 1, e->y_root);
		} else if (e->x_root == screen->width - 1) {
			if (workspace_viewport_move(screen, screen->desktop, 1, 0))
				XWarpPointer(display, None, screen->root, 0, 0, 1, 1,
					0, e->y_root);
		}
	}
}

/* handler for KeyPress events */
static void event_key_press(XEvent *event) {
	XKeyEvent *e = &event->xkey;
	screen_t *screen;

	/*
	 * handle dispatching key bindings; get the screen on which
	 * the event occured and pass it to the key_press handler.
	 */
	if (XFindContext(display, e->root, root_context, (XPointer *) &screen))
		return;
	keys_press(screen, e);
}

/* handler for Expose events */
static void event_expose(XEvent *event) {
	XExposeEvent *e = &event->xexpose;
	client_t *client;
	decor_t *decor;

	/*
	 * handle exposures for client decoration.
	 */
	if (XFindContext(display, e->window, client_context, (XPointer *) &client))
		return;
	if (XFindContext(display, e->window, decor_context, (XPointer *) &decor))
		return;
	decor_expose(client, decor, e);
}

/* handler for ClientMessage */
static void event_client_message(XEvent *event) {
	XClientMessageEvent *e = &event->xclient;
	client_t *client;

	if (XFindContext(display, e->window, client_context, (XPointer *) &client))
		return;
	if (e->window != client->window)
		return;

	/*
	 * currently, only handle state change requests
	 * for iconification.
	 */
	if (e->message_type == WM_CHANGE_STATE && e->format == 32
			&& e->data.l[0] == IconicState
			&& client->state != IconicState)
		action_iconify(client);
}

/* handler for PropertyNotify */
static void event_property_notify(XEvent *event) {
	XPropertyEvent *e = &event->xproperty;
	client_t *client;
	long supplied;
#ifdef I18N
	XTextProperty text_prop;
	char **cl;
	int n;
#endif

	/* get associated client, the event must be for it's window */
	if (XFindContext(display, e->window, client_context, (XPointer *) &client))
		return;
	assert(e->window == client->window);

	/*
	 * notify plugins about the property event; plugins can use this
	 * be aware of client title changes, or clients changing hinting
	 * properties.  if a plugin is using the property change, and is
	 * certain noone else will be (i.e. a specific windowmanager hint
	 * protocol) it may return PLUGIN_USING, in which case we don't
	 * need to do anything else for this event.
	 */
	if (plugin_property_notify(client, e) == PLUGIN_USING)
		return;

	/*
	 * support changing the normal hints for a managed client
	 * window, and changing the client store name.
	 */
	if (e->state == PropertyNewValue)
		switch (e->atom) {
		case XA_WM_NAME:
			if (client->store_name)
				XFree(client->store_name);
#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 = "NoName";
					}
				}
			} else {
				client->store_name = "NoName";
			}
#else
			XFetchName(display, client->window, &client->store_name);
#endif

			/*
			 * XXX: need a callback to let plugins know
			 */
			if (client->state != IconicState)
				decor_titlechange(client);
			break;
		case XA_WM_NORMAL_HINTS:
			XGetWMNormalHints(display, client->window, &client->normalhints,
				&supplied);
			break;
		}
}

/* handler for EnterNotify */
static void event_enter_notify(XEvent *event) {
	XCrossingEvent *e = &event->xcrossing;
	client_t *client;

	/*
	 * we should only get this event for sloppy/pointer focusing
	 * modes; when entering the frame window in those modes, we
	 * set the focus to that client.
	 */
	if (XFindContext(display, e->window, client_context, (XPointer *) &client))
		return;
	if (e->window != client->frame)
		return;
	focus_setfocused(client);
}

/* handler for LeaveNotify */
static void event_leave_notify(XEvent *event) {
	XCrossingEvent *e = &event->xcrossing;
	client_t *client;

	/*
	 * we should only recieve this event for pointer focusing
	 * mode; when leaving a window in pointer focus, the window
	 * is unfocused.
	 */
	if (XFindContext(display, e->window, client_context, (XPointer *) &client))
		return;
	if (e->window != client->frame)
		return;

	/*
	 * we check if the client has a workspace first, because we will get
	 * a leave notify if the user iconifies a window, and the window
	 * will have left it's workspace, and thus already be unfocused.
	 */
	if (client->workspace)
		focus_unfocus(client);
}

/* handler for ShapeNotify */
static void event_shape_notify(XEvent *event) {
	client_t *client;

	/*
	 * when a client shapes it's window, we need to shape the frame
	 * window accordingly.
	 */
	if (XFindContext(display, event->xany.window, client_context, (XPointer *) &client))
		return;
	if (event->xany.window != client->window)
		return;
	client_shape(client);
}

/*
 * set up the table of event handlers; NULL out the events
 * that we don't handle.
 */
static void event_init() {
	int i;

	for (i = 0; i < LASTEvent; i++)
		handlers[i] = NULL;
	handlers[DestroyNotify]		= event_destroy_notify;
	handlers[ConfigureRequest]	= event_configure_request;
	handlers[MapRequest]		= event_map_request;
	handlers[MapNotify]		= event_map_notify;
	handlers[UnmapNotify]		= event_unmap_notify;
	handlers[ButtonPress]		= event_button_press;
	handlers[ButtonRelease]		= event_button_release;
	handlers[KeyPress]		= event_key_press;
	handlers[Expose]		= event_expose;
	handlers[ClientMessage]		= event_client_message;
	handlers[PropertyNotify]	= event_property_notify;
	handlers[EnterNotify]		= event_enter_notify;
	handlers[LeaveNotify]		= event_leave_notify;
}

/*
 * handle an X event; this is exported to the world to allow pieces
 * of code that have their own internal event loops (for instance,
 * window movement) to call into the main handlers for certain
 * events (such as exposure events).
 */
void event_handle(XEvent *e) {
	plugin_t *plugin;

	/*
	 * check event for plugin association; these events
	 * are dispatched to a plugin handler instead of getting
	 * handled here.
	 */
	if (!XFindContext(display, e->xany.window, plugin_context, (XPointer *) &plugin)) {
		plugin_handle_event(plugin, e);
		return;
	}

	/*
	 * if a handler exists in the table of event handlers,
	 * dispatch the event to it.
	 */
	if (e->type == shape_base + ShapeNotify)
		event_shape_notify(e);
	else if (handlers[e->type] != NULL)
		handlers[e->type](e);

}

/*
 * main loop for X event processing; when this routine
 * exits the window manager will die.
 */
void event_loop() {
	XEvent event;

	event_init();

	while (1) {
		XNextEvent(display, &event);
		event_handle(&event);

		/*
		 * if something sets restart_flag, we leave this function
		 * and let main() take the appropriate action.
		 */
		if (restart_flag)
			return;
	}
}
