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

#include <curses.h> /* mvwaddstr() */
#include <unistd.h> /* usleep() */

#include <limits.h> /* INT_MAX */
#include <stddef.h> /* NULL size_t */
#include <stdio.h> /* FILE SEEK_SET fclose() fdopen() feof() fseek()
                      tmpfile() */
#include <stdlib.h> /* free() */
#include <string.h> /* strcat() strlen() strncat() */

#include "../cfg/config.h"
#include "../compat/fs_limits.h"
#include "../compat/os.h"
#include "../engine/mode.h"
#include "../lua/vlua.h"
#include "../modes/dialogs/msg_dialog.h"
#include "../modes/modes.h"
#include "../modes/view.h"
#include "../utils/filemon.h"
#include "../utils/fs.h"
#include "../utils/path.h"
#include "../utils/str.h"
#include "../utils/string_array.h"
#include "../utils/utf8.h"
#include "../utils/utils.h"
#include "../filelist.h"
#include "../filetype.h"
#include "../macros.h"
#include "../status.h"
#include "../types.h"
#include "../vcache.h"
#include "cancellation.h"
#include "color_scheme.h"
#include "colors.h"
#include "escape.h"
#include "fileview.h"
#include "statusbar.h"
#include "ui.h"

/* Maximum number of lines used for preview. */
enum { MAX_PREVIEW_LINES = 256 };

/* Cached information about a single file's preview. */
typedef struct
{
	char *path;        /* Full path to the file. */
	char *viewer;      /* Viewer of the file. */
	filemon_t filemon; /* Timestamp for the file. */
	preview_area_t pa; /* Where preview is being drawn. */
	strlist_t lines;   /* Lines of graphics (not pass through. */
	int beg_x;         /* Original x coordinate of host window. */
	int beg_y;         /* Original y coordinate of host window. */
	ViewerKind kind;   /* Kind of preview. */
	int max_lines;     /* Maximum number of lines requested last time. */
	int graphics_lost; /* Whether graphics was invalidated on the screen. */
}
quickview_cache_t;

/* State of directory tree print functions. */
typedef struct
{
	FILE *fp;          /* Output preview stream. */
	int n;             /* Current line number (zero based). */
	int ndirs;         /* Number of seen directories. */
	int nfiles;        /* Number of seen files. */
	int max;           /* Maximum line number. */
	int full_stats;    /* Collect statistics for the whole tree. */
	int depth;         /* Current depth of the traversal. */
	char prefix[4096]; /* Prefix character for each tree level. */
}
tree_print_state_t;

static const char * view_entry(const dir_entry_t *entry,
		const preview_area_t *parea, quickview_cache_t *cache);
static const char * view_file(const char path[], const preview_area_t *parea,
		quickview_cache_t *cache);
static int is_cache_valid(const quickview_cache_t *cache, const char path[],
		const char viewer[], const preview_area_t *parea);
static void update_cache(quickview_cache_t *cache, const char path[],
		const char viewer[], ViewerKind kind, const preview_area_t *parea,
		int max_lines);
static strlist_t get_lines(const quickview_cache_t *cache);
static void print_tree_stats(tree_print_state_t *s);
static int print_dir_tree(tree_print_state_t *s, const char path[], int last);
static void collect_subtree_stats(tree_print_state_t *s, const char path[]);
static int enter_dir(tree_print_state_t *s, const char path[], int last);
static int visit_file(tree_print_state_t *s, const char path[], int last);
static int visit_link(tree_print_state_t *s, const char path[], int last,
		const char target[]);
static void leave_dir(tree_print_state_t *s);
static void indent_prefix(tree_print_state_t *s);
static void unindent_prefix(tree_print_state_t *s);
static void set_prefix_char(tree_print_state_t *s, char c);
static void print_tree_entry(tree_print_state_t *s, const char path[],
		int end_line);
static void print_entry_prefix(tree_print_state_t *s);
static void draw_lines(const strlist_t *lines, int wrapped,
		const preview_area_t *parea, ViewerKind kind);
static void write_message(const char msg[], const preview_area_t *parea);
static void cleanup_for_text(const preview_area_t *parea);
static void wipe_area(const preview_area_t *parea);
static void fill_area(const preview_area_t *parea);

/* Cached preview data for a single file entry. */
static quickview_cache_t qv_cache;

int
qv_ensure_is_shown(void)
{
	if(!curr_stats.preview.on && !qv_can_show())
	{
		return 1;
	}
	stats_set_quickview(1);
	return 0;
}

int
qv_can_show(void)
{
	if(curr_stats.number_of_windows == 1)
	{
		ui_sb_err("Cannot view files in one window mode");
		return 0;
	}
	if(other_view->explore_mode)
	{
		ui_sb_err("Other view is already used for file viewing");
		return 0;
	}
	return 1;
}

void
qv_toggle(void)
{
	if(!curr_stats.preview.on)
	{
		stats_set_quickview(1);
		stats_redraw_later();
		return;
	}

	stats_set_quickview(0);

	if(ui_view_is_visible(other_view))
	{
		/* Force cleaning possible leftovers of graphics, otherwise curses internal
		 * structures don't know that those parts need to be redrawn on the
		 * screen. */
		if(curr_stats.preview.cleanup_cmd != NULL ||
				curr_stats.preview.kind != VK_TEXTUAL)
		{
			qv_cleanup(other_view, curr_stats.preview.cleanup_cmd);
		}

		draw_dir_list(other_view);
		refresh_view_win(other_view);
	}

	update_string(&curr_stats.preview.cleanup_cmd, NULL);
	curr_stats.preview.kind = VK_TEXTUAL;
	qv_ui_updated();
}

void
qv_draw(view_t *view)
{
	const dir_entry_t *curr;

	if(curr_stats.load_stage < 2 || curr_stats.number_of_windows == 1 ||
	   vle_mode_is(VIEW_MODE))
	{
		return;
	}

	/* Viewers might use relative path, so try to make sure that we're at correct
	 * location. */
	(void)vifm_chdir(flist_get_dir(view));

	if(modview_detached_draw())
	{
		/* View mode handled the drawing. */
		return;
	}

	ui_view_erase(other_view, 1);

	curr = get_current_entry(view);
	if(!fentry_is_fake(curr))
	{
		const preview_area_t parea = {
			.source = view,
			.view = other_view,
			.def_col = cfg.cs.color[WIN_COLOR],
			.x = ui_qv_left(other_view),
			.y = ui_qv_top(other_view),
			.w = ui_qv_width(other_view),
			.h = ui_qv_height(other_view),
		};
		(void)view_entry(curr, &parea, &qv_cache);
	}

	refresh_view_win(other_view);
	ui_view_title_update(other_view);
}

const char *
qv_draw_on(const dir_entry_t *entry, const preview_area_t *parea)
{
	static quickview_cache_t lwin_cache, rwin_cache;

	ui_set_attr(parea->view->win, &parea->def_col, -1);
	fill_area(parea);

	quickview_cache_t *cache = (parea->view == &lwin ? &lwin_cache : &rwin_cache);

	const char *clear_cmd = view_entry(entry, parea, cache);

	parea->view->displays_graphics = (cache->kind != VK_TEXTUAL);

	/* Unconditionally invalidate graphics cache, since we don't keep track of its
	 * validity in any way. */
	cache->graphics_lost = 1;

	return clear_cmd;
}

/* Draws preview of the entry in the other view.  Returns preview clear command
 * or NULL. */
static const char *
view_entry(const dir_entry_t *entry, const preview_area_t *parea,
		quickview_cache_t *cache)
{
	char path[PATH_MAX + 1];
	qv_get_path_to_explore(entry, path, sizeof(path));

	FileType type = entry->type;

	struct stat st;
	if(os_stat(path, &st) == 0)
	{
		type = get_type_from_mode(st.st_mode);
	}

	switch(type)
	{
		case FT_CHAR_DEV:
			write_message("File is a Character Device", parea);
			break;
		case FT_BLOCK_DEV:
			write_message("File is a Block Device", parea);
			break;
#ifndef _WIN32
		case FT_SOCK:
			write_message("File is a Socket", parea);
			break;
#endif
		case FT_FIFO:
			write_message("File is a Named Pipe", parea);
			break;
		case FT_LINK:
			if(get_link_target_abs(path, entry->origin, path, sizeof(path)) != 0)
			{
				write_message("Cannot resolve Link", parea);
				break;
			}

			if(!path_exists(path, DEREF))
			{
				char *msg = format_str("Cannot access link's target: %s", path);
				write_message(msg, parea);
				free(msg);
				break;
			}

			if(!ends_with_slash(path) && is_dir(path))
			{
				strncat(path, "/", sizeof(path) - strlen(path) - 1);
			}
			/* break is omitted intentionally. */
		case FT_UNK:
		default:
			return view_file(path, parea, cache);
	}

	return NULL;
}

/* Displays contents of file or output of its viewer in the other pane
 * starting from the second line and second column.  Returns preview clear
 * command or NULL. */
static const char *
view_file(const char path[], const preview_area_t *parea,
		quickview_cache_t *cache)
{
	const char *viewer = qv_get_viewer(path);
	const char *clear_cmd = (viewer != NULL ? ma_get_clear_cmd(viewer) : NULL);

	/* Don't draw graphics while dialog is shown as they aren't combined nicely
	 * on the screen. */
	const ViewerKind kind = ft_viewer_kind(viewer);
	if(kind != VK_TEXTUAL && modes_is_dialog_like())
	{
		return NULL;
	}

	if(is_cache_valid(cache, path, viewer, parea))
	{
		/* Update area as we might draw preview at a different location. */
		cache->pa = *parea;

		draw_lines(&cache->lines, cfg.wrap_quick_view, &cache->pa, cache->kind);
		return clear_cmd;
	}

	int max_lines = is_dir(path) ? ui_qv_height(parea->view)
	                             : MAX_PREVIEW_LINES;

	/* If graphics will be displayed, clear the window and wait a bit to let
	 * terminal emulator do actual refresh (at least some of them need this). */
	if(kind != VK_TEXTUAL)
	{
		qv_cleanup_area(parea, curr_stats.preview.cleanup_cmd);
		usleep(cfg.graphics_delay);
	}
	else
	{
		/* We want to wipe the view if it was displaying graphics, but won't
		 * anymore.  Do this only if we didn't already cleared the window. */
		cleanup_for_text(parea);
	}

	curr_stats.preview.kind = kind;

	update_string(&curr_stats.preview.cleanup_cmd, clear_cmd);

	update_cache(cache, path, viewer, kind, parea, max_lines);
	strlist_t lines = get_lines(cache);
	draw_lines(&lines, cfg.wrap_quick_view, &cache->pa, cache->kind);

	if(cache->kind != VK_TEXTUAL)
	{
		free_string_array(cache->lines.items, cache->lines.nitems);
		cache->lines.items = copy_string_array(lines.items, lines.nitems);
		cache->lines.nitems = lines.nitems;
	}

	return clear_cmd;
}

/* Checks whether data in the cache is up to date with the file on disk.
 * Returns non-zero if so, otherwise zero is returned. */
static int
is_cache_valid(const quickview_cache_t *cache, const char path[],
		const char viewer[], const preview_area_t *parea)
{
	if(cache->kind == VK_TEXTUAL)
	{
		/* This level of cache deals only with graphics and ignores text. */
		return 0;
	}

	int same_viewer = (cache->viewer == NULL && viewer == NULL)
	               || (cache->viewer != NULL && viewer != NULL &&
	                   strcmp(cache->viewer, viewer) == 0);

	filemon_t filemon;
	if(same_viewer &&
			filemon_from_file(path, FMT_MODIFIED, &filemon) == 0 &&
			cache->path != NULL &&
			paths_are_equal(cache->path, path) &&
			filemon_equal(&cache->filemon, &filemon))
	{
		return !cache->graphics_lost
		    && cache->pa.h == parea->h
		    && cache->pa.w == parea->w
		    && cache->beg_x + cache->pa.x == getbegx(parea->view->win) + parea->x
		    && cache->beg_y + cache->pa.y == getbegy(parea->view->win) + parea->y;
	}

	return 0;
}

/* Updates cache data. */
static void
update_cache(quickview_cache_t *cache, const char path[], const char viewer[],
		ViewerKind kind, const preview_area_t *parea, int max_lines)
{
	(void)filemon_from_file(path, FMT_MODIFIED, &cache->filemon);

	replace_string(&cache->path, path);
	update_string(&cache->viewer, viewer);

	cache->pa = *parea;
	cache->beg_x = getbegx(parea->view->win);
	cache->beg_y = getbegy(parea->view->win);
	cache->kind = kind;
	cache->max_lines = max_lines;
	cache->graphics_lost = 0;
}

/* Retrieves lines to be printed.  Returns the lines. */
static strlist_t
get_lines(const quickview_cache_t *cache)
{
	view_t *curr = curr_view;
	curr_view = cache->pa.source;
	curr_stats.preview_hint = &cache->pa;

	const char *error;

	MacroFlags flags = MF_NONE;
	char *expanded = (cache->viewer == NULL)
	               ? NULL
	               : qv_expand_viewer(cache->pa.source, cache->viewer, &flags);

	strlist_t lines = vcache_lookup(cache->path, expanded, flags, cache->kind,
			cache->max_lines, VC_ASYNC, &error);
	free(expanded);

	if(error != NULL)
	{
		lines.nitems = add_to_string_array(&lines.items, lines.nitems, error);
	}

	curr_stats.preview_hint = NULL;
	curr_view = curr;

	return lines;
}

FILE *
qv_view_dir(const char path[], int max_lines)
{
	enum { NSPACES = 64 };

	FILE *fp = os_tmpfile();
	if(fp == NULL)
	{
		return NULL;
	}

	tree_print_state_t s = {
		.fp = fp,
		/* Increase by one to cause cached data to be recognized as incomplete
		 * when max_lines isn't enough. */
		.max = (max_lines == INT_MAX ? max_lines : max_lines + 1),
		.full_stats = cfg.top_tree_stats,
		.n = (cfg.top_tree_stats ? 2 : 0),
	};

	/* Spare blank line on the top of the view to put the (files, directories)
	 * count in case "toptreestats" option is set. */
	fprintf(fp, "%*s\n", NSPACES, "");

	const int whole_tree = (print_dir_tree(&s, path, 0) == 0 && s.n != 0);
	if(!whole_tree && ui_cancellation_requested())
	{
		fputs("(cancelled)", fp);
	}

	if(s.n == 0)
	{
		fclose(fp);
		return NULL;
	}

	if(cfg.top_tree_stats)
	{
		/* Print count on the top, spare line. */
		fseek(fp, 0, SEEK_SET);
		print_tree_stats(&s);
		fseek(fp, 0, SEEK_SET);
	}
	else
	{
		/* Print count at the bottom and "hide" the spare line away. */
		fputs("\n", fp);
		print_tree_stats(&s);
		fseek(fp, NSPACES + 1, SEEK_SET);
	}

	return fp;
}

/* Prints one-line tree statistics. */
static void
print_tree_stats(tree_print_state_t *s)
{
	fprintf(s->fp, "%s%d director%s, %d file%s\n",
			ui_cancellation_requested() ? "(cancelled)\n" : "",
			s->ndirs, (s->ndirs == 1) ? "y" : "ies",
			s->nfiles, (s->nfiles == 1) ? "" : "s");
}

/* Produces tree preview of the path.  Returns non-zero to request stopping of
 * the traversal, otherwise zero is returned. */
static int
print_dir_tree(tree_print_state_t *s, const char path[], int last)
{
	int len;
	char **lst = list_sorted_files(path, &len);
	if(len < 0)
	{
		return 1;
	}

	if(enter_dir(s, path, last) != 0)
	{
		free_string_array(lst, len);
		collect_subtree_stats(s, path);
		return 1;
	}

	/* No need to check cfg.max_tree_depth for 0, after enter_dir s->depth is
	 * greater than 0. */
	if(s->depth == cfg.max_tree_depth)
	{
		free_string_array(lst, len);
		leave_dir(s);
		return 0;
	}

	int i;
	int reached_limit = 0;
	for(i = 0; i < len && !reached_limit && !ui_cancellation_requested(); ++i)
	{
		const int last_entry = (i == len - 1);
		char *const full_path = format_str("%s/%s", path, lst[i]);

		if(is_dir(full_path))
		{
			++s->ndirs;
		}
		else
		{
			++s->nfiles;
		}

		char link_target[PATH_MAX + 1];
		if(get_link_target(full_path, link_target, sizeof(link_target)) == 0)
		{
			if(visit_link(s, full_path, last_entry, link_target) != 0)
			{
				reached_limit = 1;
			}
		}
		/* If is_dir_empty() returns non-zero then we know that it's a directory
		 * and no additional checks are needed. */
		else if(!is_dir_empty(full_path))
		{
			if(last_entry)
			{
				set_prefix_char(s, '`');
			}
			if(print_dir_tree(s, full_path, last_entry) != 0)
			{
				reached_limit = 1;
			}
		}
		else
		{
			if(visit_file(s, full_path, last_entry) != 0)
			{
				reached_limit = 1;
			}
		}

		free(full_path);
	}

	if(reached_limit && s->full_stats)
	{
		for(; i < len && !ui_cancellation_requested(); ++i)
		{
			char *const full_path = format_str("%s/%s", path, lst[i]);
			if(is_symlink(full_path))
			{
				++s->nfiles;
			}
			else if(is_dir(full_path))
			{
				++s->ndirs;
				collect_subtree_stats(s, full_path);
			}
			else
			{
				++s->nfiles;
			}
			free(full_path);
		}
	}

	free_string_array(lst, len);
	leave_dir(s);

	return reached_limit;
}

/* Collects stats for items of a directory in a much faster way than traversal
 * for printing. */
static void
collect_subtree_stats(tree_print_state_t *s, const char path[])
{
	DIR *dir = os_opendir(path);
	if(dir == NULL)
	{
		return;
	}

	struct dirent *d;
	while((d = os_readdir(dir)) != NULL)
	{
		if(is_builtin_dir(d->d_name))
		{
			continue;
		}

		char *const full_path = format_str("%s/%s", path, d->d_name);
		if(entry_is_dir(full_path, d))
		{
			++s->ndirs;
			collect_subtree_stats(s, full_path);
		}
		else if(is_dirent_targets_dir(full_path, d))
		{
			++s->ndirs;
		}
		else
		{
			++s->nfiles;
		}
		free(full_path);
	}
	os_closedir(dir);
}

/* Handles entering directory on directory tree traversal.  Returns non-zero to
 * request stopping of the traversal, otherwise zero is returned. */
static int
enter_dir(tree_print_state_t *s, const char path[], int last)
{
	print_tree_entry(s, path, 1);

	if(last)
	{
		set_prefix_char(s, ' ');
	}

	indent_prefix(s);
	set_prefix_char(s, '|');

	++s->depth;
	return (++s->n >= s->max);
}

/* Handles visiting file on directory tree traversal.  Returns non-zero to
 * request stopping of the traversal, otherwise zero is returned. */
static int
visit_file(tree_print_state_t *s, const char path[], int last)
{
	set_prefix_char(s, last ? '`' : '|');
	print_tree_entry(s, path, 1);

	return (++s->n >= s->max);
}

/* Handles visiting symbolic link on directory tree traversal.  Returns non-zero
 * to request stopping of the traversal, otherwise zero is returned. */
static int
visit_link(tree_print_state_t *s, const char path[], int last,
		const char target[])
{
	set_prefix_char(s, last ? '`' : '|');
	print_tree_entry(s, path, 0);
	fputs(" -> ", s->fp);
	fputs(target, s->fp);
	fputc('\n', s->fp);

	return (++s->n >= s->max);
}

/* Handles leaving directory on directory tree traversal. */
static void
leave_dir(tree_print_state_t *s)
{
	unindent_prefix(s);
	--s->depth;
}

/* Adds one indentation level for the following elements of tree. */
static void
indent_prefix(tree_print_state_t *s)
{
	if(strlen(s->prefix) + 1U + 1U < sizeof(s->prefix))
	{
		strcat(s->prefix, " ");
	}
}

/* Removes one indentation level for the following elements of tree. */
static void
unindent_prefix(tree_print_state_t *s)
{
	set_prefix_char(s, '\0');
}

/* Sets prefix for current tree level. */
static void
set_prefix_char(tree_print_state_t *s, char c)
{
	const size_t len = strlen(s->prefix);
	if(len >= 1U)
	{
		s->prefix[len - 1U] = c;
	}
}

/* Prints single entry of directory tree. */
static void
print_tree_entry(tree_print_state_t *s, const char path[], int end_line)
{
	print_entry_prefix(s);
	fputs(get_last_path_component(path), s->fp);
	if(is_dir(path) && !ends_with_slash(path))
	{
		fputc('/', s->fp);
	}
	if(end_line)
	{
		fputc('\n', s->fp);
	}
}

/* Prints part of the string to the left of entry name. */
static void
print_entry_prefix(tree_print_state_t *s)
{
	const char *p = s->prefix;

	/* Expand " |`" into "    |   `-- ". */
	while(p[0] != '\0')
	{
		fputc(p[0], s->fp);
		fputs(p[1] == '\0' ? "-- " : "   ", s->fp);
		++p;
	}
}

/* Displays lines in the other pane.  The wrapped parameter determines whether
 * lines should be wrapped. */
static void
draw_lines(const strlist_t *lines, int wrapped, const preview_area_t *parea,
		ViewerKind kind)
{
	if(kind == VK_PASS_THROUGH)
	{
		ui_pass_through(lines, parea->view->win, parea->x, parea->y);
		return;
	}

	const size_t left = parea->x;
	const size_t top = parea->y;
	const size_t max_width = parea->w;
	const size_t max_height = parea->h;

	int line_continued = 0;
	size_t y = top;

	esc_state state;
	esc_state_init(&state, &parea->def_col, COLORS);

	int next_line = 0;
	const char *input_line = (next_line == lines->nitems)
	                       ? NULL
	                       : lines->items[next_line++];
	while(input_line != NULL && y < top + max_height)
	{
		int printed;
		input_line += esc_print_line(input_line, parea->view->win, left, y,
				max_width, 0, !wrapped, &state, &printed);

		y += !wrapped || (!line_continued || printed);
		line_continued = (*input_line != '\0');

		if(y < top + max_height && (!wrapped || !line_continued))
		{
			input_line = (next_line == lines->nitems) ? NULL
			                                          : lines->items[next_line++];
		}
	}
}

/* Writes single line message of error or information kind instead of real
 * preview. */
static void
write_message(const char msg[], const preview_area_t *parea)
{
	cleanup_for_text(parea);

	char *items[] = { (char *)msg };
	const strlist_t lines = { .items = items, .nitems = 1 };
	draw_lines(&lines, 1, parea, VK_TEXTUAL);
}

/* Ensures that area is ready to display regular text. */
static void
cleanup_for_text(const preview_area_t *parea)
{
	if(curr_stats.preview.cleanup_cmd != NULL ||
			curr_stats.preview.kind != VK_TEXTUAL)
	{
		qv_cleanup_area(parea, curr_stats.preview.cleanup_cmd);
	}
	update_string(&curr_stats.preview.cleanup_cmd, NULL);
	curr_stats.preview.kind = VK_TEXTUAL;
}

void
qv_hide(void)
{
	if(curr_stats.preview.on)
	{
		qv_toggle();
	}
	if(lwin.explore_mode)
	{
		modview_quit_exploring(&lwin);
	}
	if(rwin.explore_mode)
	{
		modview_quit_exploring(&rwin);
	}
}

char *
qv_expand_viewer(view_t *view, const char viewer[], MacroFlags *flags)
{
	ma_flags_set(flags, MF_NONE);

	char *result;
	if(strchr(viewer, '%') == NULL)
	{
		char *escaped =
			shell_arg_escape(get_current_file_name(view), curr_stats.shell_type);
		result = format_str("%s %s", viewer, escaped);
		free(escaped);
	}
	else
	{
		result = ma_expand(viewer, NULL, flags, MER_SHELL);
	}
	return result;
}

void
qv_cleanup(view_t *view, const char cmd[])
{
	const preview_area_t parea = {
		.source = view,
		.view = view,
		.def_col = ui_view_get_cs(view)->color[WIN_COLOR],
		.x = 0,
		.y = 0,
		.w = view->window_cols,
		.h = view->window_rows,
	};
	qv_cleanup_area(&parea, cmd);
}

void
qv_cleanup_area(const preview_area_t *parea, const char cmd[])
{
	if(cmd == NULL)
	{
		wipe_area(parea);
		return;
	}

	view_t *const curr = curr_view;
	curr_view = parea->view;

	curr_stats.preview.clearing = 1;
	curr_stats.preview_hint = parea;

	char *expanded = qv_expand_viewer(parea->view, cmd, NULL);
	if(vlua_handler_cmd(curr_stats.vlua, expanded))
	{
		char path[PATH_MAX + 1];
		dir_entry_t *entry = get_current_entry(parea->source);
		qv_get_path_to_explore(entry, path, sizeof(path));

		strlist_t lines = vlua_view_file(curr_stats.vlua, expanded, path, parea);
		free_string_array(lines.items, lines.nitems);
	}
	else
	{
		/* XXX: this should probably get SHELL_BY_APP, otherwise preview command is
		 *      called with SHELL_BY_APP, but clear command with SHELL_BY_USER. */
		FILE *fp = read_cmd_output(expanded, /*preserve_stdin=*/0);
		while(fgetc(fp) != EOF);
		fclose(fp);
	}
	free(expanded);

	curr_stats.preview_hint = NULL;
	curr_stats.preview.clearing = 0;

	curr_view = curr;

	wipe_area(parea);
}

/* Ensures that area of a view is updated on the screen (e.g. to clear anything
 * put there by other programs as well). */
static void
wipe_area(const preview_area_t *parea)
{
	if(cfg.hard_graphics_clear)
	{
		wclear(parea->view->win);
		return;
	}

	/* User doesn't need to see fake filling so draw it with the color of
	 * background. */
	col_attr_t col = parea->def_col;
	col.attr = 0;
	col.gui_attr = 0;
	ui_set_attr(parea->view->win, &col, -1);

	fill_area(parea);
	/* The check is for tests, which work otherwise. */
	if(parea->view->win != NULL)
	{
		redrawwin(parea->view->win);
	}
	ui_refresh_win(parea->view->win);

	ui_set_attr(parea->view->win, &parea->def_col, -1);
}

/* Fills preview area with spaces just to clear background. */
static void
fill_area(const preview_area_t *parea)
{
	char filler[parea->w + 1];
	memset(filler, ' ', sizeof(filler) - 1U);
	filler[sizeof(filler) - 1U] = '\0';

	int line;
	for(line = parea->y; line < parea->y + parea->h; ++line)
	{
		mvwaddstr(parea->view->win, line, parea->x, filler);
	}
}

const char *
qv_get_viewer(const char path[])
{
	if(is_null_or_empty(curr_view->preview_prg))
	{
		char *const typed_fname = is_dir(path)
		                        ? format_str("%s/", path)
		                        : strdup(path);
		const char *const viewer = ft_get_viewer(typed_fname);
		free(typed_fname);
		return viewer;
	}
	else
	{
		/* 'previewprg' option has priority over :fileviewer command. */
		return curr_view->preview_prg;
	}
}

void
qv_get_path_to_explore(const dir_entry_t *entry, char buf[], size_t buf_len)
{
	if(entry->type == FT_DIR && is_parent_dir(entry->name))
	{
		char *const typed_fname = format_str("%s%s../", entry->origin,
				ends_with_slash(entry->origin) ? "" : "/");

		/* In the absence of handler for ".." entry, transform it into path to its
		 * origin. */
		if(ft_get_viewer(typed_fname) == NULL)
		{
			free(typed_fname);
			copy_str(buf, buf_len, entry->origin);
			return;
		}
		free(typed_fname);
	}

	get_full_path_of(entry, buf_len, buf);
}

void
qv_ui_updated(void)
{
	/* Invalidate graphical cache. */
	qv_cache.graphics_lost = 1;
}

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