/* 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 "menu.h"

#include <curses.h>

#include <assert.h> /* assert() */
#include <errno.h> /* errno */
#include <stddef.h> /* NULL wchar_t */
#include <stdio.h> /* pclose() popen() */
#include <stdlib.h> /* free() */
#include <string.h> /* strerror() */

#include "../cfg/config.h"
#include "../compat/curses.h"
#include "../compat/reallocarray.h"
#include "../engine/cmds.h"
#include "../engine/keys.h"
#include "../engine/mode.h"
#include "../lua/vlua.h"
#include "../menus/chistory_menu.h"
#include "../menus/menus.h"
#include "../modes/dialogs/msg_dialog.h"
#include "../ui/fileview.h"
#include "../ui/statusbar.h"
#include "../ui/ui.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/utils.h"
#include "../cmd_actions.h"
#include "../cmd_core.h"
#include "../cmd_completion.h"
#include "../flist_sel.h"
#include "../status.h"
#include "cmdline.h"
#include "modes.h"
#include "wk.h"

static const int SCROLL_GAP = 2;

static int complete_line_in_menu(const char cmd_line[], void *extra_arg);
static int swap_range(void);
static int resolve_mark(char mark);
static char * menu_expand_macros(const char str[], int for_shell, int *usr1,
		int *usr2);
static char * menu_expand_envvars(const char *str);
static void post(int id);
static void menu_select_range(int id, const cmd_info_t *cmd_info);
static int skip_at_beginning(int id, const char *args);

static int key_handler(wchar_t key);
static void cmd_ctrl_b(key_info_t key_info, keys_info_t *keys_info);
static int can_scroll_menu_up(const menu_data_t *menu);
static void cmd_ctrl_c(key_info_t key_info, keys_info_t *keys_info);
static void cmd_ctrl_d(key_info_t key_info, keys_info_t *keys_info);
static void cmd_ctrl_e(key_info_t key_info, keys_info_t *keys_info);
static void cmd_ctrl_f(key_info_t key_info, keys_info_t *keys_info);
static int can_scroll_menu_down(const menu_data_t *menu);
static void change_menu_top(menu_data_t *menu, int delta);
static void cmd_ctrl_l(key_info_t key_info, keys_info_t *keys_info);
static void cmd_return(key_info_t key_info, keys_info_t *keys_info);
static void update_ui_on_leaving(void);
static void cmd_ctrl_u(key_info_t key_info, keys_info_t *keys_info);
static int get_effective_menu_scroll_offset(const menu_data_t *menu);
static void cmd_ctrl_y(key_info_t key_info, keys_info_t *keys_info);
static void cmd_slash(key_info_t key_info, keys_info_t *keys_info);
static void cmd_percent(key_info_t key_info, keys_info_t *keys_info);
static void cmd_colon(key_info_t key_info, keys_info_t *keys_info);
static void cmd_qmark(key_info_t key_info, keys_info_t *keys_info);
static void cmd_B(key_info_t key_info, keys_info_t *keys_info);
static void cmd_G(key_info_t key_info, keys_info_t *keys_info);
static void cmd_H(key_info_t key_info, keys_info_t *keys_info);
static void cmd_L(key_info_t key_info, keys_info_t *keys_info);
static void cmd_M(key_info_t key_info, keys_info_t *keys_info);
static void cmd_N(key_info_t key_info, keys_info_t *keys_info);
static void cmd_b(key_info_t key_info, keys_info_t *keys_info);
static void dump_into_custom_view(int very);
static void cmd_dd(key_info_t key_info, keys_info_t *keys_info);
static void cmd_gf(key_info_t key_info, keys_info_t *keys_info);
static int pass_combination_to_khandler(const wchar_t keys[]);
static void cmd_gg(key_info_t key_info, keys_info_t *keys_info);
static void cmd_j(key_info_t key_info, keys_info_t *keys_info);
static void cmd_k(key_info_t key_info, keys_info_t *keys_info);
static void cmd_n(key_info_t key_info, keys_info_t *keys_info);
static void cmd_v(key_info_t key_info, keys_info_t *keys_info);
static void cmd_zb(key_info_t key_info, keys_info_t *keys_info);
static void cmd_zH(key_info_t key_info, keys_info_t *keys_info);
static void cmd_zL(key_info_t key_info, keys_info_t *keys_info);
static void cmd_zh(key_info_t key_info, keys_info_t *keys_info);
static void cmd_zl(key_info_t key_info, keys_info_t *keys_info);
static void cmd_zt(key_info_t key_info, keys_info_t *keys_info);
static void cmd_zz(key_info_t key_info, keys_info_t *keys_info);
static void handle_mouse_event(key_info_t key_info, keys_info_t *keys_info);
static int all_lines_visible(const menu_data_t *menu);

static int goto_cmd(const cmd_info_t *cmd_info);
static int chistory_cmd(const cmd_info_t *cmd_info);
static int cnewer_cmd(const cmd_info_t *cmd_info);
static int colder_cmd(const cmd_info_t *cmd_info);
static int find_cmd(const cmd_info_t *cmd_info);
static int grep_cmd(const cmd_info_t *cmd_info);
static int nohlsearch_cmd(const cmd_info_t *cmd_info);
static int quit_cmd(const cmd_info_t *cmd_info);
static int write_cmd(const cmd_info_t *cmd_info);
static void leave_menu_mode(int reset_selection);

static view_t *view;
static menu_data_t *menu;
static int last_search_backward;
static int was_redraw;
static int saved_top, saved_pos;

static keys_add_info_t builtin_cmds[] = {
	{WK_C_b,     {{&cmd_ctrl_b},  .descr = "scroll page up"}},
	{WK_C_c,     {{&cmd_ctrl_c},  .descr = "leave menu mode"}},
	{WK_C_d,     {{&cmd_ctrl_d},  .descr = "scroll half-page down"}},
	{WK_C_e,     {{&cmd_ctrl_e},  .descr = "scroll one line down"}},
	{WK_C_f,     {{&cmd_ctrl_f},  .descr = "scroll page down"}},
	{WK_C_l,     {{&cmd_ctrl_l},  .descr = "redraw"}},
	{WK_CR,      {{&cmd_return},  .descr = "pick current item"}},
	{WK_C_n,     {{&cmd_j},       .descr = "go to item below"}},
	{WK_C_p,     {{&cmd_k},       .descr = "go to item above"}},
	{WK_C_u,     {{&cmd_ctrl_u},  .descr = "scroll half-page up"}},
	{WK_C_y,     {{&cmd_ctrl_y},  .descr = "scroll one line up"}},
	{WK_ESC,     {{&cmd_ctrl_c},  .descr = "leave menu mode"}},
	{WK_SLASH,   {{&cmd_slash},   .descr = "search forward"}},
	{WK_PERCENT, {{&cmd_percent}, .descr = "go to [count]% position"}},
	{WK_COLON,   {{&cmd_colon},   .descr = "go to cmdline mode"}},
	{WK_QM,      {{&cmd_qmark},   .descr = "search backward"}},
	{WK_B,       {{&cmd_B},       .descr = "make unsorted custom view"}},
	{WK_G,       {{&cmd_G},       .descr = "go to the last item"}},
	{WK_H,       {{&cmd_H},       .descr = "go to top of viewport"}},
	{WK_L,       {{&cmd_L},       .descr = "go to bottom of viewport"}},
	{WK_M,       {{&cmd_M},       .descr = "go to middle of viewport"}},
	{WK_N,       {{&cmd_N},       .descr = "go to previous search match"}},
	{WK_Z WK_Z,  {{&cmd_ctrl_c},  .descr = "leave menu mode"}},
	{WK_Z WK_Q,  {{&cmd_ctrl_c},  .descr = "leave menu mode"}},
	{WK_b,       {{&cmd_b},       .descr = "make custom view"}},
	{WK_d WK_d,  {{&cmd_dd},      .descr = "remove files"}},
	{WK_g WK_f,  {{&cmd_gf},      .descr = "navigate to file location"}},
	{WK_g WK_g,  {{&cmd_gg},      .descr = "go to the first item"}},
	{WK_j,       {{&cmd_j},       .descr = "go to item below"}},
	{WK_k,       {{&cmd_k},       .descr = "go to item above"}},
	{WK_l,       {{&cmd_return},  .descr = "pick current item"}},
	{WK_n,       {{&cmd_n},       .descr = "go to next search match"}},
	{WK_q,       {{&cmd_ctrl_c},  .descr = "leave menu mode"}},
	{WK_v,       {{&cmd_v},       .descr = "use items as Vim quickfix list"}},
	{WK_z WK_b,  {{&cmd_zb},      .descr = "push cursor to the bottom"}},
	{WK_z WK_H,  {{&cmd_zH},      .descr = "scroll page left"}},
	{WK_z WK_L,  {{&cmd_zL},      .descr = "scroll page right"}},
	{WK_z WK_h,  {{&cmd_zh},      .descr = "scroll one column left"}},
	{WK_z WK_l,  {{&cmd_zl},      .descr = "scroll one column right"}},
	{WK_z WK_t,  {{&cmd_zt},      .descr = "push cursor to the top"}},
	{WK_z WK_z,  {{&cmd_zz},      .descr = "center cursor position"}},
#ifdef ENABLE_EXTENDED_KEYS
	{{K(KEY_PPAGE)},       {{&cmd_ctrl_b}, .descr = "scroll page up"}},
	{{K(KEY_NPAGE)},       {{&cmd_ctrl_f}, .descr = "scroll page down"}},
	{{K(KEY_UP)},          {{&cmd_k},      .descr = "go to item above"}},
	{{K(KEY_DOWN)},        {{&cmd_j},      .descr = "go to item below"}},
	{{K(KEY_RIGHT)},       {{&cmd_return}, .descr = "pick current item"}},
	{{K(KEY_HOME)},        {{&cmd_gg},     .descr = "go to the first item"}},
	{{K(KEY_END)},         {{&cmd_G},      .descr = "go to the last item"}},
	{{WC_z, K(KEY_LEFT)},  {{&cmd_zh},     .descr = "scroll one column left"}},
	{{WC_z, K(KEY_RIGHT)}, {{&cmd_zl},     .descr = "scroll one column right"}},
#endif /* ENABLE_EXTENDED_KEYS */
	{{K(KEY_MOUSE)}, {{&handle_mouse_event}, FOLLOWED_BY_NONE}},
};

/* Specification of builtin commands. */
static const cmd_add_t commands[] = {
	{ .name = "",                  .abbr = NULL,    .id = -1,
	  .descr = "navigate to specific line",
	  .flags = HAS_RANGE,
	  .handler = &goto_cmd,        .min_args = 0,   .max_args = 0, },
	{ .name = "chistory",          .abbr = "chi",   .id = -1,
	  .descr = "display history of menus",
	  .flags = 0,
	  .handler = &chistory_cmd,    .min_args = 0,   .max_args = 0, },
	{ .name = "cnewer",            .abbr = "cnew",  .id = -1,
	  .descr = "load a newer navigation menu",
	  .flags = 0,
	  .handler = &cnewer_cmd,      .min_args = 0,   .max_args = 0, },
	{ .name = "colder",            .abbr = "col",   .id = -1,
	  .descr = "load an older navigation menu",
	  .flags = 0,
	  .handler = &colder_cmd,      .min_args = 0,   .max_args = 0, },
	{ .name = "exit",              .abbr = "exi",   .id = -1,
	  .descr = "exit the menu",
	  .flags = 0,
	  .handler = &quit_cmd,        .min_args = 0,   .max_args = 0, },
	{ .name = "find",              .abbr = "fin",   .id = COM_FIND,
	  .descr = "query find results",
	  .flags = HAS_QUOTED_ARGS | HAS_MACROS_FOR_CMD,
	  .handler = &find_cmd,        .min_args = 0,   .max_args = NOT_DEF, },
	{ .name = "grep",              .abbr = "gr",    .id = COM_GREP,
	  .descr = "query grep results",
	  .flags = HAS_EMARK,
	  .handler = &grep_cmd,        .min_args = 0,   .max_args = NOT_DEF, },
	{ .name = "nohlsearch",        .abbr = "noh",   .id = -1,
	  .descr = "reset highlighting of search matches",
	  .flags = 0,
	  .handler = &nohlsearch_cmd,  .min_args = 0,   .max_args = 0, },
	{ .name = "quit",              .abbr = "q",     .id = -1,
	  .descr = "exit the menu",
	  .flags = 0,
	  .handler = &quit_cmd,        .min_args = 0,   .max_args = 0, },
	{ .name = "write",             .abbr = "w",     .id = COM_MENU_WRITE,
	  .descr = "write all menu lines into a file",
	  .flags = HAS_QUOTED_ARGS,
	  .handler = &write_cmd,       .min_args = 1,   .max_args = 1, },
	{ .name = "xit",               .abbr = "x",     .id = -1,
	  .descr = "exit the menu",
	  .flags = 0,
	  .handler = &quit_cmd,        .min_args = 0,   .max_args = 0, },
};

/* Settings for the cmds unit. */
static cmds_conf_t cmds_conf = {
	.complete_line = &complete_line_in_menu,
	.complete_args = &complete_args,
	.swap_range = &swap_range,
	.resolve_mark = &resolve_mark,
	.expand_macros = &menu_expand_macros,
	.expand_envvars = &menu_expand_envvars,
	.post = &post,
	.select_range = &menu_select_range,
	.skip_at_beginning = &skip_at_beginning,
};

/* Completes whole command-line.  Returns completion offset. */
static int
complete_line_in_menu(const char cmd_line[], void *extra_arg)
{
	return 0;
}

static int
swap_range(void)
{
	return 0;
}

static int
resolve_mark(char mark)
{
	return -1;
}

/* Implementation of macros expansion callback for cmds unit.  Returns newly
 * allocated memory. */
static char *
menu_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;
}

static char *
menu_expand_envvars(const char *str)
{
	return strdup(str);
}

static void
post(int id)
{
}

static void
menu_select_range(int id, const cmd_info_t *cmd_info)
{
}

static int
skip_at_beginning(int id, const char *args)
{
	return -1;
}

void
modmenu_init(void)
{
	int ret_code;

	ret_code = vle_keys_add(builtin_cmds, ARRAY_LEN(builtin_cmds), MENU_MODE);
	assert(ret_code == 0);

	(void)ret_code;

	vle_keys_set_def_handler(MENU_MODE, &key_handler);

	/* Double initialization can happen in tests. */
	if(cmds_conf.inner == NULL)
	{
		vle_cmds_init(0, &cmds_conf);
		vle_cmds_add(commands, ARRAY_LEN(commands));
	}
}

static int
key_handler(wchar_t key)
{
	const wchar_t shortcut[] = {key, L'\0'};

	if(pass_combination_to_khandler(shortcut) && menu->len == 0)
	{
		show_error_msg("No more items in the menu", "Menu will be closed");
		leave_menu_mode(1);
	}

	return 0;
}

void
modmenu_enter(menu_data_t *m, view_t *target_view)
{
	if(curr_stats.load_stage >= 0 && curr_stats.load_stage < 2)
	{
		return;
	}

	assert(m->len > 0 && "Menu cannot be empty.");

	ui_hide_graphics();
	werase(status_bar);

	view = target_view;
	menu = m;
	vle_mode_set(MENU_MODE, VMT_PRIMARY);
	stats_refresh_later();
	was_redraw = 0;

	vle_cmds_init(0, &cmds_conf);
}

void
modmenu_reenter(view_t *target_view)
{
	assert(vle_mode_is(MENU_MODE) && "Menu mode must be active!");
	view = target_view;
}

void
modmenu_abort(void)
{
	leave_menu_mode(1);
}

void
modmenu_pre(void)
{
	touchwin(ruler_win);
	ui_refresh_win(ruler_win);
}

void
modmenu_post(void)
{
	if(stats_update_fetch() != UT_NONE)
	{
		modmenu_full_redraw();
	}
	ui_sb_msg(curr_stats.save_msg ? NULL : "");
}

void
modmenu_full_redraw(void)
{
	was_redraw = 1;
	menus_full_redraw(menu->state);
}

static void
cmd_ctrl_b(key_info_t key_info, keys_info_t *keys_info)
{
	if(can_scroll_menu_up(menu))
	{
		const int s = get_effective_menu_scroll_offset(menu);
		const int off = (getmaxy(menu_win) - 2) - SCROLL_GAP;
		menu->pos = modmenu_last_line(menu) - off;
		change_menu_top(menu, -off);
		if(cfg.scroll_off > 0 &&
				menu->top + (getmaxy(menu_win) - 3) - menu->pos < s)
		{
			menu->pos -= s - (menu->top + (getmaxy(menu_win) - 3) - menu->pos);
		}

		modmenu_partial_redraw();
	}
}

/* Returns non-zero if menu can be scrolled up. */
static int
can_scroll_menu_up(const menu_data_t *menu)
{
	return menu->top > 0;
}

static void
cmd_ctrl_c(key_info_t key_info, keys_info_t *keys_info)
{
	leave_menu_mode(1);
}

static void
cmd_ctrl_d(key_info_t key_info, keys_info_t *keys_info)
{
	const int s = get_effective_menu_scroll_offset(menu);
	menus_erase_current(menu->state);
	menu->top += DIV_ROUND_UP(getmaxy(menu_win) - 3, 2);
	menu->pos += DIV_ROUND_UP(getmaxy(menu_win) - 3, 2);
	if(cfg.scroll_off > 0 && menu->pos - menu->top < s)
	{
		menu->pos += s - (menu->pos - menu->top);
	}

	modmenu_partial_redraw();
}

static void
cmd_ctrl_e(key_info_t key_info, keys_info_t *keys_info)
{
	if(can_scroll_menu_down(menu))
	{
		int off = MAX(cfg.scroll_off, 0);
		if(menu->pos <= menu->top + off)
		{
			menu->pos = menu->top + 1 + off;
		}

		++menu->top;
		modmenu_partial_redraw();
	}
}

static void
cmd_ctrl_f(key_info_t key_info, keys_info_t *keys_info)
{
	if(can_scroll_menu_down(menu))
	{
		const int s = get_effective_menu_scroll_offset(menu);
		const int off = (getmaxy(menu_win) - 2) - SCROLL_GAP;
		menu->pos = menu->top + off;
		change_menu_top(menu, off);
		if(cfg.scroll_off > 0 && menu->pos - menu->top < s)
		{
			menu->pos += s - (menu->pos - menu->top);
		}

		modmenu_partial_redraw();
	}
}

/* Returns non-zero if menu can be scrolled down. */
static int
can_scroll_menu_down(const menu_data_t *menu)
{
	return modmenu_last_line(menu) < menu->len - 1;
}

/* Moves top line of the menu ensuring that its value is correct. */
static void
change_menu_top(menu_data_t *menu, int delta)
{
	menu->top =
		MAX(MIN(menu->top + delta, menu->len - (getmaxy(menu_win) - 2)), 0);
}

int
modmenu_last_line(const menu_data_t *menu)
{
	return menu->top + (getmaxy(menu_win) - 2) - 1;
}

/* Redraw TUI. */
static void
cmd_ctrl_l(key_info_t key_info, keys_info_t *keys_info)
{
	modmenu_full_redraw();
}

static void
cmd_return(key_info_t key_info, keys_info_t *keys_info)
{
	static menu_data_t *saved_menu;

	saved_menu = menu;
	if(!menu->menu_context)
	{
		vle_mode_set(NORMAL_MODE, VMT_PRIMARY);
	}

	if(menu->execute_handler != NULL && menu->execute_handler(view, menu))
	{
		vle_mode_set(MENU_MODE, VMT_PRIMARY);
		modmenu_full_redraw();
		return;
	}

	if(!vle_mode_is(MENU_MODE))
	{
		menus_reset_data(saved_menu);
	}
	else if(menu != saved_menu)
	{
		menus_reset_data(saved_menu);
		modmenu_partial_redraw();
	}

	update_ui_on_leaving();
}

/* Updates UI on leaving the mode trying to minimize efforts to do this. */
static void
update_ui_on_leaving(void)
{
	if(was_redraw)
	{
		update_screen(UT_FULL);
	}
	else
	{
		ui_view_title_update(curr_view);
		update_all_windows();
	}
}

static void
cmd_ctrl_u(key_info_t key_info, keys_info_t *keys_info)
{
	const int s = get_effective_menu_scroll_offset(menu);
	menus_erase_current(menu->state);

	if(cfg.scroll_off > 0 && menu->top + getmaxy(menu_win) - menu->pos < s)
	{
		menu->pos -= s - (menu->top + (getmaxy(menu_win) - 3) - menu->pos);
	}

	menu->top -= DIV_ROUND_UP(getmaxy(menu_win) - 3, 2);
	if(menu->top < 0)
	{
		menu->top = 0;
	}
	menu->pos -= DIV_ROUND_UP(getmaxy(menu_win) - 3, 2);

	modmenu_partial_redraw();
}

/* Returns scroll offset value for the menu taking menu height into account. */
static int
get_effective_menu_scroll_offset(const menu_data_t *menu)
{
	return MIN(DIV_ROUND_UP(getmaxy(menu_win) - 3, 2) - 1, cfg.scroll_off);
}

static void
cmd_ctrl_y(key_info_t key_info, keys_info_t *keys_info)
{
	if(can_scroll_menu_up(menu))
	{
		int off = MAX(cfg.scroll_off, 0);
		if(menu->pos >= menu->top + getmaxy(menu_win) - 3 - off)
		{
			menu->pos = menu->top - 1 + getmaxy(menu_win) - 3 - off;
		}

		--menu->top;
		modmenu_partial_redraw();
	}
}

static void
cmd_slash(key_info_t key_info, keys_info_t *keys_info)
{
	last_search_backward = 0;
	menus_search_reset(menu->state, last_search_backward,
			def_count(key_info.count));
	modcline_in_menu(CLS_MENU_FSEARCH, /*initial=*/"", menu);
}

/* Jump to percent of list. */
static void
cmd_percent(key_info_t key_info, keys_info_t *keys_info)
{
	int line;
	if(key_info.count == NO_COUNT_GIVEN || key_info.count > 100)
	{
		return;
	}
	line = (key_info.count*menu->len)/100;
	menus_set_pos(menu->state, line);
	ui_refresh_win(menu_win);
}

static void
cmd_colon(key_info_t key_info, keys_info_t *keys_info)
{
	cmds_conf.begin = 1;
	cmds_conf.current = menu->pos;
	cmds_conf.end = menu->len;
	modcline_in_menu(CLS_MENU_COMMAND, /*initial=*/"", menu);
}

static void
cmd_qmark(key_info_t key_info, keys_info_t *keys_info)
{
	last_search_backward = 1;
	menus_search_reset(menu->state, last_search_backward,
			def_count(key_info.count));
	modcline_in_menu(CLS_MENU_BSEARCH, /*initial=*/"", menu);
}

/* Populates very custom (unsorted) view with list of files. */
static void
cmd_B(key_info_t key_info, keys_info_t *keys_info)
{
	dump_into_custom_view(1);
}

static void
cmd_G(key_info_t key_info, keys_info_t *keys_info)
{
	if(key_info.count == NO_COUNT_GIVEN)
	{
		key_info.count = menu->len;
	}

	menus_erase_current(menu->state);
	menus_set_pos(menu->state, key_info.count - 1);
	ui_refresh_win(menu_win);
}

static void
cmd_H(key_info_t key_info, keys_info_t *keys_info)
{
	int top;
	int off = MAX(cfg.scroll_off, 0);
	if(off > getmaxy(menu_win)/2)
	{
		return;
	}

	top = (menu->top == 0) ? 0 : (menu->top + off);

	menus_erase_current(menu->state);
	menus_set_pos(menu->state, top);
	ui_refresh_win(menu_win);
}

static void
cmd_L(key_info_t key_info, keys_info_t *keys_info)
{
	int top;
	int off;
	if(menu->key_handler != NULL)
	{
		if(pass_combination_to_khandler(L"L"))
		{
			return;
		}
	}

	off = MAX(cfg.scroll_off, 0);
	if(off > getmaxy(menu_win)/2)
	{
		return;
	}

	top = menu->top + getmaxy(menu_win);
	if(menu->top + getmaxy(menu_win) < menu->len - 1)
	{
		top -= off;
	}

	menus_erase_current(menu->state);
	menus_set_pos(menu->state, top - 3);
	ui_refresh_win(menu_win);
}

/* Moves cursor to the middle of the window. */
static void
cmd_M(key_info_t key_info, keys_info_t *keys_info)
{
	int new_pos;
	if(menu->len < getmaxy(menu_win))
	{
		new_pos = DIV_ROUND_UP(menu->len, 2);
	}
	else
	{
		new_pos = menu->top + DIV_ROUND_UP(getmaxy(menu_win) - 3, 2);
	}

	menus_erase_current(menu->state);
	menus_set_pos(menu->state, MAX(0, new_pos - 1));
	ui_refresh_win(menu_win);
}

static void
cmd_N(key_info_t key_info, keys_info_t *keys_info)
{
	key_info.count = def_count(key_info.count);
	while(key_info.count-- > 0)
	{
		menus_search_repeat(menu->state, !last_search_backward);
	}
}

/* Populates custom view with list of files. */
static void
cmd_b(key_info_t key_info, keys_info_t *keys_info)
{
	dump_into_custom_view(0);
}

/* Makes custom view of specified type out of menu items. */
static void
dump_into_custom_view(int very)
{
	if(menus_to_custom_view(menu->state, very) != 0)
	{
		show_error_msg("Menu transformation",
				"No valid paths discovered in menu content");
		return;
	}

	leave_menu_mode(1);
}

static void
cmd_dd(key_info_t key_info, keys_info_t *keys_info)
{
	if(pass_combination_to_khandler(L"dd") && menu->len == 0)
	{
		show_error_msg("Menu is closing", "No more items in the menu");
		leave_menu_mode(1);
	}
}

/* Passes "gf" shortcut to menu as otherwise the shortcut is not available. */
static void
cmd_gf(key_info_t key_info, keys_info_t *keys_info)
{
	(void)pass_combination_to_khandler(L"gf");
}

/* Gives menu-specific keyboard routine to process the shortcut.  Returns zero
 * if the shortcut wasn't processed, otherwise non-zero is returned. */
static int
pass_combination_to_khandler(const wchar_t keys[])
{
	KHandlerResponse handler_response;

	if(menu->key_handler == NULL)
	{
		return 0;
	}

	handler_response = menu->key_handler(view, menu, keys);

	switch(handler_response)
	{
		case KHR_REFRESH_WINDOW:
			ui_refresh_win(menu_win);
			return 1;
		case KHR_CLOSE_MENU:
			leave_menu_mode(1);
			return 1;
		case KHR_MORPHED_MENU:
			assert(!vle_mode_is(MENU_MODE) && "Wrong use of KHR_MORPHED_MENU.");
			return 1;
		case KHR_UNHANDLED:
			return 0;

		default:
			assert(0 && "Unknown menu-specific keyboard handler response.");
			return 0;
	}
}

static void
cmd_gg(key_info_t key_info, keys_info_t *keys_info)
{
	menus_erase_current(menu->state);
	menus_set_pos(menu->state, def_count(key_info.count) - 1);
	ui_refresh_win(menu_win);
}

static void
cmd_j(key_info_t key_info, keys_info_t *keys_info)
{
	if(menu->pos != menu->len - 1)
	{
		menus_erase_current(menu->state);
		menu->pos += def_count(key_info.count);
		menus_set_pos(menu->state, menu->pos);
		ui_refresh_win(menu_win);
	}
}

static void
cmd_k(key_info_t key_info, keys_info_t *keys_info)
{
	if(menu->pos != 0)
	{
		menus_erase_current(menu->state);
		menu->pos -= def_count(key_info.count);
		menus_set_pos(menu->state, menu->pos);
		ui_refresh_win(menu_win);
	}
}

static void
cmd_n(key_info_t key_info, keys_info_t *keys_info)
{
	key_info.count = def_count(key_info.count);
	while(key_info.count-- > 0)
	{
		menus_search_repeat(menu->state, last_search_backward);
	}
}

/* Handles current content of the menu to Vim as quickfix list. */
static void
cmd_v(key_info_t key_info, keys_info_t *keys_info)
{
	FILE *vim_stdin;
	char *cmd;
	int i;
	int qf = 1;

	int bg;
	const char *vi_cmd = cfg_get_vicmd(&bg);

	/* If both first and last lines do not contain colons, treat lines as list of
	 * file names. */
	if(strchr(menu->items[0], ':') == NULL &&
			strchr(menu->items[menu->len - 1], ':') == NULL)
	{
		qf = 0;
	}

	if(vlua_handler_cmd(curr_stats.vlua, vi_cmd))
	{
		if(vlua_edit_list(curr_stats.vlua, vi_cmd, menu->items, menu->len,
					menu->pos, qf) != 0)
		{
			show_error_msg("List View", "Failed to view list via handler");
		}
		return;
	}

	ui_shutdown();
	stats_refresh_later();

	if(!qf)
	{
		ShellType shell_type = (get_env_type() == ET_UNIX ? ST_POSIX : ST_CMD);
		char *const arg = shell_arg_escape("+exe 'bd!|args' "
				"join(map(getline('1','$'),'fnameescape(v:val)'))",
				shell_type);
		cmd = format_str("%s %s +argument%d -", vi_cmd, arg, menu->pos + 1);
		free(arg);
	}
	else if(menu->pos == 0)
	{
		/* For some reason +cc1 causes noisy messages on status line, so handle this
		 * case separately. */
		cmd = format_str("%s +cgetbuffer +bd! +cfirst -", vi_cmd);
	}
	else
	{
		cmd = format_str("%s +cgetbuffer +bd! +cfirst +cc%d -", vi_cmd,
				menu->pos + 1);
	}

	vim_stdin = popen(cmd, "w");
	free(cmd);

	if(vim_stdin == NULL)
	{
		recover_after_shellout();
		show_error_msg("Vim QuickFix", "Failed to send list of files to editor.");
		return;
	}

	for(i = 0; i < menu->len; ++i)
	{
		fputs(menu->items[i], vim_stdin);
		putc('\n', vim_stdin);
	}

	pclose(vim_stdin);
	recover_after_shellout();
}

static void
cmd_zb(key_info_t key_info, keys_info_t *keys_info)
{
	if(can_scroll_menu_up(menu))
	{
		if(menu->pos < getmaxy(menu_win))
		{
			menu->top = 0;
		}
		else
		{
			menu->top = menu->pos - (getmaxy(menu_win) - 3);
		}
		modmenu_partial_redraw();
	}
}

static void
cmd_zH(key_info_t key_info, keys_info_t *keys_info)
{
	if(menu->hor_pos != 0)
	{
		menu->hor_pos = MAX(0, menu->hor_pos - (getmaxx(menu_win) - 4));
		modmenu_partial_redraw();
	}
}

static void
cmd_zL(key_info_t key_info, keys_info_t *keys_info)
{
	menu->hor_pos += getmaxx(menu_win) - 4;
	modmenu_partial_redraw();
}

static void
cmd_zh(key_info_t key_info, keys_info_t *keys_info)
{
	if(menu->hor_pos != 0)
	{
		menu->hor_pos = MAX(0, menu->hor_pos - def_count(key_info.count));
		modmenu_partial_redraw();
	}
}

static void
cmd_zl(key_info_t key_info, keys_info_t *keys_info)
{
	menu->hor_pos += def_count(key_info.count);
	modmenu_partial_redraw();
}

static void
cmd_zt(key_info_t key_info, keys_info_t *keys_info)
{
	if(can_scroll_menu_down(menu))
	{
		if(menu->len - menu->pos >= getmaxy(menu_win) - 3 + 1)
			menu->top = menu->pos;
		else
			menu->top = menu->len - (getmaxy(menu_win) - 3 + 1);
		modmenu_partial_redraw();
	}
}

static void
cmd_zz(key_info_t key_info, keys_info_t *keys_info)
{
	if(!all_lines_visible(menu))
	{
		if(menu->pos <= (getmaxy(menu_win) - 3)/2)
			menu->top = 0;
		else if(menu->pos > menu->len - DIV_ROUND_UP(getmaxy(menu_win) - 3, 2))
			menu->top = menu->len - (getmaxy(menu_win) - 3 + 1);
		else
			menu->top = menu->pos - DIV_ROUND_UP(getmaxy(menu_win) - 3, 2);

		modmenu_partial_redraw();
	}
}

/* Returns non-zero if all menu lines are visible, so no scrolling is needed. */
static int
all_lines_visible(const menu_data_t *menu)
{
	return menu->len <= getmaxy(menu_win) - 2;
}

void
modmenu_partial_redraw(void)
{
	menus_partial_redraw(menu->state);
	menus_set_pos(menu->state, menu->pos);
	ui_refresh_win(menu_win);
}

static int
goto_cmd(const cmd_info_t *cmd_info)
{
	if(cmd_info->end != NOT_DEF)
	{
		menus_erase_current(menu->state);
		menus_set_pos(menu->state, cmd_info->end);
		ui_refresh_win(menu_win);
	}
	return 0;
}

/* Displays list of remembered menus. */
static int
chistory_cmd(const cmd_info_t *cmd_info)
{
	/* Make sure current menu is stored to the stash before displaying the
	 * stash. */
	menus_put_on_stash(menu->state);

	return show_chistory_menu(view) != 0;
}

/* Loads a newer navigation menu if there is one. */
static int
cnewer_cmd(const cmd_info_t *cmd_info)
{
	if(menus_unstash_newer(menu->state) != 0)
	{
		ui_sb_err("There is no newer menu");
		return CMDS_ERR_CUSTOM;
	}
	return 0;
}

/* Loads an older navigation menu if there is one. */
static int
colder_cmd(const cmd_info_t *cmd_info)
{
	if(menus_unstash_older(menu->state) != 0)
	{
		ui_sb_err("There is no older menu");
		return CMDS_ERR_CUSTOM;
	}
	return 0;
}

/* Looks for files matching a pattern. */
static int
find_cmd(const cmd_info_t *cmd_info)
{
	return act_find(cmd_info->args, cmd_info->argc, cmd_info->argv);
}

/* Handles :grep command with possible inversion of matches. */
static int
grep_cmd(const cmd_info_t *cmd_info)
{
	return act_grep(cmd_info->argc > 0 ? cmd_info->args : NULL, cmd_info->emark);
}

/* Disables highlight of search result matches. */
static int
nohlsearch_cmd(const cmd_info_t *cmd_info)
{
	menus_search_reset_hilight(menu->state);
	return 0;
}

static int
quit_cmd(const cmd_info_t *cmd_info)
{
	leave_menu_mode(1);
	return 0;
}

/* Writes all menu lines into file specified as argument. */
static int
write_cmd(const cmd_info_t *cmd_info)
{
	char *const no_tilde = expand_tilde(cmd_info->argv[0]);
	if(write_file_of_lines(no_tilde, menu->items, menu->len) != 0)
	{
		show_error_msg("Failed to open output file", strerror(errno));
	}
	free(no_tilde);
	return 0;
}

void
modmenu_save_pos(void)
{
	saved_top = menu->top;
	saved_pos = menu->pos;
}

void
modmenu_restore_pos(void)
{
	menu->top = saved_top;
	menu->pos = saved_pos;
}

int
modmenu_get_ruler_width(void)
{
	assert(vle_mode_is(MENU_MODE) && "Menu mode must be active!");
	return menus_get_ruler_width(menu);
}

void
modmenu_morph_into_cline(CmdLineSubmode submode, const char input[],
		int external)
{
	/* input might point to part of menu data. */
	char *input_copy;

	if(input[0] == '\0')
	{
		show_error_msg("Command insertion", "Ignoring empty command");
		return;
	}

	input_copy = external ? format_str("!%s", input) : strdup(input);
	if(input_copy == NULL)
	{
		show_error_msg("Error", "Not enough memory");
		return;
	}

	if(submode == CLS_MENU_COMMAND)
	{
		modcline_in_menu(submode, input_copy, menu);
	}
	else
	{
		leave_menu_mode(0);
		modcline_enter(submode, input_copy);
	}

	free(input_copy);
}

/* Leaves menu mode, possibly resetting selection.  Does nothing if current mode
 * isn't menu mode. */
static void
leave_menu_mode(int reset_selection)
{
	/* Some menu implementation could have switched mode from one of handlers. */
	if(!vle_mode_is(MENU_MODE))
	{
		return;
	}

	menus_reset_data(menu);

	if(reset_selection)
	{
		flist_sel_stash(view);
		redraw_view(view);
	}

	vle_mode_set(NORMAL_MODE, VMT_PRIMARY);

	update_ui_on_leaving();
}

void
modmenu_run_command(const char cmd[])
{
	if(cmds_dispatch1(cmd, view, CIT_COMMAND) < 0)
	{
		ui_sb_err("An error occurred while trying to execute command");
	}
	vle_cmds_init(0, &cmds_conf);
}

/* Provides access to tests to a local static variable. */
TSTATIC menu_data_t *
menu_get_current(void)
{
	return menu;
}

/* Provides access to a static variable from tests. */
TSTATIC view_t *
menu_get_view(void)
{
	return view;
}

/* Processes events from the mouse. */
static void
handle_mouse_event(key_info_t key_info, keys_info_t *keys_info)
{
	MEVENT e;
	if(ui_get_mouse(&e) != OK)
	{
		return;
	}

	if(!wenclose(menu_win, e.y, e.x))
	{
		return;
	}

	if(e.bstate & BUTTON1_PRESSED)
	{
		wmouse_trafo(menu_win, &e.y, &e.x, FALSE);

		int old_pos = menu->pos;

		menus_erase_current(menu->state);
		menus_set_pos(menu->state, menu->top + e.y - 1);
		ui_refresh_win(menu_win);

		if(menu->pos == old_pos)
		{
			cmd_return(key_info, keys_info);
		}
	}
	else if(e.bstate & BUTTON4_PRESSED)
	{
		cmd_ctrl_y(key_info, keys_info);
	}
	else if(e.bstate & (BUTTON2_PRESSED | BUTTON5_PRESSED))
	{
		cmd_ctrl_e(key_info, keys_info);
	}
}

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