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

/* the list of loaded plugins */
pluginlist_t	plugin_list	= LIST_HEAD_INITIALIZER(plugin_list);

/*
 * when calling into a plugin this is set to it's plugin_t,
 * all calls into plugins must save the previous value of
 * this and restore it after the call.
 */
plugin_t	*plugin_this	= NULL;

/*
 * lists for all plugin callbacks; when events generating
 * callbacks occur, the corresponding list is traversed and all
 * callout routines are called.
 */
static callbacklist_t callbacklist[PCALL_COUNT];

/*
 * plugin initialization, just set up all the plugin
 * callback lists.
 */
void plugin_init() {
	int i;

	for (i = 0; i < PCALL_COUNT; i++)
		LIST_INIT(&callbacklist[i]);
}

/* unload all plugins */
void plugin_shutdown() {
	plugin_t *plugin;

	while (!LIST_EMPTY(&plugin_list)) {
		plugin = LIST_FIRST(&plugin_list);
		plugin_unload(plugin);
	}
}

/*
 * find a plugin on the plugin path, and attempt to load it.  return
 * a null if unsuccessful, or the dlopen hnd if successful.
 */
static __inline void *loadplugin(char *name) {
	char path[MAXPATHLEN];
	char *pluginpath, *tmp, *c, *plugindir;
	char *usedplugdir = NULL;
	void *hnd;

	/* make a local copy of pluginpath for strsep */
	pluginpath = strdup(PLUGINPATH);
	if (!pluginpath)
		return NULL;
	tmp = pluginpath;

	/*
	 * loop through the : deliminated plugin path and look for
	 * a file at each node.  if the file exists, a dlopen is
	 * attempted.
	 */
	hnd = NULL;
	while ((plugindir = strsep(&tmp, ":")) != NULL) {
		/*
		 * replace ~ with the path to the user's
		 * home directory
		 */
		if ((c = strchr(plugindir, '~')) != NULL) {
			char *newdir;
			int olen, hplen;

			olen = strlen(plugindir);
			newdir = malloc(olen + 2);
			if (!newdir)
				continue;

			*c = '\0';
			strcpy(newdir, plugindir);
			strcat(&newdir[c - plugindir], "%s");
			strcat(&newdir[c - plugindir + 2], &c[1]);

			hplen = strlen(homedir_path);
			usedplugdir = malloc(hplen + olen + 1);
			if (!usedplugdir) {
				free(newdir);
				continue;
			}
			snprintf(usedplugdir, hplen + olen + 1,
				newdir, homedir_path);
			free(newdir);

			plugindir = usedplugdir;
		}

		/* build the full path */
		if (snprintf(path, MAXPATHLEN, "%s/%s.so",
				plugindir, name) >= MAXPATHLEN) {
			if (usedplugdir) {
				free(usedplugdir);
				usedplugdir = NULL;
			}
			warnx("path to plugin exceeds MAXPATHLEN");
			continue;
		}

		/*
		 * free usedplugdir if we allocated it when translating
		 * ~ to $HOME, and try to access the file.
		 */
		if (usedplugdir) {
			free(usedplugdir);
			usedplugdir = NULL;
		}
		if (access(path, R_OK) != 0)
			continue;

		/*
		 * the access worked, so try to dlopen it.
		 */
		hnd = dlopen(path, RTLD_LAZY);
		if (hnd != NULL)
			break;
		warnx("failed dlopen for %s: %s", path, dlerror());
	}

	free(pluginpath);
	return hnd;
}

/* load a plugin, call it's init */
plugin_t *plugin_load(char *name, subparams_t *params, int doinit) {
	plugin_t *plugin;
	void *hnd;

	/* attempt to load the plugin */
	hnd = loadplugin(name);
	if (!hnd) {
		warnx("unable to load plugin %s", name);
		return NULL;
	}

	/* get memory for the plugin structure */
	plugin = malloc(sizeof(plugin_t));
	if (!plugin)
		return NULL;
	warnx("%s: loaded", name);

	/* fill in the plugin structure */
	plugin->hnd		= hnd;
	plugin->name		= name;
	plugin->params.count	= params ? params->count : 0;
	plugin->params.params	= params ? params->params : NULL;
	plugin->init		= dlsym(hnd, SYM_PREFIX "init");
	plugin->shutdown	= dlsym(hnd, SYM_PREFIX "shutdown");
	plugin->start		= dlsym(hnd, SYM_PREFIX "start");
	plugin->xevent_handler	= dlsym(hnd, SYM_PREFIX "xevent_handler");
	LIST_INIT(&plugin->callback_list);
	LIST_INSERT_HEAD(&plugin_list, plugin, p_list);

	/*
	 * call the plugin init function if we're supposed to
	 * and it exists.  we may be instructed not to call init
	 * because if a plugin forplug exists for this plugin,
	 * the call to init will be delayed until after the
	 * forplug is parsed.
	 */
	if (doinit && plugin->init) {
		plugin_t *oldthis = plugin_this;

		plugin_this = plugin;
		if (plugin->init() == PLUGIN_UNLOAD)
			plugin_unload(plugin);
		else
			plugin_subparams_free(&plugin->params);
		plugin_this = oldthis;
	}

	return plugin;
}

/* unload a plugin, call it's shutdown */
void plugin_unload(plugin_t *plugin) {
	plugin_t *oldthis;

	/* call shutdown if it's there */
	if (plugin->shutdown) {
		oldthis = plugin_this;
		plugin_this = plugin;
		plugin->shutdown();
		plugin_this = oldthis;
	}

	/*
	 * deallocate all callbacks that the plugin
	 * registered.
	 */
	while (!LIST_EMPTY(&plugin->callback_list))
		plugin_callback_rm(LIST_FIRST(&plugin->callback_list));

	/*
	 * get rid of the params (they still exist if init()
	 * unloaded the plugin)
	 */
	plugin_subparams_free(&plugin->params);

#ifndef USE_DMALLOC
	/* don't do this while dmallocing or dmalloc freaks out */
	if (dlclose(plugin->hnd))
		warnx("failed dlclose: %s", dlerror());
#endif
	warnx("%s: unloaded", plugin->name);
	LIST_REMOVE(plugin, p_list);
	free(plugin->name);
	free(plugin);
}

/* start all the loaded plugins */
void plugin_start() {
	plugin_t *plugin, *oldthis, *next;

	oldthis = plugin_this;
	plugin = LIST_FIRST(&plugin_list);
	while (plugin) {
		next = LIST_NEXT(plugin, p_list);
		if (plugin->start) {
			plugin_this = plugin;
			if (plugin->start() == PLUGIN_UNLOAD)
				plugin_unload(plugin);
		}
		plugin = next;
	}
	plugin_this = oldthis;
}

/* call the xevent handler for a plugin */
void plugin_handle_event(plugin_t *plugin, XEvent *e) {
	plugin_t *oldthis;

	if (!plugin->xevent_handler)
		return;
	oldthis = plugin_this;
	plugin_this = plugin;
	if (plugin->xevent_handler(e) == PLUGIN_UNLOAD)
		plugin_unload(plugin);
	plugin_this = oldthis;
}

/* add a callback handler for a plugin */
callback_t *plugin_callback_add(plugin_t *plugin, int pcall, void *handler) {
	callback_t *callback;

	/* no bogus callback numbers */
	assert(pcall < PCALL_COUNT && pcall >= 0);

	/* create the callback */
	callback = malloc(sizeof(callback_t));
	if (!callback)
		return NULL;
	callback->handler = handler;
	callback->plugin = plugin;
	callback->pcall = pcall;
	LIST_INSERT_HEAD(&plugin->callback_list, callback, cb_plug);
	LIST_INSERT_HEAD(&callbacklist[pcall], callback, cb_list);

	return callback;
}

/* remove a callback entry */
void plugin_callback_rm(callback_t *callback) {
	LIST_REMOVE(callback, cb_plug);
	LIST_REMOVE(callback, cb_list);
	free(callback);
}

/*
 * call a callback handler function with the specified
 * arguments; also provide the implicit pcall argument to the
 * callback handler.
 *
 * XXX: the translation here of va_list into real
 * parameters is somewhat hackish...
 */
static int callhandler(callback_t *callback, int argc, va_list ap) {
	intptr_t args[argc];
	plugin_t *oldthis;
	int i, ret;

	/*
	 * save old plugin_this and set the new one for
	 * the entry into the plugin callback
	 */
	oldthis = plugin_this;
	plugin_this = callback->plugin;

	/*
	 * build argument list for the call into
	 * the plugin.
	 */
	for (i = 0; i < argc; i++)
		args[i] = va_arg(ap, intptr_t);

	/*
	 * call the plugin routine passing the appropriate
	 * number of parameters.
	 */
	switch (argc) {
	case 0:
		ret = callback->handler(callback->pcall);
		break;
	case 1:
		ret = callback->handler(callback->pcall, args[0]);
		break;
	case 2:
		ret = callback->handler(callback->pcall, args[0], args[1]);
		break;
	case 3:
		ret = callback->handler(callback->pcall, args[0], args[1], args[2]);
		break;
	default:
		trace("%d arguments is currently too many...", argc);
		assert(0);
		ret = PLUGIN_OK;
	}

	/*
	 * handle possibility of a plugin unloading
	 * itself, restore plugin_this.
	 */
	if (ret == PLUGIN_UNLOAD)
		plugin_unload(callback->plugin);
	plugin_this = oldthis;

	return ret;
}

/*
 * call into all the callback handlers of type pcall that
 * are registered by this plugin.
 */
int plugin_callback(plugin_t *plugin, int pcall, int argc, ...) {
	callback_t *callback, *next;
	va_list ap;
	int ret;

	/*
	 * it is neccesary to get the next pointer before
	 * calling the callback, because the callback could
	 * cause unloading of the plugin.
	 */
	callback = LIST_FIRST(&plugin->callback_list);
	while (callback) {
		next = LIST_NEXT(callback, cb_plug);

		if (callback->pcall == pcall) {
			va_start(ap, argc);
			ret = callhandler(callback, argc, ap);
			va_end(ap);
			if (ret == PLUGIN_USING)
				return PLUGIN_USING;
		}

		callback = next;
	}

	return PLUGIN_OK;
}

/*
 * call into all the callback handlers of type pcall that
 * are registered by any plugin.
 */
int plugin_callback_all(int pcall, int argc, ...) {
	callback_t *callback, *next;
	va_list ap;
	int ret;

	callback = LIST_FIRST(&callbacklist[pcall]);
	while (callback) {
		next = LIST_NEXT(callback, cb_list);

		va_start(ap, argc);
		ret = callhandler(callback, argc, ap);
		va_end(ap);
		if (ret == PLUGIN_USING)
			return PLUGIN_USING;

		callback = next;
	}

	return PLUGIN_OK;
}

/* make a new plugin parameter */
param_t *plugin_param(char *name, char *value) {
	param_t *param;

	param = malloc(sizeof(param_t));
	if (!param)
		return NULL;
	param->name = name;
	param->value = value;

	return param;
}

/* add a parameter to a subparams */
int plugin_subparams_add(subparams_t *subparams, param_t *param) {
	param_t **tmp;

	tmp = realloc(subparams->params, (subparams->count + 1) * sizeof(param_t *));
	if (!tmp)
		return -1;
	subparams->params = tmp;
	subparams->params[subparams->count++] = param;
	
	return 0;
}

/* merge the second param of subparams onto the first */
int plugin_subparams_merge(subparams_t *subparams, subparams_t *addition) {
	param_t **tmp;

	tmp = realloc(subparams->params,
		(subparams->count + addition->count) * sizeof(param_t *));
	if (!tmp)
		return -1;
	subparams->params = tmp;
	memcpy(&subparams->params[subparams->count], addition->params, addition->count * sizeof(param_t *));
	subparams->count += addition->count;
	free(addition->params);

	return 0;
}

/* free up a tree of subparams */
void plugin_subparams_free(subparams_t *subparams) {
	int i;

	if (subparams->count) {
		for (i = 0; i < subparams->count; i++)
			plugin_param_free(subparams->params[i]);
		free(subparams->params);
		subparams->count = 0;
	}
}

/* free up a param and it's subparams */
void plugin_param_free(param_t *param) {
	plugin_subparams_free(&param->subparams);
	free(param->name);
	free(param->value);
	free(param);
}

/* try to find a plugin parameter in the first depth of a tree */
param_t *plugin_find_param(subparams_t *subparams, char *name) {
	int i;

	for (i = 0; i < subparams->count; i++)
		if (strcmp(subparams->params[i]->name, name) == 0)
			return subparams->params[i];

	return NULL;
}

/* string parameters */
int plugin_string_param(subparams_t *subparams, char *name, char **ret) {
	param_t *param;

	param = plugin_find_param(subparams, name);
	if (!param)
		return -1;

	*ret = strdup(param->value);
	return *ret ? 0 : -1;
}

/* color parameters; return an array of Pixel: one for each screen */
int plugin_color_param(subparams_t *subparams, char *name, Pixel **ret) {
	XColor clr;
	param_t *param;
	int i;

	param = plugin_find_param(subparams, name);
	if (!param)
		return -1;

	/*
	 * because differnt screens can have differnt bit depths
	 * we need to return a Pixel for each screen on the display.
	 */
	*ret = malloc(sizeof(Pixel) * ScreenCount(display));
	if (!*ret)
		return -1;
	for (i = 0; i < ScreenCount(display); i++)
		if (XParseColor(display, DefaultColormap(display, i),
				param->value, &clr)) {	
			XAllocColor(display, DefaultColormap(display, i), &clr);
			(*ret)[i] = clr.pixel;
		} else
			return free(*ret), -1;

	return 0;
}

/* pixmap parameters */
int plugin_pixmap_param(subparams_t *subparams, char *name, pixmap_t **ret) {
	param_t *param;

	param = plugin_find_param(subparams, name);
	if (!param)
		return -1;

	*ret = pixmap_ident(param->value);
	return *ret ? 0 : -1;
}

/* dgroup parameters */
int plugin_dgroup_param(subparams_t *subparams, char *name, dgroup_t **ret) {
	param_t *param;

	param = plugin_find_param(subparams, name);
	if (!param)
		return -1;

	*ret = dgroup_ident(param->value);
	return *ret ? 0 : -1;
}

/* integer parameters */
int plugin_int_param(subparams_t *subparams, char *name, int *ret) {
	param_t *param;

	param = plugin_find_param(subparams, name);
	if (!param)
		return -1;

	*ret = atoi(param->value);
	return 0;
}

/* decimal parameters */
int plugin_double_param(subparams_t *subparams, char *name, double *ret) {
	param_t *param;

	param = plugin_find_param(subparams, name);
	if (!param)
		return -1;

	*ret = atof(param->value);
	return 0;
}

/* boolean parameters */
int plugin_bool_param(subparams_t *subparams, char *name, int *ret) {
	param_t *param;

	param = plugin_find_param(subparams, name);
	if (!param)
		return -1;

	/* "true", "false", or just ret -1 */
	if (strcmp(param->value, "true") == 0)
		return *ret = 1, 0;
	else if (strcmp(param->value, "false") == 0)
		return *ret = 0, 0;
	return -1;
}

/* stacking layer parameters */
int plugin_stacklayer_param(subparams_t *subparams, char *name, int *ret) {
	param_t *param;

	param = plugin_find_param(subparams, name);
	if (!param)
		return -1;

	/* find out the layer */
	if (strcmp(param->value, "bottom") == 0)
		*ret = STACKLAYER_BOTTOM;
	else if (strcmp(param->value, "below") == 0)
		*ret = STACKLAYER_BELOW;
	else if (strcmp(param->value, "normal") == 0)
		*ret = STACKLAYER_NORMAL;
	else if (strcmp(param->value, "above") == 0)
		*ret = STACKLAYER_ABOVE;
	else if (strcmp(param->value, "top") == 0)
		*ret = STACKLAYER_TOP;
	else if (strcmp(param->value, "tiptop") == 0)
		*ret = STACKLAYER_TIPTOP;
	else
		return -1;
	return 0;
}

/* set the plugin_context of a window so plugin will get events */
void plugin_setcontext(plugin_t *plugin, Window wnd) {
	XSaveContext(display, wnd, plugin_context, (XPointer) plugin);
}

/*
 * this must be done for all windows that a plugin called
 * plugin_setcontext on before the plugin exits, otherwise
 * an event may cause an attempted xevent_handler call on an
 * already-unloaded plugin, which is bad.
 */
void plugin_rmcontext(Window wnd) {
	XDeleteContext(display, wnd, plugin_context);
}
