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

#include <assert.h> /* assert() */
#include <ctype.h> /* tolower() */
#include <limits.h> /* INT_MAX */
#include <string.h> /* memmove() memset() strdup() */

#include "cfg/config.h"
#include "compat/os.h"
#include "compat/reallocarray.h"
#include "engine/text_buffer.h"
#include "modes/dialogs/msg_dialog.h"
#include "modes/modes.h"
#include "modes/wk.h"
#include "ui/cancellation.h"
#include "ui/statusbar.h"
#include "utils/fs.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/utf8.h"
#include "utils/utils.h"
#include "background.h"
#include "filelist.h"
#include "flist_pos.h"
#include "fops_common.h"
#include "fops_cpmv.h"
#include "ops.h"
#include "registers.h"
#include "trash.h"
#include "undo.h"

/* Information necessary for composing conflict message's body. */
typedef struct
{
	const char *src; /* Full path of the source file. */
	const char *dst; /* Full path of the destination file. */
}
conflict_prompt_data_t;

static void put_files_in_bg(bg_op_t *bg_op, void *arg);
static int initiate_put_files(view_t *view, int at, CopyMoveLikeOp op,
		const char descr[], int reg_name);
static void reset_put_confirm(CopyMoveLikeOp main_op, const char descr[],
		const char dst_dir[]);
static OPS cmlo_to_op(CopyMoveLikeOp op);
static int path_depth_sort(const void *one, const void *two);
static int is_dir_clash(const char src_path[], const char dst_dir[]);
static int put_files_i(view_t *view, int start);
static void update_cursor_position(view_t *view);
static int put_next(int force);
static int unprocessed_dirs_present(void);
TSTATIC OpsResult merge_dirs(const char src[], const char dst[], ops_t *ops);
static int handle_clashing(int move, const char src[], const char dst[]);
static void prompt_what_to_do(const char fname[], const char caused_by[]);
static void handle_prompt_response(const char fname[], const char caused_by[],
		char response);
static void prompt_dst_name(const char src_name[]);
static void prompt_dst_name_cb(const char dst_name[], void *arg);
static void put_continue(int force);
static char * make_conflict_title(CopyMoveLikeOp op);
static char * make_conflict_message(int max_w, int max_h, void *data);
static char * make_conflict_prompt(const conflict_prompt_data_t *data,
		int max_w, int *block_center);
static char * prettify_fname(const char full_path[], const struct stat *stat);
static char cmp_mark(int cmp);

/* Global state for file putting and name conflicts resolution that happen in
 * the process. */
static struct
{
	const reg_t *reg;    /* Register used for the operation. */
	int *file_order;     /* Defines custom ordering of files in register. */
	view_t *view;        /* View in which operation takes place. */
	CopyMoveLikeOp op;   /* Type of current operation. */
	int index;           /* Index of the next file of the register to process. */
	int processed;       /* Number of successfully processed files. */
	int skip_all;        /* Skip all conflicting files/directories. */
	int overwrite_all;   /* Overwrite all future conflicting files/directories. */
	int append;          /* Whether we're appending ending of a file or not. */
	int allow_merge;     /* Allow merging of files in directories. */
	int allow_merge_all; /* Allow merging of files in directories by default. */
	int merge;           /* Merge conflicting directory once. */
	int merge_all;       /* Merge all conflicting directories. */
	ops_t *ops;          /* Currently running operation. */
	char *dst_name;      /* Name of destination file. */
	char *dst_dir;       /* Destination path. */
	strlist_t put;       /* List of files that were successfully put. */
	char *last_conflict; /* Path to the file of the last conflict. */
}
put_confirm;

int
fops_put(view_t *view, int at, int reg_name, int move)
{
	const CopyMoveLikeOp op = move ? CMLO_MOVE : CMLO_COPY;
	const char *const descr = move ? "Putting" : "putting";
	return initiate_put_files(view, at, op, descr, reg_name);
}

int
fops_put_bg(view_t *view, int at, int reg_name, int move)
{
	char task_desc[COMMAND_GROUP_INFO_LEN];
	size_t task_desc_len;
	int i;
	bg_args_t *args;
	const char *const dst_dir = fops_get_dst_dir(view, at);

	/* Check that operation generally makes sense given our input. */

	if(!fops_view_can_be_extended(view, at))
	{
		return 0;
	}

	regs_sync_from_shared_memory();
	const reg_t *reg = regs_find(tolower(reg_name));
	if(reg == NULL || reg->nfiles < 1)
	{
		ui_sb_err(reg == NULL ? "No such register" : "Register is empty");
		return 1;
	}

	/* Prepare necessary data for background procedure and perform checks to
	 * ensure there will be no conflicts. */

	args = calloc(1, sizeof(*args));
	args->move = move;
	copy_str(args->path, sizeof(args->path), dst_dir);

	snprintf(task_desc, sizeof(task_desc), "%cut in %s: ", move ? 'P' : 'p',
			replace_home_part(dst_dir));
	task_desc_len = strlen(task_desc);
	for(i = 0; i < reg->nfiles; ++i)
	{
		char *const src = reg->files[i];
		const char *dst_name;
		char *dst;
		int j;

		chosp(src);

		if(!path_exists(src, NODEREF))
		{
			/* Skip nonexistent files. */
			continue;
		}

		fops_append_fname(task_desc, task_desc_len, src);
		task_desc_len = strlen(task_desc);

		args->sel_list_len = add_to_string_array(&args->sel_list,
				args->sel_list_len, src);

		dst_name = fops_get_dst_name(src, trash_has_path(src));

		/* Check that no destination files have the same name. */
		for(j = 0; j < args->nlines; ++j)
		{
			if(stroscmp(get_last_path_component(args->list[j]), dst_name) == 0)
			{
				ui_sb_errf("Two destination files have name \"%s\"", dst_name);
				fops_free_bg_args(args);
				return 1;
			}
		}

		dst = join_paths(args->path, dst_name);
		args->nlines = put_into_string_array(&args->list, args->nlines, dst);

		if(!paths_are_equal(src, dst) && path_exists(dst, NODEREF))
		{
			char *escaped_dst = escape_unreadable(dst);
			ui_sb_errf("File \"%s\" already exists", escaped_dst);
			free(escaped_dst);

			fops_free_bg_args(args);
			return 1;
		}
	}

	/* Initiate the operation. */

	args->ops = fops_get_bg_ops((args->move ? OP_MOVE : OP_COPY),
			move ? "Putting" : "putting", args->path);

	if(bg_execute(task_desc, "...", args->sel_list_len, 1, &put_files_in_bg,
				args) != 0)
	{
		fops_free_bg_args(args);

		show_error_msg("Can't put files",
				"Failed to initiate background operation");
	}

	return 0;
}

/* Entry point for background task that puts files. */
static void
put_files_in_bg(bg_op_t *bg_op, void *arg)
{
	size_t i;
	bg_args_t *const args = arg;
	ops_t *ops = args->ops;
	fops_bg_ops_init(ops, bg_op);

	if(ops->use_system_calls)
	{
		size_t i;
		bg_op_set_descr(bg_op, "estimating...");
		for(i = 0U; i < args->sel_list_len; ++i)
		{
			const char *const src = args->sel_list[i];
			const char *const dst = args->list[i];
			ops_enqueue(ops, src, dst);
		}
	}

	for(i = 0U; i < args->sel_list_len; ++i, ++bg_op->done)
	{
		struct stat src_st;
		const char *const src = args->sel_list[i];
		const char *const dst = args->list[i];

		if(paths_are_equal(src, dst))
		{
			/* Just ignore this file. */
			continue;
		}

		if(os_lstat(src, &src_st) != 0)
		{
			/* File isn't there, assume that it's fine and don't error in this
			 * case. */
			continue;
		}

		if(path_exists(dst, NODEREF))
		{
			/* This file wasn't here before (when checking in fops_put_bg()), won't
			 * overwrite. */
			continue;
		}

		bg_op_set_descr(bg_op, src);
		(void)perform_operation(ops->main_op, ops, NULL, src, dst);
	}

	fops_free_bg_args(args);
}

int
fops_put_links(view_t *view, int reg_name, int relative)
{
	const CopyMoveLikeOp op = relative ? CMLO_LINK_REL : CMLO_LINK_ABS;
	return initiate_put_files(view, -1, op, "Symlinking", reg_name);
}

/* Performs preparations necessary for putting files/links.  Returns new value
 * for save_msg flag. */
static int
initiate_put_files(view_t *view, int at, CopyMoveLikeOp op, const char descr[],
		int reg_name)
{
	int i;
	const char *const dst_dir = fops_get_dst_dir(view, at);

	if(!fops_view_can_be_extended(view, -1))
	{
		return 0;
	}

	regs_sync_from_shared_memory();
	const reg_t *reg = regs_find(tolower(reg_name));
	if(reg == NULL || reg->nfiles < 1)
	{
		ui_sb_err("Register is empty");
		return 1;
	}

	reset_put_confirm(op, descr, dst_dir);

	put_confirm.op = op;
	put_confirm.reg = reg;
	put_confirm.view = view;

	/* Map each element onto itself initially. */
	put_confirm.file_order = reallocarray(NULL, reg->nfiles,
			sizeof(*put_confirm.file_order));
	for(i = 0; i < reg->nfiles; ++i)
	{
		put_confirm.file_order[i] = i;
	}

	/* If clashes are harmful, order files by descending depth in the file system
	 * and then move all that will clash to the tail of the array. */
	if(op == CMLO_MOVE || op == CMLO_COPY)
	{
		safe_qsort(put_confirm.file_order, reg->nfiles,
				sizeof(put_confirm.file_order[0]), &path_depth_sort);

		/* We basically do partition by "clash" predicate moving all files for which
		 * it's true to the tail.  Mind that moved files end up in reverse order,
		 * which is beneficial for us as we can move larger sub-tree and discard
		 * some of other files. */
		int nclashes = 0;
		int i;
		for(i = 0; i < reg->nfiles - nclashes; ++i)
		{
			const int id = put_confirm.file_order[i];
			if(is_dir_clash(reg->files[id], dst_dir))
			{
				/* Rotate unvisited elements of the array in place. */
				memmove(&put_confirm.file_order[i], &put_confirm.file_order[i + 1],
						sizeof(id)*(reg->nfiles - nclashes - (i + 1)));
				put_confirm.file_order[reg->nfiles - 1 - nclashes++] = id;
				--i;
			}
		}
	}

	ui_cancellation_push_on();

	for(i = 0; i < reg->nfiles && !ui_cancellation_requested(); ++i)
	{
		ops_enqueue(put_confirm.ops, reg->files[i], dst_dir);
	}

	ui_cancellation_pop();

	return put_files_i(view, 1);
}

/* Resets state of global put_confirm variable in this module. */
static void
reset_put_confirm(CopyMoveLikeOp main_op, const char descr[],
		const char dst_dir[])
{
	fops_free_ops(put_confirm.ops);
	free(put_confirm.dst_name);
	free(put_confirm.dst_dir);
	free(put_confirm.file_order);
	free_string_array(put_confirm.put.items, put_confirm.put.nitems);
	free(put_confirm.last_conflict);

	memset(&put_confirm, 0, sizeof(put_confirm));

	put_confirm.dst_dir = strdup(dst_dir);
	put_confirm.ops = fops_get_ops(cmlo_to_op(main_op), descr, dst_dir, dst_dir);
}

/* Gets operation kind that corresponds to copy/move-like operation.  Returns
 * the kind. */
static OPS
cmlo_to_op(CopyMoveLikeOp op)
{
	switch(op)
	{
		case CMLO_COPY:
			return OP_COPY;
		case CMLO_MOVE:
			return OP_MOVE;
		case CMLO_LINK_REL:
		case CMLO_LINK_ABS:
			return OP_SYMLINK;

		default:
			assert(0 && "Unexpected operation type.");
			return OP_COPY;
	}
}

/* Compares two entries by the depth of their real paths.  Returns positive
 * value if one is greater than two, zero if they are equal, otherwise negative
 * value is returned. */
static int
path_depth_sort(const void *one, const void *two)
{
	const char *s = put_confirm.reg->files[*(const int *)one];
	const char *t = put_confirm.reg->files[*(const int *)two];

	char s_real[PATH_MAX + 1], t_real[PATH_MAX + 1];

	if(os_realpath(s, s_real) != s_real)
	{
		copy_str(s_real, sizeof(s_real), s);
	}
	if(os_realpath(t, t_real) != t_real)
	{
		copy_str(t_real, sizeof(t_real), t);
	}

	return SORT_CMP(chars_in_str(t_real, '/'), chars_in_str(s_real, '/'));
}

/* Checks whether moving the file into specified directory might potentially
 * result in loss of some other files scheduled for processing (because we
 * overwrite one of its parents).  Returns non-zero if so, otherwise zero is
 * returned. */
static int
is_dir_clash(const char src_path[], const char dst_dir[])
{
	char dst_path[PATH_MAX + 1];

	build_path(dst_path, sizeof(dst_path), dst_dir,
			fops_get_dst_name(src_path, trash_has_path(src_path)));
	chosp(dst_path);

	return is_dir(dst_path);
}

/* Returns new value for save_msg flag. */
static int
put_files_i(view_t *view, int start)
{
	if(start)
	{
		char undo_msg[COMMAND_GROUP_INFO_LEN + 1];
		const char *descr;
		const int from_trash =
			trash_has_path(put_confirm.reg->files[put_confirm.file_order[0]]);

		if(put_confirm.op == CMLO_LINK_ABS)
		{
			descr = "put absolute links";
		}
		else if(put_confirm.op == CMLO_LINK_REL)
		{
			descr = "put relative links";
		}
		else
		{
			descr = (put_confirm.op == CMLO_MOVE || from_trash) ? "Put" : "put";
		}

		snprintf(undo_msg, sizeof(undo_msg), "%s in %s: ", descr,
				replace_home_part(flist_get_dir(view)));
		un_group_open(undo_msg);
		un_group_close();
	}

	if(vifm_chdir(put_confirm.dst_dir) != 0)
	{
		show_error_msg("Directory Return", "Can't chdir() to current directory");
		return 1;
	}

	while(put_confirm.index < put_confirm.reg->nfiles)
	{
		int put_result;

		update_string(&put_confirm.dst_name, NULL);

		put_result = put_next(0);
		if(put_result > 0)
		{
			/* In this case put_next() takes care of interacting with a user. */
			return 0;
		}
		else if(put_result < 0)
		{
			break;
		}
		++put_confirm.index;
	}

	/* Free ops here when we're done, because as a side-effect it also prints
	 * error messages. */
	fops_free_ops(put_confirm.ops);
	put_confirm.ops = NULL;

	regs_pack(put_confirm.reg->name);
	update_cursor_position(view);
	ui_sb_msgf("%d file%s inserted%s", put_confirm.processed,
			(put_confirm.processed == 1) ? "" : "s", fops_get_cancellation_suffix());

	return 1;
}

/* Moves cursor to one of processed or conflicting files, if any. */
static void
update_cursor_position(view_t *view)
{
	if(put_confirm.last_conflict == NULL && put_confirm.put.nitems == 0)
	{
		/* Apparently, nothing has changed. */
		return;
	}

	populate_dir_list(view, 1);
	ui_view_schedule_redraw(view);

	if(put_confirm.last_conflict != NULL)
	{
		dir_entry_t *const entry = entry_from_path(view, view->dir_entry,
				view->list_rows, put_confirm.last_conflict);
		if(entry != NULL)
		{
			fpos_set_pos(view, entry_to_pos(view, entry));
		}
		return;
	}

	int i;
	int new_pos = -1;
	for(i = 0; i < put_confirm.put.nitems; ++i)
	{
		dir_entry_t *const entry = entry_from_path(view, view->dir_entry,
				view->list_rows, put_confirm.put.items[i]);
		if(entry != NULL)
		{
			const int pos = entry_to_pos(view, entry);
			if(new_pos == -1 || pos < new_pos)
			{
				new_pos = pos;
			}
		}
	}
	if(new_pos != -1)
	{
		fpos_set_pos(view, new_pos);
	}
}

/* The force argument enables overwriting/replacing/merging.  Returns 0 on
 * success, otherwise non-zero is returned where value greater then zero means
 * error already reported to the user, while negative one means unreported
 * error. */
static int
put_next(int force)
{
	char *filename;
	const char *dst_name;
	const char *const dst_dir = put_confirm.dst_dir;
	struct stat src_st;
	char src_buf[PATH_MAX + 1], dst_buf[PATH_MAX + 1];
	int from_trash;
	int op;
	int move;
	int success;
	int merge;
	int clashing_op = 0;

	/* TODO: refactor this function (put_next()) */

	if(ui_cancellation_requested())
	{
		return -1;
	}

	force = force || put_confirm.overwrite_all || put_confirm.merge_all;
	merge = put_confirm.merge || put_confirm.merge_all;

	filename = put_confirm.reg->files[put_confirm.file_order[put_confirm.index]];
	if(filename == NULL)
	{
		/* This file has been excluded from processing. */
		return 0;
	}

	chosp(filename);
	if(os_lstat(filename, &src_st) != 0)
	{
		/* File isn't there, assume that it's fine and don't error in this case. */
		return 0;
	}

	from_trash = trash_has_path(filename);
	move = from_trash || put_confirm.op == CMLO_MOVE;

	copy_str(src_buf, sizeof(src_buf), filename);

	dst_name = put_confirm.dst_name;
	if(dst_name == NULL)
	{
		dst_name = fops_get_dst_name(src_buf, from_trash);
	}

	build_path(dst_buf, sizeof(dst_buf), dst_dir, dst_name);
	chosp(dst_buf);

	if(!put_confirm.append && path_exists(dst_buf, NODEREF))
	{
		if(force)
		{
			struct stat dst_st;

			if(paths_are_equal(src_buf, dst_buf))
			{
				/* Skip if destination matches source. */
				return 0;
			}

			if(os_lstat(dst_buf, &dst_st) == 0 && (!merge ||
					S_ISDIR(dst_st.st_mode) != S_ISDIR(src_st.st_mode)))
			{
				if(S_ISDIR(dst_st.st_mode))
				{
					if(handle_clashing(move, src_buf, dst_buf))
					{
						return 1;
					}

					if(is_in_subtree(src_buf, dst_buf, 0))
					{
						/* Don't delete /a/b before moving /a/b/c to /a/b. */
						clashing_op = 1;
					}
				}

				if(!clashing_op && perform_operation(OP_REMOVESL, put_confirm.ops, NULL,
							dst_buf, NULL) != OPS_SUCCEEDED)
				{
					show_error_msgf("Can't replace a file",
							"Failed to remove a file:\n%s", dst_buf);
					return 0;
				}
				/* Schedule view update to reflect changes in UI. */
				ui_view_schedule_reload(put_confirm.view);
			}
			else if(!cfg.use_system_calls && get_env_type() == ET_UNIX)
			{
				remove_last_path_component(dst_buf);
			}
		}
		else if(put_confirm.skip_all)
		{
			return 0;
		}
		else if(put_confirm.op == CMLO_COPY || put_confirm.op == CMLO_MOVE)
		{
			struct stat dst_st;
			put_confirm.allow_merge = S_ISDIR(src_st.st_mode)
			                       && (os_lstat(dst_buf, &dst_st) == 0)
			                       && S_ISDIR(dst_st.st_mode);

			/* Enable "merge all" in conflict resolution options if at least one of
			 * source paths left to process (including current) is a directory. */
			put_confirm.allow_merge_all = unprocessed_dirs_present();

			prompt_what_to_do(dst_name, src_buf);
			return 1;
		}
		else
		{
			/* Merging makes sense only for copy and move. */
			put_confirm.allow_merge = 0;
			put_confirm.allow_merge_all = 0;
			prompt_what_to_do(dst_name, src_buf);
			return 1;
		}
	}

	if(put_confirm.op == CMLO_LINK_REL || put_confirm.op == CMLO_LINK_ABS)
	{
		op = OP_SYMLINK;
		if(put_confirm.op == CMLO_LINK_REL)
		{
			copy_str(src_buf, sizeof(src_buf), make_rel_path(filename, dst_dir));
		}
	}
	else if(put_confirm.append)
	{
		op = move ? OP_MOVEA : OP_COPYA;
		put_confirm.append = 0;
	}
	else if(move)
	{
		op = merge ? OP_MOVEF : OP_MOVE;
	}
	else
	{
		op = merge ? OP_COPYF : OP_COPY;
	}

	fops_progress_msg("Putting files", put_confirm.index,
			put_confirm.reg->nfiles);

	/* Merging directory on move requires special handling as it can't be done by
	 * `mv` and it's better to use the same code regardless of the state of
	 * 'syscalls'. */
	if(move && merge)
	{
		char dst_path[PATH_MAX + 1];

		un_group_reopen_last();

		build_path(dst_path, sizeof(dst_path), dst_dir, dst_name);

		OpsResult result = merge_dirs(src_buf, dst_path, put_confirm.ops);
		success = (result != OPS_FAILED);
		if(result == OPS_SKIPPED)
		{
			char *pretty_src = prettify_fname(src_buf, &src_st);
			show_error_msgf("Directory merge notice",
					"Source directory \"%s\" still exists and contains files which "
					"weren't moved.", pretty_src);
			free(pretty_src);
		}

		un_group_close();
	}
	else if(clashing_op)
	{
		/* We're replacing parent with its child.  To make this possible use a
		 * temporary path next to the parent first, remove the parent, then move
		 * data to where the parent was. */
		const char *const unique_dst = make_name_unique(dst_buf);

		/* An optimization: if we're going to remove original anyway, don't bother
		 * copying it, just move. */
		if(op == OP_COPY || op == OP_COPYF)
		{
			op = OP_MOVE;
		}

		success = (perform_operation(op, put_confirm.ops, NULL, src_buf,
					unique_dst) == OPS_SUCCEEDED);
		if(success)
		{
			success = (perform_operation(OP_REMOVESL, put_confirm.ops, NULL, dst_buf,
						NULL) == OPS_SUCCEEDED);
		}
		if(success)
		{
			success = (perform_operation(OP_MOVE, put_confirm.ops, NULL, unique_dst,
						dst_buf) == OPS_SUCCEEDED);
		}
	}
	else
	{
		success = (perform_operation(op, put_confirm.ops, NULL, src_buf,
					dst_buf) == OPS_SUCCEEDED);
	}

	ops_advance(put_confirm.ops, success);

	if(success)
	{
		char *msg, *p;
		size_t len;

		/* For some reason "mv" sometimes returns 0 on cancellation. */
		if(!path_exists(dst_buf, NODEREF))
		{
			return -1;
		}

		un_group_reopen_last();

		msg = un_replace_group_msg(NULL);
		len = strlen(msg);
		p = realloc(msg, COMMAND_GROUP_INFO_LEN);
		if(p == NULL)
			len = COMMAND_GROUP_INFO_LEN;
		else
			msg = p;

		snprintf(msg + len, COMMAND_GROUP_INFO_LEN - len, "%s%s",
				(msg[len - 2] != ':') ? ", " : "", dst_name);
		un_replace_group_msg(msg);
		free(msg);

		if(!(move && merge))
		{
			un_group_add_op(op, NULL, NULL, src_buf, dst_buf);
		}

		un_group_close();
		++put_confirm.processed;
		if(move)
		{
			update_string(
					&put_confirm.reg->files[put_confirm.file_order[put_confirm.index]],
					NULL);
		}

		char dst_path[PATH_MAX + 1];
		build_path(dst_path, sizeof(dst_path), dst_dir, dst_name);
		put_confirm.put.nitems = add_to_string_array(&put_confirm.put.items,
				put_confirm.put.nitems, dst_path);
	}

	return 0;
}

/* Checks whether current or any other unprocessed paths point to directories.
 * Returns non-zero if so, otherwise zero is returned. */
static int
unprocessed_dirs_present(void)
{
	int i;
	for(i = put_confirm.index; i < put_confirm.reg->nfiles; ++i)
	{
		const char *path = put_confirm.reg->files[put_confirm.file_order[i]];
		if(path == NULL)
		{
			/* This file has been excluded from processing. */
			continue;
		}

		struct stat src_st;
		if(os_lstat(path, &src_st) == 0 && S_ISDIR(src_st.st_mode))
		{
			return 1;
		}
	}
	return 0;
}

/* Merges src into dst.  Returns status where OPS_SKIPPED means that some source
 * files weren't moved. */
TSTATIC OpsResult
merge_dirs(const char src[], const char dst[], ops_t *ops)
{
	struct stat st;
	if(os_stat(src, &st) != 0)
	{
		return -1;
	}

	DIR *dir = os_opendir(src);
	if(dir == NULL)
	{
		return -1;
	}

	/* Make sure target directory exists.  Ignore error as we don't care whether
	 * it existed before we try to create it and following operations will fail
	 * if we can't create this directory for some reason. */
	(void)perform_operation(OP_MKDIR, NULL, (void *)(size_t)1, dst, NULL);

	int skipped = 0;
	OpsResult result = OPS_SUCCEEDED;

	struct dirent *d;
	while(result != OPS_FAILED && (d = os_readdir(dir)) != NULL)
	{
		char src_path[PATH_MAX + 1];
		char dst_path[PATH_MAX + 1];

		if(is_builtin_dir(d->d_name))
		{
			continue;
		}

		build_path(src_path, sizeof(src_path), src, d->d_name);
		build_path(dst_path, sizeof(dst_path), dst, d->d_name);

		if(fops_is_dir_entry(dst_path, d))
		{
			result = merge_dirs(src_path, dst_path, ops);
		}
		else
		{
			result = perform_operation(OP_MOVEF, put_confirm.ops, NULL, src_path,
					dst_path);
			if(result == OPS_SUCCEEDED)
			{
				un_group_add_op(OP_MOVEF, put_confirm.ops, NULL, src_path, dst_path);
			}
		}

		if(result == OPS_SKIPPED)
		{
			++skipped;
		}
	}
	os_closedir(dir);

	if(d != NULL)
	{
		return 1;
	}

	/* Directory removal will fail unless it contains no files. */
	if(skipped == 0)
	{
		result = perform_operation(OP_RMDIR, put_confirm.ops, NULL, src, NULL);
		if(result == OPS_SUCCEEDED)
		{
			un_group_add_op(OP_RMDIR, NULL, NULL, src, "");
		}
	}

	/* Clone file properties as the last step, because modifying directory affects
	 * timestamps and permissions can affect operations. */
	clone_attribs(dst, src, &st);
	(void)chmod(dst, st.st_mode);

	if(result == OPS_SUCCEEDED && skipped > 0)
	{
		result = OPS_SKIPPED;
	}

	return result;
}

/* Goes through the rest of files in the register to see whether they are inside
 * path that we're going to overwrite or move and asks/warns the user if
 * necessary.  Returns non-zero on abort, otherwise zero is returned. */
static int
handle_clashing(int move, const char src[], const char dst[])
{
	int i;
	vle_textbuf *const lost = vle_tb_create();
	vle_textbuf *const to_exclude = vle_tb_create();

	for(i = put_confirm.index + 1; i < put_confirm.reg->nfiles; ++i)
	{
		const char *const another_src =
			put_confirm.reg->files[put_confirm.file_order[i]];
		const int sub_path = is_in_subtree(another_src, src, 0);
		if(is_in_subtree(another_src, dst, 0) && !sub_path)
		{
			vle_tb_append_line(lost, replace_home_part(another_src));
		}
		if(sub_path)
		{
			vle_tb_append_line(to_exclude, replace_home_part(another_src));
		}
	}

	if(*vle_tb_get_data(lost) != '\0')
	{
		int i;
		char msg[PATH_MAX + 1];
		response_variant responses[] = {
			{ .key = 'y', .descr = "[y]es /" },
			{ .key = 'n', .descr = " [n]o\n" },
			{ .key = NC_C_c, .descr = "\nEsc or Ctrl-C to abort" },
			{}
		};

		/* Screen needs to be restored after displaying progress dialog. */
		modes_update();

		snprintf(msg, sizeof(msg), "Overwriting\n%s\nwith\n%s\nwill result "
				"in loss of the following files.  Are you sure?\n%s",
				replace_home_part(dst), replace_home_part(src), vle_tb_get_data(lost));

		const custom_prompt_t prompt = {
			.title = "Possible data loss",
			.message = msg,
			.variants = responses,
		};
		switch(fops_options_prompt(&prompt))
		{
			case 'y':
				/* Do nothing. */
				break;
			case 'n':
				prompt_what_to_do(get_last_path_component(dst), src);
				/* Fall through. */
			case NC_C_c:
				vle_tb_free(to_exclude);
				vle_tb_free(lost);
				return 1;
		}

		for(i = put_confirm.index + 1; i < put_confirm.reg->nfiles; ++i)
		{
			char **const another_src =
				&put_confirm.reg->files[put_confirm.file_order[i]];
			const int sub_path = is_in_subtree(*another_src, src, 0);
			if(is_in_subtree(*another_src, dst, 0) && !sub_path)
			{
				update_string(another_src, NULL);
			}
		}
	}

	if(*vle_tb_get_data(to_exclude) != '\0')
	{
		int i;

		show_error_msgf("Operation Warning",
				"The following files got excluded from further processing:\n%s",
				vle_tb_get_data(to_exclude));

		for(i = put_confirm.index + 1; i < put_confirm.reg->nfiles; ++i)
		{
			char **const another_src =
				&put_confirm.reg->files[put_confirm.file_order[i]];
			if(is_in_subtree(*another_src, src, 0))
			{
				update_string(another_src, NULL);
			}
		}
	}

	vle_tb_free(to_exclude);
	vle_tb_free(lost);

	return 0;
}

/* Prompt user for conflict resolution strategy about given filename. */
static void
prompt_what_to_do(const char fname[], const char caused_by[])
{
	/* Strange spacing is for left alignment.  Doesn't look nice here, but it is
	 * problematic to get such alignment otherwise. */
	static const response_variant
		rename         = { .key = 'r', .descr = "[r]ename (also Enter)\n" },
		enter          = { .key = '\r', .descr = "" },
		skip           = { .key = 's', .descr = "[s]kip /" },
		skip_all       = { .key = 'S', .descr = " [S]kip all\n" },
		append         = { .key = 'a', .descr = "[a]ppend the tail\n" },
		overwrite      = { .key = 'o', .descr = "[o]verwrite /" },
		overwrite_all  = { .key = 'O', .descr = " [O]verwrite all\n" },
		merge          = { .key = 'm', .descr = "[m]erge /" },
		merge_all      = { .key = 'M', .descr = " [M]erge all\n" },
		merge_all_only = { .key = 'M', .descr = "[M]erge all\n" },
		escape         = { .key = NC_C_c, .descr = "\n   Esc or Ctrl-C to abort" };

	/* Last element is a terminator. */
	response_variant responses[12] = {};
	size_t i = 0;

	char dst_buf[PATH_MAX + 1];
	build_path(dst_buf, sizeof(dst_buf), put_confirm.dst_dir, fname);
	const int same_file = paths_are_equal(dst_buf, caused_by);

	responses[i++] = rename;
	responses[i++] = enter;

	responses[i++] = skip;
	responses[i++] = skip_all;

	if(!same_file)
	{
		if(cfg.use_system_calls && is_regular_file_noderef(dst_buf) &&
				is_regular_file_noderef(caused_by))
		{
			responses[i++] = append;
		}
		responses[i++] = overwrite;
		responses[i++] = overwrite_all;
		if(put_confirm.allow_merge)
		{
			responses[i++] = merge;
		}
		if(put_confirm.allow_merge_all)
		{
			responses[i++] = (put_confirm.allow_merge ? merge_all : merge_all_only);
		}
	}

	responses[i++] = escape;
	assert(i + 1 <= ARRAY_LEN(responses) && "Array is too small.");

	/* Screen needs to be restored after displaying progress dialog. */
	modes_update();

	/* Invoking make_conflict_prompt() to get block_center flag and validate the
	 * operation (we're not showing the prompt if NULL is returned). */
	conflict_prompt_data_t prompt_data = {
		.src = caused_by,
		.dst = dst_buf,
	};
	int block_center;
	char *msg =
		make_conflict_prompt(&prompt_data, /*max_w=*/INT_MAX, &block_center);
	free(msg);

	char response = NC_C_c;
	char *title = make_conflict_title(put_confirm.op);
	if(title != NULL && msg != NULL)
	{
		const custom_prompt_t prompt = {
			.title = title,
			.make_message = &make_conflict_message,
			.user_data = &prompt_data,
			.variants = responses,
			.block_center = block_center,
		};
		response = fops_options_prompt(&prompt);
	}
	free(title);

	handle_prompt_response(fname, caused_by, response);
}

/* Handles response to the prompt asked by prompt_what_to_do(). */
static void
handle_prompt_response(const char fname[], const char caused_by[],
		char response)
{
	char dst_path[PATH_MAX + 1];
	build_path(dst_path, sizeof(dst_path), put_confirm.dst_dir, fname);

	/* Record last conflict to position cursor at it later. */
	update_string(&put_confirm.last_conflict, dst_path);

	if(response == '\r' || response == 'r')
	{
		prompt_dst_name(fname);
	}
	else if(response == 's' || response == 'S')
	{
		if(response == 'S')
		{
			put_confirm.skip_all = 1;
		}

		++put_confirm.index;
		curr_stats.save_msg = put_files_i(put_confirm.view, 0);
	}
	else if(response == 'o')
	{
		put_continue(1);
	}
	else if(response == 'a' && cfg.use_system_calls && !is_dir(fname))
	{
		put_confirm.append = 1;
		put_continue(0);
	}
	else if(response == 'O')
	{
		put_confirm.overwrite_all = 1;
		put_continue(1);
	}
	else if(put_confirm.allow_merge && response == 'm')
	{
		put_confirm.merge = 1;
		put_continue(1);
	}
	else if(put_confirm.allow_merge_all && response == 'M')
	{
		put_confirm.merge_all = 1;
		put_continue(1);
	}
	else if(response == NC_C_c)
	{
		/* After user cancels at conflict resolution, put the cursor at the last
		 * file that caused the conflict. */
		dir_entry_t *const entry = entry_from_path(put_confirm.view,
				put_confirm.view->dir_entry, put_confirm.view->list_rows, dst_path);
		if(entry != NULL)
		{
			fpos_set_pos(put_confirm.view, entry_to_pos(put_confirm.view, entry));
		}
	}
	else
	{
		prompt_what_to_do(fname, caused_by);
	}
}

/* Prompts user for new name for the file being inserted.  Callback based. */
static void
prompt_dst_name(const char src_name[])
{
	char prompt[128 + PATH_MAX];

	snprintf(prompt, ARRAY_LEN(prompt), "New name for %s: ", src_name);
	fops_line_prompt(prompt, src_name, &prompt_dst_name_cb, /*cb_arg=*/NULL,
			/*complete=*/NULL);
}

/* Callback for line prompt result. */
static void
prompt_dst_name_cb(const char dst_name[], void *arg)
{
	if(is_null_or_empty(dst_name))
	{
		return;
	}

	/* Record new destination path. */
	char dst_path[PATH_MAX + 1];
	build_path(dst_path, sizeof(dst_path), put_confirm.dst_dir, dst_name);
	update_string(&put_confirm.last_conflict, dst_path);

	if(replace_string(&put_confirm.dst_name, dst_name) != 0)
	{
		show_error_msg("Memory Error", "Unable to allocate enough memory");
		return;
	}

	if(put_next(0) == 0)
	{
		++put_confirm.index;
		curr_stats.save_msg = put_files_i(put_confirm.view, 0);
	}
}

/* Continues putting files. */
static void
put_continue(int force)
{
	if(put_next(force) == 0)
	{
		++put_confirm.index;
		curr_stats.save_msg = put_files_i(put_confirm.view, 0);
	}
}

/* Formats a title for conflict message.  Returns newly allocated string. */
static char *
make_conflict_title(CopyMoveLikeOp op)
{
	const char *action = "?";
	switch(op)
	{
		case CMLO_COPY:
			action = "Copying";
			break;
		case CMLO_MOVE:
			action = "Moving";
			break;
		case CMLO_LINK_REL:
		case CMLO_LINK_ABS:
			action = "Symlinking";
			break;
	}

	return format_str("File Conflict on %s", action);
}

/* Adapts make_conflict_prompt() for use with fops_options_prompt().  Returns
 * newly allocated string. */
static char *
make_conflict_message(int max_w, int max_h, void *data)
{
	int block_center;
	return make_conflict_prompt(data, max_w, &block_center);
}

/* Produces text for a conflict prompt.  Returns newly allocated string. */
static char *
make_conflict_prompt(const conflict_prompt_data_t *data, int max_w,
		int *block_center)
{
	*block_center = 1;

	struct stat src;
	if(os_lstat(data->src, &src) != 0)
	{
		show_error_msgf("Conflict handling error", "Unable to query metadata of %s",
				data->src);
		return NULL;
	}

	char *pretty_src = prettify_fname(data->src, &src);
	if(pretty_src == NULL)
	{
		return NULL;
	}

	if(paths_are_equal(data->src, data->dst))
	{
		*block_center = 0;
		char *text =
			format_str("Same file is both source and destination:\n%s", pretty_src);
		free(pretty_src);
		return text;
	}

	struct stat dst;
	if(os_lstat(data->dst, &dst) != 0)
	{
		show_error_msgf("Conflict handling error", "Unable to query metadata of %s",
				data->dst);
		free(pretty_src);
		return NULL;
	}

	char *pretty_dst = prettify_fname(data->dst, &dst);
	if(pretty_dst == NULL)
	{
		free(pretty_src);
		return NULL;
	}

	char src_size[64], dst_size[64];
	(void)friendly_size_notation(src.st_size, sizeof(src_size), src_size);
	(void)friendly_size_notation(dst.st_size, sizeof(dst_size), dst_size);

	char src_bsize[64], dst_bsize[64];
	snprintf(src_bsize, sizeof(src_bsize), " (%" PRINTF_ULL ")",
			(unsigned long long)src.st_size);
	snprintf(dst_bsize, sizeof(dst_bsize), " (%" PRINTF_ULL ")",
			(unsigned long long)dst.st_size);

	char src_mtime[64], dst_mtime[64];
	format_iso_time(src.st_mtime, src_mtime, sizeof(src_mtime));
	format_iso_time(dst.st_mtime, dst_mtime, sizeof(dst_mtime));

	int fields[3] = { };
	fields[0] = MAX(strlen(src_size), strlen(dst_size));
	fields[1] = MAX(strlen(src_bsize), strlen(dst_bsize));
	fields[2] = MAX(utf8_strsw(src_mtime), utf8_strsw(dst_mtime));

	const char *info_fmt = "      %c %*s%*s   %c %*s";
	int l = snprintf(NULL, 0, info_fmt,
			'x', fields[0], "", fields[1], "",
			'y', fields[2], "");
	if(l > max_w)
	{
		info_fmt = "      %c %*s%*s\n      %c %*s";
	}

#define CMP(a, b) ((a) == (b) ? 0 : (a) < (b) ? -1 : 1)
	int size_cmp = CMP(src.st_size, dst.st_size);
	int mtime_cmp = CMP(src.st_mtime, dst.st_mtime);
#undef CMP

	vle_textbuf *text = vle_tb_create();

	vle_tb_append_linef(text, "From: %s", pretty_src);
	vle_tb_append_linef(text, info_fmt,
			cmp_mark(size_cmp), fields[0], src_size, fields[1], src_bsize,
			cmp_mark(mtime_cmp), fields[2], src_mtime);

	vle_tb_append_line(text, " ");

	vle_tb_append_linef(text, "  To: %s", pretty_dst);
	vle_tb_append_linef(text, info_fmt,
			cmp_mark(-size_cmp), fields[0], dst_size, fields[1], dst_bsize,
			cmp_mark(-mtime_cmp), fields[2], dst_mtime);

	free(pretty_src);
	free(pretty_dst);

	return vle_tb_release(text);
}

/* Prepares file name for being displayed in a dialog.  Returns newly allocated
 * string. */
static char *
prettify_fname(const char full_path[], const struct stat *stat)
{
	char *pretty = escape_unreadable(replace_home_part(full_path));
	if(pretty != NULL && S_ISDIR(stat->st_mode))
	{
		put_string(&pretty, format_str("%s/", pretty));
	}
	return pretty;
}

/* Translates comparison result to one of three marks: <, =, >.  Returns the
 * mark. */
static char
cmp_mark(int cmp)
{
	return (cmp == 0 ? '=' : cmp < 0 ? '<' : '>');
}

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