/*
 * Copyright © 2005 Novell, Inc.
 *
 * Permission to use, copy, modify, distribute, and sell this software
 * and its documentation for any purpose is hereby granted without
 * fee, provided that the above copyright notice appear in all copies
 * and that both that copyright notice and this permission notice
 * appear in supporting documentation, and that the name of
 * Novell, Inc. not be used in advertising or publicity pertaining to
 * distribution of the software without specific, written prior permission.
 * Novell, Inc. makes no representations about the suitability of this
 * software for any purpose. It is provided "as is" without express or
 * implied warranty.
 *
 * NOVELL, INC. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
 * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
 * NO EVENT SHALL NOVELL, INC. BE LIABLE FOR ANY SPECIAL, INDIRECT OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
 * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * Author: David Reveman <davidr@novell.com>
 */

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

#include <X11/cursorfont.h>
#include <X11/Xresource.h>
#include <X11/Xatom.h>

#include <compiz-core.h>

static CompMetadata moveMetadata;

struct _MoveKeys {
    char *name;
    int  dx;
    int  dy;
} mKeys[] = {
    { "Left",  -1,  0 },
    { "Right",  1,  0 },
    { "Up",     0, -1 },
    { "Down",   0,  1 }
};

#define NUM_KEYS (sizeof (mKeys) / sizeof (mKeys[0]))

#define KEY_MOVE_INC 24

#define SNAP_BACK 20
#define SNAP_OFF  100

static int displayPrivateIndex;

#define MOVE_DISPLAY_OPTION_INITIATE_BUTTON           0
#define MOVE_DISPLAY_OPTION_INITIATE_KEY              1
#define MOVE_DISPLAY_OPTION_OPACITY	              2
#define MOVE_DISPLAY_OPTION_CONSTRAIN_Y	              3
#define MOVE_DISPLAY_OPTION_SNAPOFF_MAXIMIZED         4
#define MOVE_DISPLAY_OPTION_LAZY_POSITIONING          5
#define MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL          6
#define MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_SPEED    7
#define MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_DOWN     8
#define MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_UP       9
#define MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_RIGHT    10
#define MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_LEFT     11
#define MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_COLOR    12
#define MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_WIDTH    13
#define MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_GRADIENT 14
#define MOVE_DISPLAY_OPTION_NUM		              15

typedef struct _MoveDisplay {
    int		    screenPrivateIndex;
    HandleEventProc handleEvent;

    CompOption opt[MOVE_DISPLAY_OPTION_NUM];

    CompWindow *w;
    int	       savedX;
    int	       savedY;
    int	       x;
    int	       y;
    Region     region;
    int        status;
    Bool       constrainY;
    KeyCode    key[NUM_KEYS];

    int releaseButton;

    GLushort moveOpacity;
} MoveDisplay;

typedef struct _MoveScreen {
    PaintWindowProc paintWindow;

    int grabIndex;

    Cursor moveCursor;
    CompTimeoutHandle cursorUpdateTimeoutHandle;

    unsigned int origState;
    unsigned int savedWidth;

    int	snapOffX;
    int	snapBackX;
    int	snapOffY;
    int	snapBackY;
} MoveScreen;

#define GET_MOVE_DISPLAY(d)					  \
    ((MoveDisplay *) (d)->base.privates[displayPrivateIndex].ptr)

#define MOVE_DISPLAY(d)		           \
    MoveDisplay *md = GET_MOVE_DISPLAY (d)

#define GET_MOVE_SCREEN(s, md)					      \
    ((MoveScreen *) (s)->base.privates[(md)->screenPrivateIndex].ptr)

#define MOVE_SCREEN(s)						        \
    MoveScreen *ms = GET_MOVE_SCREEN (s, GET_MOVE_DISPLAY (s->display))

#define NUM_OPTIONS(s) (sizeof ((s)->opt) / sizeof (CompOption))

static Bool
moveInitiate (CompDisplay     *d,
	      CompAction      *action,
	      CompActionState state,
	      CompOption      *option,
	      int	      nOption)
{
    CompWindow *w;
    Window     xid;

    MOVE_DISPLAY (d);

    xid = getIntOptionNamed (option, nOption, "window", 0);

    w = findWindowAtDisplay (d, xid);

    /* make window stay above fullscreen windows while moving */
    if (w)
	updateWindowAttributes (w, CompStackingUpdateModeAboveFullscreen);

    if (w && (w->actions & CompWindowActionMoveMask))
    {
	XRectangle   workArea;
	unsigned int mods;
	int          x, y, button;
	Bool         sourceExternalApp;

	MOVE_SCREEN (w->screen);

	mods = getIntOptionNamed (option, nOption, "modifiers", 0);

	x = getIntOptionNamed (option, nOption, "x",
			       w->attrib.x + (w->width / 2));
	y = getIntOptionNamed (option, nOption, "y",
			       w->attrib.y + (w->height / 2));

	button = getIntOptionNamed (option, nOption, "button", -1);

	if (otherScreenGrabExist (w->screen, "move", NULL))
	    return FALSE;

	if (md->w)
	    return FALSE;

	if (w->type & (CompWindowTypeDesktopMask |
		       CompWindowTypeDockMask    |
		       CompWindowTypeFullscreenMask))
	    return FALSE;

	if (w->attrib.override_redirect)
	    return FALSE;

	if (state & CompActionStateInitButton)
	    action->state |= CompActionStateTermButton;

	if (md->region)
	{
	    XDestroyRegion (md->region);
	    md->region = NULL;
	}

	md->status = RectangleOut;

	md->savedX = w->serverX;
	md->savedY = w->serverY;

	md->x = 0;
	md->y = 0;

	sourceExternalApp = getBoolOptionNamed (option, nOption, "external",
						FALSE);
	md->constrainY = md->opt[MOVE_DISPLAY_OPTION_CONSTRAIN_Y].value.b;

	lastPointerX = x;
	lastPointerY = y;

	ms->origState = w->state;

	ms->savedWidth = 0;

	getWorkareaForOutput (w->screen,
			      outputDeviceForWindow (w),
			      &workArea);

	ms->snapBackX = w->serverX - workArea.x;
	ms->snapOffX  = x - workArea.x;
	ms->snapBackY = w->serverY - workArea.y;
	ms->snapOffY  = y - workArea.y;

	if (!ms->grabIndex)
	    ms->grabIndex = pushScreenGrab (w->screen, ms->moveCursor, "move");

	if (ms->grabIndex)
	{
	    unsigned int grabMask = CompWindowGrabMoveMask |
				    CompWindowGrabButtonMask;

	    if (sourceExternalApp)
		grabMask |= CompWindowGrabExternalAppMask;

	    md->w = w;

	    md->releaseButton = button;

	    (w->screen->windowGrabNotify) (w, x, y, mods, grabMask);

	    if (d->opt[COMP_DISPLAY_OPTION_RAISE_ON_CLICK].value.b)
		updateWindowAttributes (w,
					CompStackingUpdateModeAboveFullscreen);

	    if (state & CompActionStateInitKey)
	    {
		int xRoot, yRoot;

		xRoot = w->attrib.x + (w->width  / 2);
		yRoot = w->attrib.y + (w->height / 2);

		warpPointer (w->screen, xRoot - pointerX, yRoot - pointerY);
	    }

	    if (md->moveOpacity != OPAQUE)
		addWindowDamage (w);
	}
    }

    return FALSE;
}

static Bool
moveTerminate (CompDisplay     *d,
	       CompAction      *action,
	       CompActionState state,
	       CompOption      *option,
	       int	       nOption)
{
    MOVE_DISPLAY (d);

    if (md->w)
    {
	MOVE_SCREEN (md->w->screen);

	if (state & CompActionStateCancel)
	    moveWindow (md->w,
			md->savedX - md->w->attrib.x,
			md->savedY - md->w->attrib.y,
			TRUE, FALSE);

	syncWindowPosition (md->w);

	/* update window size as window constraints may have
	   changed - needed e.g. if a maximized window was moved
	   to another output device */
	updateWindowSize (md->w);

	(md->w->screen->windowUngrabNotify) (md->w);

	if (ms->grabIndex)
	{
	    removeScreenGrab (md->w->screen, ms->grabIndex, NULL);
	    ms->grabIndex = 0;
	}

	if (md->moveOpacity != OPAQUE)
	    addWindowDamage (md->w);

	md->w             = 0;
	md->releaseButton = 0;
    }

    action->state &= ~(CompActionStateTermKey | CompActionStateTermButton);

    return FALSE;
}

/* creates a region containing top and bottom struts. only struts that are
   outside the screen workarea are considered. */
static Region
moveGetYConstrainRegion (CompScreen *s)
{
    CompWindow *w;
    Region     region;
    REGION     r;
    XRectangle workArea;
    BoxRec     extents;
    int	       i;

    region = XCreateRegion ();
    if (!region)
	return NULL;

    r.rects    = &r.extents;
    r.numRects = r.size = 1;

    r.extents.x1 = MINSHORT;
    r.extents.y1 = 0;
    r.extents.x2 = 0;
    r.extents.y2 = s->height;

    XUnionRegion (&r, region, region);

    r.extents.x1 = s->width;
    r.extents.x2 = MAXSHORT;

    XUnionRegion (&r, region, region);

    for (i = 0; i < s->nOutputDev; i++)
    {
	XUnionRegion (&s->outputDev[i].region, region, region);

	getWorkareaForOutput (s, i, &workArea);
	extents = s->outputDev[i].region.extents;

	for (w = s->windows; w; w = w->next)
	{
	    if (!w->mapNum)
		continue;

	    if (w->struts)
	    {
		r.extents.x1 = w->struts->top.x;
		r.extents.y1 = w->struts->top.y;
		r.extents.x2 = r.extents.x1 + w->struts->top.width;
		r.extents.y2 = r.extents.y1 + w->struts->top.height;

		if (r.extents.x1 < extents.x1)
		    r.extents.x1 = extents.x1;
		if (r.extents.x2 > extents.x2)
		    r.extents.x2 = extents.x2;
		if (r.extents.y1 < extents.y1)
		    r.extents.y1 = extents.y1;
		if (r.extents.y2 > extents.y2)
		    r.extents.y2 = extents.y2;

		if (r.extents.x1 < r.extents.x2 && r.extents.y1 < r.extents.y2)
		{
		    if (r.extents.y2 <= workArea.y)
			XSubtractRegion (region, &r, region);
		}

		r.extents.x1 = w->struts->bottom.x;
		r.extents.y1 = w->struts->bottom.y;
		r.extents.x2 = r.extents.x1 + w->struts->bottom.width;
		r.extents.y2 = r.extents.y1 + w->struts->bottom.height;

		if (r.extents.x1 < extents.x1)
		    r.extents.x1 = extents.x1;
		if (r.extents.x2 > extents.x2)
		    r.extents.x2 = extents.x2;
		if (r.extents.y1 < extents.y1)
		    r.extents.y1 = extents.y1;
		if (r.extents.y2 > extents.y2)
		    r.extents.y2 = extents.y2;

		if (r.extents.x1 < r.extents.x2 && r.extents.y1 < r.extents.y2)
		{
		    if (r.extents.y1 >= (workArea.y + workArea.height))
			XSubtractRegion (region, &r, region);
		}
	    }
	}
    }

    return region;
}

static Bool
handleMaximizedSnapping (CompScreen *s,
                         CompWindow *w,
                         int xRoot, int yRoot,
                         XRectangle workArea)
{
	MOVE_DISPLAY (s->display);
	MOVE_SCREEN (s);

	if (!md->opt[MOVE_DISPLAY_OPTION_SNAPOFF_MAXIMIZED].value.b)
		return FALSE;

	if (w->state & CompWindowStateMaximizedVertMask)
	{
		if (abs ((yRoot - workArea.y) - ms->snapOffY) >= SNAP_OFF)
		{
		if (!otherScreenGrabExist (s, "move", NULL))
		{
			int width = w->serverWidth;

			w->saveMask |= CWX | CWY;

			if (w->saveMask & CWWidth)
			width = w->saveWc.width;

			w->saveWc.x = xRoot - (width >> 1);
			w->saveWc.y = yRoot + (w->input.top >> 1);
			if (!ms->savedWidth)
			ms->savedWidth = w->serverWidth;

			md->x = md->y = 0;

			maximizeWindow (w, 0);

			ms->snapOffY = ms->snapBackY;

			return TRUE;
		}
		}
	}
	else if (ms->origState & CompWindowStateMaximizedVertMask)
	{
		if (abs ((yRoot - workArea.y) - ms->snapBackY) < SNAP_BACK)
		{
		if (!otherScreenGrabExist (s, "move", NULL))
		{
			int wy;

			/* update server position before maximizing
			   window again so that it is maximized on
			   correct output */
			syncWindowPosition (w);

			maximizeWindow (w, ms->origState);

			XWindowChanges xwc;

			if (ms->origState & CompWindowStateMaximizedHorzMask)
				/* MAXIMIZE_STATE */
				xwc.x = workArea.x;
			else
				/* CompWindowStateMaximizedVertMask */
				xwc.x = xRoot - (ms->savedWidth >> 1);

			xwc.width = ms->savedWidth;
			ms->savedWidth = 0;
			configureXWindow (w, CWX | CWWidth, &xwc);

			wy  = workArea.y + (w->input.top >> 1);
			wy += w->sizeHints.height_inc >> 1;

			warpPointer (s, 0, wy - pointerY);

			return TRUE;
		}
		}
	}

	/* Avoid handling CompWindowStateMaximizedVertMask and
	 * CompWindowStateMaximizedHorzMask together */
	if ((w->state & MAXIMIZE_STATE) == MAXIMIZE_STATE)
		return FALSE;

	if (w->state & CompWindowStateMaximizedHorzMask)
	{
		if (abs ((xRoot - workArea.x) - ms->snapOffX) >= SNAP_OFF)
		{
		if (!otherScreenGrabExist (s, "move", NULL))
		{
			int width = w->serverWidth;

			w->saveMask |= CWX | CWY;

			if (w->saveMask & CWWidth)
			width = w->saveWc.width;

			w->saveWc.x = xRoot - (width >> 1);
			w->saveWc.y = yRoot + (w->input.top >> 1);

			md->x = md->y = 0;

			maximizeWindow (w, 0);

			ms->snapOffX = ms->snapBackX;

			return TRUE;
		}
		}
	}

	return FALSE;
}

static void
moveHandleMotionEvent (CompScreen *s,
		       int	  xRoot,
		       int	  yRoot)
{
    MOVE_SCREEN (s);

    if (ms->grabIndex)
    {
	CompWindow *w;
	int	   dx, dy;
	int	   wX, wY;
	int	   wWidth, wHeight;

	MOVE_DISPLAY (s->display);

	w = md->w;

	wX      = w->serverX;
	wY      = w->serverY;
	wWidth  = w->serverWidth  + w->serverBorderWidth * 2;
	wHeight = w->serverHeight + w->serverBorderWidth * 2;

	md->x += xRoot - lastPointerX;
	md->y += yRoot - lastPointerY;

	if (w->type & CompWindowTypeFullscreenMask)
	{
	    dx = dy = 0;
	}
	else
	{
	    XRectangle workArea;
	    int	       min, max;

	    dx = md->x;
	    dy = md->y;

	    getWorkareaForOutput (s,
				  outputDeviceForWindow (w),
				  &workArea);

	    if (md->constrainY)
	    {
		if (!md->region)
		    md->region = moveGetYConstrainRegion (s);

		/* make sure that the top frame extents or the top row of
		   pixels are within what is currently our valid screen
		   region */
		if (md->region)
		{
		    int x, y, width, height;
		    int status;

		    x	   = (wX + dx - w->input.left) + w->clientFrame.left;
		    y	   = (wY + dy - w->input.top) + w->clientFrame.top;
		    width  = wWidth + w->input.left + w->input.right;
		    height = w->input.top ? w->input.top : 1;

		    status = XRectInRegion (md->region, x, y, width, height);

		    /* only constrain movement if previous position was valid */
		    if (md->status == RectangleIn)
		    {
			int xStatus = status;

			while (dx && xStatus != RectangleIn)
			{
			    xStatus = XRectInRegion (md->region,
						     x, y - dy,
						     width, height);

			    if (xStatus != RectangleIn)
				dx += (dx < 0) ? 1 : -1;

			    x = wX + dx - w->input.left;
			}

			while (dy && status != RectangleIn)
			{
			    status = XRectInRegion (md->region,
						    x, y,
						    width, height);

			    if (status != RectangleIn)
				dy += (dy < 0) ? 1 : -1;

			    y = wY + dy - w->input.top;
			}
		    }
		    else
		    {
			md->status = status;
		    }
		}
	    }

	    if (handleMaximizedSnapping(s, w, xRoot, yRoot, workArea))
		    return;

	    if (w->state & CompWindowStateMaximizedVertMask)
	    {
		min = workArea.y + w->input.top;
		max = workArea.y + workArea.height - w->input.bottom - wHeight;

		if (wY + dy < min || wY + dy > max)
		    dy = min - wY;
	    }

	    if (w->state & CompWindowStateMaximizedHorzMask)
	    {
		if (wX > s->width || wX + w->width < 0)
		    return;

		if (wX + wWidth < 0)
		    return;

		min = workArea.x + w->input.left;
		max = workArea.x + workArea.width - w->input.right - wWidth;

		if (wX + dx < min)
		    dx = min - wX;
		else if (wX + dx > max)
		    dx = max - wX;
	    }
	}

	if (dx || dy)
	{
	    moveWindow (w,
			wX + dx - w->attrib.x,
			wY + dy - w->attrib.y,
			TRUE, FALSE);

	    if (md->opt[MOVE_DISPLAY_OPTION_LAZY_POSITIONING].value.b)
	    {
		/* FIXME: This form of lazy positioning is broken and should
		   be replaced asap. Current code exists just to avoid a
		   major performance regression in the 0.5.2 release. */
		w->serverX = w->attrib.x;
		w->serverY = w->attrib.y;
	    }
	    else
	    {
		syncWindowPosition (w);
	    }

	    md->x -= dx;
	    md->y -= dy;
	}
    }
}

static CompBool
updateCursorCB (void *data)
{
    CompScreen *s = data;

    MOVE_SCREEN (s);

    if (ms->moveCursor)
	XFreeCursor (s->display->display, ms->moveCursor);
    ms->moveCursor = XCreateFontCursor (s->display->display, XC_fleur);

    return FALSE;
}

static void
moveHandleEvent (CompDisplay *d,
		 XEvent      *event)
{
    CompScreen *s;

    MOVE_DISPLAY (d);

    switch (event->type) {
    case ButtonPress:
    case ButtonRelease:
	s = findScreenAtDisplay (d, event->xbutton.root);
	if (s)
	{
	    MOVE_SCREEN (s);

	    if (ms->grabIndex)
	    {
		if (md->releaseButton == -1 ||
		    md->releaseButton == event->xbutton.button)
		{
		    CompAction *action;
		    int        opt = MOVE_DISPLAY_OPTION_INITIATE_BUTTON;

		    action = &md->opt[opt].value.action;
		    moveTerminate (d, action, CompActionStateTermButton,
				   NULL, 0);
		}
	    }

#define BUTTON_IS(direction) \
	((event->xbutton.state & (ShiftMask|ControlMask|Mod1Mask)) == \
	    md->opt[MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_##direction] \
	      .value.action.button.modifiers && \
	  event->xbutton.button == \
	    md->opt[MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_##direction] \
	      .value.action.button.button)

	    /* Scrolling wheel immediately moves the window */
	    if (md->opt[MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL].value.b &&
		(BUTTON_IS(DOWN) || BUTTON_IS(UP) ||
		 BUTTON_IS(LEFT) || BUTTON_IS(RIGHT)))
	    {
		CompWindow *w;
		w = findWindowAtDisplay (d, event->xbutton.window);
		if (w && (w->actions & CompWindowActionMoveMask))
		{
		    int left, right, top, bottom;
		    XRectangle workArea;

		    int amount =
			md->opt[MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_SPEED]
			  .value.i;
		    int dx = 0, dy = 0;

		    left   = (w->serverX - w->input.left) + w->clientFrame.left;
		    top    = (w->serverY - w->input.top) + w->clientFrame.top;
		    right  = left + w->serverWidth + w->serverBorderWidth * 2
			       + w->input.left + w->input.right;
		    bottom = top + w->serverHeight + w->serverBorderWidth * 2
			       + w->input.top + w->input.bottom;

		    getWorkareaForOutput (s,
					  outputDeviceForWindow (w),
					  &workArea);

		    if (BUTTON_IS(UP))
		    {
			int screen_bottom = workArea.y + workArea.height;

			if (bottom > screen_bottom)
			{
			    dy = -amount;
			    if (bottom + dy < screen_bottom)
				dy = screen_bottom - bottom;
			}
		    }
		    else if (BUTTON_IS(DOWN))
		    {
			if (top < workArea.y)
			{
			    dy = amount;
			    if (top + dy > workArea.y)
				dy = workArea.y - top;
			}
		    }
		    else if (BUTTON_IS(LEFT))
		    {
			int screen_right = workArea.x + workArea.width;

			if (right > screen_right)
			{
			    dx = -amount;
			    if (right + dx < screen_right)
				dx = screen_right - right;
			}
		    }
		    else if (BUTTON_IS(RIGHT))
		    {
			if (left < workArea.x)
			{
			    dx = amount;
			    if (left + dx > workArea.x)
				dx = workArea.x - left;
			}
		    }
#undef BUTTON_IS

		    if (dx != 0 || dy != 0)
		    {
			moveWindow(w, dx, dy, TRUE, TRUE);
			syncWindowPosition (w);
		    }
		}
	    }
	}
	break;
    case KeyPress:
	s = findScreenAtDisplay (d, event->xkey.root);
	if (s)
	{
	    MOVE_SCREEN (s);

	    if (ms->grabIndex)
	    {
		int i;

		for (i = 0; i < NUM_KEYS; i++)
		{
		    if (event->xkey.keycode == md->key[i])
		    {
			XWarpPointer (d->display, None, None, 0, 0, 0, 0,
				      mKeys[i].dx * KEY_MOVE_INC,
				      mKeys[i].dy * KEY_MOVE_INC);
			break;
		    }
		}
	    }
	}
	break;
    case MotionNotify:
	s = findScreenAtDisplay (d, event->xmotion.root);
	if (s)
	    moveHandleMotionEvent (s, pointerX, pointerY);
	break;
    case EnterNotify:
    case LeaveNotify:
	s = findScreenAtDisplay (d, event->xcrossing.root);
	if (s)
	    moveHandleMotionEvent (s, pointerX, pointerY);
	break;
    case ClientMessage:
	if (event->xclient.message_type == d->wmMoveResizeAtom)
	{
	    CompWindow *w;

	    if (event->xclient.data.l[2] == WmMoveResizeMove ||
		event->xclient.data.l[2] == WmMoveResizeMoveKeyboard)
	    {
		w = findWindowAtDisplay (d, event->xclient.window);
		if (w)
		{
		    CompOption o[6];
		    int	       xRoot, yRoot;
		    int	       option;

		    o[0].type    = CompOptionTypeInt;
		    o[0].name    = "window";
		    o[0].value.i = event->xclient.window;

		    o[1].type    = CompOptionTypeBool;
		    o[1].name    = "external";
		    o[1].value.b = TRUE;

		    if (event->xclient.data.l[2] == WmMoveResizeMoveKeyboard)
		    {
			option = MOVE_DISPLAY_OPTION_INITIATE_KEY;

			moveInitiate (d, &md->opt[option].value.action,
				      CompActionStateInitKey,
				      o, 2);
		    }
		    else
		    {
			unsigned int mods;
			Window	     root, child;
			int	     i;

			option = MOVE_DISPLAY_OPTION_INITIATE_BUTTON;

			XQueryPointer (d->display, w->screen->root,
				       &root, &child, &xRoot, &yRoot,
				       &i, &i, &mods);

			/* TODO: not only button 1 */
			if (mods & Button1Mask)
			{
			    o[2].type	 = CompOptionTypeInt;
			    o[2].name	 = "modifiers";
			    o[2].value.i = mods;

			    o[3].type	 = CompOptionTypeInt;
			    o[3].name	 = "x";
			    o[3].value.i = event->xclient.data.l[0];

			    o[4].type	 = CompOptionTypeInt;
			    o[4].name	 = "y";
			    o[4].value.i = event->xclient.data.l[1];

			    o[5].type    = CompOptionTypeInt;
			    o[5].name    = "button";
			    o[5].value.i = event->xclient.data.l[3] ?
				           event->xclient.data.l[3] : -1;

			    moveInitiate (d,
					  &md->opt[option].value.action,
					  CompActionStateInitButton,
					  o, 6);

			    moveHandleMotionEvent (w->screen, xRoot, yRoot);
			}
		    }
		}
	    }
	    else if (md->w && event->xclient.data.l[2] == WmMoveResizeCancel)
	    {
		if (md->w->id == event->xclient.window)
		{
		    int option;

		    option = MOVE_DISPLAY_OPTION_INITIATE_BUTTON;
		    moveTerminate (d,
				   &md->opt[option].value.action,
				   CompActionStateCancel, NULL, 0);
		    option = MOVE_DISPLAY_OPTION_INITIATE_KEY;
		    moveTerminate (d,
				   &md->opt[option].value.action,
				   CompActionStateCancel, NULL, 0);
		}
	    }
	}
	break;
    case DestroyNotify:
	if (md->w && md->w->id == event->xdestroywindow.window)
	{
	    int option;

	    option = MOVE_DISPLAY_OPTION_INITIATE_BUTTON;
	    moveTerminate (d,
			   &md->opt[option].value.action,
			   0, NULL, 0);
	    option = MOVE_DISPLAY_OPTION_INITIATE_KEY;
	    moveTerminate (d,
			   &md->opt[option].value.action,
			   0, NULL, 0);
	}
	break;
    case UnmapNotify:
	if (md->w && md->w->id == event->xunmap.window)
	{
	    int option;

	    option = MOVE_DISPLAY_OPTION_INITIATE_BUTTON;
	    moveTerminate (d,
			   &md->opt[option].value.action,
			   0, NULL, 0);
	    option = MOVE_DISPLAY_OPTION_INITIATE_KEY;
	    moveTerminate (d,
			   &md->opt[option].value.action,
			   0, NULL, 0);
	}
	break;
    case PropertyNotify:
	if (event->xproperty.atom == XA_RESOURCE_MANAGER)
        {
	    if (event->xproperty.window == DefaultRootWindow(event->xproperty.display))
	    {
		s = findScreenAtDisplay (d, DefaultRootWindow(event->xproperty.display));
		if (s)
		{
		    MOVE_SCREEN (s);

		    /* dedup and update later when propagated everywhere */
		    if (ms->cursorUpdateTimeoutHandle)
			compRemoveTimeout(ms->cursorUpdateTimeoutHandle);
		    ms->cursorUpdateTimeoutHandle = compAddTimeout (500, 500,
							updateCursorCB, s);
		}
	    }
        }
	break;
    default:
	break;
    }

    UNWRAP (md, d, handleEvent);
    (*d->handleEvent) (d, event);
    WRAP (md, d, handleEvent, moveHandleEvent);
}

static void
drawRect (double x1, double y1, double x2, double y2,
	  unsigned short *colorTL, unsigned short *colorTR,
	  unsigned short *colorBL, unsigned short *colorBR)
{
    glBegin(GL_QUADS);
    glColor4usv(colorTL);
    glVertex3f(x1, y1, 0);
    glColor4usv(colorBL);
    glVertex3f(x1, y2, 0);
    glColor4usv(colorBR);
    glVertex3f(x2, y2, 0);
    glColor4usv(colorTR);
    glVertex3f(x2, y1, 0);
    glEnd();
}

static Bool
movePaintWindow (CompWindow		 *w,
		 const WindowPaintAttrib *attrib,
		 const CompTransform	 *transform,
		 Region			 region,
		 unsigned int		 mask)
{
    WindowPaintAttrib sAttrib;
    CompScreen	      *s = w->screen;
    Bool	      status;
    CompTransform     sTransform = *transform;

    MOVE_SCREEN (s);
    MOVE_DISPLAY (s->display);

    if (ms->grabIndex)
    {
	if (md->w == w && md->moveOpacity != OPAQUE)
	{
	    /* modify opacity of windows that are not active */
	    sAttrib = *attrib;
	    attrib  = &sAttrib;

	    sAttrib.opacity = (sAttrib.opacity * md->moveOpacity) >> 16;
	}
    }

    UNWRAP (ms, s, paintWindow);
    status = (*s->paintWindow) (w, attrib, transform, region, mask);
    WRAP (ms, s, paintWindow, movePaintWindow);

    if (md->opt[MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL].value.b &&
        w->actions & CompWindowActionMoveMask)
    {
	int left, right, top, bottom;
	XRectangle workArea;

	left   = (w->serverX - w->input.left) + w->clientFrame.left;
	top    = (w->serverY - w->input.top) + w->clientFrame.top;
	right  = left + w->serverWidth + w->serverBorderWidth * 2
		      + w->input.left + w->input.right;
	bottom = top + w->serverHeight + w->serverBorderWidth * 2
		     + w->input.top + w->input.bottom;

	getWorkareaForOutput (s, outputDeviceForWindow (w), &workArea);

	int screen_top = workArea.y;
	int screen_bottom = screen_top + workArea.height;
	int screen_left = workArea.x;
	int screen_right = screen_left + workArea.width;

	if (!(bottom < screen_top || top > screen_bottom ||
	      right < screen_left || left > screen_right) &&
	     (top < screen_top || bottom > screen_bottom ||
	      left < screen_left || right > screen_right))
	{
	    glPushMatrix ();
	    glLoadMatrixf (sTransform.m);

	    Bool gradient =
	      md->opt[MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_GRADIENT].value.b;
	    unsigned short *color =
	      md->opt[MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_COLOR].value.c;
	    unsigned short colorT[4] = { color[0], color[1], color[2],
					 gradient ? 0 : color[3] };
	    unsigned width =
	      md->opt[MOVE_DISPLAY_OPTION_OFFSCREEN_SCROLL_WIDTH].value.i;

	    glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	    glEnable (GL_BLEND);
	    if (top < screen_top)
		drawRect (left, screen_top, right, screen_top + width,
			  color, color, colorT, colorT);
	    if (bottom > screen_bottom)
		drawRect (left, screen_bottom - width, right, screen_bottom,
			  colorT, colorT, color, color);
	    if (left < screen_left)
		drawRect (screen_left, top, screen_left + width, bottom,
			  color, colorT, color, colorT);
	    if (right > screen_right)
		drawRect (screen_right - width, top, screen_right, bottom,
			  colorT, color, colorT, color);
	    glDisable (GL_BLEND);

	    glPopMatrix ();
	    glColor4usv (defaultColor);
	}
    }

    return status;
}

static CompOption *
moveGetDisplayOptions (CompPlugin  *plugin,
		       CompDisplay *display,
		       int	   *count)
{
    MOVE_DISPLAY (display);

    *count = NUM_OPTIONS (md);
    return md->opt;
}

static Bool
moveSetDisplayOption (CompPlugin      *plugin,
		      CompDisplay     *display,
		      const char      *name,
		      CompOptionValue *value)
{
    CompOption *o;
    int	       index;

    MOVE_DISPLAY (display);

    o = compFindOption (md->opt, NUM_OPTIONS (md), name, &index);
    if (!o)
	return FALSE;

    switch (index) {
    case MOVE_DISPLAY_OPTION_OPACITY:
	if (compSetIntOption (o, value))
	{
	    md->moveOpacity = (o->value.i * OPAQUE) / 100;
	    return TRUE;
	}
	break;
    default:
	return compSetDisplayOption (display, o, value);
    }

    return FALSE;
}

static const CompMetadataOptionInfo moveDisplayOptionInfo[] = {
    { "initiate_button", "button", 0, moveInitiate, moveTerminate },
    { "initiate_key", "key", 0, moveInitiate, moveTerminate },
    { "opacity", "int", "<min>0</min><max>100</max>", 0, 0 },
    { "constrain_y", "bool", 0, 0, 0 },
    { "snapoff_maximized", "bool", 0, 0, 0 },
    { "lazy_positioning", "bool", 0, 0, 0 },
    { "offscreen_scroll", "bool", 0, 0, 0 },
    { "offscreen_scroll_speed", "int", "<min>1</min><max>100</max>", 0, 0 },
    { "offscreen_scroll_down", "button", 0, 0, 0 },
    { "offscreen_scroll_up", "button", 0, 0, 0 },
    { "offscreen_scroll_right", "button", 0, 0, 0 },
    { "offscreen_scroll_left", "button", 0, 0, 0 },
    { "offscreen_scroll_color", "color", 0, 0, 0 },
    { "offscreen_scroll_width", "int", "<min>0</min><max>100</max>", 0, 0 },
    { "offscreen_scroll_gradient", "bool", 0, 0, 0 },
};

static Bool
moveInitDisplay (CompPlugin  *p,
		 CompDisplay *d)
{
    MoveDisplay *md;
    int	        i;

    if (!checkPluginABI ("core", CORE_ABIVERSION))
	return FALSE;

    md = malloc (sizeof (MoveDisplay));
    if (!md)
	return FALSE;

    if (!compInitDisplayOptionsFromMetadata (d,
					     &moveMetadata,
					     moveDisplayOptionInfo,
					     md->opt,
					     MOVE_DISPLAY_OPTION_NUM))
    {
	free (md);
	return FALSE;
    }

    md->screenPrivateIndex = allocateScreenPrivateIndex (d);
    if (md->screenPrivateIndex < 0)
    {
	compFiniDisplayOptions (d, md->opt, MOVE_DISPLAY_OPTION_NUM);
	free (md);
	return FALSE;
    }

    md->moveOpacity =
	(md->opt[MOVE_DISPLAY_OPTION_OPACITY].value.i * OPAQUE) / 100;

    md->w             = 0;
    md->region        = NULL;
    md->status        = RectangleOut;
    md->releaseButton = 0;
    md->constrainY    = FALSE;

    for (i = 0; i < NUM_KEYS; i++)
	md->key[i] = XKeysymToKeycode (d->display,
				       XStringToKeysym (mKeys[i].name));

    WRAP (md, d, handleEvent, moveHandleEvent);

    d->base.privates[displayPrivateIndex].ptr = md;

    return TRUE;
}

static void
moveFiniDisplay (CompPlugin  *p,
		 CompDisplay *d)
{
    MOVE_DISPLAY (d);

    freeScreenPrivateIndex (d, md->screenPrivateIndex);

    UNWRAP (md, d, handleEvent);

    compFiniDisplayOptions (d, md->opt, MOVE_DISPLAY_OPTION_NUM);

    if (md->region)
	XDestroyRegion (md->region);

    free (md);
}

static Bool
moveInitScreen (CompPlugin *p,
		CompScreen *s)
{
    MoveScreen *ms;

    MOVE_DISPLAY (s->display);

    ms = malloc (sizeof (MoveScreen));
    if (!ms)
	return FALSE;

    ms->grabIndex = 0;

    ms->moveCursor = XCreateFontCursor (s->display->display, XC_fleur);
    ms->cursorUpdateTimeoutHandle = 0;

    WRAP (ms, s, paintWindow, movePaintWindow);

    s->base.privates[md->screenPrivateIndex].ptr = ms;

    return TRUE;
}

static void
moveFiniScreen (CompPlugin *p,
		CompScreen *s)
{
    MOVE_SCREEN (s);

    UNWRAP (ms, s, paintWindow);

    if (ms->moveCursor)
	XFreeCursor (s->display->display, ms->moveCursor);

    free (ms);
}

static CompBool
moveInitObject (CompPlugin *p,
		CompObject *o)
{
    static InitPluginObjectProc dispTab[] = {
	(InitPluginObjectProc) 0, /* InitCore */
	(InitPluginObjectProc) moveInitDisplay,
	(InitPluginObjectProc) moveInitScreen
    };

    RETURN_DISPATCH (o, dispTab, ARRAY_SIZE (dispTab), TRUE, (p, o));
}

static void
moveFiniObject (CompPlugin *p,
		CompObject *o)
{
    static FiniPluginObjectProc dispTab[] = {
	(FiniPluginObjectProc) 0, /* FiniCore */
	(FiniPluginObjectProc) moveFiniDisplay,
	(FiniPluginObjectProc) moveFiniScreen
    };

    DISPATCH (o, dispTab, ARRAY_SIZE (dispTab), (p, o));
}

static CompOption *
moveGetObjectOptions (CompPlugin *plugin,
		      CompObject *object,
		      int	 *count)
{
    static GetPluginObjectOptionsProc dispTab[] = {
	(GetPluginObjectOptionsProc) 0, /* GetCoreOptions */
	(GetPluginObjectOptionsProc) moveGetDisplayOptions
    };

    *count = 0;
    RETURN_DISPATCH (object, dispTab, ARRAY_SIZE (dispTab),
		     (void *) count, (plugin, object, count));
}

static CompBool
moveSetObjectOption (CompPlugin      *plugin,
		     CompObject      *object,
		     const char      *name,
		     CompOptionValue *value)
{
    static SetPluginObjectOptionProc dispTab[] = {
	(SetPluginObjectOptionProc) 0, /* SetCoreOption */
	(SetPluginObjectOptionProc) moveSetDisplayOption
    };

    RETURN_DISPATCH (object, dispTab, ARRAY_SIZE (dispTab), FALSE,
		     (plugin, object, name, value));
}

static Bool
moveInit (CompPlugin *p)
{
    if (!compInitPluginMetadataFromInfo (&moveMetadata,
					 p->vTable->name,
					 moveDisplayOptionInfo,
					 MOVE_DISPLAY_OPTION_NUM,
					 0, 0))
	return FALSE;

    displayPrivateIndex = allocateDisplayPrivateIndex ();
    if (displayPrivateIndex < 0)
    {
	compFiniMetadata (&moveMetadata);
	return FALSE;
    }

    compAddMetadataFromFile (&moveMetadata, p->vTable->name);

    return TRUE;
}

static void
moveFini (CompPlugin *p)
{
    freeDisplayPrivateIndex (displayPrivateIndex);
    compFiniMetadata (&moveMetadata);
}

static CompMetadata *
moveGetMetadata (CompPlugin *plugin)
{
    return &moveMetadata;
}

CompPluginVTable moveVTable = {
    "move",
    moveGetMetadata,
    moveInit,
    moveFini,
    moveInitObject,
    moveFiniObject,
    moveGetObjectOptions,
    moveSetObjectOption
};

CompPluginVTable *
getCompPluginInfo20070830 (void)
{
    return &moveVTable;
}
