/*
 * Copyright (c) 2015-2016 mniip
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
 * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */

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

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

#include <glib.h>
#include <gmodule.h>

#ifndef G_OS_WIN32
#include <pwd.h>
#endif

#include <zoitechat-plugin.h>

#define WORD_ARRAY_LEN 32

static char plugin_name[] = "Lua";
static char plugin_description[] = "Lua scripting interface";
static char plugin_version[16] = "1.3";

static char console_tab[] = ">>lua<<";
static char command_help[] =
        "Usage: /lua load <filename>\n"
        "            unload <filename>\n"
        "            reload <filename>\n"
        "            exec <code>\n"
        "            inject <filename> <code>\n"
        "            reset\n"
        "            list\n"
        "            console";

static char registry_field[] = "plugin";

static zoitechat_plugin *ph;

#if LUA_VERSION_NUM < 502
#define lua_rawlen lua_objlen
#define luaL_setfuncs(L, r, n) luaL_register(L, NULL, r)
#endif

typedef struct
{
        zoitechat_hook *hook;
        lua_State *state;
        int ref;
}
hook_info;

typedef struct
{
        char *name;
        char *description;
        char *version;
        zoitechat_plugin *handle;
        char *filename;
        lua_State *state;
        GPtrArray *hooks;
        GPtrArray *unload_hooks;
        int traceback;
        int status;
}
script_info;

#define STATUS_ACTIVE 1
#define STATUS_DEFERRED_UNLOAD 2
#define STATUS_DEFERRED_RELOAD 4

static void check_deferred(script_info *info);

static inline script_info *get_info(lua_State *L)
{
        script_info *info;

        lua_getfield(L, LUA_REGISTRYINDEX, registry_field);
        info = lua_touserdata(L, -1);
        lua_pop(L, 1);

        return info;
}

static int api_zoitechat_register(lua_State *L)
{
        char const *name, *version, *description;
        script_info *info = get_info(L);
        if(info->name)
                return luaL_error(L, "script is already registered as '%s'", info->name);

        name = luaL_checkstring(L, 1);
        version = luaL_checkstring(L, 2);
        description = luaL_checkstring(L, 3);

        info->name = g_strdup(name);
        info->description = g_strdup(description);
        info->version = g_strdup(version);
        info->handle = zoitechat_plugingui_add(ph, info->filename, info->name, info->description, info->version, NULL);

        return 0;
}

static int api_zoitechat_command(lua_State *L)
{
        zoitechat_command(ph, luaL_checkstring(L, 1));
        return 0;
}

static int tostring(lua_State *L, int n)
{
        luaL_checkany(L, n);
        switch (lua_type(L, n))
        {
                case LUA_TNUMBER:
                        lua_pushstring(L, lua_tostring(L, n));
                        break;
                case LUA_TSTRING:
                        lua_pushvalue(L, n);
                        break;
                case LUA_TBOOLEAN:
                        lua_pushstring(L, (lua_toboolean(L, n) ? "true" : "false"));
                        break;
                case LUA_TNIL:
                        lua_pushliteral(L, "nil");
                        break;
                default:
                        lua_pushfstring(L, "%s: %p", luaL_typename(L, n), lua_topointer(L, n));
                        break;
        }
        return 1;
}

static int api_zoitechat_print(lua_State *L)
{
        int i, args = lua_gettop(L);
        luaL_Buffer b;
        luaL_buffinit(L, &b);
        for(i = 1; i <= args; i++)
        {
                if(i != 1)
                        luaL_addstring(&b, " ");
                tostring(L, i);
                luaL_addvalue(&b);
        }
        luaL_pushresult(&b);
        zoitechat_print(ph, lua_tostring(L, -1));
        return 0;
}

static int api_zoitechat_emit_print(lua_State *L)
{
        zoitechat_emit_print(ph, luaL_checkstring(L, 1), luaL_optstring(L, 2, NULL), luaL_optstring(L, 3, NULL), luaL_optstring(L, 4, NULL), luaL_optstring(L, 5, NULL), luaL_optstring(L, 6, NULL), NULL);
        return 0;
}

static int api_zoitechat_emit_print_attrs(lua_State *L)
{
        zoitechat_event_attrs *attrs = *(zoitechat_event_attrs **)luaL_checkudata(L, 1, "attrs");
        zoitechat_emit_print_attrs(ph, attrs, luaL_checkstring(L, 2), luaL_optstring(L, 3, NULL), luaL_optstring(L, 4, NULL), luaL_optstring(L, 5, NULL), luaL_optstring(L, 6, NULL), luaL_optstring(L, 7, NULL), NULL);
        return 0;
}

static int api_zoitechat_send_modes(lua_State *L)
{
        size_t i, n;
        int modes;
        char const *mode;
        char const **targets;

        luaL_checktype(L, 1, LUA_TTABLE);
        n = lua_rawlen(L, 1);
        mode = luaL_checkstring(L, 2);
        if(strlen(mode) != 2)
                return luaL_argerror(L, 2, "expected sign followed by a mode letter");
        modes = luaL_optinteger(L, 3, 0);
        targets = g_new(char const *, n);

        for(i = 0; i < n; i++)
        {
                lua_rawgeti(L, 1, i + 1);
                if(lua_type(L, -1) != LUA_TSTRING)
                {
                        g_free(targets);
                        return luaL_argerror(L, 1, "expected an array of strings");
                }
                targets[i] = lua_tostring(L, -1);
                lua_pop(L, 1);
        }
        zoitechat_send_modes(ph, targets, n, modes, mode[0], mode[1]);
        g_free(targets);
        return 0;
}

static int api_zoitechat_nickcmp(lua_State *L)
{
        lua_pushinteger(L, zoitechat_nickcmp(ph, luaL_checkstring(L, 1), luaL_checkstring(L, 2)));
        return 1;
}

static int api_zoitechat_strip(lua_State *L)
{
        size_t len;
        char const *text;
        gboolean leave_colors, leave_attrs;
        char *result;

        luaL_checktype(L, 1, LUA_TSTRING);
        text = lua_tolstring(L, 1, &len);
        leave_colors = lua_toboolean(L, 2);
        leave_attrs = lua_toboolean(L, 3);
        result = zoitechat_strip(ph, text, len, (leave_colors ? 0 : 1) | (leave_attrs ? 0 : 2));
        if(result)
        {
                lua_pushstring(L, result);
                zoitechat_free(ph, result);
                return 1;
        }
        return 0;
}

static void register_hook(hook_info *hook)
{
        script_info *info = get_info(hook->state);
        g_ptr_array_add(info->hooks, hook);
}

static void free_hook(hook_info *hook)
{
        if(hook->state)
                luaL_unref(hook->state, LUA_REGISTRYINDEX, hook->ref);
        if(hook->hook)
                zoitechat_unhook(ph, hook->hook);
        g_free(hook);
}

static int unregister_hook(hook_info *hook)
{
        script_info *info = get_info(hook->state);

        if(g_ptr_array_remove_fast(info->hooks, hook))
                return 1;

        if(g_ptr_array_remove_fast(info->unload_hooks, hook))
                return 1;

        return 0;
}

static int api_command_closure(char *word[], char *word_eol[], void *udata)
{
        int base, i, ret;
        hook_info *info = udata;
        lua_State *L = info->state;
        script_info *script = get_info(L);

        lua_rawgeti(L, LUA_REGISTRYINDEX, script->traceback);
        base = lua_gettop(L);
        lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
        lua_newtable(L);
        for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
        {
                lua_pushstring(L, word[i]);
                lua_rawseti(L, -2, i);
        }
        lua_newtable(L);
        for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
        {
                lua_pushstring(L, word_eol[i]);
                lua_rawseti(L, -2, i);
        }
        script->status |= STATUS_ACTIVE;
        if(lua_pcall(L, 2, 1, base))
        {
                char const *error = lua_tostring(L, -1);
                lua_pop(L, 2);
                zoitechat_printf(ph, "Lua error in command hook: %s", error ? error : "(non-string error)");
                check_deferred(script);
                return ZOITECHAT_EAT_NONE;
        }
        ret = lua_tointeger(L, -1);
        lua_pop(L, 2);
        check_deferred(script);
        return ret;
}

static int api_zoitechat_hook_command(lua_State *L)
{
        hook_info *info, **u;
        char const *command, *help;
        int ref, pri;

        command = luaL_optstring(L, 1, "");
        lua_pushvalue(L, 2);
        ref = luaL_ref(L, LUA_REGISTRYINDEX);
        help = luaL_optstring(L, 3, NULL);
        pri = luaL_optinteger(L, 4, ZOITECHAT_PRI_NORM);
        info = g_new(hook_info, 1);
        info->state = L;
        info->ref = ref;
        info->hook = zoitechat_hook_command(ph, command, pri, api_command_closure, help, info);
        u = lua_newuserdata(L, sizeof(hook_info *));
        *u = info;
        luaL_newmetatable(L, "hook");
        lua_setmetatable(L, -2);
        register_hook(info);
        return 1;
}

static int api_print_closure(char *word[], void *udata)
{
        hook_info *info = udata;
        lua_State *L = info->state;
        script_info *script = get_info(L);
        int i, j, base, ret;

        lua_rawgeti(L, LUA_REGISTRYINDEX, script->traceback);
        base = lua_gettop(L);
        lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);

        for(j = 31; j >= 1; j--)
        {
                if(*word[j])
                        break;
        }
        lua_newtable(L);
        for(i = 1; i <= j; i++)
        {
                lua_pushstring(L, word[i]);
                lua_rawseti(L, -2, i);
        }
        script->status |= STATUS_ACTIVE;
        if(lua_pcall(L, 1, 1, base))
        {
                char const *error = lua_tostring(L, -1);
                lua_pop(L, 2);
                zoitechat_printf(ph, "Lua error in print hook: %s", error ? error : "(non-string error)");
                check_deferred(script);
                return ZOITECHAT_EAT_NONE;
        }
        ret = lua_tointeger(L, -1);
        lua_pop(L, 2);
        check_deferred(script);
        return ret;
}

static int api_zoitechat_hook_print(lua_State *L)
{
        char const *event = luaL_checkstring(L, 1);
        hook_info *info, **u;
        int ref, pri;

        lua_pushvalue(L, 2);
        ref = luaL_ref(L, LUA_REGISTRYINDEX);
        pri = luaL_optinteger(L, 3, ZOITECHAT_PRI_NORM);
        info = g_new(hook_info, 1);
        info->state = L;
        info->ref = ref;
        info->hook = zoitechat_hook_print(ph, event, pri, api_print_closure, info);
        u = lua_newuserdata(L, sizeof(hook_info *));
        *u = info;
        luaL_newmetatable(L, "hook");
        lua_setmetatable(L, -2);
        register_hook(info);
        return 1;
}

static zoitechat_event_attrs *event_attrs_copy(const zoitechat_event_attrs *attrs)
{
        zoitechat_event_attrs *copy = zoitechat_event_attrs_create(ph);
        copy->server_time_utc = attrs->server_time_utc;
        return copy;
}

static int api_print_attrs_closure(char *word[], zoitechat_event_attrs *attrs, void *udata)
{
        hook_info *info = udata;
        lua_State *L = info->state;
        script_info *script = get_info(L);
        int base, i, j, ret;
        zoitechat_event_attrs **u;

        lua_rawgeti(L, LUA_REGISTRYINDEX, script->traceback);
        base = lua_gettop(L);
        lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
        for(j = 31; j >= 1; j--)
        {
                if(*word[j])
                        break;
        }
        lua_newtable(L);
        for(i = 1; i <= j; i++)
        {
                lua_pushstring(L, word[i]);
                lua_rawseti(L, -2, i);
        }
        u = lua_newuserdata(L, sizeof(zoitechat_event_attrs *));
        *u = event_attrs_copy(attrs);
        luaL_newmetatable(L, "attrs");
        lua_setmetatable(L, -2);
        script->status |= STATUS_ACTIVE;
        if(lua_pcall(L, 2, 1, base))
        {
                char const *error = lua_tostring(L, -1);
                lua_pop(L, 2);
                zoitechat_printf(ph, "Lua error in print_attrs hook: %s", error ? error : "(non-string error)");
                check_deferred(script);
                return ZOITECHAT_EAT_NONE;
        }
        ret = lua_tointeger(L, -1);
        lua_pop(L, 2);
        check_deferred(script);
        return ret;
}

static int api_zoitechat_hook_print_attrs(lua_State *L)
{
        hook_info *info, **u;
        int ref, pri;
        char const *event = luaL_checkstring(L, 1);

        lua_pushvalue(L, 2);
        ref = luaL_ref(L, LUA_REGISTRYINDEX);
        pri = luaL_optinteger(L, 3, ZOITECHAT_PRI_NORM);
        info = g_new(hook_info, 1);
        info->state = L;
        info->ref = ref;
        info->hook = zoitechat_hook_print_attrs(ph, event, pri, api_print_attrs_closure, info);
        u = lua_newuserdata(L, sizeof(hook_info *));
        *u = info;
        luaL_newmetatable(L, "hook");
        lua_setmetatable(L, -2);
        register_hook(info);
        return 1;
}

static int api_server_closure(char *word[], char *word_eol[], void *udata)
{
        hook_info *info = udata;
        lua_State *L = info->state;
        script_info *script = get_info(L);
        int base, i, ret;

        lua_rawgeti(L, LUA_REGISTRYINDEX, script->traceback);
        base = lua_gettop(L);
        lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
        lua_newtable(L);
        for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
        {
                lua_pushstring(L, word[i]);
                lua_rawseti(L, -2, i);
        }
        lua_newtable(L);
        for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
        {
                lua_pushstring(L, word_eol[i]);
                lua_rawseti(L, -2, i);
        }
        script->status |= STATUS_ACTIVE;
        if(lua_pcall(L, 2, 1, base))
        {
                char const *error = lua_tostring(L, -1);
                lua_pop(L, 2);
                zoitechat_printf(ph, "Lua error in server hook: %s", error ? error : "(non-string error)");
                check_deferred(script);
                return ZOITECHAT_EAT_NONE;
        }
        ret = lua_tointeger(L, -1);
        lua_pop(L, 2);
        check_deferred(script);
        return ret;
}

static int api_zoitechat_hook_server(lua_State *L)
{
        char const *command = luaL_optstring(L, 1, "RAW LINE");
        hook_info *info, **u;
        int ref, pri;

        lua_pushvalue(L, 2);
        ref = luaL_ref(L, LUA_REGISTRYINDEX);
        pri = luaL_optinteger(L, 3, ZOITECHAT_PRI_NORM);
        info = g_new(hook_info, 1);
        info->state = L;
        info->ref = ref;
        info->hook = zoitechat_hook_server(ph, command, pri, api_server_closure, info);
        u = lua_newuserdata(L, sizeof(hook_info *));
        *u = info;
        luaL_newmetatable(L, "hook");
        lua_setmetatable(L, -2);
        register_hook(info);
        return 1;
}

static int api_server_attrs_closure(char *word[], char *word_eol[], zoitechat_event_attrs *attrs, void *udata)
{
        hook_info *info = udata;
        lua_State *L = info->state;
        script_info *script = get_info(L);
        int base, i, ret;
        zoitechat_event_attrs **u;

        lua_rawgeti(L, LUA_REGISTRYINDEX, script->traceback);
        base = lua_gettop(L);
        lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
        lua_newtable(L);
        for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
        {
                lua_pushstring(L, word[i]);
                lua_rawseti(L, -2, i);
        }
        lua_newtable(L);
        for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
        {
                lua_pushstring(L, word_eol[i]);
                lua_rawseti(L, -2, i);
        }

        u = lua_newuserdata(L, sizeof(zoitechat_event_attrs *));
        *u = event_attrs_copy(attrs);
        luaL_newmetatable(L, "attrs");
        lua_setmetatable(L, -2);
        script->status |= STATUS_ACTIVE;
        if(lua_pcall(L, 3, 1, base))
        {
                char const *error = lua_tostring(L, -1);
                lua_pop(L, 2);
                zoitechat_printf(ph, "Lua error in server_attrs hook: %s", error ? error : "(non-string error)");
                check_deferred(script);
                return ZOITECHAT_EAT_NONE;
        }
        ret = lua_tointeger(L, -1);
        lua_pop(L, 2);
        check_deferred(script);
        return ret;
}

static int api_zoitechat_hook_server_attrs(lua_State *L)
{
        char const *command = luaL_optstring(L, 1, "RAW LINE");
        int ref, pri;
        hook_info *info, **u;

        lua_pushvalue(L, 2);
        ref = luaL_ref(L, LUA_REGISTRYINDEX);
        pri = luaL_optinteger(L, 3, ZOITECHAT_PRI_NORM);
        info = g_new(hook_info, 1);
        info->state = L;
        info->ref = ref;
        info->hook = zoitechat_hook_server_attrs(ph, command, pri, api_server_attrs_closure, info);
        u = lua_newuserdata(L, sizeof(hook_info *));
        *u = info;
        luaL_newmetatable(L, "hook");
        lua_setmetatable(L, -2);
        register_hook(info);
        return 1;
}

static int api_timer_closure(void *udata)
{
        hook_info *info = udata;
        lua_State *L = info->state;
        script_info *script = get_info(L);
        int base, ret;

        lua_rawgeti(L, LUA_REGISTRYINDEX, script->traceback);
        base = lua_gettop(L);
        lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
        script->status |= STATUS_ACTIVE;
        if(lua_pcall(L, 0, 1, base))
        {
                char const *error = lua_tostring(L, -1);
                lua_pop(L, 2);
                zoitechat_printf(ph, "Lua error in timer hook: %s", error ? error : "(non-string error)");
                check_deferred(script);
                return 0;
        }
        ret = lua_toboolean(L, -1);
        lua_pop(L, 2);
        check_deferred(script);
        return ret;
}

static int api_zoitechat_hook_timer(lua_State *L)
{
        int ref, timeout = luaL_checkinteger (L, 1);
        hook_info *info, **u;

        lua_pushvalue(L, 2);
        ref = luaL_ref(L, LUA_REGISTRYINDEX);
        info = g_new(hook_info, 1);
        info->state = L;
        info->ref = ref;
        info->hook = zoitechat_hook_timer(ph, timeout, api_timer_closure, info);
        u = lua_newuserdata(L, sizeof(hook_info *));
        *u = info;
        luaL_newmetatable(L, "hook");
        lua_setmetatable(L, -2);
        register_hook(info);
        return 1;
}

static int api_zoitechat_hook_unload(lua_State *L)
{
        script_info *script;
        hook_info *info, **u;
        int ref;

        lua_pushvalue(L, 1);
        ref = luaL_ref(L, LUA_REGISTRYINDEX);
        info = g_new(hook_info, 1);
        info->state = L;
        info->ref = ref;
        info->hook = NULL;
        u = lua_newuserdata(L, sizeof(hook_info *));
        *u = info;
        luaL_newmetatable(L, "hook");
        lua_setmetatable(L, -2);
        script = get_info(info->state);

        g_ptr_array_add(script->unload_hooks, info);
        return 1;
}

static int api_zoitechat_unhook(lua_State *L)
{
        hook_info **info = (hook_info **)luaL_checkudata(L, 1, "hook");
        if(*info)
        {
                unregister_hook(*info);
                *info = 0;
                return 0;
        }
        else
        {
                tostring(L, 1);
                return luaL_error(L, "hook %s is already unhooked", lua_tostring(L, -1));
        }
}

static int api_zoitechat_find_context(lua_State *L)
{
        char const *server = luaL_optstring(L, 1, NULL);
        char const *channel = luaL_optstring(L, 2, NULL);
        zoitechat_context *context = zoitechat_find_context(ph, server, channel);
        if(context)
        {
                zoitechat_context **u = lua_newuserdata(L, sizeof(zoitechat_context *));
                *u = context;
                luaL_newmetatable(L, "context");
                lua_setmetatable(L, -2);
                return 1;
        }
        else
        {
                lua_pushnil(L);
                return 1;
        }
}

static int api_zoitechat_get_context(lua_State *L)
{
        zoitechat_context *context = zoitechat_get_context(ph);
        zoitechat_context **u = lua_newuserdata(L, sizeof(zoitechat_context *));
        *u = context;
        luaL_newmetatable(L, "context");
        lua_setmetatable(L, -2);
        return 1;
}

static int api_zoitechat_set_context(lua_State *L)
{
        zoitechat_context *context = *(zoitechat_context **)luaL_checkudata(L, 1, "context");
        int success = zoitechat_set_context(ph, context);
        lua_pushboolean(L, success);
        return 1;
}

static int wrap_context_closure(lua_State *L)
{
        zoitechat_context *old, *context = *(zoitechat_context **)luaL_checkudata(L, 1, "context");
        lua_pushvalue(L, lua_upvalueindex(1));
        lua_replace(L, 1);
        old = zoitechat_get_context(ph);
        if(!zoitechat_set_context(ph, context))
                return luaL_error(L, "could not switch into context");
        lua_call(L, lua_gettop(L) - 1, LUA_MULTRET);
        zoitechat_set_context(ph, old);
        return lua_gettop(L);
}

static inline void wrap_context(lua_State *L, char const *field, lua_CFunction func)
{
        lua_pushcfunction(L, func);
        lua_pushcclosure(L, wrap_context_closure, 1);
        lua_setfield(L, -2, field);
}

static int api_zoitechat_context_meta_eq(lua_State *L)
{
        zoitechat_context *this = *(zoitechat_context **)luaL_checkudata(L, 1, "context");
        zoitechat_context *that = *(zoitechat_context **)luaL_checkudata(L, 2, "context");
        lua_pushboolean(L, this == that);
        return 1;
}

static int api_zoitechat_get_info(lua_State *L)
{
        char const *key = luaL_checkstring(L, 1);
        char const *data = zoitechat_get_info(ph, key);
        if(data)
        {
                if(!strcmp(key, "gtkwin_ptr") || !strcmp(key, "win_ptr"))
                        lua_pushlightuserdata(L, (void *)data);
                else
                        lua_pushstring(L, data);
                return 1;
        }
        lua_pushnil(L);
        return 1;
}

static int api_zoitechat_attrs(lua_State *L)
{
        zoitechat_event_attrs *attrs = zoitechat_event_attrs_create(ph);
        zoitechat_event_attrs **u = lua_newuserdata(L, sizeof(zoitechat_event_attrs *));
        *u = attrs;
        luaL_newmetatable(L, "attrs");
        lua_setmetatable(L, -2);
        return 1;
}

static int api_iterate_closure(lua_State *L)
{
        zoitechat_list *list = *(zoitechat_list **)luaL_checkudata(L, lua_upvalueindex(1), "list");
        if(zoitechat_list_next(ph, list))
        {
                lua_pushvalue(L, lua_upvalueindex(1));
                return 1;
        }
        else
                return 0;
}

static int api_zoitechat_iterate(lua_State *L)
{
        char const *name = luaL_checkstring(L, 1);
        zoitechat_list *list = zoitechat_list_get(ph, name);
        if(list)
        {
                zoitechat_list **u = lua_newuserdata(L, sizeof(zoitechat_list *));
                *u = list;
                luaL_newmetatable(L, "list");
                lua_setmetatable(L, -2);
                lua_pushcclosure(L, api_iterate_closure, 1);
                return 1;
        }
        else
                return luaL_argerror(L, 1, "invalid list name");
}

static int api_zoitechat_prefs_meta_index(lua_State *L)
{
        char const *key = luaL_checkstring(L, 2);
        char const *string;
        int number;
        int ret = zoitechat_get_prefs(ph, key, &string, &number);
        switch(ret)
        {
                case 0:
                        lua_pushnil(L);
                        return 1;
                case 1:
                        lua_pushstring(L, string);
                        return 1;
                case 2:
                        lua_pushnumber(L, number);
                        return 1;
                case 3:
                        lua_pushboolean(L, number);
                        return 1;
                default:
                        return 0;
        }
}

static int api_zoitechat_prefs_meta_newindex(lua_State *L)
{
        return luaL_error(L, "zoitechat.prefs is read-only");
}

static inline int list_marshal(lua_State *L, const char *key, zoitechat_list *list)
{
        char const *str = zoitechat_list_str(ph, list, key);
        int number;
        if(str)
        {
                if(!strcmp(key, "context"))
                {
                        zoitechat_context **u = lua_newuserdata(L, sizeof(zoitechat_context *));
                        *u = (zoitechat_context *)str;
                        luaL_newmetatable(L, "context");
                        lua_setmetatable(L, -2);
                        return 1;
                }
                lua_pushstring(L, str);
                return 1;
        }
        number = zoitechat_list_int(ph, list, key);
        if(number != -1)
        {
                lua_pushinteger(L, number);
                return 1;
        }
        if (list != NULL)
        {
                time_t tm = zoitechat_list_time(ph, list, key);
                if(tm != -1)
                {
                        lua_pushinteger(L, tm);
                        return 1;
                }
        }

        lua_pushnil(L);
        return 1;
}

static int api_zoitechat_props_meta_index(lua_State *L)
{
        char const *key = luaL_checkstring(L, 2);
        return list_marshal(L, key, NULL);
}

static int api_zoitechat_props_meta_newindex(lua_State *L)
{
        return luaL_error(L, "zoitechat.props is read-only");
}

static int api_zoitechat_pluginprefs_meta_index(lua_State *L)
{
        script_info *script = get_info(L);
        const char *key;
        zoitechat_plugin *h;
        char str[512];
        int r;

        if(!script->name)
                return luaL_error(L, "cannot use zoitechat.pluginprefs before registering with zoitechat.register");

        key = luaL_checkstring(L, 2);
        h = script->handle;
        r = zoitechat_pluginpref_get_int(h, key);
        if(r != -1)
        {
                lua_pushinteger(L, r);
                return 1;
        }
        if(zoitechat_pluginpref_get_str(h, key, str))
        {
                /* Wasn't actually a failure */
                if (!strcmp(str, "-1"))
                        lua_pushinteger(L, r);
                else
                        lua_pushstring(L, str);
                return 1;
        }
        lua_pushnil(L);
        return 1;
}

static int api_zoitechat_pluginprefs_meta_newindex(lua_State *L)
{
        script_info *script = get_info(L);
        const char *key;
        zoitechat_plugin *h;

        if(!script->name)
                return luaL_error(L, "cannot use zoitechat.pluginprefs before registering with zoitechat.register");

        key = luaL_checkstring(L, 2);
        h = script->handle;
        switch(lua_type(L, 3))
        {
                case LUA_TSTRING:
                        zoitechat_pluginpref_set_str(h, key, lua_tostring(L, 3));
                        return 0;
                case LUA_TNUMBER:
                        zoitechat_pluginpref_set_int(h, key, lua_tointeger(L, 3));
                        return 0;
                case LUA_TNIL: case LUA_TNONE:
                        zoitechat_pluginpref_delete(h, key);
                        return 0;
                default:
                        return luaL_argerror(L, 3, "expected string, number, or nil");
        }
}

static int api_zoitechat_pluginprefs_meta_pairs_closure(lua_State *L)
{
        char *dest = lua_touserdata(L, lua_upvalueindex(1));
        zoitechat_plugin *h = get_info(L)->handle;

        if(dest && *dest)
        {
                int r;
                char str[512];
                char *key = dest;

                dest = strchr(dest, ',');
                if(dest)
                        *(dest++) = 0;
                lua_pushlightuserdata(L, dest);
                lua_replace(L, lua_upvalueindex(1));
                lua_pushstring(L, key);
                r = zoitechat_pluginpref_get_int(h, key);
                if(r != -1)
                {
                        lua_pushinteger(L, r);
                        return 2;
                }
                if(zoitechat_pluginpref_get_str(h, key, str))
                {
                        lua_pushstring(L, str);
                        return 2;
                }
                lua_pushnil(L);
                return 2;
        }
        else
                return 0;
}

static int api_zoitechat_pluginprefs_meta_pairs(lua_State *L)
{
        script_info *script = get_info(L);
        char *dest;
        zoitechat_plugin *h;

        if(!script->name)

                return luaL_error(L, "cannot use zoitechat.pluginprefs before registering with zoitechat.register");

        dest = lua_newuserdata(L, 4096);

        h = script->handle;
        if(!zoitechat_pluginpref_list(h, dest))
                strcpy(dest, "");
        lua_pushlightuserdata(L, dest);
        lua_pushlightuserdata(L, dest);
        lua_pushcclosure(L, api_zoitechat_pluginprefs_meta_pairs_closure, 2);
        lua_insert(L, -2); // Return the userdata (second return value from pairs),
                                                // even though it's not used by the closure (first return
                                                // value from pairs), so that Lua knows not to GC it.
        return 2;
}

static int api_attrs_meta_index(lua_State *L)
{
        zoitechat_event_attrs *attrs = *(zoitechat_event_attrs **)luaL_checkudata(L, 1, "attrs");
        char const *key = luaL_checkstring(L, 2);
        if(!strcmp(key, "server_time_utc"))
        {
                lua_pushinteger(L, attrs->server_time_utc);
                return 1;
        }
        else
        {
                lua_pushnil(L);
                return 1;
        }
}

static int api_attrs_meta_newindex(lua_State *L)
{
        zoitechat_event_attrs *attrs = *(zoitechat_event_attrs **)luaL_checkudata(L, 1, "attrs");
        char const *key = luaL_checkstring(L, 2);
        if(!strcmp(key, "server_time_utc"))
        {
                attrs->server_time_utc = luaL_checkinteger(L, 3);
                return 0;
        }
        else
                return 0;
}

static int api_attrs_meta_gc(lua_State *L)
{
        zoitechat_event_attrs *attrs = *(zoitechat_event_attrs **)luaL_checkudata(L, 1, "attrs");
        zoitechat_event_attrs_free(ph, attrs);
        return 0;
}

static int api_list_meta_index(lua_State *L)
{
        zoitechat_list *list = *(zoitechat_list **)luaL_checkudata(L, 1, "list");
        char const *key = luaL_checkstring(L, 2);
        return list_marshal(L, key, list);
}

static int api_list_meta_newindex(lua_State *L)
{
        return luaL_error(L, "zoitechat.iterate list is read-only");
}

static int api_list_meta_gc(lua_State *L)
{
        zoitechat_list *list = *(zoitechat_list **)luaL_checkudata(L, 1, "list");
        zoitechat_list_free(ph, list);
        return 0;
}

static luaL_Reg api_zoitechat[] = {
        {"register", api_zoitechat_register},
        {"command", api_zoitechat_command},
        {"print", api_zoitechat_print},
        {"emit_print", api_zoitechat_emit_print},
        {"emit_print_attrs", api_zoitechat_emit_print_attrs},
        {"send_modes", api_zoitechat_send_modes},
        {"nickcmp", api_zoitechat_nickcmp},
        {"strip", api_zoitechat_strip},
        {"get_info", api_zoitechat_get_info},
        {"hook_command", api_zoitechat_hook_command},
        {"hook_print", api_zoitechat_hook_print},
        {"hook_print_attrs", api_zoitechat_hook_print_attrs},
        {"hook_server", api_zoitechat_hook_server},
        {"hook_server_attrs", api_zoitechat_hook_server_attrs},
        {"hook_timer", api_zoitechat_hook_timer},
        {"hook_unload", api_zoitechat_hook_unload},
        {"unhook", api_zoitechat_unhook},
        {"get_context", api_zoitechat_get_context},
        {"find_context", api_zoitechat_find_context},
        {"set_context", api_zoitechat_set_context},
        {"attrs", api_zoitechat_attrs},
        {"iterate", api_zoitechat_iterate},
        {NULL, NULL}
};

static luaL_Reg api_zoitechat_props_meta[] = {
        {"__index", api_zoitechat_props_meta_index},
        {"__newindex", api_zoitechat_props_meta_newindex},
        {NULL, NULL}
};

static luaL_Reg api_zoitechat_prefs_meta[] = {
        {"__index", api_zoitechat_prefs_meta_index},
        {"__newindex", api_zoitechat_prefs_meta_newindex},
        {NULL, NULL}
};

static luaL_Reg api_zoitechat_pluginprefs_meta[] = {
        {"__index", api_zoitechat_pluginprefs_meta_index},
        {"__newindex", api_zoitechat_pluginprefs_meta_newindex},
        {"__pairs", api_zoitechat_pluginprefs_meta_pairs},
        {NULL, NULL}
};

static luaL_Reg api_hook_meta_index[] = {
        {"unhook", api_zoitechat_unhook},
        {NULL, NULL}
};

static luaL_Reg api_attrs_meta[] = {
        {"__index", api_attrs_meta_index},
        {"__newindex", api_attrs_meta_newindex},
        {"__gc", api_attrs_meta_gc},
        {NULL, NULL}
};

static luaL_Reg api_list_meta[] = {
        {"__index", api_list_meta_index},
        {"__newindex", api_list_meta_newindex},
        {"__gc", api_list_meta_gc},
        {NULL, NULL}
};

static int luaopen_zoitechat(lua_State *L)
{
        lua_newtable(L);
        luaL_setfuncs(L, api_zoitechat, 0);

        lua_pushinteger(L, ZOITECHAT_PRI_HIGHEST); lua_setfield(L, -2, "PRI_HIGHEST");
        lua_pushinteger(L, ZOITECHAT_PRI_HIGH); lua_setfield(L, -2, "PRI_HIGH");
        lua_pushinteger(L, ZOITECHAT_PRI_NORM); lua_setfield(L, -2, "PRI_NORM");
        lua_pushinteger(L, ZOITECHAT_PRI_LOW); lua_setfield(L, -2, "PRI_LOW");
        lua_pushinteger(L, ZOITECHAT_PRI_LOWEST); lua_setfield(L, -2, "PRI_LOWEST");
        lua_pushinteger(L, ZOITECHAT_EAT_NONE); lua_setfield(L, -2, "EAT_NONE");
        lua_pushinteger(L, ZOITECHAT_EAT_ZOITECHAT); lua_setfield(L, -2, "EAT_ZOITECHAT");
        lua_pushinteger(L, ZOITECHAT_EAT_ZOITECHAT); lua_setfield(L, -2, "EAT_XCHAT");
        lua_pushinteger(L, ZOITECHAT_EAT_ZOITECHAT); lua_setfield(L, -2, "EAT_HEXCHAT");
        lua_pushinteger(L, ZOITECHAT_EAT_PLUGIN); lua_setfield(L, -2, "EAT_PLUGIN");
        lua_pushinteger(L, ZOITECHAT_EAT_ALL); lua_setfield(L, -2, "EAT_ALL");

        lua_newtable(L);
        lua_newtable(L);
        luaL_setfuncs(L, api_zoitechat_prefs_meta, 0);
        lua_setmetatable(L, -2);
        lua_setfield(L, -2, "prefs");

        lua_newtable(L);
        lua_newtable(L);
        luaL_setfuncs(L, api_zoitechat_props_meta, 0);
        lua_setmetatable(L, -2);
        lua_setfield(L, -2, "props");

        lua_newtable(L);
        lua_newtable(L);
        luaL_setfuncs(L, api_zoitechat_pluginprefs_meta, 0);
        lua_setmetatable(L, -2);
        lua_setfield(L, -2, "pluginprefs");

        luaL_newmetatable(L, "hook");
        lua_newtable(L);
        luaL_setfuncs(L, api_hook_meta_index, 0);
        lua_setfield(L, -2, "__index");
        lua_pop(L, 1);

        luaL_newmetatable(L, "context");
        lua_newtable(L);
        lua_pushcfunction(L, api_zoitechat_set_context);
        lua_setfield(L, -2, "set");
        wrap_context(L, "find_context", api_zoitechat_find_context);
        wrap_context(L, "print", api_zoitechat_print);
        wrap_context(L, "emit_print", api_zoitechat_emit_print);
        wrap_context(L, "emit_print_attrs", api_zoitechat_emit_print_attrs);
        wrap_context(L, "command", api_zoitechat_command);
        wrap_context(L, "nickcmp", api_zoitechat_nickcmp);
        wrap_context(L, "get_info", api_zoitechat_get_info);
        wrap_context(L, "iterate", api_zoitechat_iterate);
        lua_setfield(L, -2, "__index");
        lua_pushcfunction(L, api_zoitechat_context_meta_eq);
        lua_setfield(L, -2, "__eq");
        lua_pop(L, 1);


        luaL_newmetatable(L, "attrs");
        luaL_setfuncs(L, api_attrs_meta, 0);
        lua_pop(L, 1);

        luaL_newmetatable(L, "list");
        luaL_setfuncs(L, api_list_meta, 0);
        lua_pop(L, 1);

        return 1;
}

static int pairs_closure(lua_State *L)
{
        lua_settop(L, 1);
        if(luaL_getmetafield(L, 1, "__pairs"))
        {
                lua_insert(L, 1);
                lua_call(L, 1, LUA_MULTRET);
                return lua_gettop(L);
        }
        else
        {
                lua_pushvalue(L, lua_upvalueindex(1));
                lua_insert(L, 1);
                lua_call(L, 1, LUA_MULTRET);
                return lua_gettop(L);
        }
}

static void patch_pairs(lua_State *L)
{
        lua_getglobal(L, "pairs");
        lua_pushcclosure(L, pairs_closure, 1);
        lua_setglobal(L, "pairs");
}

static void patch_clibs(lua_State *L)
{
        lua_pushnil(L);
        while(lua_next(L, LUA_REGISTRYINDEX))
        {
                if(lua_type(L, -2) == LUA_TLIGHTUSERDATA && lua_type(L, -1) == LUA_TTABLE)
                {
                        lua_setfield(L, LUA_REGISTRYINDEX, "_CLIBS");
                        lua_pop(L, 1);
                        break;
                }
                lua_pop(L, 1);
        }
}

static GPtrArray *scripts;

static char *expand_buffer = NULL;
static char const *expand_path(char const *path)
{
        if(g_path_is_absolute(path))
                return path;
#ifndef G_OS_WIN32
        if(path[0] == '~')
        {
                if(!path[1] || path[1] == '/')
                {
                        g_free(expand_buffer);
                        expand_buffer = g_build_filename(g_get_home_dir(), path + 1, NULL);
                        return expand_buffer;
                }
                else
                {
                        char *user = g_strdup(path + 1);
                        char *slash_pos = strchr(user, '/');
                        struct passwd *pw;
                        if(slash_pos)
                                *slash_pos = 0;
                        pw = getpwnam(user);
                        g_free(user);
                        if(pw)
                        {
                                slash_pos = strchr(path, '/');
                                if(!slash_pos)
                                        return pw->pw_dir;

                                g_free(expand_buffer);
                                expand_buffer = g_strconcat(pw->pw_dir, slash_pos, NULL);
                                return expand_buffer;
                        }
                        else
                        {
                                return path;
                        }
                }
        }
        else
#endif
        {
                g_free(expand_buffer);
                expand_buffer = g_build_filename(zoitechat_get_info(ph, "configdir"), "addons", path, NULL);
                return expand_buffer;
        }
}

static inline int is_lua_file(char const *file)
{
        return g_str_has_suffix(file, ".lua") || g_str_has_suffix(file, ".luac");
}

static void prepare_state(lua_State *L, script_info *info)
{
        luaL_openlibs(L);
        if(LUA_VERSION_NUM < 502)
                patch_pairs(L);
        if(LUA_VERSION_NUM > 502)
                patch_clibs(L);
        lua_getglobal(L, "debug");
        lua_getfield(L, -1, "traceback");
        info->traceback = luaL_ref(L, LUA_REGISTRYINDEX);
        lua_pop(L, 1);
        lua_pushlightuserdata(L, info);
        lua_setfield(L, LUA_REGISTRYINDEX, registry_field);
        luaopen_zoitechat(L);
        lua_setglobal(L, "zoitechat");

        lua_getglobal(L, "zoitechat");
        lua_setglobal(L, "xchat");

        lua_getglobal(L, "zoitechat");
        lua_setglobal(L, "hexchat");

        lua_getglobal(L, "zoitechat");
        lua_getfield(L, -1, "print");
        lua_setglobal(L, "print");
        lua_pop(L, 1);
}

static void run_unload_hook(hook_info *hook, lua_State *L)
{
        int base = lua_gettop(L);

        lua_rawgeti(L, LUA_REGISTRYINDEX, hook->ref);
        if(lua_pcall(L, 0, 0, base))
        {
                char const *error = lua_tostring(L, -1);
                zoitechat_printf(ph, "Lua error in unload hook: %s", error ? error : "(non-string error)");
        }
        lua_settop(L, base);
}

static void run_unload_hooks(script_info *info, void *unused)
{
        lua_State *L = info->state;
        lua_rawgeti(L, LUA_REGISTRYINDEX, info->traceback);
        g_ptr_array_foreach(info->unload_hooks, (GFunc)run_unload_hook, L);
        lua_pop(L, 1);
}

static void destroy_script(script_info *info)
{
        if (info)
        {
                g_clear_pointer(&info->hooks, g_ptr_array_unref);
                g_clear_pointer(&info->unload_hooks, g_ptr_array_unref);
                g_clear_pointer(&info->state, lua_close);
                if (info->handle)
                        zoitechat_plugingui_remove(ph, info->handle);
                g_free(info->filename);
                g_free(info->name);
                g_free(info->description);
                g_free(info->version);
                g_free(info);
        }
}

static script_info *create_script(char const *file)
{
        int base;
        char *filename_fs;
        lua_State *L;
        script_info *info = g_new0(script_info, 1);
        info->hooks = g_ptr_array_new_with_free_func((GDestroyNotify)free_hook);
        info->unload_hooks = g_ptr_array_new_with_free_func((GDestroyNotify)free_hook);
        info->filename = g_strdup(expand_path(file));
        L = luaL_newstate();
        info->state = L;
        if(!L)
        {
                zoitechat_print(ph, "\00304Could not allocate memory for the script");
                destroy_script(info);
                return NULL;
        }
        prepare_state(L, info);
        lua_rawgeti(L, LUA_REGISTRYINDEX, info->traceback);
        base = lua_gettop(L);
        filename_fs = g_filename_from_utf8(info->filename, -1, NULL, NULL, NULL);
        if(!filename_fs)
        {
                zoitechat_printf(ph, "Invalid filename: %s", info->filename);
                destroy_script(info);
                return NULL;
        }
        if(luaL_loadfile(L, filename_fs))
        {
                g_free(filename_fs);
                zoitechat_printf(ph, "Lua syntax error: %s", luaL_optstring(L, -1, ""));
                destroy_script(info);
                return NULL;
        }
        g_free(filename_fs);
        info->status |= STATUS_ACTIVE;
        if(lua_pcall(L, 0, 0, base))
        {
                char const *error = lua_tostring(L, -1);
                zoitechat_printf(ph, "Lua error: %s", error ? error : "(non-string error)");
                destroy_script(info);
                return NULL;
        }
        lua_pop(L, 1);
        if(!info->name)
        {
                zoitechat_printf(ph, "Lua script didn't register with zoitechat.register");
                destroy_script(info);
                return NULL;
        }
        return info;
}

static script_info *get_script_by_file(char const *filename)
{
        guint i;
        for(i = 0; i < scripts->len; i++)
        {
                script_info *script = scripts->pdata[i];
                if(!strcmp(script->filename, filename))
                {
                        return script;
                }
                if(g_path_is_absolute(filename) || strchr(filename, '/'))
                {
                        char const *expanded = expand_path(filename);
                        if(!strcmp(script->filename, expanded))
                                return script;
                }
                else
                {
                        char *basename = g_path_get_basename(script->filename);
                        gboolean match = !strcmp(basename, filename);
                        g_free(basename);
                        if(match)
                                return script;
                }
        }

        return NULL;
}

static int load_script(char const *file)
{
        script_info *info = get_script_by_file(file);

        if (info != NULL)
        {
                zoitechat_print(ph, "Lua script is already loaded");
                return 0;
        }

        info = create_script(file);
        if (!info)
        {
                return 0;
        }

        g_ptr_array_add(scripts, info);
        check_deferred(info);

        return 1;
}

static int unload_script(char const *filename)
{
        script_info *script = get_script_by_file(filename);

        if (!script)
                return 0;

        if(script->status & STATUS_ACTIVE)
                script->status |= STATUS_DEFERRED_UNLOAD;
        else
        {
                run_unload_hooks(script, NULL);
                g_ptr_array_remove_fast(scripts, script);
        }

        return 1;

}

static int reload_script(char const *filename)
{
        script_info *script = get_script_by_file(filename);

        if (!script)
                return 0;

        if(script->status & STATUS_ACTIVE)
        {
                script->status |= STATUS_DEFERRED_RELOAD;
        }
        else
        {
                char *filename = g_strdup(script->filename);
                run_unload_hooks(script, NULL);
                g_ptr_array_remove_fast(scripts, script);
                load_script(filename);
                g_free(filename);
        }

        return 1;
}

static void autoload_scripts(void)
{
        char *path = g_build_filename(zoitechat_get_info(ph, "configdir"), "addons", NULL);
        GDir *dir = g_dir_open(path, 0, NULL);
        if(dir)
        {
                char const *filename;
                while((filename = g_dir_read_name(dir)))
                {
                        if(is_lua_file(filename))
                                load_script(filename);
                }
                g_dir_close(dir);
        }
        g_free(path);
}

static script_info *interp = NULL;
static void create_interpreter(void)
{
        lua_State *L;
        interp = g_new0(script_info, 1);
        interp->hooks = g_ptr_array_new_with_free_func((GDestroyNotify)free_hook);
        interp->unload_hooks = g_ptr_array_new_with_free_func((GDestroyNotify)free_hook);
        interp->name = "lua interpreter";
        interp->description = "";
        interp->version = "";
        interp->handle = ph;
        interp->filename = "";
        L = luaL_newstate();
        interp->state = L;
        if(!L)
        {
                zoitechat_print(ph, "\00304Could not allocate memory for the interpreter");
                g_free(interp);
                interp = NULL;
                return;
        }
        prepare_state(L, interp);
}

static void destroy_interpreter(void)
{
        if(interp)
        {
                g_clear_pointer(&interp->hooks, g_ptr_array_unref);
                g_clear_pointer(&interp->unload_hooks, g_ptr_array_unref);
                g_clear_pointer(&interp->state, lua_close);
                g_clear_pointer(&interp, g_free);
        }
}

static void inject_string(script_info *info, char const *line)
{
        lua_State *L = info->state;
        int base, top;
        char *ret_line;
        gboolean force_ret = FALSE;

        if(line[0] == '=')
        {
                line++;
                force_ret = TRUE;
        }
        ret_line = g_strconcat("return ", line, NULL);

        lua_rawgeti(L, LUA_REGISTRYINDEX, info->traceback);
        base = lua_gettop(L);
        if(luaL_loadbuffer(L, ret_line, strlen(ret_line), "@interpreter"))
        {
                if(!force_ret)
                        lua_pop(L, 1);
                if(force_ret || luaL_loadbuffer(L, line, strlen(line), "@interpreter"))
                {
                        zoitechat_printf(ph, "Lua syntax error: %s", luaL_optstring(L, -1, ""));
                        lua_pop(L, 2);
                        g_free(ret_line);
                        return;
                }
        }
        g_free(ret_line);
        info->status |= STATUS_ACTIVE;
        if(lua_pcall(L, 0, LUA_MULTRET, base))
        {
                char const *error = lua_tostring(L, -1);
                lua_pop(L, 2);
                zoitechat_printf(ph, "Lua error: %s", error ? error : "(non-string error)");
                return;
        }
        top = lua_gettop(L);
        if(top > base)
        {
                int i;
                luaL_Buffer b;
                luaL_buffinit(L, &b);
                for(i = base + 1; i <= top; i++)
                {
                        if(i != base + 1)
                                luaL_addstring(&b, " ");
                        tostring(L, i);
                        luaL_addvalue(&b);
                }
                luaL_pushresult(&b);
                zoitechat_print(ph, lua_tostring(L, -1));
                lua_pop(L, top - base + 1);
        }
        lua_pop(L, 1);
        check_deferred(info);
}

static int command_load(char *word[], char *word_eol[], void *userdata)
{
        if(is_lua_file(word[2]))
        {
                load_script(word[2]);
                return ZOITECHAT_EAT_ALL;
        }
        else
                return ZOITECHAT_EAT_NONE;
}

static int command_unload(char *word[], char *word_eol[], void *userdata)
{
        if(unload_script(word[2]))
                return ZOITECHAT_EAT_ALL;
        else
                return ZOITECHAT_EAT_NONE;
}

static int command_reload(char *word[], char *word_eol[], void *userdata)
{
        if(reload_script(word[2]))
                return ZOITECHAT_EAT_ALL;
        else
                return ZOITECHAT_EAT_NONE;
}

static int command_console_exec(char *word[], char *word_eol[], void *userdata)
{
        char const *channel = zoitechat_get_info(ph, "channel");
        if(channel && !strcmp(channel, console_tab))
        {
                if(interp)
                {
                        zoitechat_printf(ph, "> %s", word_eol[1]);
                        inject_string(interp, word_eol[1]);
                }
                return ZOITECHAT_EAT_ALL;
        }
        return ZOITECHAT_EAT_NONE;
}

static void check_deferred(script_info *info)
{
        info->status &= ~STATUS_ACTIVE;
        if(info->status & STATUS_DEFERRED_UNLOAD)
        {
                run_unload_hooks(info, NULL);
                g_ptr_array_remove_fast(scripts, info);
        }
        else if(info->status & STATUS_DEFERRED_RELOAD)
        {
                if(info == interp)
                {
                        run_unload_hooks(interp, NULL);
                        destroy_interpreter();
                        create_interpreter();
                }
                else
                {
                        char *filename = g_strdup(info->filename);
                        run_unload_hooks(info, NULL);
                        g_ptr_array_remove_fast(scripts, info);
                        load_script(filename);
                        g_free(filename);
                }
        }
}

static int command_lua(char *word[], char *word_eol[], void *userdata)
{
        if(!strcmp(word[2], "load"))
        {
                if(load_script(word[3]))
                        zoitechat_printf(ph, "Loaded Lua script '%s'", word[3]);
                else
                        zoitechat_printf(ph, "Failed to load Lua script '%s'", word[3]);
        }
        else if(!strcmp(word[2], "unload"))
        {
                script_info *script = get_script_by_file(word[3]);
                if(script)
                {
                        gboolean deferred = script->status & STATUS_ACTIVE;
                        unload_script(word[3]);
                        if(deferred)
                                zoitechat_printf(ph, "Unload scheduled for Lua script '%s'", word[3]);
                        else
                                zoitechat_printf(ph, "Unloaded Lua script '%s'", word[3]);
                }
                else
                        zoitechat_printf(ph, "Could not find a script by the name '%s'", word[3]);
        }
        else if(!strcmp(word[2], "reload"))
        {
                if(!reload_script(word[3]))
                        zoitechat_printf(ph, "Could not find a script by the name '%s'", word[3]);
        }
        else if(!strcmp(word[2], "exec"))
        {
                if(interp)
                        inject_string(interp, word_eol[3]);
        }
        else if(!strcmp(word[2], "inject"))
        {
                script_info *script = get_script_by_file(word[3]);
                if (script)
                {
                        inject_string(script, word_eol[4]);
                }
                else
                {
                        zoitechat_printf(ph, "Could not find a script by the name '%s'", word[3]);
                }
        }
        else if(!strcmp(word[2], "reset"))
        {
                if(interp)
                {
                        if(interp->status & STATUS_ACTIVE)
                        {
                                interp->status |= STATUS_DEFERRED_RELOAD;
                        }
                        else
                        {
                                run_unload_hooks(interp, NULL);
                                destroy_interpreter();
                                create_interpreter();
                        }
                }
        }
        else if(!strcmp(word[2], "list"))
        {
                guint i;
                zoitechat_print(ph,
                   "Name             Version  Filename             Description\n"
                   "----             -------  --------             -----------\n");
                for(i = 0; i < scripts->len; i++)
                {
                        script_info *info = scripts->pdata[i];
                        char *basename = g_path_get_basename(info->filename);
                        zoitechat_printf(ph, "%-16s %-8s %-20s %-10s\n", info->name, info->version,
                                                   basename, info->description);
                        g_free(basename);
                }
                if(interp)
                        zoitechat_printf(ph, "%-16s %-8s %-20s %-10s", interp->name, plugin_version, "", "");
        }
        else if(!strcmp(word[2], "console"))
        {
                zoitechat_commandf(ph, "query %s", console_tab);
        }
        else
        {
                zoitechat_command(ph, "help lua");
        }
        return ZOITECHAT_EAT_ALL;
}

/* Reinitialization safegaurd */
static int initialized = 0;

G_MODULE_EXPORT int zoitechat_plugin_init(zoitechat_plugin *plugin_handle, char **name, char **description, char **version, char *arg)
{
        if(initialized != 0)
        {
                zoitechat_print(plugin_handle, "Lua interface already loaded\n");
                return 0;
        }

        if (g_str_has_prefix(LUA_VERSION, "Lua "))
        {
                strcat(plugin_version, "/");
                g_strlcat(plugin_version, LUA_VERSION + 4, sizeof(plugin_version));
        }

        *name = plugin_name;
        *description = plugin_description;
        *version = plugin_version;

        ph = plugin_handle;
        initialized = 1;

        zoitechat_hook_command(ph, "", ZOITECHAT_PRI_NORM, command_console_exec, NULL, NULL);
        zoitechat_hook_command(ph, "LOAD", ZOITECHAT_PRI_NORM, command_load, NULL, NULL);
        zoitechat_hook_command(ph, "UNLOAD", ZOITECHAT_PRI_NORM, command_unload, NULL, NULL);
        zoitechat_hook_command(ph, "RELOAD", ZOITECHAT_PRI_NORM, command_reload, NULL, NULL);
        zoitechat_hook_command(ph, "lua", ZOITECHAT_PRI_NORM, command_lua, command_help, NULL);

        zoitechat_printf(ph, "%s version %s loaded.\n", plugin_name, plugin_version);

        scripts = g_ptr_array_new_with_free_func((GDestroyNotify)destroy_script);
        create_interpreter();

        if(!arg)
                autoload_scripts();
        return 1;
}

G_MODULE_EXPORT int zoitechat_plugin_deinit(zoitechat_plugin *plugin_handle)
{
        guint i;
        gboolean active = FALSE;
        for(i = 0; i < scripts->len; i++)
        {
                if(((script_info*)scripts->pdata[i])->status & STATUS_ACTIVE)
                {
                        active = TRUE;
                        break;
                }
        }
        if(interp && interp->status & STATUS_ACTIVE)
                active = TRUE;
        if(active)
        {
                zoitechat_print(ph, "\00304Cannot unload the lua plugin while there are active states");
                return 0;
        }
        if(interp)
                run_unload_hooks(interp, NULL);
        destroy_interpreter();
        g_ptr_array_foreach(scripts, (GFunc)run_unload_hooks, NULL);
        g_clear_pointer(&scripts, g_ptr_array_unref);
        g_clear_pointer(&expand_buffer, g_free);
        return 1;
}
