lua.c
author Myhailo Danylenko <isbear@ukrpost.net>
Wed, 28 Nov 2012 20:17:53 +0200
changeset 146 04d19c9c1196
parent 144 690b5524b895
child 147 66a63c9609de
permissions -rw-r--r--
Fix module loading problem


/* Copyright 2009-2012 Myhailo Danylenko
 * Copyright 2011      Mikael Berthe

This file is part of mcabber-lua.

mcabber-lua 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.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>. */

#include <glib.h>
#include <gmodule.h>   // g_module_*
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <stdio.h>
#include <stdlib.h>    // getenv
#include <string.h>    // strcmp

#include <mcabber/logprint.h>    // scr_log_print
#include <mcabber/screen.h>      // scr_Beep, scr_WriteIncomingMessage
#include <mcabber/hbuf.h>        // HBB_PREFIX_INFO
#include <mcabber/commands.h>    // process_command, cmd_add, cmd_del
#include <mcabber/xmpp.h>        // xmpp_getstatus, xmpp_getstatusmsg, lconnection
#include <mcabber/xmpp_helper.h> // xmpp_add_feature, xmpp_del_feature
#include <mcabber/roster.h>      // imstatus2char, foreach_buddy, buddy_*, current_buddy, BUDDATA, ROSTER_TYPE_*
#include <mcabber/utils.h>       // from_utf8, jidtodisp
#include <mcabber/hooks.h>       // hk_add_handler, hk_del_handler
#include <mcabber/settings.h>    // settings_set, settings_del, settings_get
#include <mcabber/compl.h>       // compl_new_category, compl_add_category_word, compl_del_category_word
#include <mcabber/events.h>      // evs_*
#include <mcabber/modules.h>     // module_info_t
#include <mcabber/api.h>         // mcabber_branch, mcabber_api_version, MCABBER_API_HAVE_CMD_ID
#include <mcabber/main.h>        // mcabber_version

#include "config.h"
#include "util.h"

/// Lua for mcabber
/// Module provides embedded lua environment with some accessors to
/// mcabber functionality.

/// Options
/// * lua_init_filename - lua script, that will be loaded at startup
/// * lua_lm_debug      - if lm log handler is enabled, this option controls, whether lm log messages are dropped or passed to mcabber logging facility

//
//  module description
//

void mlua_init   (void);
void mlua_uninit (void);

#ifdef LLM_LOG_HANDLER
#define DESCRIPTION ( \
	LUA_RELEASE " scripting interface\n" \
	"Options: " OPT_MLUA_RC ", " OPT_MLUA_LM_DEBUG "\n" \
	"Command: /" MLUA_COMMAND_NAME )
#else
#define DESCRIPTION ( \
	LUA_RELEASE " scripting interface\n" \
	"Options: " OPT_MLUA_RC "\n" \
	"Command: /" MLUA_COMMAND_NAME )
#endif

module_info_t info_lua52 = {
	.branch      = MCABBER_BRANCH,
	.api         = MCABBER_API_VERSION,
	.version     = PROJECT_VERSION,
	.description = DESCRIPTION,
	.requires    = NULL,
	.init        = mlua_init,
	.uninit      = mlua_uninit,
	.next        = NULL,
};

//
//  globals
//

#ifdef MCABBER_API_HAVE_CMD_ID
static gpointer lua_cmdid;
#endif

// global lua state object, necessary for uninitialization function
static lua_State *lua = NULL;

//
//  code
//

/// Provided lua functions

// caller sould g_free result
static char *mcabber_config_filename (const char *file)
{
	const char *home = getenv ("HOME");
	if (!home)
		return NULL;
	return g_strconcat (home, "/.mcabber/", file ? file : "", NULL);
}

/// print
/// Prints its arguments to log with default priority.
/// A: something, ...
static int lua_global_print (lua_State *L)
{
	int         top = lua_gettop (L);
	int         i;
	luaL_Buffer B;
	luaL_buffinit (L, &B);
	for (i = 1; i <= top; i++) {
		int type = lua_type (L, i);
		if (i > 1)
			luaL_addchar (&B, '\t');
		if (type == LUA_TSTRING) {
			size_t len;
			const char *str = lua_tolstring (L, i, &len);
			luaL_addlstring (&B, str, len);
		} else if (type == LUA_TNUMBER)
			luaL_addstring (&B, lua_tostring (L, i)); // XXX: modifies
		else if (type == LUA_TBOOLEAN) {
			if (lua_toboolean (L, i))
				 luaL_addstring (&B, "true");
			else
				luaL_addstring (&B, "false");
		} else if (type == LUA_TNIL)
			luaL_addstring (&B, "nil");
		else {
			char xbuf[32];
			luaL_addstring (&B, luaL_typename (L, i));
			luaL_addstring (&B, ": 0x");
			snprintf (&xbuf[0], 32, "%p", lua_topointer (L, i));
			luaL_addlstring (&B, xbuf, 8); // XXX
		}
	}
	luaL_pushresult (&B);

	scr_log_print (LPRINT_LOGNORM | LPRINT_NOTUTF8, "%s", lua_tostring (L, -1));
	return 0;
}

/// dopath
/// Loads lua file from default location.
/// XXX: g_filename_from_utf8?
/// A: string (filename, without ".lua")
/// R: string (error message, optional)
static int lua_global_dopath (lua_State *L)
{
	const char *name = luaL_checkstring (L, 1);
	size_t      size = lua_rawlen (L, 1);
	char       *path;
	int         ret = 0;
	if (size > 4 && !strncmp (name + size - 4, ".lua", 4))
		path = mcabber_config_filename (name);
	else {
		char *fname = g_strconcat (name, ".lua", NULL);
		path = mcabber_config_filename (fname);
		g_free (fname);
	}

	if ((ret = luaL_loadfile (L, path)))
		scr_log_print (LPRINT_LOGNORM, "lua: Unable to compile file %s: %s", path, lua_tostring (L, -1));
	else if ((ret = lua_pcall (L, 0, LUA_MULTRET, 0)))
		scr_log_print (LPRINT_LOGNORM, "lua: Runtime error in file %s: %s", path, lua_tostring (L, -1));
	g_free (path);

	if (ret)
		return 1;
	else
		return 0;
}

/// yes or no ansvers
/// G:
static const string2enum_t lua_yesno[] = {
	{ "1",       1 },
	{ "0",       0 },
	{ "enable",  1 },
	{ "disable", 0 },
	{ "true",    1 },
	{ "false",   0 },
	{ "on",      1 },
	{ "off",     0 },
	{ "yes",     1 },
	{ "no",      0 },
	{ NULL,     -1 },
};

/// main.yesno
/// According to yes or no ansvers returns true or false.
/// If ansver is not recognized, returns nil.
/// A: anything (string expected)
/// R: boolean or nil
static int lua_main_yesno (lua_State *L)
{
	int type = lua_type (L, 1);
	if (type == LUA_TSTRING) {
		int ret = luaL_checkenum (L, 1, lua_yesno);
		if (ret == -1)
			lua_pushnil (L);
		else
			lua_pushboolean (L, ret);
	} else if (type == LUA_TNUMBER)
		lua_pushboolean (L, lua_tointeger (L, 1));
	else if (type != LUA_TBOOLEAN)
		lua_pushnil (L);
	return 1;
}

/// main.version
/// Returns information about mcabber version
/// R: table
static int lua_main_version (lua_State *L)
{
	lua_createtable (L, 0, 3);
	lua_pushstring (L, mcabber_branch);
	lua_setfield (L, -2, "branch");
	lua_pushinteger (L, mcabber_api_version);
	lua_setfield (L, -2, "api");
	{
		gchar *version = mcabber_version ();
		lua_pushstring (L, version);
		g_free (version);
	}
	lua_setfield (L, -2, "version");
	lua_pushliteral (L, MCABBER_BRANCH);
	lua_setfield (L, -2, "build_branch");
	lua_pushinteger (L, MCABBER_API_VERSION);
	lua_setfield (L, -2, "build_api");
	lua_pushinteger (L, MCABBER_API_MIN);
	lua_setfield (L, -2, "build_api_min");
	return 1;
}

/// log message type
/// G:
static const string2enum_t lua_lprint[] = {
	{ "normal",  LPRINT_NORMAL  },
	{ "log",     LPRINT_LOG     },
	{ "debug",   LPRINT_DEBUG   },
	{ "notutf8", LPRINT_NOTUTF8 },
	{ NULL,      0              },
};

/// roster type
/// G:
static const string2enum_t lua_roster_type[] = {
	{ "user",    ROSTER_TYPE_USER    },
	{ "group",   ROSTER_TYPE_GROUP   },
	{ "agent",   ROSTER_TYPE_AGENT   },
	{ "room",    ROSTER_TYPE_ROOM    },
	{ "special", ROSTER_TYPE_SPECIAL },
	{ NULL,      0                   },
};

/// main.log
/// Prints message to log.
/// Note: most likely you need notutf8 flag enabled.
/// A: log message type, message, message...
static int lua_main_log (lua_State *L)
{
	int type = luaL_checkenum_multi (L, 1, lua_lprint);
	lua_concat (L, lua_gettop (L) - 1);
	scr_log_print (type, lua_tostring (L, -1));
	return 0;
}

// expects table on top
static void lua_options_callback (char *key, char *value, lua_State *L)
{
	char *loc = from_utf8 (key);
	lua_pushstring (L, loc);
	g_free (loc);
	loc = from_utf8 (value);
	lua_pushstring (L, loc);
	g_free (loc);
	lua_settable (L, -3);
}

/// main.option
/// Sets or gets value of mcabber option.
/// You can specify nil as a value to delete option.
/// If you omit option name, it returns hash table of all options.
/// A: string (option name, optional), string (value, optional)
/// R: string (value, optional)
static int lua_main_option (lua_State *L)
{
	int top = lua_gettop (L);
	if (top > 0) {
		char *name = to_utf8 (luaL_checkstring (L, 1));
		if (top > 1) { // Set
			if (lua_type (L, 2) == LUA_TNIL) // Unset
				settings_del (SETTINGS_TYPE_OPTION, name);
			else { // Set
				char *value = to_utf8 (luaL_checkstring (L, 2));
				settings_set (SETTINGS_TYPE_OPTION, name, value);
				g_free (value);
			}
			g_free (name);
			return 0;
		} else { // Get
			char *value = from_utf8 (settings_get (SETTINGS_TYPE_OPTION, name));
			if (value) {
				lua_pushstring (L, value);
				g_free (value);
			} else
				lua_pushnil (L);
			g_free (name);
			return 1;
		}
	} else { // List
		lua_newtable (L);
		settings_foreach (SETTINGS_TYPE_OPTION, (void (*)(char *key, char *val, void *ud)) lua_options_callback, L);
		return 1;
	}
}

/// main.alias
/// Sets or gets alias.
/// You can specify nil as a command to delete alias.
/// If you omit alias name, it will return hash table of all aliases.
/// A: string (alias name, optional), string (command, optional)
/// R: string (command, optional)
static int lua_main_alias (lua_State *L)
{
	int top = lua_gettop (L);
	if (top > 0) {
		char *name = to_utf8 (luaL_checkstring (L, 1));
		if (top > 1) { // Set
			if (lua_type (L, 2) == LUA_TNIL) { // Unset
				settings_del (SETTINGS_TYPE_ALIAS, name);
				compl_del_category_word (COMPL_CMD, name);
			} else { // Set
				char *value = to_utf8 (luaL_checkstring (L, 2));
				if (!settings_get (SETTINGS_TYPE_ALIAS, name))
					compl_add_category_word (COMPL_CMD, name);
				settings_set (SETTINGS_TYPE_ALIAS, name, value);
				g_free (value);
			}
			g_free (name);
			return 0;
		} else { // Get
			char *value = from_utf8 (settings_get (SETTINGS_TYPE_ALIAS, name));
			if (value) {
				lua_pushstring (L, value);
				g_free (value);
			} else
				lua_pushnil (L);
			g_free (name);
			return 1;
		}
	} else { // List
		lua_newtable (L);
		settings_foreach (SETTINGS_TYPE_ALIAS, (void (*)(char *key, char *val, void *ud)) lua_options_callback, L);
		return 1;
	}
}

/// main.bind
/// Sets or gets binding.
/// You can specify nil as a command to unbind key.
/// If you omit keycode, it will return hash table of all bindings.
/// A: string (keycode, optional), string (command, optional)
/// R: string (command, optional)
static int lua_main_binding (lua_State *L)
{
	int top = lua_gettop (L);
	if (top > 0) {
		// just to be sure...
		char *name = to_utf8 (luaL_checkstring (L, 1));
		if (top > 1) { // Set
			if (lua_type (L, 2) == LUA_TNIL) // Unset
				settings_del (SETTINGS_TYPE_BINDING, name);
			else { // Set
				char *value = to_utf8 (luaL_checkstring (L, 2));
				settings_set (SETTINGS_TYPE_BINDING, name, value);
				g_free (value);
			}
			g_free (name);
			return 0;
		} else { // Get
			char *value = from_utf8 (settings_get (SETTINGS_TYPE_BINDING, name));
			if (value) {
				lua_pushstring (L, value);
				g_free (value);
			} else
				lua_pushnil (L);
			g_free (name);
			return 1;
		}
	} else { // List
		lua_newtable (L);
		settings_foreach (SETTINGS_TYPE_BINDING, (void (*)(char *key, char *val, void *ud)) lua_options_callback, L);
		return 1;
	}
}

/// main.fileoption
/// Gets option, expanding it as filename.
/// A: string (option name)
/// R: string (expanded option value) or nil
static int lua_main_fileoption (lua_State *L)
{
	char *fname = expand_filename (settings_opt_get (luaL_checkstring (L, 1)));
	if (fname) {
		lua_pushstring (L, fname);
		g_free (fname);
	} else
		lua_pushnil (L);
	return 1;
}

#ifdef LLM_CONNECTION_ENABLE
/// main.connection
/// Returns lightuserdata of mcabber's loudmouth connection.
/// This can be very useful with lua-loudmouth, and not much otherwise.
/// R: lightuserdata or nil
static int lua_main_connection (lua_State *L)
{
	if (xmpp_is_online ())
		lua_pushlightuserdata (L, lconnection);
	else
		lua_pushnil (L);
	return 1;
}
#endif

/// main.print_info
/// Prints a system message to buddy's window.
/// A: string (jid), string (message)
static int lua_main_print_info (lua_State *L)
{
	char *jid  = to_utf8 (luaL_checkstring (L, 1));
	char *to   = jidtodisp (jid);
	char *mesg = to_utf8 (luaL_checkstring (L, 2));
	scr_write_incoming_message (to, mesg, 0, HBB_PREFIX_INFO, 0);
	g_free (mesg);
	g_free (to);
	g_free (jid);
	return 0;
}

/// main.beep
/// Beeps with system speaker.
static int lua_main_beep (lua_State *L)
{
	scr_beep ();
	return 0;
}

/// main.run
/// Runs specified mcabber command.
/// A: string
static int lua_main_run (lua_State *L)
{
	process_command (luaL_checkstring (L, 1), TRUE);
	return 0;
}

/// main.status
/// Returns your current status.
/// R: string (status letter), string (status message)
static int lua_main_status (lua_State *L)
{
	char *sm = from_utf8 (xmpp_getstatusmsg ());
	lua_pushlstring (L, &imstatus2char[xmpp_getstatus ()], 1);
	lua_pushstring (L, sm);
	g_free (sm);
	return 2;
}

// expects table on top
static void lua_rosterlist_callback (gpointer buddy, lua_State *L)
{
	char *jid = from_utf8 (buddy_getjid (buddy));
	lua_pushnumber (L, lua_rawlen (L, -1) + 1);
	lua_pushstring (L, jid);
	lua_settable (L, -3);
	g_free (jid);
}

/// main.roster
/// Returns array of jids of buddies in roster.
/// R: table
static int lua_main_roster (lua_State *L)
{
	lua_newtable (L);
	foreach_buddy (ROSTER_TYPE_USER|ROSTER_TYPE_AGENT|ROSTER_TYPE_ROOM, (void (*) (gpointer buddy, void *data)) lua_rosterlist_callback, L);
	return 1;
}

/// main.current_buddy
/// Returns jid of current selected buddy or sets current buddy to buddy with specified jid.
/// A: string (optional)
/// R: string (optional)
static int lua_main_current_buddy (lua_State *L)
{
	if (lua_gettop (L) > 0) { // Set
		// XXX: we need not convert to utf, RS works on jids/names in locale charset,
		// but will jidtodisp always correctly work on such tings?
		char *jid = jidtodisp (luaL_checkstring (L, 1));
		scr_roster_search (jid);
		g_free (jid);
		return 0;
	} else { // Get
		char *jid = from_utf8 (buddy_getjid (BUDDATA (current_buddy)));
		lua_pushstring (L, jid);
		g_free (jid);
		return 1;
	}
}

/// main.full_jid
/// Returns full jid (with current resource) of specified buddy (or current, if not specified).
/// Note, that if there are no resources online, it will return just what it got.
/// A: string (jid, optional)
/// R: string (jid)
static int lua_main_full_jid (lua_State *L)
{
	GList  *buddy;
	GSList *resources;
	GSList *resource;
	if (lua_gettop (L) > 0) {
		char *jid = from_utf8 (luaL_checkstring (L, 1));
		buddy = buddy_search_jid (jid);
		g_free (jid);
	} else
		buddy = current_buddy;
	if (!buddy)
		return 0;
	resources = buddy_getresources (BUDDATA (buddy));
	if (!resources) {
		char *loc = from_utf8 (buddy_getjid (BUDDATA (buddy)));
		lua_pushstring (L, loc);
		g_free (loc);
	} else {
		char *jid = from_utf8 (buddy_getjid (BUDDATA (buddy)));
		char *res = from_utf8 (g_slist_last (resources)->data);
		lua_pushfstring (L, "%s%c%s", jid, JID_RESOURCE_SEPARATOR, res);
		for (resource = resources; resource; resource = g_slist_next (resource))
			g_free (resource->data);
		g_slist_free (resources);
		g_free (jid);
		g_free (res);
	}
	return 1;
}

typedef struct {
	lua_State *L;
	gpointer   buddy;
} lua_state_and_buddy_t; // :)

/// resources table
/// Hash table with resource name as keys and another hash tables as values.
/// Inner tables contain resource-specific information: priority, status and message.
static void lua_buddy_resources_callback (gpointer resource, lua_state_and_buddy_t *d)
{
	char *loc = from_utf8 (resource);
	lua_pushstring (d->L, loc);
	g_free (loc);
	lua_createtable (d->L, 0, 3);
	lua_pushstring (d->L, "priority");
	lua_pushnumber (d->L, buddy_getresourceprio (d->buddy, resource));
	lua_settable   (d->L, -3);
	lua_pushstring  (d->L, "status");
	lua_pushlstring (d->L, &imstatus2char[buddy_getstatus (d->buddy, resource)], 1);
	lua_settable    (d->L, -3);
	lua_pushstring (d->L, "message");
	loc = from_utf8 (buddy_getstatusmsg (d->buddy, resource));
	lua_pushstring (d->L, loc);
	g_free (loc);
	lua_settable   (d->L, -3);
	lua_settable (d->L, -3);
	g_free (resource);
}

/// main.buddy_info
/// Returns a hash table with information on specified buddy.
/// Table contains fields type, name, onserver and resources (which points to resources table).
/// A: string (jid)
/// R: table
static int lua_main_buddy_info (lua_State *L)
{
	char                  *loc   = to_utf8 (luaL_checkstring (L, 1));
	char                  *jid   = jidtodisp (loc);
	GSList                *buddy = roster_find (jid, jidsearch, ROSTER_TYPE_USER|ROSTER_TYPE_AGENT|ROSTER_TYPE_ROOM);
	GSList                *resources;
	lua_state_and_buddy_t  snb;
	g_free (jid);
	g_free (loc);

	if (!buddy) {
		lua_pushnil (L);
		return 1;
	}

	lua_createtable (L, 0, 3);
	lua_pushstring (L, "type");
	luaL_pushenum  (L, buddy_gettype (BUDDATA (buddy)), lua_roster_type);
	lua_settable   (L, -3);
	lua_pushstring (L, "name");
	loc = from_utf8 (buddy_getname (BUDDATA (buddy)));
	lua_pushstring (L, loc);
	g_free (loc);
	lua_settable   (L, -3);
	lua_pushstring  (L, "onserver");
	lua_pushboolean (L, buddy_getonserverflag (BUDDATA (buddy)));
	lua_settable    (L, -3);
	lua_pushstring  (L, "resources");
	lua_createtable (L, 0, 0);
	snb.L     = L;
	snb.buddy = BUDDATA (buddy);
	resources = buddy_getresources (BUDDATA (buddy));
	g_slist_foreach (buddy_getresources (BUDDATA (buddy)), (GFunc) lua_buddy_resources_callback, &snb);
	g_slist_free (resources);
	lua_settable (L, -3);
	
	return 1;
}

// XMPP DISCO FEATURES

GSList *lua_added_features = NULL;

/// main.add_feature
/// Adds xmlns to disco#info features list.
/// A: string (xmlns)
static int lua_main_add_feature (lua_State *L)
{
	char *xmlns = to_utf8 (luaL_checkstring (L, 1));
	xmpp_add_feature (xmlns);
	lua_added_features = g_slist_prepend (lua_added_features, xmlns);
	return 0;
}

/// main.del_feature
/// Removes xmlns from disco#info features list.
/// A: stirng (xmlns)
static int lua_main_del_feature (lua_State *L)
{
	char   *xmlns = to_utf8 (luaL_checkstring (L, 1));
	GSList *el    = g_slist_find_custom (lua_added_features, xmlns, (GCompareFunc) strcmp);
	xmpp_del_feature (xmlns);
	if (el) {
		g_free (el->data);
		lua_added_features = g_slist_delete_link (lua_added_features, el);
	}
	return 0;
}

// MCABBER EVENTS

/// event context
/// Enum, indicating what exactly caused event function firing.
/// G:
static const string2enum_t lua_event_context[] = {
	{ "timeout", EVS_CONTEXT_TIMEOUT  },
	{ "cancel",  EVS_CONTEXT_CANCEL   },
	{ "reject",  EVS_CONTEXT_REJECT   },
	{ "accept",  EVS_CONTEXT_ACCEPT   },
	{ NULL,      0                    },
};

typedef struct {
	lua_State *L;
	int        reference;
	int        evid;
} lua_event_callback_t;

static GSList *lua_events = NULL;

static void lua_event_callback_destroy_notify (gpointer udata)
{
	lua_event_callback_t *cb = udata;

	luaL_unref (cb -> L, LUA_REGISTRYINDEX, cb->reference);
	luaL_unref (cb -> L, LUA_REGISTRYINDEX, cb->evid);
	luaL_free  (cb -> L, cb);
}

/// event function
/// Function to be called, when some event state change occurs
/// A: event context, string (event args)
/// R: boolean (if event shoud be preserved)
static gboolean lua_event_callback (guint context, const gchar *arg, gpointer userdata)
{
	lua_event_callback_t *cb = userdata;

	lua_rawgeti (cb->L, LUA_REGISTRYINDEX, cb->reference);
	luaL_pushenum (cb->L, context, lua_event_context);
	lua_pushstring (cb -> L, arg);
	if (lua_pcall (cb->L, 2, 1, 0)) {
		scr_log_print (LPRINT_LOGNORM, "lua: Event callback execution error: %s", lua_tostring (cb->L, -1));
		lua_pop (cb->L, 1);
	}

	if (lua_toboolean (cb -> L, -1))
		return TRUE;
	else {
		lua_events = g_slist_remove (lua_events, cb); // XXX
		return FALSE;
	}
}

/// main.event
/// Creates new event. If called without arguments, returns event id list.
/// A: event function (optional), string (event id), string (description, optional), integer (expiration timeout, optional)
/// R: string (event id) or nothing (creation error) or table (list of event names)
static int lua_main_event (lua_State *L)
{
	int top = lua_gettop (L);
	if (top > 0) { // Create
		lua_event_callback_t *cb;
		const char           *evid    = NULL;
		int                   timeout = 0;
		const char           *desc    = NULL;
		luaL_argcheck (L, lua_type (L, 1) == LUA_TFUNCTION, 1, "event function expected");

		if (top > 1) {
			evid = luaL_checkstring (L, 2);
			if (top > 2) {
				timeout = luaL_checkinteger (L, 3);
				if (top > 2) {
					desc = luaL_checkstring (L, 4);
					lua_pop (L, 3);
				} else
					lua_pop (L, 2);
			} else
				lua_pop (L, 1);
		}

		lua_pushvalue (L, 1); // XXX
		cb              = luaL_malloc (L, sizeof (lua_event_callback_t));
		cb -> L         = L;
		cb -> reference = luaL_ref (L, LUA_REGISTRYINDEX);
		cb -> evid      = LUA_NOREF;
		lua_events      = g_slist_prepend (lua_events, cb);

		evid = evs_new (desc, evid, timeout, lua_event_callback, cb, lua_event_callback_destroy_notify);
		if (!evid) {
			lua_events = g_slist_remove (lua_events, cb); // XXX
			return 0;
		}

		lua_pushstring (L, evid);
		lua_pushvalue (L, -1);
		cb -> evid = luaL_ref (L, LUA_REGISTRYINDEX); // XXX
		return 1;

	} else { // List
		GSList *events = evs_geteventslist ();
		GSList *event;

		lua_newtable (L);
		for (event = events; event; event = g_slist_next (event)) {
			lua_pushstring (L, event->data);
			luaL_ref (L, -2);
		}
		g_slist_free (events);

		return 1;
	}
}

// MCABBER COMMANDS

/// completion type
/// Built-it completion types can be specified as string, instead of id.
/// G:
static string2enum_t lua_completion_type[] = { // not const,  we need to modify yesno
	{ "cmd",       COMPL_CMD       },
	{ "jid",       COMPL_JID       },
	{ "urljid",    COMPL_URLJID    },
	{ "name",      COMPL_NAME      },
	{ "status",    COMPL_STATUS    },
	{ "filename",  COMPL_FILENAME  },
	{ "roster",    COMPL_ROSTER    },
	{ "buffer",    COMPL_BUFFER    },
	{ "group",     COMPL_GROUP     },
	{ "groupname", COMPL_GROUPNAME },
	{ "multiline", COMPL_MULTILINE },
	{ "room",      COMPL_ROOM      },
	{ "resource",  COMPL_RESOURCE  },
	{ "auth",      COMPL_AUTH      },
	{ "request",   COMPL_REQUEST   },
	{ "events",    COMPL_EVENTS    },
	{ "eventsid",  COMPL_EVENTSID  },
	{ "pgp",       COMPL_PGP       },
	{ "color",     COMPL_COLOR     },
	{ "otr",       COMPL_OTR       },
	{ "ortpolicy", COMPL_OTRPOLICY },
	{ "module",    COMPL_MODULE    },
	{ "yesno",     0               },
	{ NULL,        0               },
};
#define MLUA_YESNO_POS ( 22 )

#ifdef MCABBER_API_HAVE_COMPL_FLAGS
/// completion sorting order
/// Sorting order for completion category.
/// G:
static const string2enum_t lua_completion_sortorder[] = {
	{ "sort",    COMPL_FLAGS_SORT    },
	{ "direct",  COMPL_FLAGS_SORT    },
	{ "reverse", COMPL_FLAGS_REVERSE },
	{ "append",  COMPL_FLAGS_APPEND  },
	{ "prepend", COMPL_FLAGS_PREPEND },
	{ NULL,        0                 },
};
#endif

typedef struct {
	int        cbref;
	int        parse_args;
#ifdef MCABBER_API_HAVE_CMD_ID
	gpointer   cmid;
#else
	int        nameref;
#endif
	int        selfref;
	lua_State *L;
} lua_command_t;

static GSList *lua_added_categories = NULL;

// returns true if string contains errors - unclosed quotes or unvalued option
/// command arguments table
/// It can parse barewords (with escapes), double-quoted strings (with escapes), single-quoted strings (without escapes), options and arguments.
/// Arguments are separated only by whitespace, so, consequential quoted strings or barewords are one argument.
/// This strings are equal:
/// * ab\ cd\'e\\f\"
/// * "ab cd'e\\f\""
/// * 'ab cd'\''e\f"'
/// * ab" cd'"'e\f"'
/// Returned table have option names as keys, option values as values, and arguments as sequential members. -- option is supported.
/// Example: "-t jid -m 9 -- -aa bb cc" will result in { t = 'jid', m = 9, '-aa', 'bb', 'cc' }
/// Implementation notes:
/// * All options should be before any arguments. First non-option argument disables options recognizing.
/// * EOL is a cutting edge, that can cut much earlier, than you expect. Non-closed quoted strings lose leading quote and option without value loses its leading minus.
/// * Escape character just before EOL is preserved.
static int luaL_pushargs (lua_State *L, const char *args)
{
	const char  *p       = args;
	luaL_Buffer  buf;
	int          option  = 0;
	int          options = 1;

	lua_newtable (L);
	luaL_buffinit (L, &buf);
	while (*p) {
		if (*p == ' ') {
			++p;
			continue;
		}
		if (*p == '"') { // soft quote
			const char *start = ++p;
			while (*p) {
				if (*p == '\\') { // escape symbol
					luaL_addlstring (&buf, start, p - start);
					start = ++p;
					if (*p) // skip symbol
						++p;
					else // add last \ in line
						luaL_addchar (&buf, '\\');
				} else if (*p == '"') // quotation end
					break;
				else
					++p;
			}
			luaL_addlstring (&buf, start, p - start); // XXX: eats quote on eol
			if (*p)
				++p;
			else
				return 1;
		} else if (*p == '\'') { // no-escape quote
			const char *start = ++p;
			while (*p && *p != '\'')
				p++;
			luaL_addlstring (&buf, start, p - start); // XXX: eats quote on eol
			if (*p)
				++p;
			else
				return 1;
		} else { // bareword
			const char *start = p;
			while (*p) {
				if (*p == '\\') {
					luaL_addlstring (&buf, start, p - start);
					start = ++p;
					if (*p) // skip symbol
						++p;
					else // add last \ in line
						luaL_addchar (&buf, '\\');
				} else if (*p == ' ' || *p == '\'' || *p == '"')
					break;
				else
					++p;
			}
			luaL_addlstring (&buf, start, p - start);
		}

		if ((!*p) || *p == ' ') {
			const char *result;
			luaL_pushresult (&buf);

			result = lua_tostring (L, -1);
			if (options && !option && *result == '-') { // option
				if (*(result+1) == '-' && !*(result+2)) { // end of options
					lua_pop (L, 1);
					options = 0;
				} else { // option name
					lua_pushstring (L, result + 1);
					lua_remove (L, -2);
					option = 1;
				}
			} else if (option) { // opion value
				lua_settable (L, -3);
				option = 0;
			} else { // argument
				options = 0;
				luaL_ref (L, -2);
			}

			luaL_buffinit (L, &buf);
		}
	}

	if (option) {
		luaL_ref (L, -2); // XXX: eats minus on eol
		return 1;
	}

	return 0;
}

/// command function
/// Function to handle newly registered command.
/// Argument type passed depends on how command is registered.
/// A: string (arguments) or command arguments table
static void lua_main_command_handler (char *args, lua_command_t *cb)
{
	lua_rawgeti (cb->L, LUA_REGISTRYINDEX, cb->cbref);

	if (cb->parse_args)
		luaL_pushargs (cb->L, args);
	else
		lua_pushstring (cb->L, args);

	if (lua_pcall (cb->L, 1, 0, 0)) {
		scr_log_print (LPRINT_LOGNORM, "lua: Command execution error: %s", lua_tostring (cb->L, -1));
		lua_pop (cb->L, 1);
	}
}

/// main.parse_args
/// Function to parse command argument string to command arguments table.
/// A: string
/// R: table
static int lua_main_parse_args (lua_State *L)
{
	luaL_pushargs (L, luaL_checkstring (L, 1));
	return 1;
}

/// main.add_category
/// Adds completion category.
/// A: table (values are used as words for completion, optional), argument enum value (completion sorting order, optional)
/// R: integer (category id, in fact completion type) or nil
static int lua_main_add_category (lua_State *L)
{
	guint cid;
	int   top = lua_gettop ( L );
#ifdef MCABBER_API_HAVE_COMPL_FLAGS
	if ( top > 1 )
		cid = compl_new_category ( luaL_checkenum ( L, 2, lua_completion_sortorder ) );
	else
		cid = compl_new_category ( COMPL_FLAGS_SORT );
#else
	cid = compl_new_category ();
#endif
	if ( cid ) {
		if ( ( top > 0 ) && ( lua_type ( L, 1 ) == LUA_TTABLE ) ) {
			lua_pushnil ( L );
			while ( lua_next ( L, 1 ) ) {
				char * word = to_utf8 ( lua_tostring ( L, 4 ) );
				if ( word ) {
					compl_add_category_word ( cid, word );
					g_free ( word );
				}
				lua_pop ( L, 1 );
			}
		}

		lua_added_categories = g_slist_prepend ( lua_added_categories, (gpointer) ((gsize) cid) );
		lua_pushinteger ( L, cid );
	} else
		lua_pushnil ( L );

	return 1;
}

/// main.del_category
/// Removes completion category.
/// A: integer (category id)
static int lua_main_del_category (lua_State *L)
{
	guint cid = luaL_checkinteger (L, 1);
	compl_del_category (cid);
	lua_added_categories = g_slist_remove (lua_added_categories, (gpointer) ((gsize) cid));
	return 0;
}

/// main.add_completion
/// Adds word to a completion list.
/// A: integer (completion group id), string (word)
static int lua_main_add_completion (lua_State *L)
{
	guint  cid  = luaL_checkinteger (L, 1);
	char  *word = to_utf8 (luaL_checkstring (L, 2)); // XXX
	compl_add_category_word (cid, word);
	g_free (word);
	return 0;
}

/// main.del_completion
/// Removes word from a completion list.
/// A: integer (completion group id), string (word)
static int lua_main_del_completion (lua_State *L)
{
	guint  cid  = luaL_checkinteger (L, 1);
	char  *word = to_utf8 (luaL_checkstring (L, 2)); // XXX
	compl_del_category_word (cid, word);
	g_free (word);
	return 0;
}

/// main.command
/// Associates mcabber command name and lua function.
/// As a completion you can also specify a string name (see completion type) instead of table, for non-builtin, you can just pass integer id.
/// Note, that for now there are no way to unregister completion group, so, resources can be exausted easily.
/// Also note, that it ignores keys in a completion table.
/// A: string (command name), command function, boolean (parse args flag, optional), table (completions, optional)/completion type (or integer comletion group id, optional)
/// R: userdata (command object)
static int lua_main_command (lua_State *L)
{
	const char    *name  = luaL_checkstring (L, 1); // XXX: to_utf8? looks like no :/
	lua_command_t *cb;
	int            top   = lua_gettop (L);
	guint          cid   = 0;
	int            parse = 0;
	luaL_argcheck (L, lua_isfunction (L, 2), 2, "function expected");

	if (top > 2) { // parse flag provided
		parse = lua_toboolean (L, 3);

		if (top > 3) { // Completions provided
			if (lua_type (L, 4) == LUA_TTABLE) {
#ifdef MCABBER_API_HAVE_COMPL_FLAGS
				cid = compl_new_category (COMPL_FLAGS_SORT);
#else
				cid = compl_new_category ();
#endif
				if (cid) {
					lua_added_categories = g_slist_prepend (lua_added_categories, (gpointer) ((gsize) cid));
					lua_pushnil (L);
					while (lua_next (L, 4)) {
						char *word = to_utf8 (luaL_checkstring (L, -1));
						compl_add_category_word (cid, word);
						lua_pop (L, 1);
						g_free (word);
					}
				}
			} else
				cid = luaL_checkenum (L, 4, lua_completion_type);
		}
	}

	cb = lua_newuserdata (L, sizeof (lua_command_t));
	luaL_getmetatable (L, "mcabber.command");
	lua_setmetatable (L, -2);
	lua_pushvalue (L, 2);
	cb -> cbref      = luaL_ref (L, LUA_REGISTRYINDEX);
	lua_pushvalue (L, -1);
	cb -> selfref    = luaL_ref (L, LUA_REGISTRYINDEX);
	cb -> parse_args = parse;
	cb -> L          = L;
#ifdef MCABBER_API_HAVE_CMD_ID
	cb -> cmid       = cmd_add (name, "", cid, 0, (void (*) (char *p)) lua_main_command_handler, cb);
#else
	lua_pushvalue (L, 1);
	cb -> nameref    = luaL_ref (L, LUA_REGISTRYINDEX);
	cmd_add (name, "", cid, 0, (void (*) (char *p)) lua_main_command_handler, cb);
#endif

	return 1;
}

static void lua_mcabber_unregister_command (lua_State *L, lua_command_t *cb)
{
#ifdef MCABBER_API_HAVE_CMD_ID
	if (cb -> cmid)
		cmd_del (cb -> cmid);
#else
	const char *name;

	if (cb -> nameref == LUA_NOREF)
		return;

	lua_rawgeti (L, LUA_REGISTRYINDEX, cb -> nameref);
	name = lua_tostring (L, -1);
	cmd_del (name);
	luaL_unref (L, LUA_REGISTRYINDEX, cb -> nameref);
	cb -> nameref = LUA_NOREF;
	lua_pop (L, 1);
#endif
}

/// command:del
/// Unregisters given command from mcabber. Object will be destroyed later.
static int lua_mcabber_command_del (lua_State *L)
{
	lua_command_t *cb = luaL_checkudata (L, 1, "mcabber.command");
	luaL_argcheck (L, cb != NULL, 1, "mcabber command object expected");
	lua_mcabber_unregister_command (L, cb);
	if (cb -> selfref != LUA_NOREF) {
		luaL_unref (L, LUA_REGISTRYINDEX, cb -> selfref);
		cb -> selfref = LUA_NOREF;
	}
	return 0;
}

static int lua_mcabber_command_gc (lua_State *L)
{
	lua_command_t *cb = luaL_checkudata (L, 1, "mcabber.command");
	luaL_argcheck (L, cb != NULL, 1, "mcabber command object expected");
	lua_mcabber_unregister_command (L, cb);
#ifndef MCABBER_API_HAVE_CMD_ID
	if (cb -> nameref != LUA_NOREF)
		luaL_unref (L, LUA_REGISTRYINDEX, cb -> nameref);
#endif
	if (cb -> cbref != LUA_NOREF)
		luaL_unref (L, LUA_REGISTRYINDEX, cb -> cbref);
	return 0;
}

static const luaL_Reg lua_mcabber_command_reg_m[] = {
	{ "del",  lua_mcabber_command_del },
	{ "__gc", lua_mcabber_command_gc  },
	{ NULL,   NULL                 },
};

static void lua_command_init (lua_State *L)
{
	luaL_newmetatable (L, "mcabber.command");
	lua_pushvalue (L, -1);
	lua_setfield (L, -2, "__index");
	luaL_setfuncs (L, lua_mcabber_command_reg_m, 0);
	lua_pop (L, 1);
}

// TIMER

typedef struct {
	int        reference;
	guint      source;
	lua_State *L;
} lua_timer_callback_t;

static GSList *lua_timers = NULL;

static void lua_timer_callback_destroy (lua_timer_callback_t *cb)
{
	luaL_unref (cb->L, LUA_REGISTRYINDEX, cb->reference);
	lua_timers = g_slist_remove (lua_timers, (gpointer) ((gsize) cb->source));
	luaL_free (cb->L, cb);
}

/// timer function
/// Function, that will be called periodically until it returns false.
/// R: boolean
static gboolean lua_timer_callback (lua_timer_callback_t *cb)
{
	int ret;
	lua_rawgeti (cb->L, LUA_REGISTRYINDEX, cb->reference);
	if (lua_pcall (cb->L, 0, 1, 0)) {
		scr_log_print (LPRINT_LOGNORM, "lua: Timer callback execution error: %s", lua_tostring (cb->L, -1));
		lua_pop (cb->L, 1);
		return FALSE;
	}
	ret = lua_toboolean (cb->L, -1);
	lua_pop (cb->L, 1);
	return ret;
}

/// main.timer
/// Creates new timer function, that will be called periodically.
/// A: integer (interval, seconds), timer function
static int lua_main_timer (lua_State *L)
{
	int                   interval = luaL_checkint (L, 1);
	guint                 source;
	lua_timer_callback_t *cb;
	luaL_argcheck (L, lua_isfunction (L, 2), 2, "function expected");

	cb = luaL_malloc (L, sizeof (lua_timer_callback_t));
	cb->reference = luaL_ref (L, LUA_REGISTRYINDEX);
	cb->L         = L;

	source = g_timeout_add_seconds_full (MLUA_SOURCE_PRIORITY, interval, (GSourceFunc) lua_timer_callback, cb, (GDestroyNotify) lua_timer_callback_destroy);
	cb->source = source;
	lua_timers = g_slist_prepend (lua_timers, (gpointer) ((gsize) source));

	return 0;
}

// BACKGROUND PIPE READING

typedef struct {
	lua_State *L;
	guint      source;
	FILE      *fd;
	int        reference;
} lua_bgread_callback_t;

static GSList *lua_bgreads = NULL;

static gchar lua_bgread_buffer[MLUA_BGREAD_BUFFER];

static void lua_bgread_callback_destroy (lua_bgread_callback_t *cb)
{
	luaL_unref (cb->L, LUA_REGISTRYINDEX, cb->reference);
	pclose (cb->fd); // Not necessary?
	lua_bgreads = g_slist_remove (lua_bgreads, (gpointer) ((gsize) cb->source));
	luaL_free (cb->L, cb);
}

/// background reading function
/// Function, that processes output from pipe in asynchronous way.
/// A: string (data) or nil (eof)
/// R: boolean (false if reading should be terminated)
static gboolean lua_bgread_callback (GIOChannel *source, GIOCondition condition, lua_bgread_callback_t *cb)
{
	int ret = TRUE;

	if (condition | G_IO_IN) { // data
		while (TRUE) {
			gsize read = 0;
			g_io_channel_read_chars (source, lua_bgread_buffer, MLUA_BGREAD_BUFFER, &read, NULL);
			if (!read) // exhausted
				break;

			lua_rawgeti (cb->L, LUA_REGISTRYINDEX, cb->reference);
			lua_pushlstring (cb->L, lua_bgread_buffer, read);
			if (lua_pcall (cb->L, 1, 1, 0)) {
				scr_log_print (LPRINT_LOGNORM, "lua: Bgread callback execution error: %s", lua_tostring (cb->L, -1));
				lua_pop (cb->L, 1);
				return FALSE;
			}
			ret = lua_toboolean (cb->L, -1);
			lua_pop (cb->L, 1);
			if (!ret) // enough
				return FALSE;
		}
	}

	if (condition & ~G_IO_IN) { // err or hup
		lua_rawgeti (cb->L, LUA_REGISTRYINDEX, cb->reference);
		lua_pushnil (cb->L);
		if (lua_pcall (cb->L, 1, 1, 0)) {
			scr_log_print (LPRINT_LOGNORM, "lua: Bgread callback execution error: %s", lua_tostring (cb->L, -1));
			lua_pop (cb->L, 1);
			return FALSE;
		}
		ret = lua_toboolean (cb->L, -1);
		lua_pop (cb->L, 1);
	}

	return ret;
}

/// main.bgread
/// Runs specified command and passes its output to a given function.
/// A: string (command), background reading function
static int lua_main_bgread (lua_State *L)
{
	const char            *command = luaL_checkstring (L, 1);
	lua_bgread_callback_t *cb;
	FILE                  *fd;
	GIOChannel            *channel;
	const char            *charset = NULL;
	guint                  source;
	luaL_argcheck (L, lua_isfunction (L, 2), 2, "function expected");

	fd = popen (command, "r");
	if (!fd) {
		lua_pushstring (L, "Error opening pipe");
		lua_error (L);
	}

	channel = g_io_channel_unix_new (fileno (fd));
	// We, most likely, need this,
	// But we cannot use this,
	// It will block.
	//if (!g_get_charset (&charset))
	g_io_channel_set_encoding (channel, charset, NULL);
	g_io_channel_set_buffered (channel, FALSE);
	g_io_channel_set_flags (channel, G_IO_FLAG_NONBLOCK, NULL);
	g_io_channel_set_close_on_unref (channel, TRUE);

	cb = luaL_malloc (L, sizeof (lua_bgread_callback_t));
	cb->reference = luaL_ref (L, LUA_REGISTRYINDEX);
	cb->L         = L;
	cb->fd        = fd;

	source = g_io_add_watch_full (channel, MLUA_SOURCE_PRIORITY, G_IO_IN|G_IO_HUP|G_IO_ERR, (GIOFunc) lua_bgread_callback, cb, (GDestroyNotify) lua_bgread_callback_destroy);
	cb->source = source;
	lua_bgreads = g_slist_prepend (lua_bgreads, (gpointer) ((gsize) source));

	// unref?

	return 0;
}

// HOOK HANDLING

typedef struct {
	lua_State *L;       // lua environment for handler use
	int        nameref; // reference to hook name string
	int        cbref;   // reference to hook handler function
	int        selfref; // self-reference to object
	guint      hid;     // hook id for object destruction
} lua_hook_t;

/// hook handler result
/// What to do with hook processing afterwards
/// G:
static const string2enum_t lua_hook_handler_result[] = {
	{ "proceed", HOOK_HANDLER_RESULT_ALLOW_MORE_HANDLERS       },
	{ "stop",    HOOK_HANDLER_RESULT_NO_MORE_HANDLER           },
	{ "drop",    HOOK_HANDLER_RESULT_NO_MORE_HANDLER_DROP_DATA },
	{ NULL,      HOOK_HANDLER_RESULT_ALLOW_MORE_HANDLERS       },
};

/// hook handler priority
/// Feel free to specify just a number instead of some preset value.
/// G:
static const string2enum_t lua_hook_handler_priority[] = {
	{ "high",      G_PRIORITY_HIGH         },
	{ "default",   G_PRIORITY_DEFAULT      },
	{ "idle-high", G_PRIORITY_HIGH_IDLE    },
	{ "idle",      G_PRIORITY_DEFAULT_IDLE },
	{ "low",       G_PRIORITY_LOW          },
	{ NULL,        G_PRIORITY_DEFAULT      },
};

/// hook function
/// Function to be called, when hook will be processsed.
/// XXX: we can provide object as argument, but is this necessary?
/// A: table (arguments hash)
/// R: hook handler result
static guint lua_hook_cb (const gchar *hookid, hk_arg_t *args, gpointer data)
{
	lua_hook_t *cb  = data;
	hk_arg_t   *arg = args;
	guint       ret = HOOK_HANDLER_RESULT_ALLOW_MORE_HANDLERS;
	lua_State  *L   = cb -> L;

	if (cb -> cbref == LUA_NOREF)
		return HOOK_HANDLER_RESULT_ALLOW_MORE_HANDLERS;

	lua_rawgeti (L, LUA_REGISTRYINDEX, cb -> cbref);
	if (!lua_isfunction (L, -1)) {
		lua_pop (L, 1);
		return HOOK_HANDLER_RESULT_ALLOW_MORE_HANDLERS;
	}

	lua_newtable (L);
	lua_pushliteral (L, "hook");
	lua_pushstring  (L, hookid);
	lua_settable (L, -3);
	while (arg->name != NULL) {
		char *name  = from_utf8 (arg->name);
		char *value = from_utf8 (arg->value);
		lua_pushstring (L, name);
		lua_pushstring (L, value);
		lua_settable (L, -3);
		g_free (name);
		g_free (value);
		arg++;
	}

	if (lua_pcall (L, 1, 1, 0)) {
		scr_log_print (LPRINT_NORMAL, "lua: Error in hook handler: %s", lua_tostring (L, -1));
		lua_pop (L, 1);
	} else {
		switch (lua_type (L, -1)) {
		case LUA_TSTRING:
		case LUA_TNUMBER:
			ret = luaL_checkenum (L, -1, lua_hook_handler_result);
			break;
		default:
			ret = HOOK_HANDLER_RESULT_ALLOW_MORE_HANDLERS;
			break;
		}
		lua_pop (L, 1);
	}

	return ret;
}

/// main.hook_run
/// Runs handlers for given hook with supplied arguments.
/// A: string (hook name), table (hook arguments)
/// R: return enum field (hook handler result)
static int lua_main_hook_run ( lua_State *L )
{
	const char * hook_name = luaL_checkstring ( L, 1 );
	int          num       = 1;
	hk_arg_t *   args;
	guint        ret;
	lua_settop ( L, 2 );                            // 1 name, 2 args_in
	luaL_argcheck ( L, lua_type ( L, 2 ) == LUA_TTABLE, 2, "table with hook arguments expected" );
	// count fields, build temporary stringified table
	lua_newtable ( L );                             // 3 args
	lua_pushnil ( L );                              // 4 key
	while ( lua_next ( L, 2 ) != 0 ) {              // 5 value
		int ktype = lua_type ( L, 4 );
		if ( ktype == LUA_TSTRING || ktype == LUA_TNUMBER ) {
			int vtype = lua_type ( L, 5 );
			if ( vtype == LUA_TSTRING || vtype == LUA_TNUMBER ) {
				lua_pushvalue ( L, 4 ); // 6 key copy
				lua_pushvalue ( L, 5 ); // 7 value copy
				lua_tostring ( L, 6 );
				lua_tostring ( L, 7 );
				lua_settable ( L, 3 );  // 5 value
				num ++;
			}
		}
		lua_pop ( L, 1 );                       // 4 key
	}
	// alloc args, populate from temporary table
	args = luaL_malloc ( L, num * sizeof ( hk_arg_t ) );
	num  = 0;
	lua_pushnil ( L );                              // 4 key
	while ( lua_next ( L, 3 ) != 0 ) {              // 5 value
		( args + num ) -> name  = lua_tostring ( L, 4 );
		( args + num ) -> value = lua_tostring ( L, 5 );
		num ++;
		lua_pop ( L, 1 );                       // 4 key
	}
	( args + num ) -> name  = NULL;
	( args + num ) -> value = NULL;
	// run hook
	ret = hk_run_handlers ( hook_name, args );
	luaL_free ( L, args );
	luaL_pushenum ( L, ret, lua_hook_handler_result );
	return 1;
}

/// main.hook
/// Installs hook handler, returns an object, that you need to keep until
/// hook handling is no more needed.
/// A: string (hook name), hook function, integer (priority, optional)
/// R: userdata (hook object)
static int lua_main_hook (lua_State *L)
{
	const char   *hook_name = luaL_checkstring (L, 1);
	int           priority  = G_PRIORITY_DEFAULT;
	lua_hook_t   *cb;

	luaL_argcheck (L, lua_isfunction (L, 2), 2, "function expected");

	if (lua_gettop (L) > 2)
		priority = luaL_checkenum (L, 3, lua_hook_handler_priority);

	cb = lua_newuserdata (L, sizeof (lua_hook_t));
	luaL_getmetatable (L, "mcabber.hook");
	lua_setmetatable (L, -2);

	lua_pushvalue (L, -1);
	cb -> selfref = luaL_ref (L, LUA_REGISTRYINDEX);
	lua_pushvalue (L, 1);
	cb -> nameref = luaL_ref (L, LUA_REGISTRYINDEX);
	lua_pushvalue (L, 2);
	cb -> cbref   = luaL_ref (L, LUA_REGISTRYINDEX);
	cb -> L       = L;
	cb -> hid     = hk_add_handler (lua_hook_cb, hook_name, priority, cb);

	return 1;
}

static void lua_mcabber_unregister_hook (lua_State *L, lua_hook_t *cb)
{
	const char *name;

	if (!cb -> hid || cb -> nameref == LUA_NOREF)
		return;

	lua_rawgeti (L, LUA_REGISTRYINDEX, cb -> nameref);
	name = lua_tostring (L, -1);
	if (name) {
		hk_del_handler (name, cb -> hid);
		cb -> hid = 0;
	}

	lua_pop (L, 1);
}

/// hook:del
/// Unregisters given hook handler from mcabber. Object will be destroyed later.
static int lua_mcabber_hook_del (lua_State *L)
{
	lua_hook_t *cb = luaL_checkudata (L, 1, "mcabber.hook");
	luaL_argcheck (L, cb != NULL, 1, "mcabber hook object expected");
	lua_mcabber_unregister_hook (L, cb);
	if (cb -> selfref != LUA_NOREF) {
		luaL_unref (L, LUA_REGISTRYINDEX, cb -> selfref);
		cb -> selfref = LUA_NOREF;
	}
	return 0;
}

static int lua_mcabber_hook_gc (lua_State *L)
{
	lua_hook_t *cb = luaL_checkudata (L, 1, "mcabber.hook");
	luaL_argcheck (L, cb != NULL, 1, "mcabber hook object expected");
	lua_mcabber_unregister_hook (L, cb);
	if (cb -> nameref != LUA_NOREF)
		luaL_unref (L, LUA_REGISTRYINDEX, cb -> nameref);
	if (cb -> cbref != LUA_NOREF)
		luaL_unref (L, LUA_REGISTRYINDEX, cb -> cbref);
	return 0;
}

static const luaL_Reg lua_mcabber_hook_reg_m[] = {
	{ "del",  lua_mcabber_hook_del },
	{ "__gc", lua_mcabber_hook_gc  },
	{ NULL,   NULL                 },
};

static void lua_hook_init (lua_State *L)
{
	luaL_newmetatable (L, "mcabber.hook");
	lua_pushvalue (L, -1);
	lua_setfield (L, -2, "__index");
	luaL_setfuncs (L, lua_mcabber_hook_reg_m, 0);
	lua_pop (L, 1);
}

// OPTION GUARDS

#define MLUA_GUARD_REGISTRY "mcabber_guards"

/// guard function
/// Function to be called, when option changes it's value.
/// Old option value is still accessible through main.option.
/// A: string (key), string (new value)
/// R: string (value to save in hash table)
static gchar *lua_guard_cb (const gchar *key, const gchar *value)
{
	lua_State *L = lua; // FIXME

	lua_getfield ( L, LUA_REGISTRYINDEX, MLUA_GUARD_REGISTRY ); // +1
	lua_getfield ( L, -1, key );                                // +2
	if ( ! lua_isfunction ( L, -1 ) ) {
		lua_pop ( L, 2 );
		return g_strdup ( value );
	}

	lua_pushstring ( L, key );                                  // +3
	lua_pushstring ( L, value );                                // +4

	if ( lua_pcall ( L, 2, 1, 0 ) ) {                           // +2
		scr_log_print ( LPRINT_NORMAL, "lua: Error in hook handler: %s", lua_tostring ( L, -1 ) );
		lua_pop ( L, 2 );
		return g_strdup ( value );
	} else {
		gchar *result = g_strdup ( lua_tostring ( L, -1 ) );
		lua_pop ( L, 2 );
		return result;
	}
}

/// main.add_guard
/// Installs option guard for given option.
/// A: string ( option name ), guard function
/// R: boolean ( success )
static int lua_main_add_guard ( lua_State *L )
{
	const char *name = luaL_checkstring ( L, 1 );
	luaL_argcheck ( L, lua_isfunction (L, 2), 2, "function expected" );
	lua_settop ( L, 2 );

	if ( settings_set_guard ( name, lua_guard_cb ) ) {
		lua_getfield ( L, LUA_REGISTRYINDEX, MLUA_GUARD_REGISTRY );
		lua_pushvalue ( L, 2 );
		lua_setfield ( L, 3, name );
		lua_pushboolean ( L, 1 );
	} else
		lua_pushboolean ( L, 0 );

	return 1;
}

/// main.del_guard
/// Removes option guard from given option.
/// By default, lua will refuse to remove guards, not installed
/// by lua. Still, you can force guard removal.
/// A: string ( option name ), boolean ( force removal )
/// R: boolean ( success )
static int lua_main_del_guard ( lua_State *L )
{
	const char *name = luaL_checkstring ( L, 1 );
	lua_settop ( L, 2 );

	lua_getfield ( L, LUA_REGISTRYINDEX, MLUA_GUARD_REGISTRY );
	lua_getfield ( L, 3, name );

	if ( ! lua_isnil ( L, 4 ) || lua_toboolean ( L, 2 ) ) {
		settings_del_guard ( name );
		lua_pushnil ( L );
		lua_setfield ( L, 3, name );
		lua_pushboolean ( L, 1 );
	} else
		lua_pushboolean ( L, 0 );

	return 1;
}

static void lua_guard_init (lua_State *L)
{
	lua_createtable ( L, 0, 0 );
	lua_setfield ( L, LUA_REGISTRYINDEX, MLUA_GUARD_REGISTRY );
}

static void lua_guard_uninit (lua_State *L)
{
	lua_getfield ( L, LUA_REGISTRYINDEX, MLUA_GUARD_REGISTRY );
	lua_pushnil ( L );
	while ( lua_next ( L, -2 ) ) {
		const char *key = lua_tostring ( L, -2 );
		settings_del_guard ( key );
		lua_pop ( L, 1 );
	}
	lua_pop ( L, 1 );
}

// MAIN INITIALIZATION CODE

#ifdef LLM_LOG_HANDLER
// FIXME: this should not be here
guint lua_lm_log_handler_id;

void lua_lm_log_handler (const gchar *domain, GLogLevelFlags log_level, const gchar *message, gpointer ignore)
{
	if (settings_opt_get_int (OPT_MLUA_LM_DEBUG))
		scr_log_print (LPRINT_LOGNORM, "%s: %s", domain, message);
}
#endif

static void do_lua(char *arg, lua_State *L)
{
	if (luaL_loadbuffer (L, arg, strlen (arg), "line")) {
		scr_log_print (LPRINT_LOGNORM, "lua: Compilation error: %s", lua_tostring (L, -1));
		lua_pop (L, 1);
		return;
	}
 
	if (lua_pcall (L, 0, 0, 0)) {
		scr_log_print (LPRINT_NORMAL, "lua: Runtime error: %s", lua_tostring(L, -1));
		lua_pop (L, 1);
		return;
	}
}

static void *lua_alloc (void *ud, void *ptr, size_t osize, size_t nsize) {
	if (nsize == 0) {
		g_free (ptr);
		return NULL;
	} else
		return g_realloc (ptr, nsize);
}

#define reg(NAME)                   \
	{ #NAME, lua_main_##NAME },
static const luaL_Reg lua_reg_main[] = {
	reg ( yesno          ) 
#ifdef LLM_CONNECTION_ENABLE
	reg ( connection     ) 
#endif
	reg ( version        )
	reg ( log            ) 
	reg ( option         ) 
	reg ( alias          ) 
	reg ( binding        ) 
	reg ( fileoption     ) 
	reg ( add_feature    ) 
	reg ( del_feature    ) 
	reg ( event          )
	reg ( parse_args     ) 
	reg ( add_category   ) 
	reg ( del_category   ) 
	reg ( add_completion ) 
	reg ( del_completion ) 
	reg ( command        ) 
	reg ( print_info     ) 
	reg ( beep           ) 
	reg ( run            ) 
	reg ( status         ) 
	reg ( roster         ) 
	reg ( current_buddy  ) 
	reg ( full_jid       ) 
	reg ( buddy_info     ) 
	reg ( timer          ) 
	reg ( alias          ) 
	reg ( binding        ) 
	reg ( fileoption     ) 
	reg ( add_feature    ) 
	reg ( del_feature    ) 
	reg ( event          )
	reg ( parse_args     ) 
	reg ( add_category   ) 
	reg ( del_category   ) 
	reg ( add_completion ) 
	reg ( del_completion ) 
	reg ( command        ) 
	reg ( print_info     ) 
	reg ( beep           ) 
	reg ( run            ) 
	reg ( status         ) 
	reg ( roster         ) 
	reg ( current_buddy  ) 
	reg ( full_jid       ) 
	reg ( buddy_info     ) 
	reg ( timer          ) 
	reg ( bgread         ) 
	reg ( hook           )
	reg ( hook_run       )
	reg ( add_guard      )
	reg ( del_guard      )
	{ NULL, NULL },
};
#undef reg

const gchar *g_module_check_init (GModule *module)
{
	lua = lua_newstate (lua_alloc, NULL);
	if (!lua)
		return "Lua initialization error";
	else
		return NULL;
}

void g_module_unload (GModule *module)
{
	if (lua) {
		lua_close (lua);
		lua = NULL;
	}
}

void mlua_init (void)
{
	luaL_openlibs (lua);

	lua_newtable(lua);
	luaL_setfuncs (lua, lua_reg_main, 0);
	lua_setglobal (lua, "main");
	lua_register (lua, "dopath", lua_global_dopath);
	lua_register (lua, "print",  lua_global_print );

	{
#ifdef MCABBER_API_HAVE_COMPL_FLAGS
		int cid = compl_new_category (COMPL_FLAGS_PREPEND);
#else
		int cid = compl_new_category ();
#endif

		if (cid) {
			const string2enum_t *word = lua_yesno;
			lua_completion_type[MLUA_YESNO_POS].value = cid;
			lua_added_categories = g_slist_prepend (lua_added_categories, (gpointer) ((gsize) cid));
			while (word->string) {
				compl_add_category_word (cid, word->string);
				++word;
			}
		}
	}

#ifdef MCABBER_API_HAVE_CMD_ID
	lua_cmdid = cmd_add (MLUA_COMMAND_NAME, "Evaluate lua string", 0, 0, (void (*) (char *p)) do_lua, lua);
#else
	cmd_add (MLUA_COMMAND_NAME, "Evaluate lua string", 0, 0, (void (*) (char *p)) do_lua, lua);
#endif

#ifdef ENABLE_LUA_ALIAS
	settings_set(SETTINGS_TYPE_ALIAS, "lua", MLUA_COMMAND_NAME);
#endif

#ifdef LLM_LOG_HANDLER
	// FIXME: this should not be here.
	lua_lm_log_handler_id = g_log_set_handler ("lua-lm", G_LOG_LEVEL_MASK, (GLogFunc) lua_lm_log_handler, NULL);
#endif

	lua_hook_init    (lua);
	lua_command_init (lua);
	lua_guard_init   (lua);

	{
		char *initfile = expand_filename (settings_opt_get (OPT_MLUA_RC));

		if (!initfile)
			scr_log_print (LPRINT_LOGNORM, "lua: Cannot determine config file name");
		else {
			if (luaL_loadfile(lua, initfile)) {
				scr_log_print (LPRINT_LOGNORM, "lua: Unable to compile rc file: %s", lua_tostring (lua, -1));
				lua_pop (lua, 1);
			} else if (lua_pcall (lua, 0, LUA_MULTRET, 0)) {
				scr_log_print (LPRINT_LOGNORM, "lua: Runtime error in rc file: %s", lua_tostring(lua, -1));
				lua_pop (lua, 1);
			} else
				scr_log_print (LPRINT_LOGNORM, "lua: Loaded %s", initfile);
			g_free (initfile);
		}
	}

	{
		hk_arg_t args[] = {
			{ NULL, NULL },
		};
		hk_run_handlers("hook-lua-start", args);
	}
}

static void lua_events_destroy ( gpointer data, gpointer udata )
{
	lua_event_callback_t *cb = data;
	const char *evid;
	if (cb->evid == LUA_NOREF)
		return;
	lua_rawgeti (cb->L, LUA_REGISTRYINDEX, cb->evid);
	evid = lua_tostring (cb ->L, -1);
	evs_callback (evid, EVS_CONTEXT_CANCEL, "Module unloading");
	evs_del (evid);
}

static void lua_bgreads_destroy ( gpointer data, gpointer udata )
{
	g_source_remove ( ( gulong ) data );
}

static void lua_timers_destroy ( gpointer data, gpointer udata )
{
	g_source_remove ( ( gulong ) data );
}

static void lua_features_destroy ( gpointer data, gpointer udata )
{
	gchar *xmlns = data;
	xmpp_del_feature (xmlns);
	g_free (xmlns);
}

static void lua_categories_destroy ( gpointer data, gpointer udata )
{
	compl_del_category ( ( gulong ) data );
}

void mlua_uninit (void)
{
	if (lua) {
		hk_arg_t args[] = {
			{ NULL, NULL },
		};
		hk_run_handlers ("hook-lua-quit", args);

		// hook handlers and commands will be unregistered upon objects destruction

		lua_guard_uninit ( lua );

		g_slist_foreach ( lua_bgreads, lua_bgreads_destroy, NULL );
		g_slist_free ( lua_bgreads );
		lua_bgreads = NULL;

		g_slist_foreach ( lua_timers, lua_timers_destroy, NULL );
		g_slist_free ( lua_timers );
		lua_timers = NULL;

		g_slist_foreach ( lua_events, lua_events_destroy, NULL );
		g_slist_free ( lua_events );
		lua_events = NULL;

#ifdef MCABBER_API_HAVE_CMD_ID
		cmd_del (lua_cmdid);
		lua_cmdid = NULL;
#else
		cmd_del (MLUA_COMMAND_NAME);
#endif

		lua_close (lua);
		lua = NULL;

		g_slist_foreach ( lua_added_features, lua_features_destroy, NULL );
		g_slist_free ( lua_added_features );
		lua_added_features = NULL;

		g_slist_foreach ( lua_added_categories, lua_categories_destroy, NULL );
		g_slist_free ( lua_added_categories );
		lua_added_categories = NULL;

#ifdef LLM_LOG_HANDLER
		// FIXME: shouldn't be here
		g_log_remove_handler ("lua-lm", lua_lm_log_handler_id);
#endif
	}
}