/* vifm
 * Copyright (C) 2020 xaizek.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include "vlua.h"

#include <assert.h> /* assert() */
#include <stddef.h> /* NULL size_t */
#include <stdlib.h> /* free() */
#include <string.h> /* strdup() */

#include "../compat/dtype.h"
#include "../compat/fs_limits.h"
#include "../engine/variables.h"
#include "../ui/statusbar.h"
#include "../ui/ui.h"
#include "../utils/fs.h"
#include "../utils/str.h"
#include "../utils/string_array.h"
#include "../cmd_core.h"
#include "../macros.h"
#include "../plugins.h"
#include "../status.h"
#include "lua/lauxlib.h"
#include "lua/lua.h"
#include "api.h"
#include "common.h"
#include "vifm.h"
#include "vifm_cmds.h"
#include "vifm_events.h"
#include "vifm_handlers.h"
#include "vifm_viewcolumns.h"
#include "vifmjob.h"
#include "vlua_cbacks.h"
#include "vlua_state.h"

static void patch_env(lua_State *lua);
static void load_api(lua_State *lua);
static int VLUA_API(print)(lua_State *lua);
static int VLUA_API(os_getenv)(lua_State *lua);
static int VLUA_API(vifm_plugin_require)(lua_State *lua);
static int VLUA_IMPL(require_plugin_module)(lua_State *lua);
static int load_plugin(lua_State *lua, plug_t *plug);
static void setup_plugin_env(lua_State *lua, plug_t *plug);

VLUA_DECLARE_SAFE(print);
VLUA_DECLARE_SAFE(os_getenv);
VLUA_DECLARE_UNSAFE(vifm_plugin_require);

/* Defined in another unit. */
VLUA_DECLARE_SAFE(vifm_addhandler);

/* Address of this variable serves as a key in Lua table. */
static char plugin_envs;

vlua_t *
vlua_init(void)
{
	vlua_t *vlua = vlua_state_alloc();
	if(vlua == NULL)
	{
		return NULL;
	}

	patch_env(vlua->lua);
	load_api(vlua->lua);

	vlua_cbacks_init(vlua);
	vifm_viewcolumns_init(vlua);
	vifm_handlers_init(vlua);

	vlua_state_make_table(vlua, &plugin_envs);

	return vlua;
}

void
vlua_finish(vlua_t *vlua)
{
	if(vlua != NULL)
	{
		vifmjob_finish(vlua->lua);
		vlua_state_free(vlua);
	}
}

/* Adjusts standard libraries. */
static void
patch_env(lua_State *lua)
{
	lua_pushcfunction(lua, VLUA_REF(print));
	lua_setglobal(lua, "print");

	lua_getglobal(lua, "os");
	lua_createtable(lua, /*narr=*/0, /*nrec=*/6);
	lua_getfield(lua, -2, "clock");
	lua_setfield(lua, -2, "clock");
	lua_getfield(lua, -2, "date");
	lua_setfield(lua, -2, "date");
	lua_getfield(lua, -2, "difftime");
	lua_setfield(lua, -2, "difftime");
	lua_pushcfunction(lua, VLUA_REF(os_getenv));
	lua_setfield(lua, -2, "getenv");
	lua_getfield(lua, -2, "time");
	lua_setfield(lua, -2, "time");
	lua_getfield(lua, -2, "tmpname");
	lua_setfield(lua, -2, "tmpname");
	lua_setglobal(lua, "os");
	lua_pop(lua, 1);
}

/* Fills Lua state with application-specific API. */
static void
load_api(lua_State *lua)
{
	vlua_cmn_make_metatable(lua, "VifmPluginEnv");
	lua_pushglobaltable(lua);
	lua_setfield(lua, -2, "__index");
	lua_pop(lua, 1);

	vifm_init(lua);
	lua_setglobal(lua, "vifm");
}

/* Replacement of standard global `print` function.  If plugin is present,
 * outputs to a plugin log, otherwise outputs to status bar.  Doesn't return
 * anything. */
static int
VLUA_API(print)(lua_State *lua)
{
	char *msg = strdup("");
	size_t msg_len = 0U;

	int nargs = lua_gettop(lua);
	int i;
	for(i = 0; i < nargs; ++i)
	{
		const char *piece = luaL_tolstring(lua, i + 1, NULL);
		if(i > 0)
		{
			(void)strappendch(&msg, &msg_len, '\t');
		}
		(void)strappend(&msg, &msg_len, piece);
		lua_pop(lua, 1);
	}

	plug_t *plug = lua_touserdata(lua, lua_upvalueindex(1));
	if(plug != NULL)
	{
		plug_log(plug, msg);
	}
	else
	{
		ui_sb_msg(msg);
		curr_stats.save_msg = 1;
	}

	free(msg);
	return 0;
}

/* os.getenv() that's aware of Vifm's internal variables. */
static int
VLUA_API(os_getenv)(lua_State *lua)
{
	lua_pushstring(lua, local_getenv_null(luaL_checkstring(lua, 1)));
	return 1;
}

int
vlua_load_plugin(vlua_t *vlua, plug_t *plug)
{
	if(load_plugin(vlua->lua, plug) == 0)
	{
		lua_getglobal(vlua->lua, "vifm");
		lua_getfield(vlua->lua, -1, "plugins");
		lua_getfield(vlua->lua, -1, "all");
		lua_pushvalue(vlua->lua, -4);
		lua_setfield(vlua->lua, -2, plug->name);
		lua_pop(vlua->lua, 4);
		return 0;
	}
	return 1;
}

/* Loads a single plugin as a module.  Returns zero on success and places value
 * that corresponds to the module onto the stack, otherwise non-zero is
 * returned. */
static int
load_plugin(lua_State *lua, plug_t *plug)
{
	char full_path[PATH_MAX + 32];
	snprintf(full_path, sizeof(full_path), "%s/init.lua", plug->path);

	if(luaL_loadfile(lua, full_path) != LUA_OK)
	{
		const char *error = lua_tostring(lua, -1);
		plug_log(plug, error);
		ui_sb_errf("Failed to load '%s' plugin: %s", plug->name, error);
		lua_pop(lua, 1);
		return 1;
	}

	setup_plugin_env(lua, plug);
	if(lua_pcall(lua, 0, 1, 0) != LUA_OK)
	{
		const char *error = lua_tostring(lua, -1);
		plug_log(plug, error);
		ui_sb_errf("Failed to start '%s' plugin: %s", plug->name, error);
		lua_pop(lua, 1);
		return 1;
	}

	if(lua_gettop(lua) == 0 || !lua_istable(lua, -1))
	{
		ui_sb_errf("Failed to load '%s' plugin: %s", plug->name,
				"it didn't return a table");
		if(lua_gettop(lua) > 0)
		{
			lua_pop(lua, 1);
		}
		return 1;
	}

	return 0;
}

/* Sets upvalue #1 to a plugin-specific version of environment. */
static void
setup_plugin_env(lua_State *lua, plug_t *plug)
{
	/* Global environment table. */
	lua_createtable(lua, /*narr=*/0, /*nrec=*/1);
	luaL_getmetatable(lua, "VifmPluginEnv");
	lua_setmetatable(lua, -2);
	/* Don't let a plugin access true global table by using _G explicitly. */
	lua_pushvalue(lua, -1);
	lua_setfield(lua, -2, "_G");

	/* Plugin-specific `vifm` table. */
	lua_createtable(lua, /*narr=*/0, /*nrec=*/2);
	/* Meta-table for it. */
	vlua_cmn_make_metatable(lua, /*name=*/NULL);
	lua_getglobal(lua, "vifm");
	lua_setfield(lua, -2, "__index");
	lua_setmetatable(lua, -2);

	/* Plugin-specific `vifm.plugin` table. */
	lua_createtable(lua, /*narr=*/0, /*nrec=*/3);
	lua_pushstring(lua, plug->name);
	lua_setfield(lua, -2, "name");
	lua_pushstring(lua, plug->path);
	lua_setfield(lua, -2, "path");
	/* Plugin-specific `vifm.plugin.require`. */
	lua_pushlightuserdata(lua, plug);
	lua_pushcclosure(lua, VLUA_REF(vifm_plugin_require), 1);
	lua_setfield(lua, -2, "require");
	lua_setfield(lua, -2, "plugin");  /* vifm.plugin */

	/* Plugin-specific `vifm.addhandler()`. */
	lua_pushlightuserdata(lua, plug);
	lua_pushcclosure(lua, VLUA_REF(vifm_addhandler), 1);
	lua_setfield(lua, -2, "addhandler"); /* vifm.addhandler */

	/* Assign `vifm` as a plugin-specific global. */
	lua_setfield(lua, -2, "vifm");

	/* Plugin-specific `print()`. */
	lua_pushlightuserdata(lua, plug);
	lua_pushcclosure(lua, VLUA_REF(print), 1);
	lua_setfield(lua, -2, "print");

	/* Map plug to plugin environment for future queries. */
	vlua_state_get_table(vlua_state_get(lua), &plugin_envs);
	lua_pushlightuserdata(lua, plug);
	lua_pushvalue(lua, -3);
	lua_settable(lua, -3);
	lua_pop(lua, 1);

	if(lua_setupvalue(lua, -2, 1) == NULL)
	{
		lua_pop(lua, 1);
	}
}

/* Member of `vifm.plugin` that loads a module relative to the plugin's root.
 * Returns module's return. */
static int
VLUA_API(vifm_plugin_require)(lua_State *lua)
{
	const char *mod_name = luaL_checkstring(lua, 1);

	plug_t *plug = lua_touserdata(lua, lua_upvalueindex(1));
	if(plug == NULL)
	{
		assert(0 && "vifm.plugin.require() called outside a plugin?");
		return 0;
	}

	char full_path[PATH_MAX + 1];
	snprintf(full_path, sizeof(full_path), "%s/%s.lua", plug->path, mod_name);
	if(!path_exists(full_path, DEREF))
	{
		snprintf(full_path, sizeof(full_path), "%s/%s/init.lua", plug->path,
				mod_name);
	}

	/* luaL_requiref() equivalent that passes plug to require_plugin_module(). */
	luaL_getsubtable(lua, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
	lua_getfield(lua, -1, full_path);
	if(!lua_toboolean(lua, -1))
	{
		lua_pop(lua, 1);
		lua_pushlightuserdata(lua, plug);
		lua_pushcclosure(lua, VLUA_IREF(require_plugin_module), 1);
		lua_pushstring(lua, full_path);
		lua_call(lua, 1, 1);
		lua_pushvalue(lua, -1);
		lua_setfield(lua, -3, full_path);
	}
	lua_remove(lua, -2);

	return 1;
}

/* Helper that loads a module.  Returns module's return. */
static int
VLUA_IMPL(require_plugin_module)(lua_State *lua)
{
	const char *mod = luaL_checkstring(lua, 1);
	if(luaL_loadfile(lua, mod) != LUA_OK)
	{
		const char *error = lua_tostring(lua, -1);
		return luaL_error(lua, "vifm.plugin.require('%s'): %s", mod, error);
	}

	/* Fetch custom environment of the current plugin. */
	plug_t *plug = lua_touserdata(lua, lua_upvalueindex(1));
	assert(plug != NULL && "Invalid call to require_plugin_module()");
	vlua_state_get_table(vlua_state_get(lua), &plugin_envs);
	lua_pushlightuserdata(lua, plug);
	if(lua_gettable(lua, -2) != LUA_TTABLE)
	{
		return luaL_error(lua,
				"vifm.plugin.require('%s'): failed to fetch plugin env", mod);
	}
	lua_remove(lua, -2);

	/* Use that environment for the newly loaded module. */
	if(lua_setupvalue(lua, -2, 1) == NULL)
	{
		return luaL_error(lua,
				"vifm.plugin.require('%s'): failed to copy plugin env", mod);
	}

	if(lua_pcall(lua, 0, 1, 0) != LUA_OK)
	{
		const char *error = lua_tostring(lua, -1);
		return luaL_error(lua, "vifm.plugin.require('%s'): %s", mod, error);
	}
	return 1;
}

int
vlua_run_string(vlua_t *vlua, const char str[])
{
	int old_top = lua_gettop(vlua->lua);

	int errored = luaL_dostring(vlua->lua, str);
	if(errored)
	{
		ui_sb_err(lua_tostring(vlua->lua, -1));
	}

	lua_settop(vlua->lua, old_top);
	return errored;
}

int
vlua_complete_cmd(vlua_t *vlua, const struct cmd_info_t *cmd_info, int arg_pos)
{
	return vifm_cmds_complete(vlua->lua, cmd_info, arg_pos);
}

int
vlua_viewcolumns_next_id(vlua_t *vlua)
{
	return vifm_viewcolumns_next_id(vlua);
}

int
vlua_viewcolumn_map(vlua_t *vlua, const char name[])
{
	return vifm_viewcolumns_map(vlua, name);
}

char *
vlua_viewcolumn_map_back(vlua_t *vlua, int id)
{
	return vifm_viewcolumns_map_back(vlua, id);
}

int
vlua_viewcolumn_is_primary(vlua_t *vlua, int column_id)
{
	return vifm_viewcolumns_is_primary(vlua, column_id);
}

int
vlua_handler_cmd(vlua_t *vlua, const char cmd[])
{
	return vifm_handlers_check(vlua, cmd);
}

int
vlua_handler_present(vlua_t *vlua, const char cmd[])
{
	return vifm_handlers_present(vlua, cmd);
}

strlist_t
vlua_view_file(vlua_t *vlua, const char viewer[], const char path[],
		const struct preview_area_t *parea)
{
	return vifm_handlers_view(vlua, viewer, path, parea);
}

void
vlua_open_file(vlua_t *vlua, const char prog[], const struct dir_entry_t *entry)
{
	return vifm_handlers_open(vlua, prog, entry);
}

char *
vlua_make_status_line(vlua_t *vlua, const char format[], struct view_t *view,
		int width)
{
	return vifm_handlers_make_status_line(vlua, format, view, width);
}

char *
vlua_make_tab_line(vlua_t *vlua, const char format[], int other, int width)
{
	return vifm_handlers_make_tab_line(vlua, format, other, width);
}

int
vlua_open_help(vlua_t *vlua, const char handler[], const char topic[])
{
	return vifm_handlers_open_help(vlua, handler, topic);
}

int
vlua_edit_one(vlua_t *vlua, const char handler[], const char path[], int line,
		int column, int must_wait)
{
	return vifm_handlers_edit_one(vlua, handler, path, line, column, must_wait);
}

int
vlua_edit_many(vlua_t *vlua, const char handler[], char *files[], int nfiles)
{
	return vifm_handlers_edit_many(vlua, handler, files, nfiles);
}

int
vlua_edit_list(vlua_t *vlua, const char handler[], char *entries[],
		int nentries, int current, int quickfix_format)
{
	return vifm_handlers_edit_list(vlua, handler, entries, nentries, current,
			quickfix_format);
}

void
vlua_process_callbacks(vlua_t *vlua)
{
	vlua_cbacks_process(vlua);
}

void
vlua_events_app_exit(vlua_t *vlua)
{
	if(vlua != NULL)
	{
		vifm_events_app_exit(vlua);
	}
}

void
vlua_events_app_fsop(vlua_t *vlua, OPS op, const char path[],
		const char target[], void *extra, int dir)
{
	if(vlua != NULL)
	{
		vifm_events_app_fsop(vlua, op, path, target, extra, dir);
	}
}

/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
/* vim: set cinoptions+=t0 : */
