/* vifm
 * Copyright (C) 2021 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 "vifm_handlers.h"

#include <assert.h> /* assert() */
#include <stdio.h> /* snprintf() */
#include <string.h> /* strchr() strcspn() strdup() */

#include <curses.h>

#include "../compat/fs_limits.h"
#include "../ui/quickview.h"
#include "../ui/statusbar.h"
#include "../ui/ui.h"
#include "../utils/str.h"
#include "../utils/string_array.h"
#include "../utils/utils.h"
#include "../plugins.h"
#include "../vifm.h"
#include "lua/lauxlib.h"
#include "lua/lua.h"
#include "common.h"
#include "vifmentry.h"
#include "vifmview.h"
#include "vlua_state.h"

static char * extract_handler_name(const char viewer[]);
static char * run_format_handler(vlua_t *vlua, const char handler[]);
static int run_editor_handler(vlua_t *vlua, const char handler[]);
static int check_handler_name(vlua_t *vlua, const char name[]);

/* Characters considered to be whitespace. */
static const char *WHITESPACE = " \t";

/* Address of this variable serves as a key in Lua table.  The associated table
 * is doubly keyed: by column name and by corresponding ID. */
static char handlers_key;

int
vifm_handlers_check(vlua_t *vlua, const char cmd[])
{
	return (cmd[0] == '#' && strchr(cmd + 1, '#') != NULL);
}

void
vifm_handlers_init(vlua_t *vlua)
{
	vlua_state_make_table(vlua, &handlers_key);
}

int
vifm_handlers_present(vlua_t *vlua, const char cmd[])
{
	char *name = extract_handler_name(cmd);

	/* Don't need lua_pcall() to handle errors, because no one should be able to
	 * mess with internal tables. */
	vlua_state_get_table(vlua, &handlers_key);
	const int present = (lua_getfield(vlua->lua, -1, name) != LUA_TNIL);
	lua_pop(vlua->lua, 2);

	free(name);

	return present;
}

strlist_t
vifm_handlers_view(vlua_t *vlua, const char viewer[], const char path[],
		const preview_area_t *parea)
{
	strlist_t result = {};

	char *name = extract_handler_name(viewer);

	/* Don't need lua_pcall() to handle errors, because no one should be able to
	 * mess with internal tables. */
	vlua_state_get_table(vlua, &handlers_key);
	if(lua_getfield(vlua->lua, -1, name) != LUA_TTABLE)
	{
		free(name);
		lua_pop(vlua->lua, 2);
		return result;
	}

	free(name);

	assert(lua_getfield(vlua->lua, -1, "handler") == LUA_TFUNCTION &&
			"Handler must be a function here.");

	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/6);
	lua_pushstring(vlua->lua, viewer);
	lua_setfield(vlua->lua, -2, "command");
	lua_pushstring(vlua->lua, path);
	lua_setfield(vlua->lua, -2, "path");

	/* This is probably always non-NULL, but check won't hurt. */
	if(parea != NULL)
	{
		lua_pushinteger(vlua->lua, getbegx(parea->view->win) + parea->x);
		lua_setfield(vlua->lua, -2, "x");
		lua_pushinteger(vlua->lua, getbegy(parea->view->win) + parea->y);
		lua_setfield(vlua->lua, -2, "y");
		lua_pushinteger(vlua->lua, parea->w);
		lua_setfield(vlua->lua, -2, "width");
		lua_pushinteger(vlua->lua, parea->h);
		lua_setfield(vlua->lua, -2, "height");
	}

	const int sm_cookie = vlua_state_safe_mode_on(vlua->lua);
	if(lua_pcall(vlua->lua, 1, 1, 0) != LUA_OK)
	{
		vlua_state_safe_mode_off(vlua->lua, sm_cookie);

		const char *error = lua_tostring(vlua->lua, -1);
		ui_sb_err(error);
		lua_pop(vlua->lua, 3);
		return result;
	}

	vlua_state_safe_mode_off(vlua->lua, sm_cookie);

	if(!lua_istable(vlua->lua, -1))
	{
		lua_pop(vlua->lua, 3);
		return result;
	}

	if(lua_getfield(vlua->lua, -1, "lines") != LUA_TTABLE)
	{
		lua_pop(vlua->lua, 4);
		return result;
	}

	lua_pushnil(vlua->lua);
	while(lua_next(vlua->lua, -2) != 0)
	{
		const char *line = lua_tostring(vlua->lua, -1);
		result.nitems = add_to_string_array(&result.items, result.nitems, line);
		lua_pop(vlua->lua, 1);
	}

	lua_pop(vlua->lua, 4);
	return result;
}

void
vifm_handlers_open(vlua_t *vlua, const char prog[], const dir_entry_t *entry)
{
	char *name = extract_handler_name(prog);

	/* Don't need lua_pcall() to handle errors, because no one should be able to
	 * mess with internal tables. */
	vlua_state_get_table(vlua, &handlers_key);
	if(lua_getfield(vlua->lua, -1, name) != LUA_TTABLE)
	{
		free(name);
		lua_pop(vlua->lua, 2);
		return;
	}

	free(name);

	assert(lua_getfield(vlua->lua, -1, "handler") == LUA_TFUNCTION &&
			"Handler must be a function here.");

	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/2);
	lua_pushstring(vlua->lua, prog);
	lua_setfield(vlua->lua, -2, "command");
	vifmentry_new(vlua->lua, entry);
	lua_setfield(vlua->lua, -2, "entry");

	const int sm_cookie = vlua_state_safe_mode_on(vlua->lua);
	if(lua_pcall(vlua->lua, 1, 0, 0) != LUA_OK)
	{
		vlua_state_safe_mode_off(vlua->lua, sm_cookie);

		const char *error = lua_tostring(vlua->lua, -1);
		ui_sb_err(error);
		lua_pop(vlua->lua, 3);
		return;
	}

	vlua_state_safe_mode_off(vlua->lua, sm_cookie);
	lua_pop(vlua->lua, 2);
}

char *
vifm_handlers_make_status_line(vlua_t *vlua, const char format[], view_t *view,
		int width)
{
	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/2);
	vifmview_new(vlua->lua, view);
	lua_setfield(vlua->lua, -2, "view");
	lua_pushinteger(vlua->lua, width);
	lua_setfield(vlua->lua, -2, "width");

	char *result = run_format_handler(vlua, format);
	lua_pop(vlua->lua, 1);
	return result;
}

char *
vifm_handlers_make_tab_line(vlua_t *vlua, const char format[], int other,
		int width)
{
	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/2);
	lua_pushboolean(vlua->lua, other);
	lua_setfield(vlua->lua, -2, "other");
	lua_pushinteger(vlua->lua, width);
	lua_setfield(vlua->lua, -2, "width");

	char *result = run_format_handler(vlua, format);
	lua_pop(vlua->lua, 1);
	return result;
}

/* Invokes a format handler.  Expects a table argument to it at the top of Lua
 * stack.  Returns format string on success, otherwise NULL is returned. */
static char *
run_format_handler(vlua_t *vlua, const char handler[])
{
	char *name = extract_handler_name(handler);

	/* Don't need lua_pcall() to handle errors, because no one should be able to
	 * mess with internal tables. */
	vlua_state_get_table(vlua, &handlers_key);
	if(lua_getfield(vlua->lua, -1, name) != LUA_TTABLE)
	{
		free(name);
		lua_pop(vlua->lua, 2);
		return strdup("Invalid handler");
	}

	free(name);

	assert(lua_getfield(vlua->lua, -1, "handler") == LUA_TFUNCTION &&
			"Handler must be a function here.");

	lua_pushvalue(vlua->lua, -4);

	const int sm_cookie = vlua_state_safe_mode_on(vlua->lua);
	if(lua_pcall(vlua->lua, 1, 1, 0) != LUA_OK)
	{
		vlua_state_safe_mode_off(vlua->lua, sm_cookie);

		char *error = strdup(lua_tostring(vlua->lua, -1));
		lua_pop(vlua->lua, 3);
		return error;
	}

	vlua_state_safe_mode_off(vlua->lua, sm_cookie);

	if(!lua_istable(vlua->lua, -1))
	{
		lua_pop(vlua->lua, 3);
		return strdup("Return value isn't a table.");
	}

	if(lua_getfield(vlua->lua, -1, "format") == LUA_TNIL)
	{
		lua_pop(vlua->lua, 4);
		return strdup("Return value is missing 'format' key.");
	}

	const char *result = lua_tostring(vlua->lua, -1);
	char *format = strdup(result == NULL ? "" : result);

	lua_pop(vlua->lua, 4);

	return format;
}

int
vifm_handlers_open_help(vlua_t *vlua, const char handler[], const char topic[])
{
	char vimdoc_dir[PATH_MAX + 1];
	snprintf(vimdoc_dir, sizeof(vimdoc_dir), "%s/vim-doc",
			get_installed_data_dir());

	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/3);

	lua_pushstring(vlua->lua, "open-help");
	lua_setfield(vlua->lua, -2, "action");

	lua_pushstring(vlua->lua, vimdoc_dir);
	lua_setfield(vlua->lua, -2, "vimdocdir");

	if(topic[0] != '\0')
	{
		lua_pushstring(vlua->lua, topic);
		lua_setfield(vlua->lua, -2, "topic");
	}

	int result = run_editor_handler(vlua, handler);
	lua_pop(vlua->lua, 1);
	return result;
}

int
vifm_handlers_edit_one(vlua_t *vlua, const char handler[], const char path[],
		int line, int column, int must_wait)
{
	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/5);
	lua_pushstring(vlua->lua, "edit-one");
	lua_setfield(vlua->lua, -2, "action");
	lua_pushstring(vlua->lua, path);
	lua_setfield(vlua->lua, -2, "path");
	lua_pushboolean(vlua->lua, must_wait);
	lua_setfield(vlua->lua, -2, "mustwait");
	if(line >= 0)
	{
		lua_pushinteger(vlua->lua, line);
		lua_setfield(vlua->lua, -2, "line");
		if(column >= 0)
		{
			lua_pushinteger(vlua->lua, column);
			lua_setfield(vlua->lua, -2, "column");
		}
	}

	int result = run_editor_handler(vlua, handler);
	lua_pop(vlua->lua, 1);
	return result;
}

int
vifm_handlers_edit_many(struct vlua_t *vlua, const char handler[],
		char *files[], int nfiles)
{
	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/2);
	lua_pushstring(vlua->lua, "edit-many");
	lua_setfield(vlua->lua, -2, "action");
	vlua_cmn_push_str_array(vlua->lua, files, nfiles);
	lua_setfield(vlua->lua, -2, "paths");

	int result = run_editor_handler(vlua, handler);
	lua_pop(vlua->lua, 1);
	return result;
}

int
vifm_handlers_edit_list(vlua_t *vlua, const char handler[], char *entries[],
		int nentries, int current, int quickfix_format)
{
	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/4);
	lua_pushstring(vlua->lua, "edit-list");
	lua_setfield(vlua->lua, -2, "action");
	vlua_cmn_push_str_array(vlua->lua, entries, nentries);
	lua_setfield(vlua->lua, -2, "entries");
	lua_pushinteger(vlua->lua, current + 1);
	lua_setfield(vlua->lua, -2, "current");
	lua_pushboolean(vlua->lua, quickfix_format);
	lua_setfield(vlua->lua, -2, "isquickfix");

	int result = run_editor_handler(vlua, handler);
	lua_pop(vlua->lua, 1);
	return result;
}

/* Invokes an editor handler.  Expects a table argument to it at the top of Lua
 * stack.  Returns zero on success (no issues running the handler and handler
 * reports success), otherwise non-zero is returned. */
static int
run_editor_handler(vlua_t *vlua, const char handler[])
{
	char *name = extract_handler_name(handler);

	/* Don't need lua_pcall() to handle errors, because no one should be able to
	 * mess with internal tables. */
	vlua_state_get_table(vlua, &handlers_key);
	if(lua_getfield(vlua->lua, -1, name) != LUA_TTABLE)
	{
		free(name);
		lua_pop(vlua->lua, 2);
		return -1;
	}

	free(name);

	assert(lua_getfield(vlua->lua, -1, "handler") == LUA_TFUNCTION &&
			"Handler must be a function here.");

	lua_pushvalue(vlua->lua, -4);

	lua_pushstring(vlua->lua, handler);
	lua_setfield(vlua->lua, -2, "command");

	const int sm_cookie = vlua_state_safe_mode_on(vlua->lua);
	if(lua_pcall(vlua->lua, 1, 1, 0) != LUA_OK)
	{
		vlua_state_safe_mode_off(vlua->lua, sm_cookie);

		ui_sb_err(lua_tostring(vlua->lua, -1));
		lua_pop(vlua->lua, 3);
		return -1;
	}

	vlua_state_safe_mode_off(vlua->lua, sm_cookie);

	if(!lua_istable(vlua->lua, -1))
	{
		lua_pop(vlua->lua, 3);
		return -1;
	}

	if(lua_getfield(vlua->lua, -1, "success") != LUA_TNIL)
	{
		int success = lua_toboolean(vlua->lua, -1);
		lua_pop(vlua->lua, 4);
		return (success ? 0 : -1);
	}

	lua_pop(vlua->lua, 4);
	return -1;
}

/* Extracts name of the handler from a command.  Returns a newly allocated
 * string. */
static char *
extract_handler_name(const char viewer[])
{
	const int name_len = strcspn(viewer, WHITESPACE);
	return format_str("%.*s", name_len, viewer);
}

int
VLUA_API(vifm_addhandler)(lua_State *lua)
{
	vlua_t *vlua = vlua_state_get(lua);

	const char *namespace = NULL;
	plug_t *plug = lua_touserdata(lua, lua_upvalueindex(1));
	if(plug != NULL)
	{
		namespace = plug->name;
	}
	else if(vifm_testing())
	{
		namespace = "vifmtest";
	}
	else
	{
		return luaL_error(lua, "%s", "call to addhandler() outside of a plugin");
	}

	luaL_checktype(lua, 1, LUA_TTABLE);

	vlua_cmn_check_field(lua, 1, "name", LUA_TSTRING);
	const char *name = lua_tostring(lua, -1);
	check_handler_name(vlua, name);

	vlua_cmn_check_field(lua, 1, "handler", LUA_TFUNCTION);

	char *full_name = format_str("#%s#%s", namespace, name);

	vlua_state_get_table(vlua, &handlers_key); /* handlers table */

	/* Check if handler already exists. */
	if(lua_getfield(vlua->lua, -1, full_name) != LUA_TNIL)
	{
		free(full_name);
		lua_pushboolean(lua, 0);
		return 1;
	}
	lua_pop(vlua->lua, 1); /* nil */

	lua_createtable(lua, /*narr=*/0, /*nrec=*/1); /* handler table */
	lua_pushvalue(lua, -3);                       /* info.handler function */
	lua_setfield(lua, -2, "handler");             /* handler.handler */
	lua_setfield(lua, -2, full_name);             /* add to handlers table */

	free(full_name);

	lua_pushboolean(lua, 1);
	return 1;
}

/* Verifies validity of a handler's name.  Return value has no particular
 * meaning. */
static int
check_handler_name(vlua_t *vlua, const char name[])
{
	if(name[0] == '\0')
	{
		return luaL_error(vlua->lua, "%s", "Handler's name can't be empty");
	}
	if(name[strcspn(name, WHITESPACE)] != '\0')
	{
		return luaL_error(vlua->lua, "%s",
				"Handler's name can't contain whitespace");
	}

	return 0;
}

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