/*
 * Copyright (C) 2001-2004 Sistina Software, Inc. All rights reserved.
 * Copyright (C) 2004-2007 Red Hat, Inc. All rights reserved.
 *
 * This file is part of LVM2.
 *
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU General Public License v.2.
 *
 * 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 "tools.h"

#include "lvm2cmdline.h"

int main(int argc, char **argv)
{
	/* coverity[os_cmd_sink] intentionally passing argv */
	return lvm2_main(argc, argv);
}

#if defined(READLINE_SUPPORT) || defined(EDITLINE_SUPPORT)

#  ifdef READLINE_SUPPORT
#    include <readline/readline.h>
#    include <readline/history.h>
#    ifndef HAVE_RL_COMPLETION_MATCHES
#      define rl_completion_matches(a, b) completion_matches((char *)a, b)
#      define rl_completion_func_t CPPFunction
#    endif
#  elif defined(EDITLINE_SUPPORT)
#    include <editline/readline.h>
#  endif

static struct cmdline_context *_cmdline;

/* List matching commands */
static char *_list_cmds(const char *text, int state)
{
	static int i = 0;
	static size_t len = 0;

	/* Initialise if this is a new completion attempt */
	if (!state) {
		i = 0;
		len = strlen(text);
	}

	for (;i < _cmdline->num_command_names;++i)
		if (!strncmp(text, _cmdline->command_names[i].name, len))
			/* increase position for next iteration */
			return strdup(_cmdline->command_names[i++].name);

	return NULL;
}

/* List matching arguments */
static char *_list_args(const char *text, int state)
{
	static int match_no = 0;
	static size_t len = 0;
	static const struct command_name *cname;
	static const struct command_name_args *cna;

	/* Initialise if this is a new completion attempt */
	if (!state) {
		char *s = rl_line_buffer;
		int j;

		match_no = 0;
		cname = NULL;
		cna = NULL;
		len = strlen(text);

		/* Find start of first word in line buffer */
		while (isspace(*s))
			s++;

		/* Look for word in list of command names */
		for (j = 0; j < _cmdline->num_command_names; j++) {
			const char *p;
			char *q = s;

			p = _cmdline->command_names[j].name;
			while (*p == *q) {
				p++;
				q++;
			}
			if ((!*p) && *q == ' ') {
				cname = _cmdline->command_names + j;
				cna = _cmdline->command_names_args + j;
				break;
			}
		}
	}

	if (!cname)
		return NULL;

	/* Short form arguments */
	if (len < 3) {
		while (match_no < cna->num_args) {
			char s[3];
			/* increase position for next iteration */
			char c = _cmdline->opt_names[cna->valid_args[match_no++]].short_opt;
			if (c) {
				sprintf(s, "-%c", c);
				if (!strncmp(text, s, len))
					return strdup(s);
			}
		}
	}

	/* Long form arguments */
	if (match_no < cna->num_args)
		match_no = cna->num_args;

	while ((match_no - cna->num_args) < cna->num_args) {
		/* increase position for next iteration */
		const char *l = _cmdline->opt_names[cna->valid_args[match_no++ - cna->num_args]].long_opt;
		if (*(l + 2) && !strncmp(text, l, len))
			return strdup(l);
	}

	return NULL;
}

/* Custom completion function */
static char **_completion(const char *text, int start_pos,
			  int end_pos __attribute__((unused)))
{
	char **match_list = NULL;
	int p = 0;

	while (isspace((int) *(rl_line_buffer + p)))
		p++;

	/* First word should be one of our commands */
	if (start_pos == p)
		match_list = rl_completion_matches(text, _list_cmds);

	else if (*text == '-')
		match_list = rl_completion_matches(text, _list_args);
	/* else other args */

	/* No further completion */
	rl_attempted_completion_over = 1;
	return match_list;
}

static int _hist_file(char *buffer, size_t size)
{
	char *e = getenv("HOME");

	if (dm_snprintf(buffer, size, "%s%s.lvm_history", e ? :"", e ? "/" : "") < 0) {
		log_error("$HOME/.lvm_history: path too long");
		return 0;
	}

	return 1;
}

static void _read_history(struct cmd_context *cmd)
{
	char hist_file[PATH_MAX];

	if (!_hist_file(hist_file, sizeof(hist_file)))
		return;

	if (read_history(hist_file))
		log_very_verbose("Couldn't read history from %s.", hist_file);

	stifle_history(find_config_tree_int(cmd, shell_history_size_CFG, NULL));
}

static void _write_history(void)
{
	char hist_file[PATH_MAX];

	if (!_hist_file(hist_file, sizeof(hist_file)))
		return;

	if (write_history(hist_file))
		log_very_verbose("Couldn't write history to %s.", hist_file);
}

static int _log_shell_command_status(struct cmd_context *cmd, int ret_code)
{
	log_report_t log_state;

	if (!cmd->cmd_report.log_rh)
		return 1;

	log_state = log_get_report_state();

	return report_cmdlog(cmd->cmd_report.log_rh, REPORT_OBJECT_CMDLOG_NAME,
			     log_get_report_context_name(log_state.context),
			     log_get_report_object_type_name(log_state.object_type),
			     log_state.object_name, log_state.object_id,
			     log_state.object_group, log_state.object_group_id,
			     ret_code == ECMD_PROCESSED ? REPORT_OBJECT_CMDLOG_SUCCESS
							: REPORT_OBJECT_CMDLOG_FAILURE,
			     stored_errno(), ret_code);
}

static void _discard_log_report_content(struct cmd_context *cmd)
{
	if (cmd->cmd_report.log_rh)
		dm_report_destroy_rows(cmd->cmd_report.log_rh);
}

int lvm_shell(struct cmd_context *cmd, struct cmdline_context *cmdline)
{
	log_report_t saved_log_report_state = log_get_report_state();
	char *orig_command_log_selection = NULL;
	int is_lastlog_cmd = 0, argc, ret, i;
	char *input = NULL, *args[MAX_ARGS], **argv;

	rl_readline_name = "lvm";
	rl_attempted_completion_function = (rl_completion_func_t *) _completion;

	_read_history(cmd);
	_cmdline = cmdline;

	cmd->is_interactive = 1;

	if (!report_format_init(cmd))
		return_ECMD_FAILED;

	orig_command_log_selection = dm_pool_strdup(cmd->libmem, find_config_tree_str(cmd, log_command_log_selection_CFG, NULL));
	log_set_report_context(LOG_REPORT_CONTEXT_SHELL);
	log_set_report_object_type(LOG_REPORT_OBJECT_TYPE_PRE_CMD);

	while (1) {
		/*
		 * Note: If we need to output the log report before we get to the dm_report_group_output_and_pop_all
		 * at the end of this loop, like hitting a failure situation before we execute the command itself,
		 * don't forget to directly call dm_report_group_output_and_pop_all, otherwise no log message will
		 * appear on output (for output formats other than 'basic').
		 *
		 * Obviously, you can't output the 'log report' if the error is in initializing or setting
		 * the report itself. In this case, we can only return an error code, but no message.
		 */

		report_reset_cmdlog_seqnum();
		if (cmd->cmd_report.log_rh) {
			/*
			 * If previous command was lastlog, reset log report selection to
			 * its original value as set by log/command_log_selection config setting.
			 */
			if (is_lastlog_cmd &&
			    !dm_report_set_selection(cmd->cmd_report.log_rh, orig_command_log_selection))
				log_error("Failed to reset log report selection.");
		}

		log_set_report_object_type(LOG_REPORT_OBJECT_TYPE_PRE_CMD);
		log_set_report(cmd->cmd_report.log_rh);
		log_set_report_object_name_and_id(NULL, NULL);

		free(input);
		input = readline("lvm> ");

		/* EOF */
		if (!input) {
			_discard_log_report_content(cmd);
			/* readline sends prompt to stdout */
			printf("\n");
			break;
		}

		/* empty line */
		if (!*input) {
			_discard_log_report_content(cmd);
			continue;
		}

		log_set_report_object_name_and_id(input, NULL);

		add_history(input);

		for (i = 0; i < MAX_ARGS; i++)
			args[i] = NULL;

		argv = args;

		if (lvm_split(input, &argc, argv, MAX_ARGS) == MAX_ARGS) {
			_discard_log_report_content(cmd);
			log_error("Too many arguments, sorry.");
			goto report_log;
		}

		if (!argc) {
			_discard_log_report_content(cmd);
			continue;
		}

		if (!strcmp(argv[0], "lvm")) {
			argv++;
			argc--;
		}

		if (!argc) {
			_discard_log_report_content(cmd);
			continue;
		}

		log_set_report_object_type(LOG_REPORT_OBJECT_TYPE_CMD);
		log_set_report_object_name_and_id(argv[0], NULL);

		is_lastlog_cmd = !strcmp(argv[0], "lastlog");

		if (!is_lastlog_cmd)
			_discard_log_report_content(cmd);

		if (!strcmp(argv[0], "quit") || !strcmp(argv[0], "exit")) {
			_discard_log_report_content(cmd);
			remove_history(history_length - 1);
			log_error("Exiting.");
			break;
		}

		ret = lvm_run_command(cmd, argc, argv);
		if (ret == ENO_SUCH_CMD)
			log_error("No such command '%s'.  Try 'help'.",
				  argv[0]);

                if ((ret != ECMD_PROCESSED) && !error_message_produced()) {
			log_debug(INTERNAL_ERROR "Failed command did not use log_error");
			log_error("Command failed with status code %d.", ret);
		}
		_write_history();

		if (!is_lastlog_cmd)
			_log_shell_command_status(cmd, ret);
report_log:
		log_set_report(NULL);
		dm_report_group_output_and_pop_all(cmd->cmd_report.report_group);

		if (cmd->cmd_report.log_rh &&
		    !(dm_report_group_push(cmd->cmd_report.report_group,
					   cmd->cmd_report.log_rh,
					   (void *) cmd->cmd_report.log_name))) {
			log_set_report(NULL);
			log_error("Failed to add log report.");
			break;
		}
	}

	log_restore_report_state(saved_log_report_state);
	cmd->is_interactive = 0;

	free(input);

	if (cmd->cmd_report.report_group) {
		if (!dm_report_group_destroy(cmd->cmd_report.report_group))
			stack;
		cmd->cmd_report.report_group = NULL;
	}

	if (cmd->cmd_report.log_rh) {
		dm_report_free(cmd->cmd_report.log_rh);
		cmd->cmd_report.report_group = NULL;
	}

	return 0;
}

#endif	/* READLINE_SUPPORT || EDITLINE_SUPPORT */
