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

#include <regex.h>

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

#ifdef _WIN32
#include <windows.h>
#include <shellapi.h>
#endif

#include <assert.h> /* assert() */
#include <errno.h> /* errno */
#include <math.h> /* fabsf() sqrtf() */
#include <stddef.h> /* NULL size_t */
#include <stdint.h> /* uint64_t */
#include <stdio.h> /* FILE snprintf() */
#include <stdlib.h> /* free() malloc() */
#include <string.h> /* memset() strcat() strcmp() strdup() strlen() */
#include <time.h> /* clock_gettime() */

#include "cfg/config.h"
#include "compat/dtype.h"
#include "compat/fs_limits.h"
#include "compat/os.h"
#include "int/ext_edit.h"
#include "int/vim.h"
#include "io/ioeta.h"
#include "io/ionotif.h"
#include "modes/dialogs/msg_dialog.h"
#include "modes/modes.h"
#include "ui/cancellation.h"
#include "ui/fileview.h"
#include "ui/statusbar.h"
#include "ui/ui.h"
#include "utils/cancellation.h"
#ifdef _WIN32
#include "utils/env.h"
#endif
#include "utils/fs.h"
#include "utils/fsdata.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 "background.h"
#include "filelist.h"
#include "flist_pos.h"
#include "flist_sel.h"
#include "ops.h"
#include "running.h"
#include "status.h"
#include "trash.h"
#include "types.h"
#include "undo.h"

/* 10 to the power of number of digits after decimal point to take into account
 * on progress percentage counting. */
#define IO_PRECISION 10

/* Maximum value of progress_data_t::last_progress. */
#define IO_MAX_PROGRESS (100*(IO_PRECISION))

/* Key used to switch to progress dialog. */
#define IO_DETAILS_KEY 'i'

/* Number of entries in history windows used to compute ETA. */
#define ETA_HISTORY_SIZE 10

/* A circular buffer for computing weighted average. */
typedef struct
{
	/* We're only adding here, so no need to keep head index, because it's zero
	 * until wrapping and `pos` after wrapping. */
	int wrapped; /* Whether the buffer has been wrapped. */
	int pos;     /* Next position for writing (tail). */

	uint64_t entries[ETA_HISTORY_SIZE]; /* Buffer's data. */
}
history_window_t;

/* Object for auxiliary information related to progress of operations in
 * io_progress_changed() handler. */
typedef struct
{
	int bg; /* Whether this is background operation. */
	union
	{
		ops_t *ops;     /* Information for foreground operation. */
		bg_op_t *bg_op; /* Information for background operation. */
	};

	int last_progress; /* Progress of the operation during previous call.
	                      Range is [-1; IO_MAX_PROGRESS]. */
	IoPs last_stage;   /* Stage of the operation during previous call. */

	char *progress_bar;     /* String of progress bar. */
	int progress_bar_value; /* Value of progress bar during previous call. */
	int progress_bar_max;   /* Width of progress bar during previous call. */

	/* State of rate calculation. */
	long long start_time;     /* Time of starting the operation. */
	long long last_calc_time; /* Time of last rate calculation. */
	uint64_t last_seen_byte;  /* Position at the time of last call. */
	float last_rate;          /* Rate in bytes per millisecond. */
	int last_eta;             /* Last reported ETA. */
	char *rate_str;           /* Rate formatted as a string. */
	char *eta_str;            /* ETA formatted as a string. */
	history_window_t speed_window; /* Smoothing of speed. */
	history_window_t eta_window;   /* Smoothing of ETA. */

	/* Whether progress is displayed in a dialog, rather than on status bar. */
	int dialog;

	int width; /* Maximum reached width of the dialog. */
}
progress_data_t;

/* Type of function that returns next weight for weighted average. */
typedef float (*weight_f)(int idx, float last_weight);

static void io_progress_changed(const io_progress_t *state);
static int calc_io_progress(const io_progress_t *state, int *skip);
static void update_io_stats(progress_data_t *pdata, const ioeta_estim_t *estim);
static void window_add(history_window_t *win, uint64_t value);
static float window_average(const history_window_t *win, weight_f weight);
static float linear_weight(int idx, float last_weight);
static float doubling_weight(int idx, float last_weight);
static void format_io_stats(progress_data_t *pdata, long long now_ms,
		uint64_t imm_rate);
static void update_progress_bar(progress_data_t *pdata,
		const ioeta_estim_t *estim);
static void io_progress_fg(const io_progress_t *state, int progress);
static void io_progress_fg_sb(const io_progress_t *state, int progress);
static void io_progress_bg(const io_progress_t *state, int progress);
static char * format_file_progress(const ioeta_estim_t *estim, int precision);
static void format_pretty_path(const char base_dir[], const char path[],
		char pretty[], size_t pretty_size);
static int is_file_name_changed(const char old[], const char new[]);
static int ui_cancellation_hook(void *arg);
TSTATIC char ** edit_list(struct ext_edit_t *ext_edit, size_t orig_len,
		char *orig[], int *edited_len, int load_always);
TSTATIC progress_data_t * alloc_progress_data(int bg, void *info);
static long long time_in_ms(void);

line_prompt_func fops_line_prompt;
options_prompt_func fops_options_prompt;

void
fops_init(line_prompt_func line_func, options_prompt_func options_func)
{
	fops_line_prompt = line_func;
	fops_options_prompt = options_func;
	ionotif_register(&io_progress_changed);
}

/* I/O operation update callback. */
static void
io_progress_changed(const io_progress_t *state)
{
	const ioeta_estim_t *const estim = state->estim;
	progress_data_t *const pdata = estim->param;

	int redraw = 0;
	int progress, skip;

	progress = calc_io_progress(state, &skip);
	if(skip)
	{
		return;
	}

	/* Don't query for scheduled redraw or input for background operations. */
	if(!pdata->bg)
	{
		redraw = (stats_update_fetch() != UT_NONE);

		if(!pdata->dialog)
		{
			if(ui_char_pressed(IO_DETAILS_KEY))
			{
				pdata->dialog = 1;
				ui_sb_clear();
			}
		}
	}

	/* Do nothing if progress change is small, but force update on stage
	 * change or redraw request. */
	if(progress == pdata->last_progress &&
			state->stage == pdata->last_stage && !redraw)
	{
		return;
	}

	pdata->last_stage = state->stage;

	if(progress >= 0)
	{
		pdata->last_progress = progress;
	}

	if(redraw)
	{
		modes_redraw();
	}

	if(pdata->bg)
	{
		io_progress_bg(state, progress);
	}
	else
	{
		io_progress_fg(state, progress);
	}
}

/* Calculates current IO operation progress.  *skip will be set to non-zero
 * value to indicate that progress change is irrelevant.  Returns progress in
 * the range [-1; IO_MAX_PROGRESS], where -1 means "unknown". */
static int
calc_io_progress(const io_progress_t *state, int *skip)
{
	const ioeta_estim_t *const estim = state->estim;
	progress_data_t *const pdata = estim->param;

	*skip = 0;
	if(state->stage == IO_PS_ESTIMATING)
	{
		return estim->total_items/IO_PRECISION;
	}
	else if(estim->total_bytes == 0)
	{
		if(estim->total_items == 0)
		{
			return 0;
		}
		/* When files are empty, use their number for progress counting. */
		return (estim->current_item*IO_MAX_PROGRESS)/estim->total_items;
	}
	else if(pdata->last_progress >= IO_MAX_PROGRESS &&
			estim->current_byte == estim->total_bytes)
	{
		/* Special handling for unknown total size. */
		++pdata->last_progress;
		if(pdata->last_progress%IO_PRECISION != 0)
		{
			*skip = 1;
		}
		return -1;
	}
	else
	{
		return (estim->current_byte*IO_MAX_PROGRESS)/estim->total_bytes;
	}
}

/* Updates rate of operation. */
static void
update_io_stats(progress_data_t *pdata, const ioeta_estim_t *estim)
{
	long long current_time_ms = time_in_ms();
	long long elapsed_time_ms = current_time_ms - pdata->last_calc_time;

	if(elapsed_time_ms == 0 ||
			(pdata->last_seen_byte != 0 && elapsed_time_ms < 1000))
	{
		/* Rate is updated initially and then once in 1000 milliseconds. */
		return;
	}

	uint64_t bytes_difference = estim->current_byte - pdata->last_seen_byte;
	uint64_t imm_rate = bytes_difference/elapsed_time_ms;

	/* Smooth immediate rate. */
	window_add(&pdata->speed_window, imm_rate);
	float rate = window_average(&pdata->speed_window, &doubling_weight);

	/* Second round of adjusting speed to not deviate from previous value too
	 * much. */
	if(rate >= 1 && pdata->last_rate >= 1)
	{
		float minV = MIN(rate, pdata->last_rate);
		float maxV = MAX(rate, pdata->last_rate);
		rate = pdata->last_rate + (rate - pdata->last_rate)*minV/maxV;
	}

	/* Acceleration. */
	float a = (rate - pdata->last_rate)/elapsed_time_ms;
	/* Remains this much. */
	uint64_t r = estim->total_bytes - estim->current_byte;

	int eta;
	if(fabsf(a) >= 1 && rate*rate + 2*a*r >= 0)
	{
		eta = (sqrtf(rate*rate + 2*a*r) - rate)/a;
	}
	else if(fabsf(rate) >= 1)
	{
		eta = r/rate;
	}
	else
	{
		eta = pdata->last_eta;
	}

	/* Smooth ETA rate. */
	window_add(&pdata->eta_window, eta);
	float smooth_eta = window_average(&pdata->eta_window, &linear_weight);

	pdata->last_calc_time = current_time_ms;
	pdata->last_seen_byte = estim->current_byte;
	pdata->last_rate = rate;
	pdata->last_eta = smooth_eta;

	format_io_stats(pdata, current_time_ms, imm_rate);
}

/* Adds an entry to window. */
static void
window_add(history_window_t *win, uint64_t value)
{
	win->entries[win->pos++] = value;
	if(win->pos == ETA_HISTORY_SIZE)
	{
		win->pos = 0;
		win->wrapped = 1;
	}
}

/* Computes weighted average on entries of the passed in window.  Returns the
 * average. */
static float
window_average(const history_window_t *win, weight_f weight)
{
	assert((win->pos != 0 || win->wrapped) && "Window must not be empty.");

	float average = 0.0f;

	/* Current weight. */
	float w = 0.0f;
	/* Total weight. */
	float ws = 0.0f;

	/* Start positions within circular buffer. */
	int pos = (win->wrapped ? win->pos : 0);
	/* Number of used positions. */
	int count = (win->wrapped ? ETA_HISTORY_SIZE : win->pos);

	int i;
	for(i = 0; i < count; ++i)
	{
		w = weight(i, w);
		ws += w;

		average += w*win->entries[pos];
		pos = (pos + 1)%ETA_HISTORY_SIZE;
	}

	average /= ws;
	return average;
}

/* Linear weight function (1, 2, 3, ...).  Returns the next weight. */
static float
linear_weight(int idx, float last_weight)
{
	return 1 + idx;
}

/* Weight function with the factor of 2 (1, 2, 4, ...).  Returns the next
 * weight. */
static float
doubling_weight(int idx, float last_weight)
{
	return (idx == 0 ? 1.0f : last_weight*2.0f);
}

/* Updates pdata->*_str fields. */
static void
format_io_stats(progress_data_t *pdata, long long now_ms, uint64_t imm_rate)
{
	char rate_str[64];
	(void)friendly_size_notation(imm_rate*1000, sizeof(rate_str) - 8, rate_str);
	strcat(rate_str, "/s");
	replace_string(&pdata->rate_str, rate_str);

	/* Do not show ETA for the first 5 seconds.  Really short operations need no
	 * ETA and long ones might have incorrect one at first. */
	if(now_ms - pdata->start_time < 5000)
	{
		return;
	}

	enum
	{
		Minute = 60,
		Hour = 60*Minute,
		Day = 24*Hour,
	};

	int eta_s = ceil(pdata->last_eta/1000.0);
	int d = eta_s/Day;
	int h = eta_s%Day/Hour;
	int m = eta_s%Hour/Minute;
	int s = eta_s%Minute;

	char *time_str;
	if(d > 0)
	{
		time_str = format_str("%dd %02d:%02d:%02d", d, h, m, s);
	}
	else
	{
		time_str = format_str("%02d:%02d:%02d", h, m, s);
	}
	put_string(&pdata->eta_str, format_str("~%s left", time_str));
	free(time_str);
}

/* Updates progress bar of operation. */
static void
update_progress_bar(progress_data_t *pdata, const ioeta_estim_t *estim)
{
	int max_width = pdata->width - 6;
	if(max_width <= 0)
	{
		return;
	}

	int value = (pdata->last_progress*max_width)/IO_MAX_PROGRESS;
	if(value == pdata->progress_bar_value && max_width == pdata->progress_bar_max)
	{
		return;
	}

	pdata->progress_bar_value = value;
	pdata->progress_bar_max = max_width;

	free(pdata->progress_bar);
	pdata->progress_bar = malloc(max_width + 3);

	pdata->progress_bar[0] = '[';
	memset(pdata->progress_bar + 1, '=', value);
	memset(pdata->progress_bar + 1 + value, ' ', max_width - value);
	pdata->progress_bar[1 + max_width] = ']';
	pdata->progress_bar[1 + max_width + 1] = '\0';
}

/* Takes care of progress for foreground operations. */
static void
io_progress_fg(const io_progress_t *state, int progress)
{
	char current_size_str[64];
	char total_size_str[64];
	char src_path[PATH_MAX + 1];
	const char *title, *ctrl_msg;
	const char *target_name;
	char *as_part;
	const char *item_name;
	int item_num;

	const ioeta_estim_t *const estim = state->estim;
	progress_data_t *const pdata = estim->param;
	ops_t *const ops = pdata->ops;

	if(!pdata->dialog)
	{
		io_progress_fg_sb(state, progress);
		return;
	}

	(void)friendly_size_notation(estim->total_bytes, sizeof(total_size_str),
			total_size_str);

	copy_str(src_path, sizeof(src_path), replace_home_part(estim->item));
	remove_last_path_component(src_path);

	title = ops_describe(ops);
	ctrl_msg = "Press Ctrl-C to cancel";
	if(state->stage == IO_PS_ESTIMATING)
	{
		char pretty_path[PATH_MAX + 1];
		format_pretty_path(ops->base_dir, estim->item, pretty_path,
				sizeof(pretty_path));
		draw_msgf(title, ctrl_msg, pdata->width,
				"In %s\nestimating...\nItems: %" PRINTF_ULL "\n"
				"Overall: %s\nCurrent: %s",
				ops->target_dir, (unsigned long long)estim->total_items, total_size_str,
				pretty_path);
		pdata->width = getmaxx(error_win);
		return;
	}

	(void)friendly_size_notation(estim->current_byte,
			sizeof(current_size_str), current_size_str);

	item_name = get_last_path_component(estim->item);

	target_name = get_last_path_component(estim->target);
	if(stroscmp(target_name, item_name) == 0)
	{
		as_part = strdup("");
	}
	else
	{
		as_part = format_str("\nas   %s", target_name);
	}

	item_num = MIN(estim->current_item + 1, estim->total_items);

	update_io_stats(pdata, estim);

	if(progress < 0)
	{
		/* Simplified message for unknown total size. */
		draw_msgf(title, ctrl_msg, pdata->width,
				"Location: %s\nItem:     %d of %" PRINTF_ULL "\n"
				"Overall:  %s %s\n"
				" \n" /* Space is on purpose to preserve empty line. */
				"file %s\nfrom %s%s",
				replace_home_part(ops->target_dir), item_num,
				(unsigned long long)estim->total_items, total_size_str, pdata->rate_str,
				item_name, src_path, as_part);
	}
	else
	{
		char *const file_progress = format_file_progress(estim, IO_PRECISION);
		update_progress_bar(pdata, estim);

		draw_msgf(title, ctrl_msg, pdata->width,
				"Location: %s\nItem:     %d of %" PRINTF_ULL "\n"
				"Overall:  %5s/%-5s (%d%%)  |  %s  %s  %s\n"
				"%s\n"
				" \n" /* Space is on purpose to preserve empty line. */
				"file %s\nfrom %s%s%s",
				replace_home_part(ops->target_dir), item_num,
				(unsigned long long)estim->total_items, current_size_str,
				total_size_str, progress/IO_PRECISION, pdata->rate_str,
				pdata->eta_str[0] == '\0' ? "" : "|", pdata->eta_str,
				pdata->progress_bar, item_name, src_path, as_part, file_progress);

		free(file_progress);
	}

	pdata->width = getmaxx(error_win);

	free(as_part);
}

/* Takes care of progress for foreground operations displayed on status line. */
static void
io_progress_fg_sb(const io_progress_t *state, int progress)
{
	const ioeta_estim_t *const estim = state->estim;
	progress_data_t *const pdata = estim->param;
	ops_t *const ops = pdata->ops;

	char current_size_str[64];
	char total_size_str[64];
	char pretty_path[PATH_MAX + 1];
	char *suffix;

	(void)friendly_size_notation(estim->total_bytes, sizeof(total_size_str),
			total_size_str);

	if(estim->item == NULL)
	{
		strcpy(pretty_path, "-");
	}
	else
	{
		format_pretty_path(ops->base_dir, estim->item, pretty_path,
				sizeof(pretty_path));
	}

	switch(state->stage)
	{
		case IO_PS_ESTIMATING:
			suffix = format_str("estimating... %" PRINTF_ULL "; %s %s",
					(unsigned long long)estim->total_items, total_size_str, pretty_path);
			break;
		case IO_PS_IN_PROGRESS:
			(void)friendly_size_notation(estim->current_byte,
					sizeof(current_size_str), current_size_str);

			if(progress < 0)
			{
				/* Simplified message for unknown total size. */
				suffix = format_str("%" PRINTF_ULL " of %" PRINTF_ULL "; %s %s",
						(unsigned long long)estim->current_item + 1U,
						(unsigned long long)estim->total_items, total_size_str,
						pretty_path);
			}
			else
			{
				suffix = format_str("%" PRINTF_ULL " of %" PRINTF_ULL "; "
						"%s/%s (%2d%%) %s",
						(unsigned long long)estim->current_item + 1,
						(unsigned long long)estim->total_items, current_size_str,
						total_size_str, progress/IO_PRECISION, pretty_path);
			}
			break;

		default:
			assert(0 && "Unhandled progress stage");
			suffix = strdup("");
			break;
	}

	ui_sb_quick_msgf("(hit %c for details) %s: %s", IO_DETAILS_KEY,
			ops_describe(ops), suffix);
	free(suffix);
}

/* Takes care of progress for background operations. */
static void
io_progress_bg(const io_progress_t *state, int progress)
{
	const ioeta_estim_t *const estim = state->estim;
	progress_data_t *const pdata = estim->param;
	bg_op_t *const bg_op = pdata->bg_op;

	bg_op->progress = progress/IO_PRECISION;
	bg_op_changed(bg_op);
}

/* Formats file progress part of the progress message.  Returns pointer to newly
 * allocated memory. */
static char *
format_file_progress(const ioeta_estim_t *estim, int precision)
{
	char current_size[64];
	char total_size[64];

	const int file_progress = (estim->total_file_bytes == 0U) ? 0 :
		(estim->current_file_byte*100*precision)/estim->total_file_bytes;

	if(estim->total_items == 1)
	{
		return strdup("");
	}

	(void)friendly_size_notation(estim->current_file_byte, sizeof(current_size),
			current_size);
	(void)friendly_size_notation(estim->total_file_bytes, sizeof(total_size),
			total_size);

	return format_str("\nprogress %s/%s (%2d%%)", current_size, total_size,
			file_progress/precision);
}

/* Pretty prints path shortening it by skipping base directory path if
 * possible, otherwise fallbacks to the full path. */
static void
format_pretty_path(const char base_dir[], const char path[], char pretty[],
		size_t pretty_size)
{
	if(!path_starts_with(path, base_dir))
	{
		copy_str(pretty, pretty_size, path);
		return;
	}

	copy_str(pretty, pretty_size, skip_char(path + strlen(base_dir), '/'));
}

int
fops_is_name_list_ok(int count, int nlines, char *list[], char **error)
{
	int i;

	if(nlines < count)
	{
		put_string(error, format_str("Not enough file names (%d/%d)", nlines,
					count));
		return 0;
	}

	if(nlines > count)
	{
		put_string(error, format_str("Too many file names (%d/%d)", nlines, count));
		return 0;
	}

	for(i = 0; i < count; ++i)
	{
		chomp(list[i]);

		if(list[i][0] != '\0' && is_in_string_array(list, i, list[i]))
		{
			put_string(error, format_str("Name \"%s\" duplicates", list[i]));
			return 0;
		}
	}

	return 1;
}

int
fops_is_copy_list_ok(const char dst[], int count, char *list[], int force,
		char **error)
{
	if(force)
	{
		return 1;
	}

	int i;
	for(i = 0; i < count; ++i)
	{
		if(path_exists_at(dst, list[i], NODEREF))
		{
			char *escaped = escape_unreadable(list[i]);
			put_string(error, format_str("File \"%s\" already exists", escaped));
			free(escaped);
			return 0;
		}
	}
	return 1;
}

int
fops_check_moves_on_rename(int count, char *list[], char *files[], char **error)
{
	int moves = 0;

	int i;
	for(i = 0; i < count; ++i)
	{
		/* Assuming that fops_is_name_list_ok() did chomp(), so not repeating it. */

		if(list[i][0] == '\0')
		{
			/* This file won't be renamed. */
			continue;
		}

		char *file_s = find_slashr(files[i]);
		char *list_s = find_slashr(list[i]);
		if(list_s == NULL && file_s == NULL)
		{
			/* No prefixes, not a move. */
			continue;
		}

		if(list_s != NULL && file_s != NULL &&
				list_s - list[i] == file_s - files[i] &&
				strnoscmp(files[i], list[i], list_s - list[i]) == 0)
		{
			/* Prefixes are identical. */
			continue;
		}

		char *list_tail = (list_s == NULL ? list[i] : list_s);
		char *file_tail = (file_s == NULL ? files[i] : file_s);

		char list_tail_c = *list_tail;
		char file_tail_c = *file_tail;
		*list_tail = '\0';
		*file_tail = '\0';

		int same_prefix = paths_are_same(list[i][0] == '\0' ? "." : list[i],
				files[i][0] == '\0' ? "." : files[i]);
		*list_tail = list_tail_c;
		*file_tail = file_tail_c;

		if(same_prefix)
		{
			/* Even though prefixes look different, they point at the same path. */
			continue;
		}

		++moves;

		if(list_s == NULL)
		{
			/* Empty prefix is some parent directory which must already exist. */
			continue;
		}

		char list_slash = *list_s;
		*list_s = '\0';
		int dir_exists = is_dir(list[i]);
		*list_s = list_slash;

		if(!dir_exists)
		{
			char *msg = format_str("Won't create directory for \"%s\"", list[i]);
			put_string(error, msg);
			return -1;
		}
	}

	return moves;
}

int
fops_is_rename_list_ok(char *files[], char is_dup[], int len, char *list[],
		char **error)
{
	int i;
	const char *const work_dir = flist_get_dir(curr_view);
	for(i = 0; i < len; ++i)
	{
		const int check_result =
			fops_check_file_rename(work_dir, files[i], list[i], error);
		if(check_result < 0)
		{
			continue;
		}

		int j;
		for(j = 0; j < len; ++j)
		{
			if(paths_are_equal(list[i], files[j]) && !is_dup[j])
			{
				is_dup[j] = 1;
				break;
			}
		}

		if(j >= len && check_result == 0)
		{
			break;
		}
		update_string(error, NULL);
	}
	return i >= len;
}

int
fops_check_file_rename(const char dir[], const char old[], const char new[],
		char **error)
{
	if(!is_file_name_changed(old, new))
	{
		return -1;
	}

	if(path_exists_at(dir, new, NODEREF) && stroscmp(old, new) != 0 &&
			!is_case_change(old, new))
	{
		char *escaped = escape_unreadable(new);
		put_string(error, format_str("File \"%s\" already exists", escaped));
		free(escaped);
		return 0;
	}

	return 1;
}

/* Checks whether file name change was performed.  Returns non-zero if change is
 * detected, otherwise zero is returned. */
static int
is_file_name_changed(const char old[], const char new[])
{
	/* Empty new name means reuse of the old name (rename cancellation).  Names
	 * are always compared in a case sensitive way, so that changes in case of
	 * letters triggers rename operation even for systems where paths are case
	 * insensitive. */
	return (new[0] != '\0' && strcmp(old, new) != 0);
}

char **
fops_grab_marked_files(view_t *view, size_t *nmarked)
{
	char **marked = NULL;
	dir_entry_t *entry = NULL;
	*nmarked = 0;
	while(iter_marked_entries(view, &entry))
	{
		*nmarked = add_to_string_array(&marked, *nmarked, entry->name);
	}
	return marked;
}

int
fops_is_dir_entry(const char full_path[], const struct dirent *dentry)
{
#ifndef _WIN32
	struct stat s;
	const int type = get_dirent_type(dentry, full_path);
	if(type != DT_UNKNOWN)
	{
		return type == DT_DIR;
	}
	if(os_lstat(full_path, &s) == 0 && s.st_ino != 0)
	{
		return (s.st_mode&S_IFMT) == S_IFDIR;
	}
	return 0;
#else
	return is_dir(full_path);
#endif
}

int
fops_enqueue_marked_files(ops_t *ops, view_t *view, const char dst_hint[],
		int to_trash)
{
	int nmarked_files = 0;
	dir_entry_t *entry = NULL;

	ui_cancellation_enable();

	while(iter_marked_entries(view, &entry) && !ui_cancellation_requested())
	{
		char full_path[PATH_MAX + 1];

		get_full_path_of(entry, sizeof(full_path), full_path);

		if(to_trash)
		{
			char *const trash_dir = trash_pick_dir(entry->origin);
			ops_enqueue(ops, full_path, trash_dir);
			free(trash_dir);
		}
		else
		{
			ops_enqueue(ops, full_path, dst_hint);
		}

		++nmarked_files;
	}

	ui_cancellation_disable();

	return nmarked_files;
}

ops_t *
fops_get_ops(OPS main_op, const char descr[], const char base_dir[],
		const char target_dir[])
{
	ops_t *const ops = ops_alloc(main_op, 0, descr, base_dir, target_dir,
			fops_options_prompt, &prompt_msg);
	if(ops->use_system_calls)
	{
		progress_data_t *const pdata = alloc_progress_data(ops->bg, ops);
		pdata->dialog = cfg.always_show_io_details;

		const io_cancellation_t cancellation = { .hook = &ui_cancellation_hook };
		ops->estim = ioeta_alloc(pdata, cancellation);
	}
	ui_cancellation_push_off();
	return ops;
}

/* Implementation of cancellation hook for I/O unit. */
static int
ui_cancellation_hook(void *arg)
{
	return ui_cancellation_requested();
}

void
fops_progress_msg(const char text[], int ready, int total)
{
	if(!cfg.use_system_calls)
	{
		char msg[strlen(text) + 32];

		sprintf(msg, "%s %d/%d", text, ready + 1, total);
		show_progress(msg, 1);
		curr_stats.save_msg = 2;
	}
}

const char *
fops_get_dst_name(const char src_path[], int from_trash)
{
	if(from_trash)
	{
		return trash_get_real_name_of(src_path);
	}
	return get_last_path_component(src_path);
}

int
fops_can_read_marked_files(view_t *view)
{
	dir_entry_t *entry;

	if(is_unc_path(view->curr_dir))
	{
		return 1;
	}

	entry = NULL;
	while(iter_marked_entries(view, &entry))
	{
		char full_path[PATH_MAX + 1];
		get_full_path_of(entry, sizeof(full_path), full_path);

		/* We can copy links even when they are broken, so it's OK to don't check
		 * them (otherwise access() fails for broken links). */
		if(entry->type == FT_LINK || os_access(full_path, R_OK) == 0)
		{
			continue;
		}

		show_error_msgf("Access denied",
				"You don't have read permissions on \"%s\"", full_path);
		flist_sel_stash(view);
		redraw_view(view);
		return 0;
	}
	return 1;
}

int
fops_check_dir_path(const view_t *view, const char path[], char buf[],
		size_t buf_len)
{
	if(path[0] == '/' || path[0] == '~')
	{
		char *const expanded_path = expand_tilde(path);
		copy_str(buf, buf_len, expanded_path);
		free(expanded_path);
	}
	else
	{
		snprintf(buf, buf_len, "%s/%s", fops_get_dst_dir(view, -1), path);
	}

	if(is_dir(buf))
	{
		return 1;
	}

	copy_str(buf, buf_len, fops_get_dst_dir(view, -1));
	return 0;
}

char **
fops_query_list(size_t orig_len, char *orig[], int *edited_len, int load_always,
		fops_query_verify_func verify, void *verify_data)
{
	ext_edit_t ext_edit = {};
	char **list = NULL;
	int nlines = 0;
	char *error_str = NULL;

	while(1)
	{
		list = edit_list(&ext_edit, orig_len, orig, &nlines, 0);
		if(nlines == 0)
		{
			/* Cancelled. */
			break;
		}

		if(!verify(orig, orig_len, list, nlines, &error_str, verify_data))
		{
			free_string_array(list, nlines);
			list = NULL;
			nlines = 0;

			/* Stray space prevents removal of the line. */
			if(prompt_msgf("Editing error", "%s\n \nRe-edit paths?", error_str))
			{
				update_string(&ext_edit.last_error, error_str);
				continue;
			}
		}

		break;
	}

	free(error_str);
	ext_edit_discard(&ext_edit);

	*edited_len = nlines;
	return list;
}

/* Prompts user with a file containing lines from orig array of length orig_len
 * and returns modified list of strings of length *edited_len or NULL on error
 * or unchanged list unless load_always is non-zero. */
TSTATIC char **
edit_list(ext_edit_t *ext_edit, size_t orig_len, char *orig[], int *edited_len,
		int load_always)
{
	*edited_len = 0;

	char rename_file[PATH_MAX + 1];
	FILE *file = make_file_in_tmp("vifm.rename", 0600, /*auto_delete=*/0,
			rename_file, sizeof(rename_file));
	if(file == NULL)
	{
		show_error_msgf("Error Getting List Of Renames",
				"Can't create temporary file \"%s\": %s", rename_file, strerror(errno));
		return NULL;
	}

	strlist_t prepared = ext_edit_prepare(ext_edit, orig, orig_len);

	write_lines_to_file(file, prepared.items, prepared.nitems);
	fclose(file);

	free_string_array(prepared.items, prepared.nitems);

	if(vim_view_file(rename_file, -1, -1, 0) != 0)
	{
		unlink(rename_file);
		show_error_msgf("Error Editing File", "Editing of file \"%s\" failed.",
				rename_file);
		return NULL;
	}

	int result_len;
	char **result = read_file_of_lines(rename_file, &result_len);
	int error = errno;
	unlink(rename_file);
	if(result == NULL)
	{
		show_error_msgf("Error Getting List Of Renames",
				"Can't open temporary file \"%s\": %s", rename_file, strerror(error));
		return NULL;
	}

	ext_edit_done(ext_edit, orig, orig_len, result, &result_len);

	if(!load_always && string_array_equal(orig, orig_len, result, result_len))
	{
		ext_edit_discard(ext_edit);
		free_string_array(result, result_len);
		return NULL;
	}

	*edited_len = result_len;
	return result;
}

void
fops_bg_ops_init(ops_t *ops, bg_op_t *bg_op)
{
	ops->bg_op = bg_op;
	if(ops->estim != NULL)
	{
		progress_data_t *const pdata = ops->estim->param;
		pdata->bg_op = bg_op;
	}
}

ops_t *
fops_get_bg_ops(OPS main_op, const char descr[], const char dir[])
{
	ops_t *const ops = ops_alloc(main_op, 1, descr, dir, dir, fops_options_prompt,
			&prompt_msg);
	if(ops->use_system_calls)
	{
		progress_data_t *const pdata = alloc_progress_data(ops->bg, NULL);
		const io_cancellation_t no_cancellation = {};
		ops->estim = ioeta_alloc(pdata, no_cancellation);
	}
	return ops;
}

/* Allocates progress data with specified parameters and initializes all the
 * rest of structure fields with default values. */
TSTATIC progress_data_t *
alloc_progress_data(int bg, void *info)
{
	progress_data_t *const pdata = malloc(sizeof(*pdata));

	pdata->bg = bg;
	pdata->ops = info;

	pdata->last_progress = -1;
	pdata->last_stage = (IoPs)-1;

	pdata->progress_bar = strdup("");
	pdata->progress_bar_value = 0;
	pdata->progress_bar_max = 0;

	/* Time of starting the operation to have meaningful first rate. */
	pdata->start_time = time_in_ms();
	pdata->last_calc_time = pdata->start_time;
	pdata->last_seen_byte = 0;
	pdata->last_rate = 0.0f;
	pdata->last_eta = 0;
	pdata->rate_str = strdup("? B/s");
	pdata->eta_str = strdup("");
	pdata->eta_window.wrapped = 0;
	pdata->eta_window.pos = 0;
	pdata->speed_window.wrapped = 0;
	pdata->speed_window.pos = 0;

	pdata->dialog = 0;
	pdata->width = 0;

	return pdata;
}

/* Retrieves current time in milliseconds. */
static long long
time_in_ms(void)
{
	struct timespec current_time;
	if(clock_gettime(CLOCK_MONOTONIC, &current_time) != 0)
	{
		return 0;
	}

	return current_time.tv_sec*1000 + current_time.tv_nsec/1000000;
}

int
fops_active(const ops_t *ops)
{
	return !ops->aborted && !ui_cancellation_requested();
}

void
fops_free_ops(ops_t *ops)
{
	if(ops == NULL)
	{
		return;
	}

	if(!ops->bg)
	{
		ui_cancellation_pop();
		ui_drain_input();
	}

	if(ops->use_system_calls)
	{
		if(!ops->bg && ops->errors != NULL)
		{
			char *const title = format_str("Encountered errors on %s",
					ops_describe(ops));
			show_error_msg(title, ops->errors);
			free(title);
		}

		progress_data_t *pdata = ops->estim->param;
		free(pdata->progress_bar);
		free(pdata->rate_str);
		free(pdata->eta_str);
		free(pdata);
	}
	ops_free(ops);
}

int
fops_mv_file(const char src[], const char src_dir[], const char dst[],
		const char dst_dir[], OPS op, int cancellable, ops_t *ops)
{
	char full_src[PATH_MAX + 1], full_dst[PATH_MAX + 1];

	to_canonic_path(src, src_dir, full_src, sizeof(full_src));
	to_canonic_path(dst, dst_dir, full_dst, sizeof(full_dst));

	return fops_mv_file_f(full_src, full_dst, op, 0, cancellable, ops);
}

int
fops_mv_file_f(const char src[], const char dst[], OPS op, int bg,
		int cancellable, ops_t *ops)
{
	/* Compare case sensitive strings even on Windows to let user rename file
	 * changing only case of some characters. */
	if(strcmp(src, dst) == 0)
	{
		return 0;
	}

	OpsResult result = perform_operation(op, ops, cancellable ? NULL : (void *)1,
			src, dst);
	if(result != OPS_SUCCEEDED)
	{
		return 1;
	}

	if(!bg)
	{
		un_group_add_op(op, NULL, NULL, src, dst);
	}
	return 0;
}

void
fops_free_bg_args(bg_args_t *args)
{
	free_string_array(args->list, args->nlines);
	free_string_array(args->sel_list, args->sel_list_len);
	free(args->is_in_trash);
	fops_free_ops(args->ops);
	free(args);
}

void
fops_prepare_for_bg_task(view_t *view, bg_args_t *args)
{
	dir_entry_t *entry;

	entry = NULL;
	while(iter_marked_entries(view, &entry))
	{
		char full_path[PATH_MAX + 1];

		get_full_path_of(entry, sizeof(full_path), full_path);
		args->sel_list_len = add_to_string_array(&args->sel_list,
				args->sel_list_len, full_path);
	}

	ui_view_reset_selection_and_reload(view);
}

void
fops_append_marked_files(view_t *view, char buf[], char **fnames)
{
	const int custom_fnames = (fnames != NULL);
	size_t len = strlen(buf);
	dir_entry_t *entry = NULL;
	while(iter_marked_entries(view, &entry) && len < COMMAND_GROUP_INFO_LEN)
	{
		fops_append_fname(buf, len, entry->name);
		len = strlen(buf);

		if(custom_fnames)
		{
			const char *const custom_fname = *fnames++;

			strncat(buf, " to ", COMMAND_GROUP_INFO_LEN - len - 1);
			len = strlen(buf);
			strncat(buf, custom_fname, COMMAND_GROUP_INFO_LEN - len - 1);
			len = strlen(buf);
		}
	}
}

void
fops_append_fname(char buf[], size_t len, const char fname[])
{
	if(buf[len - 2] != ':')
	{
		strncat(buf, ", ", COMMAND_GROUP_INFO_LEN - len - 1);
		len = strlen(buf);
	}
	strncat(buf, fname, COMMAND_GROUP_INFO_LEN - len - 1);
}

const char *
fops_get_cancellation_suffix(void)
{
	return ui_cancellation_requested() ? " (cancelled)" : "";
}

int
fops_view_can_be_changed(const view_t *view)
{
	/* TODO: maybe add check whether directory of specific entry is writable for
	 *       custom views. */
	return flist_custom_active(view)
	    || fops_is_dir_writable(DR_CURRENT, view->curr_dir);
}

int
fops_view_can_be_extended(const view_t *view, int at)
{
	if(!flist_is_fs_backed(view))
	{
		show_error_msg("Operation error",
				"Custom view can't handle this operation.");
		return 0;
	}

	return fops_is_dir_writable(DR_DESTINATION, fops_get_dst_dir(view, at));
}

const char *
fops_get_dst_dir(const view_t *view, int at)
{
	if(flist_custom_active(view) && cv_tree(view->custom.type))
	{
		if(at < 0)
		{
			at = view->list_pos;
		}
		else if(at >= view->list_rows)
		{
			at = view->list_rows - 1;
		}
		return view->dir_entry[at].origin;
	}
	return flist_get_dir(view);
}

int
fops_is_dir_writable(DirRole dir_role, const char path[])
{
	if(is_dir_writable(path))
	{
		return 1;
	}

	if(dir_role == DR_DESTINATION)
	{
		show_error_msg("Operation error", "Destination directory is not writable");
	}
	else
	{
		show_error_msg("Operation error", "Current directory is not writable");
	}
	return 0;
}

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