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

#include <unistd.h> /* isatty() */

#include "../cfg/info.h"
#include "../engine/options.h"
#include "../menus/users_menu.h"
#include "../modes/dialogs/msg_dialog.h"
#include "../modes/cmdline.h"
#include "../ui/statusbar.h"
#include "../utils/fs.h"
#include "../utils/path.h"
#include "../utils/str.h"
#include "../utils/utils.h"
#include "../event_loop.h"
#include "../filelist.h"
#include "../filename_modifiers.h"
#include "../running.h"
#include "lua/lua.h"
#include "lua/lauxlib.h"
#include "api.h"
#include "common.h"
#include "vifm_abbrevs.h"
#include "vifm_cmds.h"
#include "vifm_events.h"
#include "vifm_fs.h"
#include "vifm_handlers.h"
#include "vifm_keys.h"
#include "vifm_tabs.h"
#include "vifm_viewcolumns.h"
#include "vifmjob.h"
#include "vifmview.h"
#include "vlua_state.h"

/* Lua API version. */
#define API_VER_MAJOR 0
#define API_VER_MINOR 2
#define API_VER_PATCH 0

/*
 * This unit contains generic part of `vifm` global table.  Plugin-specific
 * things are in vlua unit.
 *
 * Implementation for small things are here, others are in corresponding vifm_*
 * or type-specific vifm* units and are only initialized here.
 */

/* Data passed to prompt callback in vifm.input(). */
typedef struct
{
	int quit;       /* Exit flag for event loop. */
	char *response; /* Result of the prompt. */
}
input_cb_data_t;

static int VLUA_API(vifm_errordialog)(lua_State *lua);
static int VLUA_API(vifm_escape)(lua_State *lua);
static int VLUA_API(vifm_executable)(lua_State *lua);
static int VLUA_API(vifm_exists)(lua_State *lua);
static int VLUA_API(vifm_expand)(lua_State *lua);
static int VLUA_API(vifm_fnamemodify)(lua_State *lua);
static int VLUA_API(vifm_input)(lua_State *lua);
static int VLUA_API(vifm_makepath)(lua_State *lua);
static int VLUA_API(vifm_menus_loadcustom)(lua_State *lua);
static int VLUA_API(vifm_run)(lua_State *lua);
static int VLUA_API(vifm_sessions_current)(lua_State *lua);
static int VLUA_API(vifm_stdout)(lua_State *lua);
static int VLUA_IMPL(stdout_closef)(lua_State *lua);

static int VLUA_API(api_is_at_least)(lua_State *lua);
static int VLUA_API(api_has)(lua_State *lua);

static int VLUA_API(opts_global_index)(lua_State *lua);
static int VLUA_API(opts_global_newindex)(lua_State *lua);

static int VLUA_API(sb_info)(lua_State *lua);
static int VLUA_API(sb_error)(lua_State *lua);
static int VLUA_API(sb_quick)(lua_State *lua);

VLUA_DECLARE_SAFE(vifm_errordialog);
VLUA_DECLARE_SAFE(vifm_escape);
VLUA_DECLARE_SAFE(vifm_executable);
VLUA_DECLARE_SAFE(vifm_exists);
VLUA_DECLARE_SAFE(vifm_expand);
VLUA_DECLARE_SAFE(vifm_fnamemodify);
VLUA_DECLARE_SAFE(vifm_input);
VLUA_DECLARE_SAFE(vifm_makepath);
VLUA_DECLARE_UNSAFE(vifm_menus_loadcustom);
VLUA_DECLARE_SAFE(vifm_run);
VLUA_DECLARE_SAFE(vifm_sessions_current);
VLUA_DECLARE_SAFE(vifm_stdout);

VLUA_DECLARE_SAFE(api_is_at_least);
VLUA_DECLARE_SAFE(api_has);

VLUA_DECLARE_SAFE(opts_global_index);
VLUA_DECLARE_UNSAFE(opts_global_newindex);

VLUA_DECLARE_SAFE(sb_info);
VLUA_DECLARE_SAFE(sb_error);
VLUA_DECLARE_SAFE(sb_quick);

/* These are defined in other units. */
VLUA_DECLARE_UNSAFE(vifm_addcolumntype);
VLUA_DECLARE_SAFE(vifm_addhandler);
VLUA_DECLARE_SAFE(vifmview_currview);
VLUA_DECLARE_SAFE(vifmview_otherview);
VLUA_DECLARE_SAFE(vifmjob_new);

static void input_builtin_cb(const char response[], void *arg);

/* Functions of `vifm` global table. */
static const struct luaL_Reg vifm_methods[] = {
	{ "errordialog",   VLUA_REF(vifm_errordialog)   },
	{ "escape",        VLUA_REF(vifm_escape)        },
	{ "executable",    VLUA_REF(vifm_executable)    },
	{ "exists",        VLUA_REF(vifm_exists)        },
	{ "expand",        VLUA_REF(vifm_expand)        },
	{ "fnamemodify",   VLUA_REF(vifm_fnamemodify)   },
	{ "input",         VLUA_REF(vifm_input)         },
	{ "makepath",      VLUA_REF(vifm_makepath)      },
	{ "run",           VLUA_REF(vifm_run)           },
	{ "stdout",        VLUA_REF(vifm_stdout)        },

	/* Defined in other units. */
	{ "addcolumntype", VLUA_REF(vifm_addcolumntype) },
	{ "addhandler",    VLUA_REF(vifm_addhandler)    },
	{ "currview",      VLUA_REF(vifmview_currview)  },
	{ "otherview",     VLUA_REF(vifmview_otherview) },
	{ "startjob",      VLUA_REF(vifmjob_new)        },

	{ NULL,            NULL                         }
};

/* Functions of `vifm.sb` table. */
static const struct luaL_Reg sb_methods[] = {
	{ "info",   VLUA_REF(sb_info)  },
	{ "error",  VLUA_REF(sb_error) },
	{ "quick",  VLUA_REF(sb_quick) },
	{ NULL,     NULL               }
};

void
vifm_init(lua_State *lua)
{
	vifmjob_init(lua);
	vifmview_init(lua);

	luaL_newlib(lua, vifm_methods);

	/* Setup vifm.abbrevs. */
	vifm_abbrevs_init(lua);
	lua_setfield(lua, -2, "abbrevs");

	/* Setup vifm.cmds. */
	vifm_cmds_init(lua);
	lua_setfield(lua, -2, "cmds");

	/* Setup vifm.events. */
	vifm_events_init(lua);
	lua_setfield(lua, -2, "events");

	/* Setup vifm.fs. */
	vifm_fs_init(lua);
	lua_setfield(lua, -2, "fs");

	/* Setup vifm.keys. */
	vifm_keys_init(lua);
	lua_setfield(lua, -2, "keys");

	/* Setup vifm.tabs. */
	vifm_tabs_init(lua);
	lua_setfield(lua, -2, "tabs");

	/* Setup vifm.menus. */
	lua_createtable(lua, /*narr=*/0, /*nrec=*/1);
	lua_pushcfunction(lua, VLUA_REF(vifm_menus_loadcustom));
	lua_setfield(lua, -2, "loadcustom"); /* vifm.menus.loadcustom */
	lua_setfield(lua, -2, "menus");      /* vifm.menus */

	/* Setup vifm.opts. */
	lua_createtable(lua, /*narr=*/0, /*nrec=*/1); /* vifm.opts */
	lua_newtable(lua);                            /* vifm.opts.global */
	vlua_cmn_make_metatable(lua, /*name=*/NULL);  /* for vifm.opts.global */
	lua_pushcfunction(lua, VLUA_REF(opts_global_index));
	lua_setfield(lua, -2, "__index");
	lua_pushcfunction(lua, VLUA_REF(opts_global_newindex));
	lua_setfield(lua, -2, "__newindex");
	lua_setmetatable(lua, -2);       /* vifm.opts.global */
	lua_setfield(lua, -2, "global"); /* vifm.opts.global */
	lua_setfield(lua, -2, "opts");   /* vifm.opts */

	/* Setup vifm.plugins. */
	lua_createtable(lua, /*narr=*/0, /*nrec=*/1); /* vifm.plugins */
	lua_newtable(lua);                            /* vifm.plugins.all */
	lua_setfield(lua, -2, "all");
	lua_setfield(lua, -2, "plugins");

	/* Setup vifm.version. */
	lua_createtable(lua, /*narr=*/0, /*nrec=*/2); /* vifm.version */
	lua_createtable(lua, /*narr=*/0, /*nrec=*/1); /* vifm.app */
	lua_pushstring(lua, VERSION);
	lua_setfield(lua, -2, "str");                 /* vifm.app.str */
	lua_setfield(lua, -2, "app");                 /* vifm.app */
	lua_createtable(lua, /*narr=*/0, /*nrec=*/5); /* vifm.api */
	lua_pushinteger(lua, API_VER_MAJOR);
	lua_setfield(lua, -2, "major");               /* vifm.api.major */
	lua_pushinteger(lua, API_VER_MINOR);
	lua_setfield(lua, -2, "minor");               /* vifm.api.minor */
	lua_pushinteger(lua, API_VER_PATCH);
	lua_setfield(lua, -2, "patch");               /* vifm.api.patch */
	lua_pushcfunction(lua, VLUA_REF(api_has));
	lua_setfield(lua, -2, "has");                 /* vifm.api.has */
	lua_pushcfunction(lua, VLUA_REF(api_is_at_least));
	lua_setfield(lua, -2, "atleast");             /* vifm.api.atleast */
	lua_setfield(lua, -2, "api");                 /* vifm.api */
	lua_setfield(lua, -2, "version");             /* vifm.version */

	/* Setup vifm.sb. */
	luaL_newlib(lua, sb_methods);
	lua_setfield(lua, -2, "sb");

	/* Setup vifm.sessions. */
	lua_createtable(lua, /*narr=*/0, /*nrec=*/1); /* vifm.sessions */
	lua_pushcfunction(lua, VLUA_REF(vifm_sessions_current));
	lua_setfield(lua, -2, "current");             /* vifm.sessions.current */
	lua_setfield(lua, -2, "sessions");            /* vifm.sessions */
}

/* Member of `vifm` that displays an error dialog.  Doesn't return anything. */
static int
VLUA_API(vifm_errordialog)(lua_State *lua)
{
	const char *title = luaL_checkstring(lua, 1);
	const char *msg = luaL_checkstring(lua, 2);
	show_error_msg(title, msg);
	return 0;
}

/* Member of `vifm` that escapes its input string.  Returns escaped string. */
static int
VLUA_API(vifm_escape)(lua_State *lua)
{
	const char *what = luaL_checkstring(lua, 1);
	char *escaped = shell_arg_escape(what, curr_stats.shell_type);
	lua_pushstring(lua, escaped);
	free(escaped);
	return 1;
}

/* Member of `vifm` that checks whether executable exists at absolute path or
 * in directories listed in $PATH when path isn't absolute.  Checks for various
 * executable extensions on Windows.  Returns a boolean. */
static int
VLUA_API(vifm_executable)(lua_State *lua)
{
	const char *path = luaL_checkstring(lua, 1);

	int executable;
	if(contains_slash(path))
	{
		executable = executable_exists(path);
	}
	else
	{
		executable = (find_cmd_in_path(path, 0UL, NULL) == 0);
	}

	lua_pushboolean(lua, executable);
	return 1;
}

/* Member of `vifm` that checks whether specified path exists without resolving
 * symbolic links.  Returns a boolean, which is true when path does exist. */
static int
VLUA_API(vifm_exists)(lua_State *lua)
{
	const char *path = luaL_checkstring(lua, 1);
	lua_pushboolean(lua, path_exists(path, NODEREF));
	return 1;
}

/* Member of `vifm` that expands macros and environment variables.  Returns the
 * expanded string. */
static int
VLUA_API(vifm_expand)(lua_State *lua)
{
	const char *str = luaL_checkstring(lua, 1);

	char *env_expanded = expand_envvars(str,
			EEF_KEEP_ESCAPES | EEF_DOUBLE_PERCENTS);
	char *full_expanded = ma_expand(env_expanded, NULL, NULL, MER_DISPLAY);
	lua_pushstring(lua, full_expanded);
	free(env_expanded);
	free(full_expanded);

	return 1;
}

/* Member of `vifm` that modifies path according to specifiers.  Returns
 * modified path. */
static int
VLUA_API(vifm_fnamemodify)(lua_State *lua)
{
	const char *path = luaL_checkstring(lua, 1);
	const char *modifiers = luaL_checkstring(lua, 2);
	const char *base = luaL_optstring(lua, 3, flist_get_dir(curr_view));
	lua_pushstring(lua, mods_apply(path, base, modifiers, 0));
	return 1;
}

/* Member of `vifm` that asks user for input via a prompt.  Returns a string on
 * success and nil on failure. */
static int
VLUA_API(vifm_input)(lua_State *lua)
{
	luaL_checktype(lua, 1, LUA_TTABLE);

	vlua_cmn_check_field(lua, 1, "prompt", LUA_TSTRING);
	const char *prompt = lua_tostring(lua, -1);

	const char *initial = "";
	if(vlua_cmn_check_opt_field(lua, 1, "initial", LUA_TSTRING))
	{
		initial = lua_tostring(lua, -1);
	}

	complete_cmd_func complete = NULL;
	if(vlua_cmn_check_opt_field(lua, 1, "complete", LUA_TSTRING))
	{
		const char *value = lua_tostring(lua, -1);
		if(strcmp(value, "dir") == 0)
		{
			complete = &modcline_complete_dirs;
		}
		else if(strcmp(value, "file") == 0)
		{
			complete = &modcline_complete_files;
		}
		else if(strcmp(value, "") != 0)
		{
			return luaL_error(lua, "Unrecognized value for `complete`: %s", value);
		}
	}

	input_cb_data_t cb_data = { .quit = 0, .response = NULL };
	modcline_prompt(prompt, initial, &input_builtin_cb, &cb_data, complete);
	event_loop(&cb_data.quit, /*manage_marking=*/0);

	if(cb_data.response == NULL)
	{
		lua_pushnil(lua);
	}
	else
	{
		lua_pushstring(lua, cb_data.response);
		free(cb_data.response);
	}
	return 1;
}

/* Callback invoked after prompt has finished. */
static void
input_builtin_cb(const char response[], void *arg)
{
	input_cb_data_t *data = arg;

	update_string(&data->response, response);
	data->quit = 1;
}

/* Member of `vifm` that creates a directory and all of its missing parent
 * directories.  Returns a boolean, which is true on success. */
static int
VLUA_API(vifm_makepath)(lua_State *lua)
{
	const char *path = luaL_checkstring(lua, 1);
	lua_pushboolean(lua, make_path(path, 0755) == 0);
	return 1;
}

/* Member of `vifm.menus` that creates a menu out of list of items.  Returns a
 * boolean, which is true on success. */
static int
VLUA_API(vifm_menus_loadcustom)(lua_State *lua)
{
	luaL_checktype(lua, 1, LUA_TTABLE);

	view_t *view = curr_view;
	if(vlua_cmn_check_opt_field(lua, 1, "view", LUA_TUSERDATA))
	{
		view = check_view(lua, -1);
		if(view != &lwin && view != &rwin)
		{
			goto fail;
		}
	}

	int with_navigation = 0;
	if(vlua_cmn_check_opt_field(lua, 1, "withnavigation", LUA_TBOOLEAN))
	{
		with_navigation = lua_toboolean(lua, -1);
	}

	vlua_cmn_check_field(lua, 1, "title", LUA_TSTRING);
	const char *title = lua_tostring(lua, -1);

	vlua_cmn_check_field(lua, 1, "items", LUA_TTABLE);

	lua_Integer len = luaL_len(lua, -1);
	strlist_t items = {
		.items = reallocarray(NULL, len, sizeof(char *)),
		.nitems = len
	};

	if(items.items == NULL)
	{
		goto fail;
	}

	int i = 0;
	lua_pushnil(lua);
	while(lua_next(lua, -2) != 0)
	{
		if(!lua_isstring(lua, -1))
		{
			free_string_array(items.items, i);
			goto fail;
		}

		char *item = strdup(lua_tostring(lua, -1));
		if(item == NULL)
		{
			free_string_array(items.items, i);
			goto fail;
		}

		items.items[i++] = item;
		lua_pop(lua, 1);
	}

	int success = (show_custom_menu(view, title, items, with_navigation) == 0);
	lua_pushboolean(lua, success);
	return 1;

fail:
	lua_pushboolean(lua, 0);
	return 1;
}

/* Runs an external command similar to :!. */
static int
VLUA_API(vifm_run)(lua_State *lua)
{
	luaL_checktype(lua, 1, LUA_TTABLE);

	vlua_cmn_check_field(lua, 1, "cmd", LUA_TSTRING);
	const char *cmd = lua_tostring(lua, -1);

	int use_term_mux = 1;
	if(vlua_cmn_check_opt_field(lua, 1, "usetermmux", LUA_TBOOLEAN))
	{
		use_term_mux = lua_toboolean(lua, -1);
	}

	ShellPause pause = PAUSE_ON_ERROR;
	if(vlua_cmn_check_opt_field(lua, 1, "pause", LUA_TSTRING))
	{
		const char *value = lua_tostring(lua, -1);
		if(strcmp(value, "never") == 0)
		{
			pause = PAUSE_NEVER;
		}
		else if(strcmp(value, "onerror") == 0)
		{
			pause = PAUSE_ON_ERROR;
		}
		else if(strcmp(value, "always") == 0)
		{
			pause = PAUSE_ALWAYS;
		}
		else
		{
			return luaL_error(lua, "Unrecognized value for `pause`: %s", value);
		}
	}

	lua_pushinteger(lua, rn_shell(cmd, pause, use_term_mux, SHELL_BY_APP));
	return 1;
}

/* Member of `vifm` that retrieves stream associated with application's output.
 * Returns file stream object compatible with I/O library that is always the
 * same and that cannot be closed. */
static int
VLUA_API(vifm_stdout)(lua_State *lua)
{
	static char stdout_key;

	if(isatty(fileno(curr_stats.original_stdout)))
	{
		lua_pushnil(lua);
		return 1;
	}

	vlua_cmn_from_pointer(lua, &stdout_key);
	if(lua_isnil(lua, -1))
	{
		luaL_Stream *stream = lua_newuserdatauv(lua, sizeof(*stream), 0);
		stream->closef = VLUA_IREF(stdout_closef);
		luaL_setmetatable(lua, LUA_FILEHANDLE);
		stream->f = curr_stats.original_stdout;

		vlua_cmn_set_pointer(lua, &stdout_key);
	}

	return 1;
}

/* Custom destructor for luaL_Stream that prevents closing the stream.  Always
 * raises an error. */
static int
VLUA_IMPL(stdout_closef)(lua_State *lua)
{
	luaL_Stream *stream = luaL_checkudata(lua, 1, LUA_FILEHANDLE);
	stream->closef = VLUA_IREF(stdout_closef);

	luaL_pushfail(lua);
	lua_pushliteral(lua, "cannot close standard file");
	return 1;
}

/* Member of `vifm.sessions` that retrieves name of the current session.
 * Returns string or nil. */
static int
VLUA_API(vifm_sessions_current)(lua_State *lua)
{
	if(sessions_active())
	{
		lua_pushstring(lua, sessions_current());
	}
	else
	{
		lua_pushnil(lua);
	}
	return 1;
}

/* Checks version of the API.  Returns a boolean. */
static int
VLUA_API(api_is_at_least)(lua_State *lua)
{
	const int major = luaL_checkinteger(lua, 1);
	const int minor = luaL_optinteger(lua, 2, 0);
	const int patch = luaL_optinteger(lua, 3, 0);

	int result = 0;
	if(major != API_VER_MAJOR)
	{
		result = (API_VER_MAJOR > major);
	}
	else if(minor != API_VER_MINOR)
	{
		result = (API_VER_MINOR > minor);
	}
	else
	{
		result = (API_VER_PATCH >= patch);
	}

	lua_pushboolean(lua, result);
	return 1;
}

/* Performs tests for API features.  Returns a boolean. */
static int
VLUA_API(api_has)(lua_State *lua)
{
	(void)luaL_checkstring(lua, 1);
	lua_pushboolean(lua, 0);
	return 1;
}

/* Provides read access to global options by their name as
 * `vifm.opts.global[name]`. */
static int
VLUA_API(opts_global_index)(lua_State *lua)
{
	const char *opt_name = luaL_checkstring(lua, 2);

	opt_t *opt = vle_opts_find(opt_name, OPT_ANY);
	if(opt == NULL || opt->scope == OPT_LOCAL)
	{
		return 0;
	}

	return vlua_cmn_get_opt(lua, opt);
}

/* Provides write access to global options by their name as
 * `vifm.opts.global[name] = value`. */
static int
VLUA_API(opts_global_newindex)(lua_State *lua)
{
	const char *opt_name = luaL_checkstring(lua, 2);

	opt_t *opt = vle_opts_find(opt_name, OPT_ANY);
	if(opt == NULL || opt->scope == OPT_LOCAL)
	{
		return 0;
	}

	return vlua_cmn_set_opt(lua, opt);
}

/* Member of `vifm.sb` that prints a normal message on the status bar.  Doesn't
 * return anything. */
static int
VLUA_API(sb_info)(lua_State *lua)
{
	const char *msg = luaL_checkstring(lua, 1);
	ui_sb_msg(msg);
	curr_stats.save_msg = 1;
	return 0;
}

/* Member of `vifm.sb` that prints an error message on the status bar.  Doesn't
 * return anything. */
static int
VLUA_API(sb_error)(lua_State *lua)
{
	const char *msg = luaL_checkstring(lua, 1);
	ui_sb_err(msg);
	curr_stats.save_msg = 1;
	return 0;
}

/* Member of `vifm.sb` that prints status bar message that's not stored in
 * history.  Doesn't return anything. */
static int
VLUA_API(sb_quick)(lua_State *lua)
{
	const char *msg = luaL_checkstring(lua, 1);
	ui_sb_quick_msgf("%s", msg);
	return 0;
}

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