/* vifm
 * Copyright (C) 2001 Ken Steen.
 * Copyright (C) 2011 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 "cmd_core.h"

#include <curses.h>

#include <sys/stat.h> /* gid_t uid_t */
#include <unistd.h> /* unlink() */

#include <assert.h> /* assert() */
#include <ctype.h> /* isspace() */
#include <errno.h> /* errno */
#include <limits.h> /* INT_MAX */
#include <signal.h>
#include <stddef.h> /* NULL size_t */
#include <stdio.h> /* snprintf() */
#include <stdlib.h> /* free() */
#include <string.h> /* strcmp() strcpy() strlen() */

#include "cfg/config.h"
#include "cfg/info.h"
#include "compat/fs_limits.h"
#include "compat/os.h"
#include "engine/autocmds.h"
#include "engine/cmds.h"
#include "engine/mode.h"
#include "engine/parsing.h"
#include "engine/var.h"
#include "engine/variables.h"
#include "int/vim.h"
#include "modes/dialogs/msg_dialog.h"
#include "modes/modes.h"
#include "modes/normal.h"
#include "modes/view.h"
#include "modes/visual.h"
#include "ui/color_scheme.h"
#include "ui/colors.h"
#include "ui/fileview.h"
#include "ui/statusbar.h"
#include "ui/ui.h"
#include "utils/file_streams.h"
#include "utils/filter.h"
#include "utils/fs.h"
#include "utils/hist.h"
#include "utils/int_stack.h"
#include "utils/path.h"
#include "utils/str.h"
#include "utils/string_array.h"
#include "utils/test_helpers.h"
#include "utils/utils.h"
#include "bracket_notation.h"
#include "cmd_completion.h"
#include "cmd_handlers.h"
#include "filelist.h"
#include "filtering.h"
#include "flist_sel.h"
#include "macros.h"
#include "marks.h"
#include "opt_handlers.h"
#include "undo.h"

/* Type of command arguments. */
typedef enum
{
	CAT_REGULAR,       /* Can be separated by a |. */
	CAT_EXPR,          /* Accept expressions with || and terminate on |. */
	CAT_UNTIL_THE_END, /* Take the rest of line including all |. */
}
CmdArgsType;

/* Values for if_levels stack. */
typedef enum
{
	IF_SCOPE_GUARD,  /* Command scope marker, prevents mixing of levels. */
	IF_ACTIVE_SCOPE, /* Scope of a taken conditional branch. */
	IF_BEFORE_MATCH, /* Before condition that evaluates to true is found. */
	IF_MATCH,        /* Just found true condition and processing that branch. */
	IF_AFTER_MATCH,  /* Left branch corresponding to true condition. */
	IF_ELSE,         /* Else branch that should be run (no matches before it). */
	IF_FINISH,       /* After else branch, only endif is expected by now. */
}
IfFrame;

static int swap_range(void);
static int resolve_mark(char mark);
static char * cmds_expand_macros(const char str[], int for_shell, int *usr1,
		int *usr2);
static void prepare_extcmd_file(FILE *fp, const char beginning[],
		CmdInputType type);
static hist_t * history_by_type(CmdInputType type);
static char * get_file_first_line(const char path[]);
static void execute_extcmd(const char command[], CmdInputType type);
static void save_extcmd(const char command[], CmdInputType type);
static void post(int id);
TSTATIC void cmds_select_range(int id, const cmd_info_t *cmd_info);
static int skip_at_beginning(int id, const char args[]);
static void create_builtin_vars(void);
static void session_changed_callback(const char new_session[]);
static char * pattern_expand_hook(const char pattern[]);
static int is_implicit_cd(view_t *view, const char cmd[], int cmd_error);
static int cmd_should_be_processed(int cmd_id);
TSTATIC char ** break_cmdline(const char cmdline[], int for_menu);
static int is_out_of_arg(const char cmd[], const char pos[]);
TSTATIC int line_pos(const char begin[], const char end[], char sep,
		int rquoting, int max_args);
static CmdArgsType get_cmd_args_type(const char cmd[]);
static char * skip_to_cmd_name(const char cmd[]);
static int repeat_command(view_t *view, CmdInputType type);
static int is_at_scope_bottom(const int_stack_t *scope_stack);
static int is_at_scope_guard(const int_stack_t *scope_stack);

/* Settings for the cmds unit. */
static cmds_conf_t cmds_conf = {
	.complete_line = &complete_line,
	.complete_args = &complete_args,
	.swap_range = &swap_range,
	.resolve_mark = &resolve_mark,
	.expand_macros = &cmds_expand_macros,
	.expand_envvars = &cmds_expand_envvars,
	.post = &post,
	.select_range = &cmds_select_range,
	.skip_at_beginning = &skip_at_beginning,
};

/* Shows whether view selection should be preserved on command-line finishing.
 * By default it's reset. */
static int keep_view_selection;
/* Stores condition evaluation result for all nesting if-endif statements as
 * well as file scope marks (SCOPE_GUARD). */
static int_stack_t if_levels;

static int
swap_range(void)
{
	return prompt_msg("Command Error", "Backwards range given, OK to swap?");
}

static int
resolve_mark(char mark)
{
	const int result = marks_find_in_view(curr_view, mark);
	if(result < 0)
	{
		ui_sb_errf("Trying to use an invalid mark: '%c", mark);
	}
	return result;
}

/* Implementation of macros expansion callback for cmds unit.  Returns newly
 * allocated memory. */
static char *
cmds_expand_macros(const char str[], int for_shell, int *usr1, int *usr2)
{
	char *result;
	MacroFlags flags = MF_NONE;

	result = ma_expand(str, NULL, &flags, for_shell ? MER_SHELL_OP : MER_OP);

	*usr1 = flags;

	return result;
}

char *
cmds_expand_envvars(const char str[])
{
	return expand_envvars(str, EEF_ESCAPE_VALS | EEF_KEEP_ESCAPES);
}

void
cmds_run_ext(const char line[], size_t line_pos, CmdInputType type)
{
	char *const cmd = cmds_get_ext(line, line_pos, type);
	if(cmd == NULL)
	{
		save_extcmd(line, type);
	}
	else
	{
		save_extcmd(cmd, type);
		execute_extcmd(cmd, type);
		free(cmd);
	}
}

char *
cmds_get_ext(const char beginning[], size_t line_pos, CmdInputType type)
{
	char cmd_file[PATH_MAX + 1];
	FILE *file = make_file_in_tmp("vifm.cmdline", 0600, /*auto_delete=*/0,
			cmd_file, sizeof(cmd_file));
	if(file == NULL)
	{
		show_error_msgf("External Editing", "Failed to create a temporary file: %s",
				strerror(errno));
		return NULL;
	}

	prepare_extcmd_file(file, beginning, type);
	fclose(file);

	char *cmd = NULL;
	if(vim_view_file(cmd_file, 1, line_pos, 0) == 0)
	{
		cmd = get_file_first_line(cmd_file);
	}

	unlink(cmd_file);
	return cmd;
}

/* Fills the file with history (more recent goes first). */
static void
prepare_extcmd_file(FILE *fp, const char beginning[], CmdInputType type)
{
	const int is_cmd = (type == CIT_COMMAND);
	const hist_t *const hist = history_by_type(type);
	int i;

	fprintf(fp, "%s\n", beginning);
	for(i = 0; i < hist->size; i++)
	{
		fprintf(fp, "%s\n", hist->items[i].text);
	}

	if(is_cmd)
	{
		fputs("\" vim: set filetype=vifm-cmdedit syntax=vifm :\n", fp);
	}
	else
	{
		fputs("\" vim: set filetype=vifm-edit :\n", fp);
	}
}

/* Picks history by command type.  Returns pointer to history. */
static hist_t *
history_by_type(CmdInputType type)
{
	switch(type)
	{
		case CIT_COMMAND:
			return &curr_stats.cmd_hist;
		case CIT_MENU_COMMAND:
			return &curr_stats.menucmd_hist;
		case CIT_PROMPT_INPUT:
			return &curr_stats.prompt_hist;
		case CIT_EXPRREG_INPUT:
			return &curr_stats.exprreg_hist;
		case CIT_FILTER_PATTERN:
			return &curr_stats.filter_hist;

		case CIT_FSEARCH_PATTERN:
		case CIT_BSEARCH_PATTERN:
		case CIT_VFSEARCH_PATTERN:
		case CIT_VBSEARCH_PATTERN:
		case CIT_VWFSEARCH_PATTERN:
		case CIT_VWBSEARCH_PATTERN:
			return &curr_stats.search_hist;
	}

	/* Should never happen. */
	return NULL;
}

/* Reads the first line of the file specified by the path.  Returns NULL on
 * error or an empty file, otherwise a newly allocated string, which should be
 * freed by the caller, is returned. */
static char *
get_file_first_line(const char path[])
{
	FILE *const fp = os_fopen(path, "rb");
	char *result = NULL;
	if(fp != NULL)
	{
		result = read_line(fp, NULL);
		fclose(fp);
	}
	return result;
}

/* Executes the command of the type. */
static void
execute_extcmd(const char command[], CmdInputType type)
{
	if(type == CIT_COMMAND)
	{
		cmds_scope_start();
		curr_stats.save_msg = cmds_dispatch(command, curr_view, type);
		if(cmds_scope_finish() != 0)
		{
			curr_stats.save_msg = 1;
		}
	}
	else
	{
		curr_stats.save_msg = cmds_dispatch1(command, curr_view, type);
	}
}

/* Saves the command to the appropriate history. */
static void
save_extcmd(const char command[], CmdInputType type)
{
	if(type == CIT_COMMAND)
	{
		hists_commands_save(command);
	}
	else
	{
		hists_search_save(command);
	}
}

int
cmds_goes_to_history(const char command[])
{
	/* Don't add :!! or :! to history list. */
	return strcmp(command, "!!") != 0 && strcmp(command, "!") != 0;
}

int
cmds_has_expr_args(int cmd_id)
{
	/* XXX: would be nicer to complete only function names for top-level :call
	 *      expression. */
	return cmd_id == COM_ECHO
	    || cmd_id == COM_EXE
	    || cmd_id == COM_IF_STMT
	    || cmd_id == COM_ELSEIF_STMT
	    || cmd_id == COM_LET
	    || cmd_id == COM_CALL;
}

char *
cmds_insertion_escape(const char cmd_line[], int pos, const char str[])
{
	const CmdLineLocation ipt = cmds_classify_pos(cmd_line, cmd_line + pos);
	switch(ipt)
	{
		case CLL_R_QUOTING:
			/* XXX: Use of filename escape, while special one might be needed. */
		case CLL_OUT_OF_ARG:
		case CLL_NO_QUOTING:
			return posix_like_escape(str, /*type=*/0);

		case CLL_S_QUOTING:
			return escape_for_squotes(str, 0);

		case CLL_D_QUOTING:
			return escape_for_dquotes(str, 0);

		default:
			return NULL;
	}
}

static void
post(int id)
{
	/* When one command in a sequence enters visual mode, we don't want to break
	 * it by resetting selection. */
	if(!keep_view_selection && vle_mode_is(NORMAL_MODE))
	{
		flist_sel_stash_if_nonempty(curr_view);
	}

	/* Marking made by this unit is for a single command only and shouldn't be
	 * processed outside of that command. */
	curr_view->pending_marking = 0;
}

TSTATIC void
cmds_select_range(int id, const cmd_info_t *cmd_info)
{
	if(curr_view->pending_marking)
	{
		/* It must come from parent :command, so keep it. */
		return;
	}

	int mark_current = (id != COM_FIND && id != COM_GREP);
	curr_view->pending_marking = flist_sel_range(curr_view, cmd_info->begin,
			cmd_info->end, mark_current);
}

/* Command prefix remover for command parsing unit.  Returns < 0 to do nothing
 * or x to skip command name and x chars. */
static int
skip_at_beginning(int id, const char args[])
{
	if(id == COM_KEEPSEL || id == COM_WINDO)
	{
		return 0;
	}

	if(id == COM_WINRUN)
	{
		args = vle_cmds_at_arg(args);
		if(*args != '\0')
		{
			return 1;
		}
	}
	return -1;
}

void
cmds_init(void)
{
	if(cmds_conf.inner != NULL)
	{
		vle_cmds_init(1, &cmds_conf);
		return;
	}

	/* We get here when cmds_init() is called the first time. */

	vle_cmds_init(1, &cmds_conf);
	vle_cmds_add(cmds_list, cmds_list_size);

	/* Initialize modules used by this one. */
	init_bracket_notation();
	init_variables();

	create_builtin_vars();
	sessions_set_callback(&session_changed_callback);

	vle_aucmd_set_expand_hook(&pattern_expand_hook);
}

/* Creates builtin variables with some default values. */
static void
create_builtin_vars(void)
{
	var_t var = var_from_int(0);
	setvar("v:count", var);
	setvar("v:count1", var);
	var_free(var);

	var = var_from_str("");
	setvar("v:session", var);
	var_free(var);

	var = var_from_int(0);
	setvar("v:jobcount", var);
	var_free(var);

	var = var_from_int(VERSION_NUM);
	setvar("v:version", var);
	var_free(var);
}

/* Callback to be invoked when active session has changed. */
static void
session_changed_callback(const char new_session[])
{
	var_t var = var_from_str(new_session);
	setvar("v:session", var);
	var_free(var);
}

void
cmds_vars_set_count(int count)
{
	var_t var;

	var = var_from_int(count == NO_COUNT_GIVEN ? 0 : count);
	setvar("v:count", var);
	var_free(var);

	var = var_from_int(count == NO_COUNT_GIVEN ? 1 : count);
	setvar("v:count1", var);
	var_free(var);
}

/* Performs custom pattern expansion.  Allocate new expanded string. */
static char *
pattern_expand_hook(const char pattern[])
{
	char *const no_tilde = expand_tilde(pattern);
	char *const expanded_pattern = expand_envvars(no_tilde, EEF_NONE);
	free(no_tilde);

	return expanded_pattern;
}

int
cmds_exec(const char cmd[], view_t *view, int menu, int keep_sel)
{
	int id;
	int result;

	if(cmd == NULL)
	{
		flist_sel_stash_if_nonempty(view);
		return 0;
	}

	cmd = skip_to_cmd_name(cmd);

	if(cmd[0] == '"')
		return 0;

	if(cmd[0] == '\0' && !menu)
	{
		flist_sel_stash_if_nonempty(view);
		return 0;
	}

	if(!menu)
	{
		vle_cmds_init(1, &cmds_conf);
		cmds_conf.begin = 0;
		cmds_conf.current = view->list_pos;
		cmds_conf.end = view->list_rows - 1;
	}

	id = vle_cmds_identify(cmd);

	if(!cmd_should_be_processed(id))
	{
		return 0;
	}

	if(id == USER_CMD_ID)
	{
		char undo_msg[COMMAND_GROUP_INFO_LEN];

		snprintf(undo_msg, sizeof(undo_msg), "in %s: %s",
				replace_home_part(flist_get_dir(view)), cmd);

		un_group_open(undo_msg);
		un_group_close();
	}

	keep_view_selection = keep_sel;
	result = vle_cmds_run(cmd);

	if(result >= 0)
		return result;

	if(is_implicit_cd(view, cmd, result))
	{
		return cd(view, flist_get_dir(view), cmd);
	}

	switch(result)
	{
		case CMDS_ERR_LOOP:
			ui_sb_err("Loop in commands");
			break;
		case CMDS_ERR_NO_MEM:
			ui_sb_err("Unable to allocate enough memory");
			break;
		case CMDS_ERR_TOO_FEW_ARGS:
			ui_sb_err("Too few arguments");
			break;
		case CMDS_ERR_TRAILING_CHARS:
			ui_sb_err("Trailing characters");
			break;
		case CMDS_ERR_INCORRECT_NAME:
			ui_sb_err("Incorrect command name");
			break;
		case CMDS_ERR_NEED_BANG:
			ui_sb_err("Add bang to force");
			break;
		case CMDS_ERR_NO_BUILTIN_REDEFINE:
			ui_sb_err("Can't redefine builtin command");
			break;
		case CMDS_ERR_INVALID_CMD:
			ui_sb_err("Invalid command name");
			break;
		case CMDS_ERR_NO_BANG_ALLOWED:
			ui_sb_err("No ! is allowed");
			break;
		case CMDS_ERR_NO_RANGE_ALLOWED:
			ui_sb_err("No range is allowed");
			break;
		case CMDS_ERR_NO_QMARK_ALLOWED:
			ui_sb_err("No ? is allowed");
			break;
		case CMDS_ERR_INVALID_RANGE:
			ui_sb_err("Invalid range");
			break;
		case CMDS_ERR_NO_SUCH_UDF:
			ui_sb_err("No such user defined command");
			break;
		case CMDS_ERR_UDF_IS_AMBIGUOUS:
			ui_sb_err("Ambiguous use of user-defined command");
			break;
		case CMDS_ERR_INVALID_ARG:
			ui_sb_err("Invalid argument");
			break;
		case CMDS_ERR_CUSTOM:
			/* error message is posted by command handler */
			break;
		default:
			ui_sb_err("Unknown error");
			break;
	}

	if(!menu && vle_mode_is(NORMAL_MODE))
	{
		flist_sel_stash_if_nonempty(view);
	}

	return -1;
}

/* Whether need to process command-line command as implicit :cd.  Returns
 * non-zero if so, otherwise zero is returned. */
static int
is_implicit_cd(view_t *view, const char cmd[], int cmd_error)
{
	if(!cfg.auto_cd)
	{
		return 0;
	}

	if(cmd_error != CMDS_ERR_INVALID_CMD && cmd_error != CMDS_ERR_TRAILING_CHARS)
	{
		return 0;
	}

	char *expanded = expand_tilde(cmd);
	int exists = path_exists_at(flist_get_dir(view), expanded, NODEREF);
	free(expanded);
	return exists;
}

/* Decides whether next command with id cmd_id should be processed or not,
 * taking state of conditional statements into account.  Returns non-zero if the
 * command should be processed, otherwise zero is returned. */
static int
cmd_should_be_processed(int cmd_id)
{
	/* XXX: this should probably be zeroed when exiting a scope (including on
	 *      error) and yet I failed to come up with an test case which exposes an
	 *      issue with not zeroing this. */
	static int skipped_nested_if_stmts;

	if(is_at_scope_bottom(&if_levels))
	{
		return 1;
	}

	/* Get here only when in false branch of if statement. */

	if(cmd_id == COM_IF_STMT)
	{
		++skipped_nested_if_stmts;
		return 0;
	}

	if(cmd_id == COM_ELSEIF_STMT)
	{
		return (skipped_nested_if_stmts == 0);
	}

	if(cmd_id == COM_ELSE_STMT || cmd_id == COM_ENDIF_STMT)
	{
		if(skipped_nested_if_stmts > 0)
		{
			if(cmd_id == COM_ENDIF_STMT)
			{
				--skipped_nested_if_stmts;
			}
			return 0;
		}
		return 1;
	}

	return 0;
}

/* Determines current position in the command line.  The rquoting parameter when
 * non-zero specifies number of rquoted arguments by its absolute value, while
 * negative sign means that last argument doesn't need to be terminated.
 * Returns:
 *  - 0, if not inside an argument;
 *  - 1, if next character should be skipped (XXX: what does it mean?);
 *  - 2, if inside escaped argument;
 *  - 3, if inside single quoted argument;
 *  - 4, if inside double quoted argument;
 *  - 5, if inside regexp quoted argument. */
TSTATIC int
line_pos(const char begin[], const char end[], char sep, int rquoting,
		int max_args)
{
	int count;
	enum { BEGIN, NO_QUOTING, S_QUOTING, D_QUOTING, R_QUOTING } state;
	int args_left = max_args;
	/* Actual separator used for rargs or '\0' if unset. */
	char rsep = '\0';

	const char *args = vle_cmds_args(begin);
	if(args >= end)
	{
		return 0;
	}
	if(args == NULL)
	{
		count = 0;
	}
	else
	{
		begin = args;
		count = 1;
	}

	if(rquoting < 0)
	{
		rquoting = -(rquoting + 1);
	}

	state = BEGIN;
	while(begin != end)
	{
		switch(state)
		{
			case BEGIN:
				if(sep == ' ' && *begin == '\'')
					state = S_QUOTING;
				else if(sep == ' ' && *begin == '"')
					state = D_QUOTING;
				else if(rquoting && ((sep == ' ' && *begin == '/') || *begin == sep) &&
						(rsep == '\0' || *begin == rsep))
					state = R_QUOTING;
				else if(*begin == '&' && begin == end - 1)
					state = BEGIN;
				else if(*begin != sep)
					state = rquoting && (rsep == '\0' || *begin == rsep) ? R_QUOTING
					                                                     : NO_QUOTING;
				break;
			case NO_QUOTING:
				if(*begin == sep)
				{
					state = BEGIN;
					count++;
				}
				else if(*begin == '\'')
				{
					state = S_QUOTING;
				}
				else if(*begin == '"')
				{
					state = D_QUOTING;
				}
				else if(*begin == '\\')
				{
					begin++;
					if(begin == end)
						return 1;
				}
				break;
			case S_QUOTING:
				if(*begin == '\'')
					state = BEGIN;
				break;
			case D_QUOTING:
				if(*begin == '"')
				{
					state = BEGIN;
				}
				else if(*begin == '\\')
				{
					begin++;
					if(begin == end)
						return 1;
				}
				break;
			case R_QUOTING:
				if(rsep == '\0')
				{
					rsep = *(begin - 1);
				}
				if(*begin == rsep || *begin == sep)
				{
					--args_left;
					--rquoting;
					if(args_left == 0 || rquoting == 0)
					{
						state = BEGIN;
					}
				}
				else if(*begin == '\\')
				{
					begin++;
					if(begin == end)
						return 1;
				}
				break;
		}
		begin++;
	}
	if(state == NO_QUOTING)
	{
		if(sep == ' ')
		{
			/* First element is not an argument. */
			return (count > 0) ? 2 : 0;
		}
		else if(count > 0 && count < 3)
		{
			return 2;
		}
	}
	else if(state != BEGIN)
	{
		/* "Error": no closing quote. */
		switch(state)
		{
			case S_QUOTING: return 3;
			case D_QUOTING: return 4;
			case R_QUOTING: return (max_args <= 1 || args_left > 1 ? 5 : 2);

			default:
				assert(0 && "Unexpected state.");
				break;
		}
	}
	else if(sep != ' ' && count > 0 && *end != sep)
		return 2;

	return 0;
}

int
cmds_dispatch(const char cmdline[], view_t *view, CmdInputType type)
{
	int save_msg = 0;
	char **cmds = break_cmdline(cmdline, type == CIT_MENU_COMMAND);
	char **cmd = cmds;

	while(*cmd != NULL)
	{
		const int ret = cmds_dispatch1(*cmd, view, type);
		if(ret != 0)
		{
			save_msg = (ret < 0) ? -1 : 1;
		}

		free(*cmd++);
	}
	free(cmds);

	return save_msg;
}

/* Breaks command-line into sub-commands.  Returns NULL-terminated list of
 * sub-commands. */
TSTATIC char **
break_cmdline(const char cmdline[], int for_menu)
{
	char **cmds = NULL;
	int len = 0;

	char cmdline_copy[strlen(cmdline) + 1];
	char *raw, *processed;

	CmdArgsType args_kind;

	if(*cmdline == '\0')
	{
		len = add_to_string_array(&cmds, len, cmdline);
		goto finish;
	}

	strcpy(cmdline_copy, cmdline);
	cmdline = cmdline_copy;

	raw = cmdline_copy;
	processed = cmdline_copy;

	/* For non-menu commands set command-line mode configuration before calling
	 * is_out_of_arg() and get_cmd_args_type() function, which calls functions of
	 * the cmds module of the engine that use context. */
	if(!for_menu)
	{
		vle_cmds_init(1, &cmds_conf);
	}

	cmdline = skip_to_cmd_name(cmdline);
	args_kind = get_cmd_args_type(cmdline);

	/* Throughout the following loop local variables have the following meaning:
	 * - save_msg  -- resultant state of message indication;
	 * - raw       -- not yet processed part of string that can contain \;
	 * - processed -- ready to consume part of string;
	 * - cmdline   -- points to the start of the last command. */
	while(*cmdline != '\0')
	{
		if(args_kind == CAT_REGULAR && raw[0] == '\\' && raw[1] != '\0')
		{
			if(*(raw + 1) == '|')
			{
				*processed++ = '|';
				raw += 2;
			}
			else
			{
				*processed++ = *raw++;
				*processed++ = *raw++;
			}
		}
		else if((*raw == '|' && is_out_of_arg(cmdline, processed)) || *raw == '\0')
		{
			if(*raw != '\0')
			{
				++raw;
			}
			else
			{
				*processed = '\0';
			}

			/* Don't break line for whole line commands. */
			if(args_kind != CAT_UNTIL_THE_END)
			{
				if(args_kind == CAT_EXPR)
				{
					/* Move breaking point forward by consuming parts of the string after
					 * || until end of the string or | is found. */
					while(processed[0] == '|' && processed[1] == '|' &&
							processed[2] != '|')
					{
						processed = until_first(processed + 2, '|');
						raw = (processed[0] == '\0') ? processed : (processed + 1);
					}
				}

				*processed = '\0';
				processed = raw;
			}

			len = add_to_string_array(&cmds, len, cmdline);

			if(args_kind == CAT_UNTIL_THE_END)
			{
				/* Whole line command takes the rest of the string, nothing more to
				 * process. */
				break;
			}

			cmdline = skip_to_cmd_name(processed);
			args_kind = get_cmd_args_type(cmdline);
		}
		else
		{
			*processed++ = *raw++;
		}
	}

finish:
	(void)put_into_string_array(&cmds, len, NULL);
	return cmds;
}

/* Checks whether character at given position in the given command-line is
 * outside quoted argument.  Returns non-zero if so, otherwise zero is
 * returned. */
static int
is_out_of_arg(const char cmd[], const char pos[])
{
	const CmdLineLocation location = cmds_classify_pos(cmd, pos);

	if(location == CLL_NO_QUOTING)
	{
		if(get_cmd_args_type(cmd) == CAT_REGULAR)
		{
			return 1;
		}
		if(get_cmd_args_type(cmd) == CAT_EXPR &&
				pos != cmd && pos[0] == '|' && pos[-1] != '|' && pos[1] != '|')
		{
			/* For "*[^|]|[^|]*" report that we're out of argument. */
			return 1;
		}
	}

	return location == CLL_OUT_OF_ARG;
}

CmdLineLocation
cmds_classify_pos(const char cmd[], const char *pos)
{
	char separator;
	int regex_quoting;

	cmd_info_t info;
	const cmd_t *const c = vle_cmds_parse(cmd, &info);
	const int id = (c == NULL ? -1 : c->id);
	const int max_args = (c == NULL)
	                   ? -1
	                   : (c->max_args == NOT_DEF ? INT_MAX : c->max_args);

	switch(id)
	{
		case COM_CDS:
		case COM_HIGHLIGHT:
		case COM_FILTER:
		case COM_SELECT:
			separator = ' ';
			regex_quoting = -2;
			break;
		case COM_SUBSTITUTE:
			separator = info.sep;
			regex_quoting = -3;
			break;
		case COM_TR:
			separator = info.sep;
			regex_quoting = 2;
			break;

		default:
			separator = ' ';
			regex_quoting = 0;
			break;
	}

	switch(line_pos(cmd, pos, separator, regex_quoting, max_args))
	{
		case 0: return CLL_OUT_OF_ARG;
		case 1: /* Fall through. */
		case 2: return CLL_NO_QUOTING;
		case 3: return CLL_S_QUOTING;
		case 4: return CLL_D_QUOTING;
		case 5: return CLL_R_QUOTING;

		default:
			assert(0 && "Unexpected return code.");
			return CLL_NO_QUOTING;
	}
}

/* Determines what kind of processing should be applied to the command pointed
 * to by the cmd.  Returns the kind. */
static CmdArgsType
get_cmd_args_type(const char cmd[])
{
	const int cmd_id = vle_cmds_identify(cmd);
	switch(cmd_id)
	{
		case COMMAND_CMD_ID:
		case COM_AMAP:
		case COM_ANOREMAP:
		case COM_AUTOCMD:
		case COM_EXECUTE:
		case COM_CABBR:
		case COM_CMAP:
		case COM_CNOREMAP:
		case COM_COMMAND:
		case COM_DMAP:
		case COM_DNOREMAP:
		case COM_FILETYPE:
		case COM_FILEVIEWER:
		case COM_FILEXTYPE:
		case COM_KEEPSEL:
		case COM_MAP:
		case COM_MMAP:
		case COM_MNOREMAP:
		case COM_NMAP:
		case COM_NNOREMAP:
		case COM_NORMAL:
		case COM_QMAP:
		case COM_QNOREMAP:
		case COM_VMAP:
		case COM_VNOREMAP:
		case COM_NOREMAP:
		case COM_WINCMD:
		case COM_WINDO:
		case COM_WINGO:
		case COM_WINRUN:
			return CAT_UNTIL_THE_END;

		default:
			return cmds_has_expr_args(cmd_id) ? CAT_EXPR : CAT_REGULAR;
	}
}

const char *
cmds_find_last(const char cmds[])
{
	const char *p, *q;

	p = cmds;
	q = cmds;
	while(*cmds != '\0')
	{
		if(*p == '\\')
		{
			q += (p[1] == '|') ? 1 : 2;
			p += 2;
		}
		else if(*p == '\0' || (*p == '|' && is_out_of_arg(cmds, q)))
		{
			if(*p != '\0')
			{
				++p;
			}

			cmds = skip_to_cmd_name(cmds);
			if(*cmds == '!' || starts_with_lit(cmds, "com"))
			{
				break;
			}

			q = p;

			if(*q == '\0')
			{
				break;
			}

			cmds = q;
		}
		else
		{
			++q;
			++p;
		}
	}

	return cmds;
}

/* Skips consecutive whitespace or colon characters at the beginning of the
 * command.  Returns pointer to the first non whitespace and color character. */
static char *
skip_to_cmd_name(const char cmd[])
{
	while(isspace(*cmd) || *cmd == ':')
	{
		++cmd;
	}
	return (char *)cmd;
}

int
cmds_dispatch1(const char cmd[], view_t *view, CmdInputType type)
{
	int menu;
	int backward;
	int found;

	if(cmd == NULL)
	{
		return repeat_command(view, type);
	}

	menu = 0;
	backward = 0;
	switch(type)
	{
		case CIT_BSEARCH_PATTERN: backward = 1; /* Fall through. */
		case CIT_FSEARCH_PATTERN:
			return modnorm_find(view, cmd, backward, /*print_msg=*/1, &found);

		case CIT_VBSEARCH_PATTERN: backward = 1; /* Fall through. */
		case CIT_VFSEARCH_PATTERN:
			return modvis_find(view, cmd, backward, /*print_msg=*/1, &found);

		case CIT_VWBSEARCH_PATTERN: backward = 1; /* Fall through. */
		case CIT_VWFSEARCH_PATTERN:
			return modview_find(cmd, backward);

		case CIT_MENU_COMMAND: menu = 1; /* Fall through. */
		case CIT_COMMAND:
			return cmds_exec(cmd, view, menu, /*keep_sel=*/0);

		case CIT_FILTER_PATTERN:
			if(view->custom.type != CV_DIFF)
			{
				local_filter_apply(view, cmd);
			}
			else
			{
				show_error_msg("Filtering", "No local filter for diff views");
			}
			return 0;

		default:
			assert(0 && "Command execution request of unknown/unexpected type.");
			return 0;
	}
}

/* Repeats last command of the specified type.  Returns 0 on success if no
 * message should be saved in the status bar, > 0 to save message on successful
 * execution and < 0 in case of error with error message. */
static int
repeat_command(view_t *view, CmdInputType type)
{
	int backward = 0;
	int found;
	switch(type)
	{
		case CIT_BSEARCH_PATTERN: backward = 1; /* Fall through. */
		case CIT_FSEARCH_PATTERN:
			return modnorm_find(view, hists_search_last(), backward, /*print_msg=*/1,
					&found);

		case CIT_VBSEARCH_PATTERN: backward = 1; /* Fall through. */
		case CIT_VFSEARCH_PATTERN:
			return modvis_find(view, hists_search_last(), backward, /*print_msg=*/1,
					&found);

		case CIT_VWBSEARCH_PATTERN: backward = 1; /* Fall through. */
		case CIT_VWFSEARCH_PATTERN:
			return modview_find(NULL, backward);

		case CIT_COMMAND:
			return cmds_exec(/*cmd=*/NULL, view, /*menu=*/0, /*keep_sel=*/0);

		case CIT_FILTER_PATTERN:
			local_filter_apply(view, "");
			return 0;

		default:
			assert(0 && "Command repetition request of unexpected type.");
			return 0;
	}
}

void
cmds_scope_start(void)
{
	(void)int_stack_push(&if_levels, IF_SCOPE_GUARD);
}

void
cmds_scope_escape(void)
{
	while(!is_at_scope_guard(&if_levels))
	{
		int_stack_pop(&if_levels);
	}
}

int
cmds_scope_finish(void)
{
	if(!is_at_scope_guard(&if_levels))
	{
		ui_sb_err("Missing :endif");
		int_stack_pop_seq(&if_levels, IF_SCOPE_GUARD);
		return 1;
	}

	int_stack_pop(&if_levels);
	return 0;
}

void
cmds_scoped_error(void)
{
	if(int_stack_is_empty(&if_levels))
	{
		/* Can't signal error in this case because there will be no invocation of
		 * cmds_scope_finish(). */
		return;
	}

	if(is_at_scope_bottom(&if_levels))
	{
		(void)int_stack_push(&if_levels, IF_AFTER_MATCH);
	}
	else
	{
		int_stack_set_top(&if_levels, IF_AFTER_MATCH);
	}
}

int
cmds_scoped_should_eval(int cmd_id)
{
	if(int_stack_is_empty(&if_levels))
	{
		return 1;
	}

	IfFrame if_frame = int_stack_get_top(&if_levels);
	if(if_frame == IF_ACTIVE_SCOPE && cmd_id == COM_ELSEIF_STMT)
	{
		return 0;
	}

	return ONE_OF(if_frame, IF_BEFORE_MATCH, IF_SCOPE_GUARD, IF_ACTIVE_SCOPE);
}

void
cmds_scoped_if(int cond)
{
	(void)int_stack_push(&if_levels, cond ? IF_MATCH : IF_BEFORE_MATCH);
	cmds_preserve_selection();

	if(cond)
	{
		(void)int_stack_push(&if_levels, IF_ACTIVE_SCOPE);
	}
}

int
cmds_scoped_elseif(int cond)
{
	if(is_at_scope_guard(&if_levels))
	{
		return 1;
	}

	if(int_stack_top_is(&if_levels, IF_ACTIVE_SCOPE))
	{
		int_stack_pop(&if_levels);
	}

	IfFrame if_frame = int_stack_get_top(&if_levels);
	if(if_frame == IF_ELSE || if_frame == IF_FINISH)
	{
		return 1;
	}

	int_stack_set_top(&if_levels, (if_frame == IF_BEFORE_MATCH) ?
			(cond ? IF_MATCH : IF_BEFORE_MATCH) :
			IF_AFTER_MATCH);
	if(cond)
	{
		(void)int_stack_push(&if_levels, IF_ACTIVE_SCOPE);
	}

	cmds_preserve_selection();
	return 0;
}

int
cmds_scoped_else(void)
{
	if(int_stack_top_is(&if_levels, IF_ACTIVE_SCOPE))
	{
		int_stack_pop(&if_levels);
	}

	if(is_at_scope_bottom(&if_levels))
	{
		return 1;
	}

	IfFrame if_frame = int_stack_get_top(&if_levels);
	if(if_frame == IF_ELSE || if_frame == IF_FINISH)
	{
		return 1;
	}

	int_stack_set_top(&if_levels,
			(if_frame == IF_BEFORE_MATCH) ? IF_ELSE : IF_FINISH);
	if(if_frame == IF_BEFORE_MATCH)
	{
		(void)int_stack_push(&if_levels, IF_ACTIVE_SCOPE);
	}

	cmds_preserve_selection();
	return 0;
}

int
cmds_scoped_endif(void)
{
	if(int_stack_top_is(&if_levels, IF_ACTIVE_SCOPE))
	{
		int_stack_pop(&if_levels);
	}

	if(is_at_scope_bottom(&if_levels))
	{
		return 1;
	}

	int_stack_pop(&if_levels);
	return 0;
}

int
cmds_scoped_empty(void)
{
	return int_stack_is_empty(&if_levels);
}

/* Checks that bottom of block scope is reached.  Returns non-zero if so,
 * otherwise zero is returned. */
static int
is_at_scope_bottom(const int_stack_t *scope_stack)
{
	return is_at_scope_guard(scope_stack)
	    || int_stack_top_is(scope_stack, IF_ACTIVE_SCOPE);
}

/* Checks that current scope of commands is empty.  Returns non-zero if so,
 * otherwise zero is returned. */
static int
is_at_scope_guard(const int_stack_t *scope_stack)
{
	return int_stack_is_empty(scope_stack)
	    || int_stack_top_is(scope_stack, IF_SCOPE_GUARD);
}

char *
cmds_eval_args(const char args[], const char **stop_ptr)
{
	size_t len = 0;
	char *eval_result = NULL;

	assert(args[0] != '\0');

	while(args[0] != '\0')
	{
		char *free_this = NULL;
		const char *tmp_result = NULL;

		parsing_result_t result = vle_parser_eval(args, /*interactive=*/1);
		if(result.error == PE_INVALID_EXPRESSION && result.ends_with_whitespace)
		{
			if(result.value.type != VTYPE_ERROR)
			{
				tmp_result = free_this = var_to_str(result.value);
				args = result.last_parsed_char;
			}
		}
		else if(result.error == PE_NO_ERROR)
		{
			tmp_result = free_this = var_to_str(result.value);
			args = result.last_position;
		}
		else if(result.error != PE_INVALID_EXPRESSION)
		{
			vle_parser_report(&result);
		}

		if(tmp_result == NULL)
		{
			var_free(result.value);
			break;
		}

		if(!is_null_or_empty(eval_result))
		{
			eval_result = extend_string(eval_result, " ", &len);
		}
		eval_result = extend_string(eval_result, tmp_result, &len);

		var_free(result.value);
		free(free_this);

		args = skip_whitespace(args);
	}
	if(args[0] == '\0')
	{
		return eval_result;
	}
	else
	{
		free(eval_result);
		*stop_ptr = args;
		return NULL;
	}
}

void
cmds_preserve_selection(void)
{
	keep_view_selection = 1;
}

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