/**
 * Beryl Opacify 
 *
 * Copyright (c) 2006 Kristian Lyngstøl <kristian@beryl-project.org>
 *
 * 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.
 *
 *
 * Opacify increases opacity on targeted windows and reduces it on
 * blocking windows, making whatever window you are targeting easily
 * visible. 
 *
 */

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <X11/Xatom.h>
#include <X11/extensions/Xrender.h>
#include <beryl.h>

#define GET_OPACIFY_DISPLAY(d)                            \
	((OpacifyDisplay *) (d)->privates[displayPrivateIndex].ptr)
#define OPACIFY_DISPLAY(d)                                \
    OpacifyDisplay *od = GET_OPACIFY_DISPLAY (d)
#define GET_OPACIFY_SCREEN(s, od)                         \
	((OpacifyScreen *) (s)->privates[(od)->screenPrivateIndex].ptr)
#define OPACIFY_SCREEN(s)                                 \
	OpacifyScreen *os = GET_OPACIFY_SCREEN (s, GET_OPACIFY_DISPLAY (s->display))

#define OPACIFY_DISPLAY_OPTION_TOGGLE 0
#define OPACIFY_DISPLAY_OPTION_TOGGLE_FREEZE 1
#define OPACIFY_DISPLAY_OPTION_INIT_TOGGLE 2
#define OPACIFY_DISPLAY_OPTION_NUM 3

#define OPACIFY_TOGGLE_KEY "o"
#define OPACIFY_TOGGLE_MOD CompSuperMask


#define OPACIFY_SCREEN_OPTION_ACTIVE_OP 0
#define OPACIFY_SCREEN_OPTION_PASSIVE_OP 1
#define OPACIFY_SCREEN_OPTION_ONLY_IF_BLOCK 2
#define OPACIFY_SCREEN_OPTION_WINDOW_TYPE 3
#define OPACIFY_SCREEN_OPTION_TIMEOUT 4
#define OPACIFY_SCREEN_OPTION_FOCUS_INSTANT 5
#define OPACIFY_SCREEN_OPTION_NO_DELAY_CHANGE 6
#define OPACIFY_SCREEN_OPTION_NUM 7
#define NUM_OPTIONS(s) (sizeof ((s)->opt) / sizeof (CompOption))
#define N_WIN_TYPE (sizeof (winType) / sizeof (winType[0]))

/* Size of the Window array storing passive windows. */
#define MAX_WINDOWS 64

static int displayPrivateIndex = 0;

typedef struct _OpacifyDisplay
{
	int screenPrivateIndex;
	HandleEventProc handleEvent;
	Bool toggle;
	Bool toggle_reset;
	CompOption opt[OPACIFY_DISPLAY_OPTION_NUM];
} OpacifyDisplay;

static char *winType[] = {
	N_("Toolbar"),
	N_("Utility"),
	N_("Dialog"),
	N_("ModalDialog"),
	N_("Fullscreen"),
	N_("Normal")
};

typedef struct _OpacifyScreen
{
	CompWindow *new_active;
	Window active;
	Window passive[MAX_WINDOWS];
	Region intersect;
	unsigned short int passive_num;
	unsigned int active_op;
	unsigned int passive_op;
	unsigned int timeout;
	int wMask;
	Bool only_if_block;
	Bool just_moved;
	CompTimeoutHandle timeout_handle;
	CompOption opt[OPACIFY_SCREEN_OPTION_NUM];
} OpacifyScreen;

/* Core opacify functions. These do the real work. ---------------------*/

/* Finds the corrosponding CompWindow for Window id. Returns NULL if
 * there is no such window. 
 */
static CompWindow *find_window(CompScreen * s, Window id)
{
	CompWindow *w;

	for (w = s->windows; w; w = w->next)
		if (w->id == id)
			return w;
	return NULL;
}

/* Sets the real opacity and damages the window if actual opacity and 
 * requested opacity differs. */
static void set_opacity(CompWindow * w, int opacity)
{
	setWindowOpacity(w, opacity, PL_TEMP_HELLO);
}

/* Resets the Window to the original opacity if it still exists.
 */
static void reset_opacity(CompScreen * s, Window ow)
{
	CompWindow *w;

	if (!ow)
		return;
	w = find_window(s, ow);
	resetWindowOpacity(w, PL_TEMP_HELLO);
}

/* Resets the opacity of windows on the passive list.
 */
static void clear_passive(CompScreen * s)
{
	OPACIFY_SCREEN(s);
	int i;

	for (i = 0; i < os->passive_num; i++)
		reset_opacity(s, os->passive[i]);
	os->passive_num = 0;
}

/* Dim an (inactive) window. Place it on the passive list and
 * update passive_num. Then change the opacity.
 */
static void dim_window(CompScreen * s, CompWindow * w)
{
	OPACIFY_SCREEN(s);
	if (UNLIKELY(os->passive_num >= MAX_WINDOWS - 1))
	{
		fprintf(stderr, "opacify: Trying to store information "
				"about too many windows, or you hit a bug.\nIf "
				"you don't have around %d windows blocking the "
				"currently targeted window, please report this.\n",
				MAX_WINDOWS);
		return;
	}
	os->passive[os->passive_num++] = w->id;
	set_opacity(w, MIN(os->passive_op, w->paint.opacity));
}

/* Walk through all windows, skip until we've passed the active
 * window, skip if it's invisible, hidden or minimized, skip if
 * it's not a window type we're looking for. 
 * Dim it if it intersects. 
 *
 * Returns number of changed windows.
 */
static int passive_windows(CompScreen * s, Region a_region)
{
	CompWindow *w;

	OPACIFY_SCREEN(s);
	Bool flag = FALSE;
	int i = 0;

	for (w = s->windows; w; w = w->next)
	{
		if (w->id == os->active)
		{
			flag = TRUE;
			continue;
		}
		if (!flag)
			continue;
		if (!(w->type & os->wMask))
			continue;
		if (w->invisible || w->hidden || w->minimized)
			continue;
		XIntersectRegion(w->region, a_region, os->intersect);
		if (!XEmptyRegion(os->intersect))
		{
			dim_window(s, w);
			i++;
		}
	}
	return i;
}

/* Check if we switched active window, reset the old passive windows
 * if we did. If we have an active window and switched: reset that too.
 * If we have a window (w is true), update the active id and
 * passive list. just_moved is to make sure we recalculate opacity after
 * moving. We can't reset before moving because if we're using a delay
 * and the window being moved is not the active but overlapping, it will
 * be reset, which would conflict with move's opacity change. 
 * FIXME: A more final solution should be to use IPCS to signal 
 * which window is being moved. 
 */
static void opacify_handle_enter(CompScreen * s, CompWindow * w)
{
	OPACIFY_SCREEN(s);
	int num;

	if (screenGrabExist(s, "move", 0))
	{
		os->just_moved = True;
		return;
	}

	if (screenGrabExist(s, "rotate", "scale", "resize", 0))
	{
		clear_passive(s);
		reset_opacity(s, os->active);
		os->active = 0;
		return;
	}
	if (!w || os->active != w->id || os->just_moved)
	{
		os->just_moved = False;
		clear_passive(s);
		reset_opacity(s, os->active);
		os->active = 0;
	}
	if (!w)
		return;
	if (w->id != os->active && (w->type & os->wMask) && !w->shaded)
	{
		os->active = w->id;
		num = passive_windows(s, w->region);
		if (num || !os->only_if_block)
			set_opacity(w, MAX(os->active_op, w->paint.opacity));
	}
}

/* Decides what to do after a timeout occured. 
 * Either we reset the opacity because we just toggled,
 * or we handle the event.
 */
static Bool handle_timeout(void *data)
{
	CompScreen *s = (CompScreen *) data;

	OPACIFY_SCREEN(s);
	OPACIFY_DISPLAY(s->display);

	os->timeout_handle = 0;
	if (UNLIKELY(!od->toggle))
	{
		clear_passive(s);
		reset_opacity(s, os->active);
		os->active = 0;
	}
	opacify_handle_enter(s, os->new_active);

	return FALSE;
}

/* Checks whether we should delay or not.
 * Returns true if immediate execution.
 */
static inline Bool check_delay(OpacifyScreen *os, CompScreen *s, CompDisplay *d)
{
	if ((os->opt[OPACIFY_SCREEN_OPTION_FOCUS_INSTANT].value.b
	     && os->new_active && (os->new_active->id == d->activeWindow)))
		 return True;
	if (!os->timeout)
	     return True;
	if (!os->new_active || (os->new_active->id == s->root))
		return False;
	if (os->new_active->type & (CompWindowTypeDesktopMask | CompWindowTypeDockMask))
		return False;
	if (os->opt[OPACIFY_SCREEN_OPTION_NO_DELAY_CHANGE].value.b && os->passive_num)
		return True;
	return False;


}
/* Takes the inital event. 
 * If we were configured, recalculate the opacify-windows if 
 * it was our window. 
 * If a window was entered: call upon handle_timeout after os->timeout 
 * micro seconds, or directly if os->timeout is 0 (no delay).
 *
 * FIXME: In the perfect world, toggle-resetting is done in the action
 * handler that does the actual toggling.
 */
static void opacifyHandleEvent(CompDisplay * d, XEvent * event)
{
	CompScreen *s;
	CompWindow *w = NULL;

	OPACIFY_DISPLAY(d);

	UNWRAP(od, d, handleEvent);
	(*d->handleEvent) (d, event);
	WRAP(od, d, handleEvent, opacifyHandleEvent);

	if (!od->toggle && !od->toggle_reset)
		return;

	switch (event->type)
	{
	case EnterNotify:
		s = findScreenAtDisplay(d, event->xcrossing.root);
		if (LIKELY(s))
		{
			OPACIFY_SCREEN(s);
			if (!od->toggle && !os->active)
				return;
			os->new_active = findTopLevelWindowAtScreen(s, event->xcrossing.window);
			if (os->timeout_handle)
				compRemoveTimeout(os->timeout_handle);
			if (check_delay(os,s,d))
				handle_timeout(s);
			else
				os->timeout_handle =
						compAddTimeout(os->timeout, handle_timeout, s);
		}
		break;
	case ConfigureNotify:
		s = findScreenAtDisplay(d, event->xconfigure.event);
		if (LIKELY(s))
		{
			OPACIFY_SCREEN(s);
			if (os->active != event->xconfigure.window)
				break;
			clear_passive(s);
			if (LIKELY(os->active))
				w = find_window(s, os->active);
			if (w)
				passive_windows(s, w->region);
		}
		break;
	default:
		break;
	}
}


/* Configuration, initialization, boring stuff. ----------------------- */
static void opacifyFiniScreen(CompPlugin * p, CompScreen * s)
{
	OPACIFY_SCREEN(s);
	OPACIFY_DISPLAY(s->display);
	if (os->opt[OPACIFY_SCREEN_OPTION_WINDOW_TYPE].value.list.value)
		free(os->opt[OPACIFY_SCREEN_OPTION_WINDOW_TYPE].value.list.value);

	removeScreenAction(s, &od->opt[OPACIFY_DISPLAY_OPTION_TOGGLE].value.
					   action);
	if (os->timeout_handle)
		compRemoveTimeout(os->timeout_handle);
	XDestroyRegion(os->intersect);
	free(os);
}

static Bool opacifySetScreenOptions(CompScreen * screen, char *name,
									CompOptionValue * value)
{
	CompOption *o;
	int index;

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

	switch (index)
	{
	case OPACIFY_SCREEN_OPTION_ACTIVE_OP:
		if (compSetIntOption(o, value))
		{
			os->active_op = (o->value.i * 0xffff) / 100;
			return TRUE;
		}
		break;
	case OPACIFY_SCREEN_OPTION_PASSIVE_OP:
		if (compSetIntOption(o, value))
		{
			os->passive_op = (o->value.i * 0xffff) / 100;
			return TRUE;
		}
		break;
	case OPACIFY_SCREEN_OPTION_TIMEOUT:
		if (compSetIntOption(o, value))
		{
			os->timeout = o->value.i * 100;
			return TRUE;
		}
		break;
	case OPACIFY_SCREEN_OPTION_WINDOW_TYPE:
		if (compSetOptionList(o, value))
		{
			os->wMask = compWindowTypeMaskFromStringList(&o->value);
			return TRUE;
		}
		break;
	case OPACIFY_SCREEN_OPTION_FOCUS_INSTANT:
	case OPACIFY_SCREEN_OPTION_NO_DELAY_CHANGE:
		if (compSetBoolOption(o, value))
			return TRUE;
		break;
	case OPACIFY_SCREEN_OPTION_ONLY_IF_BLOCK:
		if (compSetBoolOption(o, value))
		{
			os->only_if_block = o->value.b;
			return TRUE;
		}
		break;
	default:
		break;
	}

	return FALSE;
}

static void opacifyScreenInitOptions(OpacifyScreen * os)
{
	CompOption *o;
	int i;

	o = &os->opt[OPACIFY_SCREEN_OPTION_ONLY_IF_BLOCK];
	o->name = "only_if_block";
	o->group = N_("Misc. options");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc = N_("Only increase opacity if a window is blocking");
	o->longDesc =
			N_
			("Only increase the opacity on the targeted window if it "
			 "has one or more windows blocking it from view.");
	o->type = CompOptionTypeBool;
	o->value.b = FALSE;
	o->advanced = False;
	os->only_if_block = FALSE;

	o = &os->opt[OPACIFY_SCREEN_OPTION_FOCUS_INSTANT];
	o->name = "focus_instant";
	o->group = N_("Misc. options");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc =
			N_
			("Bypass delay when the new active window is the focused window.");
	o->longDesc =
			N_("Do not wait if the window we are hovering is the focused "
			   "window. This allows us to instantly see the focused window."
			   "You probably want to disable this if you are not using "
			   "\"Click to Focus\".");
	o->type = CompOptionTypeBool;
	o->value.b = FALSE;
	o->advanced = True;

	o = &os->opt[OPACIFY_SCREEN_OPTION_NO_DELAY_CHANGE];
	o->name = "no_delay_change";
	o->group = N_("Misc. options");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc =
			N_
			("Bypass delay when Opacify is reducing the opacity on one or more windows already.");
	o->longDesc =
			N_("This enables you to let Opacify instantly opacify new "
			   "windows when you're already making one or more windows invisible. "
			   "Makes for faster behavior while looking through layers of hidden windows.");
	o->type = CompOptionTypeBool;
	o->value.b = FALSE;
	o->advanced = True;

	o = &os->opt[OPACIFY_SCREEN_OPTION_ACTIVE_OP];
	o->name = "active_op";
	o->group = N_("Misc. options");
	o->subGroup = N_("Opacity levels");
	o->displayHints = "";
	o->shortDesc = N_("Active Opacity");
	o->longDesc =
			N_
			("The minimum opacity to ensure a targeted window has. "
			 "A target window will have either this opacity or the "
			 "preset opacity, whichever is higher.");
	o->type = CompOptionTypeInt;
	o->value.i = 100;
	o->rest.i.min = 1;
	o->rest.i.max = 100;
	o->advanced = False;
	os->active_op = OPAQUE;

	o = &os->opt[OPACIFY_SCREEN_OPTION_PASSIVE_OP];
	o->name = "passive_op";
	o->group = N_("Misc. options");
	o->subGroup = N_("Opacity levels");
	o->displayHints = "";
	o->shortDesc = N_("Passive Opacity");
	o->longDesc =
			N_
			("The maximum opacity a window blocking the current targeted "
			 "window can have. A blocking window will have either this "
			 "opacity or the preset opacity, whichever is lower.");
	o->type = CompOptionTypeInt;
	o->value.i = 10;
	o->rest.i.min = 1;
	o->rest.i.max = 100;
	o->advanced = False;
	os->passive_op = 0xffff / 10;

	o = &os->opt[OPACIFY_SCREEN_OPTION_TIMEOUT];
	o->name = "timeout";
	o->group = N_("Misc. options");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc = N_("Delay until Opacification");
	o->longDesc =
			N_
			("The delay in 1/10th of a second before Opacify changes "
			 "opacity after the active window has changed.");
	o->type = CompOptionTypeInt;
	o->value.i = 7;
	o->rest.i.min = 0;
	o->rest.i.max = 100;
	o->advanced = False;
	os->timeout = 700;

	o = &os->opt[OPACIFY_SCREEN_OPTION_WINDOW_TYPE];
	o->name = "window_types";
	o->group = N_("Misc. options");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc = N_("Window Types");
	o->longDesc = N_("Window types that should be Opacified.");
	o->type = CompOptionTypeList;
	o->value.list.type = CompOptionTypeString;
	o->value.list.nValue = N_WIN_TYPE;
	o->value.list.value = malloc(sizeof(CompOptionValue) * N_WIN_TYPE);
	for (i = 0; i < N_WIN_TYPE; i++)
		o->value.list.value[i].s = strdup(winType[i]);
	o->rest.s.string = (char **)windowTypeString;
	o->rest.s.nString = nWindowTypeString;
	o->advanced = True;
	os->wMask = compWindowTypeMaskFromStringList(&o->value);


}

static Bool opacifyInitScreen(CompPlugin * p, CompScreen * s)
{
	OPACIFY_DISPLAY(s->display);
	OpacifyScreen *os = (OpacifyScreen *) calloc(1, sizeof(OpacifyScreen));

	s->privates[od->screenPrivateIndex].ptr = os;
	os->intersect = XCreateRegion();
	opacifyScreenInitOptions(os);
	os->just_moved = False;
	addScreenAction(s, &od->opt[OPACIFY_DISPLAY_OPTION_TOGGLE].value.action);
	return TRUE;
}

static CompOption *opacifyGetScreenOptions(CompScreen * screen, int *count)
{
	if (screen)
	{
		OPACIFY_SCREEN(screen);

		*count = NUM_OPTIONS(os);
		return os->opt;
	}
	else
	{
		OpacifyScreen *os = malloc(sizeof(OpacifyScreen));

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

static Bool opacify_toggle(CompDisplay * d, CompAction * ac,
						   CompActionState state, CompOption * option,
						   int nOption)
{
	OPACIFY_DISPLAY(d);
	od->toggle = !od->toggle;
	return TRUE;
}

static void opacifyDisplayInitOptions(OpacifyDisplay * od)
{
	CompOption *o;

	o = &od->opt[OPACIFY_DISPLAY_OPTION_TOGGLE];
	o->name = "toggle";
	o->group = N_("Bindings");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc = N_("Toggle Opacify");
	o->longDesc =
			N_
			("Use this to enable/disable Opacify on the fly. Previously "
			 "opacified windows will not be reset once you disable it like this.");
	o->type = CompOptionTypeAction;
	o->value.action.initiate = opacify_toggle;
	o->value.action.terminate = 0;
	o->value.action.bell = FALSE;
	o->value.action.edgeMask = 0;
	o->value.action.type = CompBindingTypeKey;
	o->value.action.state = CompActionStateInitKey;
	o->value.action.key.modifiers = OPACIFY_TOGGLE_MOD;
	o->value.action.key.keysym = XStringToKeysym(OPACIFY_TOGGLE_KEY);
	o->advanced = False;

	o = &od->opt[OPACIFY_DISPLAY_OPTION_TOGGLE_FREEZE];
	o->name = "toggle_reset";
	o->group = N_("Misc. options");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc = N_("Reset opacity to original values when toggling");
	o->longDesc =
			N_
			("Reset the opacity of all windows modified by opacify when "
			 "toggling Opacify with the defined key-combination.");
	o->type = CompOptionTypeBool;
	o->value.b = TRUE;
	o->advanced = True;
	od->toggle_reset = TRUE;

	o = &od->opt[OPACIFY_DISPLAY_OPTION_INIT_TOGGLE];
	o->name = "init_toggle";
	o->group = N_("Misc. options");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc = N_("Toggle opacify on by default");
	o->longDesc =
			N_
			("With this enabled, opacify will be on when you load Opacify, "
			 "which is usually when you start Beryl.");
	o->type = CompOptionTypeBool;
	o->value.b = True;
	o->advanced = False;
	od->toggle = True;
}

static Bool opacifySetDisplayOptions(CompDisplay * display, char *name,
									 CompOptionValue * value)
{
	CompOption *o;
	int index;

	OPACIFY_DISPLAY(display);

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

	switch (index)
	{
	case OPACIFY_DISPLAY_OPTION_TOGGLE:
		if (setDisplayAction(display, o, value))
			return TRUE;
		break;
	case OPACIFY_DISPLAY_OPTION_INIT_TOGGLE:
		if (compSetBoolOption(o, value))
		{
			od->toggle = o->value.b;
			return TRUE;
		}
		break;
	case OPACIFY_DISPLAY_OPTION_TOGGLE_FREEZE:
		if (compSetBoolOption(o, value))
		{
			od->toggle_reset = o->value.b;
			return TRUE;
		}
		break;
	default:
		break;
	}
	return FALSE;
}

static CompOption *opacifyGetDisplayOptions(CompDisplay * display, int *count)
{
	if (display)
	{
		OPACIFY_DISPLAY(display);
		*count = NUM_OPTIONS(od);
		return od->opt;
	}
	else
	{
		OpacifyDisplay *od = malloc(sizeof(OpacifyDisplay));

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

static Bool opacifyInitDisplay(CompPlugin * p, CompDisplay * d)
{
	OpacifyDisplay *od = (OpacifyDisplay *) malloc(sizeof(OpacifyDisplay));

	od->screenPrivateIndex = allocateScreenPrivateIndex(d);
	if (od->screenPrivateIndex < 0)
	{
		free(od);
		return FALSE;
	}
	opacifyDisplayInitOptions(od);
	d->privates[displayPrivateIndex].ptr = od;
	WRAP(od, d, handleEvent, opacifyHandleEvent);
	return TRUE;
}

static void opacifyFiniDisplay(CompPlugin * p, CompDisplay * d)
{
	OPACIFY_DISPLAY(d);
	UNWRAP(od, d, handleEvent);
	freeScreenPrivateIndex(d, od->screenPrivateIndex);
	free(od);
}

static Bool opacifyInit(CompPlugin * p)
{
	displayPrivateIndex = allocateDisplayPrivateIndex();
	if (displayPrivateIndex < 0)
		return FALSE;
	return TRUE;
}

static void opacifyFini(CompPlugin * p)
{
	if (displayPrivateIndex >= 0)
		freeDisplayPrivateIndex(displayPrivateIndex);
}

CompPluginVTable opacifyVTable = {
	"opacify",
	N_("Opacify"),
	N_("Make windows easily visible by hovering the mouse over them"),
	opacifyInit,
	opacifyFini,
	opacifyInitDisplay,
	opacifyFiniDisplay,
	opacifyInitScreen,
	opacifyFiniScreen,
	0,
	0,
	opacifyGetDisplayOptions,
	opacifySetDisplayOptions,
	opacifyGetScreenOptions,
	opacifySetScreenOptions,
	0,
	0,
	0,
	0,
	BERYL_ABI_INFO,
	"beryl-plugins",
	"accessibility",
	0,
	0,
	False,
};

CompPluginVTable *getCompPluginInfo(void)
{
	return &opacifyVTable;
}


syntax highlighted by Code2HTML, v. 0.9.1