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

#include <sys/types.h> /* mode_t */

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

#include "compat/fs_limits.h"
#include "compat/os.h"
#include "engine/functions.h"
#include "engine/text_buffer.h"
#include "engine/var.h"
#include "lua/vlua.h"
#include "modes/cmdline.h"
#include "ui/cancellation.h"
#include "ui/tabs.h"
#include "ui/ui.h"
#include "utils/filemon.h"
#include "utils/fs.h"
#include "utils/fsddata.h"
#include "utils/macros.h"
#include "utils/path.h"
#include "utils/str.h"
#include "utils/string_array.h"
#include "utils/test_helpers.h"
#include "utils/trie.h"
#include "utils/utils.h"
#include "event_loop.h"
#include "filelist.h"
#include "macros.h"
#include "types.h"

/* A single entry of cache of external commands. */
typedef struct
{
	fsddata_t *caches; /* A set of named caches. */
	filemon_t mon;     /* File monitor. */
}
extcache_t;

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

static var_t chooseopt_builtin(const call_info_t *call_info);
static var_t escape_builtin(const call_info_t *call_info);
static var_t executable_builtin(const call_info_t *call_info);
static var_t expand_builtin(const call_info_t *call_info);
static var_t extcached_builtin(const call_info_t *call_info);
TSTATIC void set_extcached_monitor_type(FileMonType type);
static var_t filereadable_builtin(const call_info_t *call_info);
static var_t filetype_builtin(const call_info_t *call_info);
static int get_fnum(var_t fnum);
static const char * type_of_link_target(const dir_entry_t *entry);
static var_t fnameescape_builtin(const call_info_t *call_info);
static var_t getpanetype_builtin(const call_info_t *call_info);
static var_t has_builtin(const call_info_t *call_info);
static var_t input_builtin(const call_info_t *call_info);
static complete_cmd_func pick_completer(const char type[], int *success);
static void input_builtin_cb(const char response[], void *arg);
static var_t layoutis_builtin(const call_info_t *call_info);
static var_t paneisat_builtin(const call_info_t *call_info);
static var_t selected_builtin(const call_info_t *call_info);
static var_t system_builtin(const call_info_t *call_info);
static var_t tabpagenr_builtin(const call_info_t *call_info);
static var_t term_builtin(const call_info_t *call_info);
static var_t execute_cmd(var_t cmd_arg, int interactive, int preserve_stdin);

/* Function descriptions. */
static const function_t functions[] = {
	/* Name           Description                   Args   Handler  */
	{ "chooseopt",    "query choose options",      {1,1}, &chooseopt_builtin },
	{ "escape",       "escapes characters",        {2,2}, &escape_builtin },
	{ "executable",   "check for executable file", {1,1}, &executable_builtin },
	{ "expand",       "expand macros in a string", {1,1}, &expand_builtin },
	{ "extcached",    "caches result of a command",{3,3}, &extcached_builtin },
	{ "filereadable", "checks for a readable file",{1,1}, &filereadable_builtin },
	{ "filetype",     "retrieve type of a file",   {1,2}, &filetype_builtin },
	{ "fnameescape",  "escapes string for a :cmd", {1,1}, &fnameescape_builtin },
	{ "getpanetype",  "retrieve type of file list",{0,0}, &getpanetype_builtin},
	{ "has",          "check for specific ability",{1,1}, &has_builtin },
	{ "input",        "prompt user for input",     {1,3}, &input_builtin },
	{ "layoutis",     "query current layout",      {1,1}, &layoutis_builtin },
	{ "paneisat",     "query pane location",       {1,1}, &paneisat_builtin },
	{ "selected",     "retrieve selection size",   {0,0}, &selected_builtin },
	{ "system",       "execute external command",  {1,1}, &system_builtin },
	{ "tabpagenr",    "number of current/last tab",{0,1}, &tabpagenr_builtin },
	{ "term",         "run interactive command",   {1,1}, &term_builtin },
};

/* Kind of monitor used by the extcached(). */
static FileMonType extcached_mon_type = FMT_CHANGED;

void
init_builtin_functions(void)
{
	size_t i;
	for(i = 0; i < ARRAY_LEN(functions); ++i)
	{
		int result = function_register(&functions[i]);
		assert(result == 0 && "Builtin function registration error");
		(void)result;
	}
}

/* Retrieves values of options related to file choosing as a string.  On unknown
 * arguments empty string is returned. */
static var_t
chooseopt_builtin(const call_info_t *call_info)
{
	const char *result = NULL;
	char *type;

	type = var_to_str(call_info->argv[0]);
	if(strcmp(type, "files") == 0)
	{
		result = curr_stats.chosen_files_out;
	}
	else if(strcmp(type, "dir") == 0)
	{
		result = curr_stats.chosen_dir_out;
	}
	else if(strcmp(type, "cmd") == 0)
	{
		result = curr_stats.on_choose;
	}
	else if(strcmp(type, "delimiter") == 0)
	{
		result = curr_stats.output_delimiter;
	}
	free(type);

	return var_from_str(result == NULL ? "" : result);
}

/* Escapes specified characters via slash in a string.  Returns escaped
 * string. */
static var_t
escape_builtin(const call_info_t *call_info)
{
	char *string = var_to_str(call_info->argv[0]);
	char *chars = var_to_str(call_info->argv[1]);
	if(string == NULL || chars == NULL)
	{
		free(string);
		free(chars);
		return var_error();
	}

	char *escaped = escape_chars(string, chars);
	free(string);
	free(chars);

	if(escaped == NULL)
	{
		return var_error();
	}

	return var_out_of_str(escaped);
}

/* 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 boolean value describing result of the check. */
static var_t
executable_builtin(const call_info_t *call_info)
{
	int exists;
	char *str_val;

	str_val = var_to_str(call_info->argv[0]);

	if(contains_slash(str_val))
	{
		exists = executable_exists(str_val);
	}
	else
	{
		exists = (find_cmd_in_path(str_val, 0UL, NULL) == 0);
	}

	free(str_val);

	return var_from_bool(exists);
}

/* Returns string after expanding expression. */
static var_t
expand_builtin(const call_info_t *call_info)
{
	char *arg = var_to_str(call_info->argv[0]);

	char *env_expanded = expand_envvars(arg,
			EEF_KEEP_ESCAPES | EEF_DOUBLE_PERCENTS);
	free(arg);

	char *macro_expanded = ma_expand(env_expanded, NULL, NULL, MER_DISPLAY);
	free(env_expanded);

	return var_out_of_str(macro_expanded);
}

/* Returns cached value of an external command.  Cache validity is bound to a
 * file. */
static var_t
extcached_builtin(const call_info_t *call_info)
{
	static trie_t *cache;
	if(cache == NULL)
	{
		cache = trie_create(&free);
	}

	char *cache_name = var_to_str(call_info->argv[0]);
	if(cache_name == NULL)
	{
		return var_error();
	}

	char *path = var_to_str(call_info->argv[1]);
	if(path == NULL)
	{
		return var_error();
	}

	char canonic[PATH_MAX + 1];
	to_canonic_path(path, flist_get_dir(curr_view), canonic, sizeof(canonic));
	replace_string(&path, canonic);

	extcache_t *cached = NULL;
	void *data;
	if(trie_get(cache, path, &data) == 0)
	{
		cached = data;
	}

	filemon_t current_mon = {};
	if(filemon_from_file(path, extcached_mon_type, &current_mon) == 0 &&
			cached != NULL)
	{
		if(filemon_equal(&current_mon, &cached->mon))
		{
			void *cached_output;
			if(fsddata_get(cached->caches, cache_name, &cached_output) == 0)
			{
				free(cache_name);
				free(path);
				return var_from_str(cached_output);
			}
		}
		else
		{
			fsddata_free(cached->caches);
			cached->caches = NULL;
		}
	}

	if(cached == NULL)
	{
		cached = malloc(sizeof(*cached));
		if(cached == NULL)
		{
			free(cache_name);
			free(path);
			return var_error();
		}

		cached->caches = NULL;

		if(trie_set(cache, path, cached) < 0)
		{
			free(cached);
			free(cache_name);
			free(path);
			return var_error();
		}
	}

	if(cached->caches == NULL)
	{
		cached->caches = fsddata_create(0, 0);
	}

	cached->mon = current_mon;
	free(path);

	var_t output = execute_cmd(call_info->argv[2], call_info->interactive, 0);
	(void)fsddata_set(cached->caches, cache_name, var_to_str(output));
	free(cache_name);

	return output;
}

/* Modifies kind of monitor used by the extcached(). */
TSTATIC void
set_extcached_monitor_type(FileMonType type)
{
	extcached_mon_type = type;
}

/* Checks whether path is a non-directory entry and its permissions allow
 * reading.  Returns boolean value describing result of the check. */
static var_t
filereadable_builtin(const call_info_t *call_info)
{
	int is_readable = 0;

	char *path = var_to_str(call_info->argv[0]);
	if(path != NULL)
	{
		is_readable = (os_access(path, R_OK) == 0 && !is_dir(path));
		free(path);
	}

	return var_from_bool(is_readable);
}

/* Gets string representation of file type.  Returns the string. */
static var_t
filetype_builtin(const call_info_t *call_info)
{
	const int fnum = get_fnum(call_info->argv[0]);
	int resolve_links = (call_info->argc > 1 && var_to_bool(call_info->argv[1]));

	const char *result_str = "";
	if(fnum >= 0)
	{
		const dir_entry_t *entry = &curr_view->dir_entry[fnum];
		if(entry->type == FT_LINK && resolve_links)
		{
			result_str = type_of_link_target(entry);
		}
		else
		{
			result_str = get_type_str(entry->type);
		}
	}
	else if(fnum == -2)
	{
		struct stat s;
		char *path = var_to_str(call_info->argv[0]);
		if(path != NULL && os_lstat(path, &s) == 0)
		{
			if(resolve_links && os_stat(path, &s) != 0)
			{
				result_str = "broken";
			}
			else
			{
				result_str = get_type_str(get_type_from_mode(s.st_mode));
			}
		}

		free(path);
	}

	return var_from_str(result_str);
}

/* Turns {fnum} into file position.  Returns the position, -1 if the argument
 * is a bad number and -2 if the argument is not a number. */
static int
get_fnum(var_t fnum)
{
	char *str_val = var_to_str(fnum);

	int pos = -1;
	if(strcmp(str_val, ".") == 0)
	{
		pos = curr_view->list_pos;
	}
	else if(strspn(str_val, "+-0123456789") != strlen(str_val))
	{
		/* Doesn't look like a number. */
		pos = -2;
	}
	else
	{
		int int_val = var_to_int(fnum);
		if(int_val > 0 && int_val <= curr_view->list_rows)
		{
			pos = int_val - 1;
		}
	}

	free(str_val);
	return pos;
}

/* Resoves file type of link target.  The entry parameter is expected to point
 * at symbolic link.  Returns the type as a string. */
static const char *
type_of_link_target(const dir_entry_t *entry)
{
	char path[PATH_MAX + 1];
	struct stat s;

	get_full_path_of(entry, sizeof(path), path);
	if(get_link_target_abs(path, entry->origin, path, sizeof(path)) != 0 ||
			os_stat(path, &s) != 0)
	{
		return "broken";
	}
	return get_type_str(get_type_from_mode(s.st_mode));
}

/* Escapes argument to make it suitable for use as an argument in :commands. */
static var_t
fnameescape_builtin(const call_info_t *call_info)
{
	char *const str_val = var_to_str(call_info->argv[0]);
	char *const escaped = posix_like_escape(str_val, /*type=*/1);
	free(str_val);

	return var_out_of_str(escaped);
}

/* Retrieves type of current pane as a string. */
static var_t
getpanetype_builtin(const call_info_t *call_info)
{
	if(!flist_custom_active(curr_view))
	{
		return var_from_str("regular");
	}

	return var_from_str(cv_describe(curr_view->custom.type));
}

/* Asks user for input and returns the result as a string. */
static var_t
input_builtin(const call_info_t *call_info)
{
	char *prompt = var_to_str(call_info->argv[0]);
	char *initial = (call_info->argc > 1) ? var_to_str(call_info->argv[1])
	                                      : strdup("");
	char *completion = (call_info->argc > 2) ? var_to_str(call_info->argv[2])
	                                         : strdup("");
	if(prompt == NULL || initial == NULL || completion == NULL)
	{
		goto fail;
	}

	input_cb_data_t cb_data = { .quit = 0, .response = NULL };

	int correct_completer;
	complete_cmd_func complete = pick_completer(completion, &correct_completer);
	if(!correct_completer)
	{
		vle_tb_append_linef(vle_err, "Invalid completion type: %s", completion);
		goto fail;
	}

	modcline_prompt(prompt, initial, &input_builtin_cb, &cb_data, complete);

	free(prompt);
	free(initial);
	free(completion);

	event_loop(&cb_data.quit, /*manage_marking=*/0);

	/* Not returning var_error() on cancellation to allow handling of it by the
	 * user. */
	return (cb_data.response == NULL) ? var_from_str("")
	                                  : var_out_of_str(cb_data.response);

fail:
	free(prompt);
	free(initial);
	free(completion);
	return var_error();
}

/* 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;
}

/* Maps completer name onto a function that implements it.  Always sets *success
 * to indicate whether parsing was successful (this tells apart empty string
 * from an invalid one).  Returns function pointer or NULL. */
static complete_cmd_func
pick_completer(const char type[], int *success)
{
	*success = 1;

	if(strcmp(type, "dir") == 0)
	{
		return &modcline_complete_dirs;
	}
	else if(strcmp(type, "file") == 0)
	{
		return &modcline_complete_files;
	}
	else if(strcmp(type, "") == 0)
	{
		/* No completion. */
		return NULL;
	}

	*success = 0;
	return NULL;
}

/* Checks current layout configuration.  Returns boolean value that reflects
 * state of specified layout type. */
static var_t
layoutis_builtin(const call_info_t *call_info)
{
	char *type;
	int result;

	type = var_to_str(call_info->argv[0]);
	if(strcmp(type, "only") == 0)
	{
		result = (curr_stats.number_of_windows == 1);
	}
	else if(strcmp(type, "split") == 0)
	{
		result = (curr_stats.number_of_windows == 2);
	}
	else if(strcmp(type, "vsplit") == 0)
	{
		result = (curr_stats.number_of_windows == 2 && curr_stats.split == VSPLIT);
	}
	else if(strcmp(type, "hsplit") == 0)
	{
		result = (curr_stats.number_of_windows == 2 && curr_stats.split == HSPLIT);
	}
	else
	{
		result = 0;
	}
	free(type);

	return var_from_bool(result);
}

/* Allows examining internal parameters from scripts to e.g. figure out
 * environment in which application is running. */
static var_t
has_builtin(const call_info_t *call_info)
{
	int result = 0;

	char *const str_val = var_to_str(call_info->argv[0]);

	if(strcmp(str_val, "unix") == 0)
	{
		result = (get_env_type() == ET_UNIX);
	}
	else if(strcmp(str_val, "win") == 0)
	{
		result = (get_env_type() == ET_WIN);
	}
	else if(str_val[0] == '#')
	{
		result = vlua_handler_present(curr_stats.vlua, str_val);
	}

	free(str_val);

	return var_from_bool(result);
}

/* Checks for relative position of current pane.  Returns boolean value that
 * reflects state of specified location. */
static var_t
paneisat_builtin(const call_info_t *call_info)
{
	char *loc;
	int result;

	const int only = (curr_stats.number_of_windows == 1);
	const int vsplit = (curr_stats.split == VSPLIT);
	const int first = (curr_view == &lwin);

	loc = var_to_str(call_info->argv[0]);
	if(strcmp(loc, "top") == 0)
	{
		result = (only || vsplit || first);
	}
	else if(strcmp(loc, "bottom") == 0)
	{
		result = (only || vsplit || !first);
	}
	else if(strcmp(loc, "left") == 0)
	{
		result = (only || !vsplit || first);
	}
	else if(strcmp(loc, "right") == 0)
	{
		result = (only || !vsplit || !first);
	}
	else
	{
		result = 0;
	}
	free(loc);

	return var_from_bool(result);
}

/* Retrieves number of files selected in the active pane.  Returns an
 * integer. */
static var_t
selected_builtin(const call_info_t *call_info)
{
	return var_from_int(curr_view->selected_files);
}

/* Runs the command in a shell and returns its output (joined standard output
 * and standard error streams).  All trailing newline characters are stripped to
 * allow easy appending to command output.  Returns the output. */
static var_t
system_builtin(const call_info_t *call_info)
{
	return execute_cmd(call_info->argv[0], call_info->interactive, 0);
}

/* Retrieves number of current or last tab page.  Returns integer value with the
 * number base one. */
static var_t
tabpagenr_builtin(const call_info_t *call_info)
{
	int first = 1;

	if(call_info->argc != 0)
	{
		char *const type = var_to_str(call_info->argv[0]);
		if(strcmp(type, "$") != 0)
		{
			vle_tb_append_linef(vle_err, "Invalid argument (expected \"$\"): %s",
					type);
			free(type);
			return var_error();
		}
		free(type);
		first = 0;
	}

	return var_from_int(first ? tabs_current(curr_view) + 1
	                          : tabs_count(curr_view));
}

/* Runs interactive command in a shell and returns its output (joined standard
 * output and standard error streams).  All trailing newline characters are
 * stripped to allow easy appending to command output.  Returns the output. */
static var_t
term_builtin(const call_info_t *call_info)
{
	ui_shutdown();
	return execute_cmd(call_info->argv[0], call_info->interactive, 1);
}

/* Runs interactive command in a shell and returns its output (joined standard
 * output and standard error streams).  All trailing newline characters are
 * stripped to allow easy appending to command output.  Returns the output. */
static var_t
execute_cmd(var_t cmd_arg, int interactive, int preserve_stdin)
{
	char *cmd;
	FILE *cmd_stream;
	size_t cmd_out_len;
	char *result_str;

	cmd = var_to_str(cmd_arg);
	cmd_stream = read_cmd_output(cmd, preserve_stdin);
	free(cmd);

	if(cmd_stream == NULL)
	{
		return var_error();
	}

	if(interactive)
	{
		ui_cancellation_push_on();
	}
	else
	{
		ui_cancellation_push_off();
	}

	result_str = read_nonseekable_stream(cmd_stream, &cmd_out_len, NULL, NULL);
	/* XXX: a command can close its output stream before it exists, in which case
	 *      this function returns prior to side-effects which a caller might
	 *      expect to have already occured.  Should wait for the process to
	 *      end? */
	fclose(cmd_stream);

	ui_cancellation_pop();

	if(result_str == NULL)
	{
		return var_from_str("");
	}

	/* Remove trailing new line characters. */
	while(cmd_out_len != 0U && result_str[cmd_out_len - 1] == '\n')
	{
		result_str[cmd_out_len - 1] = '\0';
		--cmd_out_len;
	}

	return var_out_of_str(result_str);
}

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