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

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

#include "utils/utf8.h"
#endif

#include <sys/stat.h> /* gid_t uid_t */

#include <assert.h> /* assert() */
#include <stddef.h> /* NULL size_t */
#include <stdio.h> /* snprintf() */
#include <stdlib.h> /* calloc() free() */
#include <string.h> /* strdup() */

#include "cfg/config.h"
#include "compat/fs_limits.h"
#include "compat/os.h"
#include "compat/reallocarray.h"
#include "io/ioeta.h"
#include "io/iop.h"
#include "io/ior.h"
#include "lua/vlua.h"
#include "modes/dialogs/msg_dialog.h"
#include "ui/cancellation.h"
#include "utils/cancellation.h"
#include "utils/fs.h"
#include "utils/log.h"
#include "utils/macros.h"
#include "utils/path.h"
#include "utils/str.h"
#include "utils/utils.h"
#include "background.h"
#include "bmarks.h"
#include "status.h"
#include "trash.h"

#ifdef SUPPORT_NO_CLOBBER
#define NO_CLOBBER "-n"
#else
#define NO_CLOBBER ""
#endif

/* Enable O(1) file cloning if it's available in installed version of
 * coreutils. */
#ifdef SUPPORT_REFLINK_AUTO
#define REFLINK_AUTO "--reflink=auto"
#else
#define REFLINK_AUTO ""
#endif

#define PRESERVE_FLAGS "-p"

/* Types of conflict resolution actions to perform. */
typedef enum
{
	CA_FAIL,      /* Fail with an error. */
	CA_OVERWRITE, /* Overwrite existing files. */
	CA_APPEND,    /* Append the rest of source file to destination file. */
}
ConflictAction;

/* Type of function that implements single operation. */
typedef OpsResult (*op_func)(ops_t *ops, void *data, const char src[],
		const char dst[]);

static OpsResult op_none(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_remove(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_removesl(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_copy(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_copyf(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_copya(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_cp(ops_t *ops, void *data, const char src[],
		const char dst[], ConflictAction conflict_action);
static OpsResult op_move(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_movef(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_movea(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_mv(ops_t *ops, void *data, const char src[],
		const char dst[], ConflictAction conflict_action);
static IoCrs ca_to_crs(ConflictAction conflict_action);
static OpsResult op_chown(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_chgrp(ops_t *ops, void *data, const char src[],
		const char dst[]);
#ifndef _WIN32
static OpsResult op_chmod(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_chmodr(ops_t *ops, void *data, const char src[],
		const char dst[]);
#else
static OpsResult op_addattr(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_subattr(ops_t *ops, void *data, const char src[],
		const char dst[]);
#endif
static OpsResult op_symlink(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_mkdir(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_rmdir(ops_t *ops, void *data, const char src[],
		const char dst[]);
static OpsResult op_mkfile(ops_t *ops, void *data, const char src[],
		const char dst[]);
static int ops_uses_syscalls(const ops_t *ops);
static ShellType ops_shell_type(const ops_t *ops);
static OpsResult exec_io_op(ops_t *ops, IoRes (*func)(io_args_t *),
		io_args_t *args, int cancellable);
static int confirm_overwrite(io_args_t *args, const char src[],
		const char dst[]);
static char * pretty_dir_path(const char path[]);
static IoErrCbResult dispatch_error(io_args_t *args, const ioe_err_t *err);
static char prompt_user(const io_args_t *args, const char title[],
		const char msg[], const response_variant variants[]);
static int ui_cancellation_hook(void *arg);
#ifndef _WIN32
static OpsResult run_operation_command(ops_t *ops, char cmd[], int cancellable);
#endif
static int ops_runs_in_bg(const ops_t *ops);
static int bg_cancellation_hook(void *arg);
static OpsResult result_from_code(int exit_code);

/* List of functions that implement operations. */
static op_func op_funcs[] = {
	[OP_NONE]     = &op_none,
	[OP_USR]      = &op_none,
	[OP_REMOVE]   = &op_remove,
	[OP_REMOVESL] = &op_removesl,
	[OP_COPY]     = &op_copy,
	[OP_COPYF]    = &op_copyf,
	[OP_COPYA]    = &op_copya,
	[OP_MOVE]     = &op_move,
	[OP_MOVEF]    = &op_movef,
	[OP_MOVEA]    = &op_movea,
	[OP_MOVETMP1] = &op_move,
	[OP_MOVETMP2] = &op_move,
	[OP_MOVETMP3] = &op_move,
	[OP_MOVETMP4] = &op_move,
	[OP_CHOWN]    = &op_chown,
	[OP_CHGRP]    = &op_chgrp,
#ifndef _WIN32
	[OP_CHMOD]    = &op_chmod,
	[OP_CHMODR]   = &op_chmodr,
#else
	[OP_ADDATTR]  = &op_addattr,
	[OP_SUBATTR]  = &op_subattr,
#endif
	[OP_SYMLINK]  = &op_symlink,
	[OP_SYMLINK2] = &op_symlink,
	[OP_MKDIR]    = &op_mkdir,
	[OP_RMDIR]    = &op_rmdir,
	[OP_MKFILE]   = &op_mkfile,
};
ARRAY_GUARD(op_funcs, OP_COUNT);

/* Operation that is processed at the moment. */
static ops_t *curr_ops;

ops_t *
ops_alloc(OPS main_op, int bg, const char descr[], const char base_dir[],
		const char target_dir[], ops_choice_func choose, ops_confirm_func confirm)
{
	ops_t *const ops = calloc(1, sizeof(*ops));

	ops->main_op = main_op;
	ops->descr = descr;
	ops->bg = bg;

	update_string(&ops->slow_fs_list, cfg.slow_fs_list);
	update_string(&ops->delete_prg, cfg.delete_prg);
	ops->use_system_calls = cfg.use_system_calls;
	ops->fast_file_cloning = cfg.fast_file_cloning;
	ops->data_sync = cfg.data_sync;
	ops->shell_type = curr_stats.shell_type;

	ops->choose = choose;
	ops->confirm = confirm;

	ops->base_dir = strdup(base_dir);
	ops->target_dir = strdup(target_dir);

	return ops;
}

const char *
ops_describe(const ops_t *ops)
{
	return ops->descr;
}

void
ops_enqueue(ops_t *ops, const char src[], const char dst[])
{
	++ops->total;

	if(ops->estim == NULL)
	{
		return;
	}

	/* Check once and cache result, it should be the same for each invocation. */
	if(ops->estim->total_items == 0)
	{
		switch(ops->main_op)
		{
			case OP_MOVE:
			case OP_MOVEF:
			case OP_MOVETMP1:
			case OP_MOVETMP2:
			case OP_MOVETMP3:
			case OP_MOVETMP4:
				if(dst != NULL && are_on_the_same_fs(src, dst))
				{
					/* Moving files/directories inside file system is cheap operation on
					 * top level items, no need to recur below. */
					ops->shallow_eta = 1;
				}
				break;

			case OP_SYMLINK:
			case OP_SYMLINK2:
				/* No need for recursive traversal if we're going to create symbolic
				 * links. */
				ops->shallow_eta = 1;
				break;

			default:
				/* No optimizations for other operations. */
				break;
		}

		if(is_on_slow_fs(src, ops->slow_fs_list))
		{
			ops->shallow_eta = 1;
		}
	}

	ioeta_calculate(ops->estim, src, ops->shallow_eta);
}

void
ops_advance(ops_t *ops, int succeeded)
{
	++ops->current;
	assert(ops->current <= ops->total && "Current and total are out of sync.");

	if(succeeded)
	{
		++ops->succeeded;
	}
}

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

	ioeta_free(ops->estim);
	free(ops->errors);
	free(ops->slow_fs_list);
	free(ops->delete_prg);
	free(ops->base_dir);
	free(ops->target_dir);
	free(ops);
}

OpsResult
perform_operation(OPS op, ops_t *ops, void *data, const char src[],
		const char dst[])
{
	if(ops_runs_in_bg(ops))
	{
		/* Not reporting events from background jobs. */
		return op_funcs[op](ops, data, src, dst);
	}

	int dir = 0;
	if(ONE_OF(op, OP_MKDIR, OP_RMDIR))
	{
		dir = 1;
	}
	else if(!ONE_OF(op, OP_NONE, OP_USR, OP_MKFILE, OP_SYMLINK, OP_SYMLINK2))
	{
		dir = is_dir(src);
	}

	OpsResult status = op_funcs[op](ops, data, src, dst);
	if(status == OPS_SUCCEEDED)
	{
		vlua_events_app_fsop(curr_stats.vlua, op, src, dst, data, dir);
	}
	return status;
}

static OpsResult
op_none(ops_t *ops, void *data, const char src[], const char dst[])
{
	return OPS_SUCCEEDED;
}

static OpsResult
op_remove(ops_t *ops, void *data, const char src[], const char dst[])
{
	if((ops == NULL || !ops->bg) && cfg_confirm_delete(0) &&
			!curr_stats.confirmed)
	{
		ops_confirm_func confirm = (ops != NULL ? ops->confirm : &prompt_msg);

		char *msg = format_str("Are you sure?  "
				"At least the following file is about to be deleted:\n \n%s\n \n"
				"If you're undoing a command and want to see file names, use "
				":undolist! command.",
				replace_home_part(src));
		curr_stats.confirmed = confirm("Permanent deletion", msg);
		free(msg);
		if(!curr_stats.confirmed)
			return OPS_SKIPPED;
	}

	return op_removesl(ops, data, src, dst);
}

static OpsResult
op_removesl(ops_t *ops, void *data, const char src[], const char dst[])
{
	const char *const delete_prg = (ops == NULL)
	                             ? cfg.delete_prg
	                             : ops->delete_prg;
	if(!is_null_or_empty(delete_prg))
	{
#ifndef _WIN32
		char *escaped;
		char cmd[2*PATH_MAX + 1];
		const int cancellable = (data == NULL);

		escaped = shell_arg_escape(src, ops_shell_type(ops));
		if(escaped == NULL)
		{
			return OPS_FAILED;
		}

		snprintf(cmd, sizeof(cmd), "%s %s", delete_prg, escaped);
		free(escaped);

		LOG_INFO_MSG("Running trash command: \"%s\"", cmd);
		return run_operation_command(ops, cmd, cancellable);
#else
		char *src_copy = strdup(src);
		internal_to_system_slashes(src_copy);

		char *escaped_src = shell_arg_escape(src_copy, ST_CMD);
		free(src_copy);

		char cmd[PATH_MAX*2 + 1];
		snprintf(cmd, sizeof(cmd), "%s %s", delete_prg, escaped_src);
		free(escaped_src);

		return result_from_code(os_system(cmd));
#endif
	}

	if(!ops_uses_syscalls(ops))
	{
#ifndef _WIN32
		char *escaped;
		char cmd[16 + PATH_MAX];
		const int cancellable = data == NULL;

		escaped = shell_arg_escape(src, ops_shell_type(ops));
		if(escaped == NULL)
			return OPS_FAILED;

		snprintf(cmd, sizeof(cmd), "rm -rf %s", escaped);
		LOG_INFO_MSG("Running rm command: \"%s\"", cmd);
		OpsResult result = run_operation_command(ops, cmd, cancellable);

		free(escaped);
		return result;
#else
		if(is_dir(src))
		{
			char path[PATH_MAX + 1];
			int err;

			copy_str(path, sizeof(path), src);
			internal_to_system_slashes(path);

			wchar_t *utf16_path = utf8_to_utf16(path);

			/* SHFileOperationW requires pFrom to be double-nul terminated. */
			const size_t len = wcslen(utf16_path);
			wchar_t *new_utf16_path = reallocarray(utf16_path, len + 1U + 1U,
					sizeof(*utf16_path));
			if(new_utf16_path == NULL)
			{
				free(utf16_path);
				return OPS_FAILED;
			}

			utf16_path = new_utf16_path;
			utf16_path[len + 1U] = L'\0';

			SHFILEOPSTRUCTW fo = {
				.hwnd = NULL,
				.wFunc = FO_DELETE,
				.pFrom = utf16_path,
				.pTo = NULL,
				.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI,
			};
			err = SHFileOperationW(&fo);

			log_msg("Error: %d", err);
			free(utf16_path);

			return result_from_code(err);
		}
		else
		{
			wchar_t *const utf16_path = utf8_to_utf16(src);
			DWORD attributes = GetFileAttributesW(utf16_path);
			if(attributes & FILE_ATTRIBUTE_READONLY)
			{
				SetFileAttributesW(utf16_path, attributes & ~FILE_ATTRIBUTE_READONLY);
			}

			BOOL success = DeleteFileW(utf16_path);
			if(!success)
			{
				LOG_WERROR(GetLastError());
			}

			free(utf16_path);
			return result_from_code(!success);
		}
#endif
	}

	io_args_t args = {
		.arg1.path = src,
	};
	return exec_io_op(ops, &ior_rm, &args, data == NULL);
}

/* OP_COPY operation handler.  Copies file/directory without overwriting
 * destination files (when it's supported by the system).  Returns status. */
static OpsResult
op_copy(ops_t *ops, void *data, const char src[], const char dst[])
{
	return op_cp(ops, data, src, dst, CA_FAIL);
}

/* OP_COPYF operation handler.  Copies file/directory overwriting destination
 * files.  Returns status. */
static OpsResult
op_copyf(ops_t *ops, void *data, const char src[], const char dst[])
{
	return op_cp(ops, data, src, dst, CA_OVERWRITE);
}

/* OP_COPYA operation handler.  Copies file appending rest of the source file to
 * the destination.  Returns status. */
static OpsResult
op_copya(ops_t *ops, void *data, const char src[], const char dst[])
{
	return op_cp(ops, data, src, dst, CA_APPEND);
}

/* Copies file/directory overwriting/appending destination files if requested.
 * Returns status. */
static OpsResult
op_cp(ops_t *ops, void *data, const char src[], const char dst[],
		ConflictAction conflict_action)
{
	const int fast_file_cloning = (ops == NULL)
	                             ? cfg.fast_file_cloning
	                             : ops->fast_file_cloning;
	const int data_sync = (ops == NULL ? cfg.data_sync : ops->data_sync);

	if(!ops_uses_syscalls(ops))
	{
#ifndef _WIN32
		char *escaped_src, *escaped_dst;
		char cmd[6 + PATH_MAX*2 + 1];
		const int cancellable = (data == NULL);

		escaped_src = shell_arg_escape(src, ops_shell_type(ops));
		escaped_dst = shell_arg_escape(dst, ops_shell_type(ops));
		if(escaped_src == NULL || escaped_dst == NULL)
		{
			free(escaped_dst);
			free(escaped_src);
			return OPS_FAILED;
		}

		snprintf(cmd, sizeof(cmd),
				"cp %s %s -R " PRESERVE_FLAGS " %s %s",
				(conflict_action == CA_FAIL) ? NO_CLOBBER : "",
				fast_file_cloning ? REFLINK_AUTO : "",
				escaped_src, escaped_dst);
		LOG_INFO_MSG("Running cp command: \"%s\"", cmd);
		OpsResult result = run_operation_command(ops, cmd, cancellable);

		free(escaped_dst);
		free(escaped_src);
		return result;
#else
		int ret;

		if(is_dir(src))
		{
			char *src_copy = strdup(src);
			internal_to_system_slashes(src_copy);

			char *dst_copy = strdup(dst);
			internal_to_system_slashes(dst_copy);

			char *escaped_src = shell_arg_escape(src_copy, ST_CMD);
			free(src_copy);

			char *escaped_dst = shell_arg_escape(dst_copy, ST_CMD);
			free(dst_copy);

			char cmd[6 + PATH_MAX*2 + 1];
			snprintf(cmd, sizeof(cmd), "xcopy %s %s ", escaped_src, escaped_dst);
			free(escaped_src);
			free(escaped_dst);

			if(is_vista_and_above())
				strcat(cmd, "/B ");
			if(conflict_action != CA_FAIL)
			{
				strcat(cmd, "/Y ");
			}
			strcat(cmd, "/E /I /H /R > NUL");
			ret = os_system(cmd);
		}
		else
		{
			wchar_t *const utf16_src = utf8_to_utf16(src);
			wchar_t *const utf16_dst = utf8_to_utf16(dst);
			ret = (CopyFileW(utf16_src, utf16_dst, 0) == 0);
			free(utf16_dst);
			free(utf16_src);
		}

		return result_from_code(ret);
#endif
	}

	io_args_t args = {
		.arg1.src = src,
		.arg2.dst = dst,
		.arg3.crs = ca_to_crs(conflict_action),
		.arg4 = {
			.fast_file_cloning = fast_file_cloning,
			.data_sync = data_sync,
		},
	};
	return exec_io_op(ops, &ior_cp, &args, data == NULL);
}

/* OP_MOVE operation handler.  Moves file/directory without overwriting
 * destination files (when it's supported by the system).  Returns status. */
static OpsResult
op_move(ops_t *ops, void *data, const char src[], const char dst[])
{
	return op_mv(ops, data, src, dst, CA_FAIL);
}

/* OP_MOVEF operation handler.  Moves file/directory overwriting destination
 * files.  Returns status. */
static OpsResult
op_movef(ops_t *ops, void *data, const char src[], const char dst[])
{
	return op_mv(ops, data, src, dst, CA_OVERWRITE);
}

/* OP_MOVEA operation handler.  Moves file appending rest of the source file to
 * the destination.  Returns status. */
static OpsResult
op_movea(ops_t *ops, void *data, const char src[], const char dst[])
{
	return op_mv(ops, data, src, dst, CA_APPEND);
}

/* Moves file/directory overwriting/appending destination files if requested.
 * Returns status. */
static OpsResult
op_mv(ops_t *ops, void *data, const char src[], const char dst[],
		ConflictAction conflict_action)
{
	OpsResult result;

	if(!ops_uses_syscalls(ops))
	{
#ifndef _WIN32
		struct stat st;
		char *escaped_src, *escaped_dst;
		char cmd[6 + PATH_MAX*2 + 1];
		const int cancellable = data == NULL;

		if(conflict_action == CA_FAIL && os_lstat(dst, &st) == 0 &&
				!is_case_change(src, dst))
		{
			return OPS_FAILED;
		}

		escaped_src = shell_arg_escape(src, ops_shell_type(ops));
		escaped_dst = shell_arg_escape(dst, ops_shell_type(ops));
		if(escaped_src == NULL || escaped_dst == NULL)
		{
			free(escaped_dst);
			free(escaped_src);
			return OPS_FAILED;
		}

		snprintf(cmd, sizeof(cmd), "mv %s %s %s",
				(conflict_action == CA_FAIL) ? NO_CLOBBER : "",
				escaped_src, escaped_dst);
		free(escaped_dst);
		free(escaped_src);

		LOG_INFO_MSG("Running mv command: \"%s\"", cmd);
		result = run_operation_command(ops, cmd, cancellable);
#else
		wchar_t *const utf16_src = utf8_to_utf16(src);
		wchar_t *const utf16_dst = utf8_to_utf16(dst);

		BOOL success = MoveFileW(utf16_src, utf16_dst);

		free(utf16_src);
		free(utf16_dst);

		if(!success && GetLastError() == 5)
		{
			const OpsResult r = op_cp(ops, data, src, dst, conflict_action);
			if(r != OPS_SUCCEEDED)
			{
				return r;
			}
			return op_removesl(ops, data, src, NULL);
		}
		result = result_from_code(!success);
#endif
	}
	else
	{
		io_args_t args = {
			.arg1.src = src,
			.arg2.dst = dst,
			.arg3.crs = ca_to_crs(conflict_action),
			.arg4 = {
				/* It's safe to always use fast file cloning on moving files. */
				.fast_file_cloning = 1,
				.data_sync = (ops == NULL ? cfg.data_sync : ops->data_sync),
			},
		};

		result = exec_io_op(ops, &ior_mv, &args, data == NULL);
	}

	/* Accounting for background jobs might take some kind of a queue of events
	 * to handle both internal updates and app.fsop Lua event. */
	if(result == OPS_SUCCEEDED && !ops_runs_in_bg(ops))
	{
		trash_file_moved(src, dst);
		bmarks_file_moved(src, dst);
	}

	return result;
}

/* Maps conflict action to conflict resolution strategy of i/o modules.  Returns
 * conflict resolution strategy type. */
static IoCrs
ca_to_crs(ConflictAction conflict_action)
{
	switch(conflict_action)
	{
		case CA_FAIL:      return IO_CRS_FAIL;
		case CA_OVERWRITE: return IO_CRS_REPLACE_FILES;
		case CA_APPEND:    return IO_CRS_APPEND_TO_FILES;
	}
	assert(0 && "Unhandled conflict action.");
	return IO_CRS_FAIL;
}

static OpsResult
op_chown(ops_t *ops, void *data, const char src[], const char dst[])
{
#ifndef _WIN32
	char cmd[10 + 32 + PATH_MAX];
	char *escaped;
	uid_t uid = (uid_t)(long)data;

	escaped = shell_arg_escape(src, ops_shell_type(ops));
	snprintf(cmd, sizeof(cmd), "chown -fR %u %s", uid, escaped);
	free(escaped);

	LOG_INFO_MSG("Running chown command: \"%s\"", cmd);
	return run_operation_command(ops, cmd, 1);
#else
	return OPS_FAILED;
#endif
}

static OpsResult
op_chgrp(ops_t *ops, void *data, const char src[], const char dst[])
{
#ifndef _WIN32
	char cmd[10 + 32 + PATH_MAX];
	char *escaped;
	gid_t gid = (gid_t)(long)data;

	escaped = shell_arg_escape(src, ops_shell_type(ops));
	snprintf(cmd, sizeof(cmd), "chown -fR :%u %s", gid, escaped);
	free(escaped);

	LOG_INFO_MSG("Running chgrp command: \"%s\"", cmd);
	return run_operation_command(ops, cmd, 1);
#else
	return OPS_FAILED;
#endif
}

#ifndef _WIN32
static OpsResult
op_chmod(ops_t *ops, void *data, const char src[], const char dst[])
{
	char cmd[128 + PATH_MAX];
	char *escaped;

	escaped = shell_arg_escape(src, ops_shell_type(ops));
	snprintf(cmd, sizeof(cmd), "chmod %s %s", (char *)data, escaped);
	free(escaped);

	LOG_INFO_MSG("Running chmod command: \"%s\"", cmd);
	return run_operation_command(ops, cmd, 1);
}

static OpsResult
op_chmodr(ops_t *ops, void *data, const char src[], const char dst[])
{
	char cmd[128 + PATH_MAX];
	char *escaped;

	escaped = shell_arg_escape(src, ops_shell_type(ops));
	snprintf(cmd, sizeof(cmd), "chmod -R %s %s", (char *)data, escaped);
	free(escaped);

	LOG_INFO_MSG("Running chmodr command: \"%s\"", cmd);
	return run_operation_command(ops, cmd, 1);
}
#else
static OpsResult
op_addattr(ops_t *ops, void *data, const char src[], const char dst[])
{
	const DWORD add_mask = (size_t)data;
	wchar_t *const utf16_path = utf8_to_utf16(src);
	const DWORD attrs = GetFileAttributesW(utf16_path);
	if(attrs == INVALID_FILE_ATTRIBUTES)
	{
		free(utf16_path);
		LOG_WERROR(GetLastError());
		return OPS_FAILED;
	}
	if(!SetFileAttributesW(utf16_path, attrs | add_mask))
	{
		free(utf16_path);
		LOG_WERROR(GetLastError());
		return OPS_FAILED;
	}
	free(utf16_path);
	return OPS_SUCCEEDED;
}

static OpsResult
op_subattr(ops_t *ops, void *data, const char src[], const char dst[])
{
	const DWORD sub_mask = (size_t)data;
	wchar_t *const utf16_path = utf8_to_utf16(src);
	const DWORD attrs = GetFileAttributesW(utf16_path);
	if(attrs == INVALID_FILE_ATTRIBUTES)
	{
		free(utf16_path);
		LOG_WERROR(GetLastError());
		return OPS_FAILED;
	}
	if(!SetFileAttributesW(utf16_path, attrs & ~sub_mask))
	{
		free(utf16_path);
		LOG_WERROR(GetLastError());
		return OPS_FAILED;
	}
	free(utf16_path);
	return OPS_SUCCEEDED;
}
#endif

static OpsResult
op_symlink(ops_t *ops, void *data, const char src[], const char dst[])
{
	if(!ops_uses_syscalls(ops))
	{
		char cmd[6 + PATH_MAX*2 + 1];
		OpsResult result;

		char *src_copy = strdup(src);
		char *dst_copy = strdup(dst);
		if(src_copy == NULL || dst_copy == NULL)
		{
			free(src_copy);
			free(dst_copy);
			return OPS_FAILED;
		}

		internal_to_system_slashes(src_copy);
		internal_to_system_slashes(dst_copy);

		ShellType shell_type = (get_env_type() == ET_UNIX) ? ops_shell_type(ops)
		                                                   : ST_CMD;

		char *escaped_src = shell_arg_escape(src_copy, shell_type);
		free(src_copy);

		char *escaped_dst = shell_arg_escape(dst_copy, shell_type);
		free(dst_copy);

		if(escaped_src == NULL || escaped_dst == NULL)
		{
			free(escaped_dst);
			free(escaped_src);
			return OPS_FAILED;
		}

#ifndef _WIN32
		snprintf(cmd, sizeof(cmd), "ln -s %s %s", escaped_src, escaped_dst);
		LOG_INFO_MSG("Running ln command: \"%s\"", cmd);
		result = run_operation_command(ops, cmd, 1);
#else
		char exe_dir[PATH_MAX + 2];
		if(get_exe_dir(exe_dir, ARRAY_LEN(exe_dir)) != 0)
		{
			free(escaped_dst);
			free(escaped_src);
			return OPS_FAILED;
		}

		internal_to_system_slashes(exe_dir);

		char helper[PATH_MAX + 16];
		snprintf(helper, sizeof(helper), "%s\\win_helper", exe_dir);

		char *escaped_helper = shell_arg_escape(helper, shell_type);

		snprintf(cmd, sizeof(cmd), "%s -s %s %s", escaped_helper, escaped_src,
				escaped_dst);
		free(escaped_helper);

		result = result_from_code(os_system(cmd));
#endif

		free(escaped_dst);
		free(escaped_src);
		return result;
	}

	io_args_t args = {
		.arg1.path = src,
		.arg2.target = dst,
		.arg3.crs = IO_CRS_REPLACE_FILES,
	};
	return exec_io_op(ops, &iop_ln, &args, 0);
}

static OpsResult
op_mkdir(ops_t *ops, void *data, const char src[], const char dst[])
{
	if(!ops_uses_syscalls(ops))
	{
#ifndef _WIN32
		char cmd[128 + PATH_MAX];
		char *escaped;

		escaped = shell_arg_escape(src, ops_shell_type(ops));
		snprintf(cmd, sizeof(cmd), "mkdir %s %s", (data == NULL) ? "" : "-p",
				escaped);
		free(escaped);
		LOG_INFO_MSG("Running mkdir command: \"%s\"", cmd);
		return run_operation_command(ops, cmd, 1);
#else
		if(data == NULL)
		{
			wchar_t *const utf16_path = utf8_to_utf16(src);
			BOOL success = CreateDirectoryW(utf16_path, NULL);
			free(utf16_path);
			return result_from_code(!success);
		}
		else
		{
			char *const partial_path = strdup(src);
			char *part = partial_path + (is_path_absolute(src) ? 2 : 0);
			char *state = NULL;

			while((part = split_and_get(part, '/', &state)) != NULL)
			{
				if(!is_dir(partial_path))
				{
					wchar_t *const utf16_path = utf8_to_utf16(partial_path);
					if(!CreateDirectoryW(utf16_path, NULL))
					{
						free(utf16_path);
						free(partial_path);
						return OPS_FAILED;
					}
					free(utf16_path);
				}
			}
			free(partial_path);
			return OPS_SUCCEEDED;
		}
#endif
	}

	io_args_t args = {
		.arg1.path = src,
		.arg2.process_parents = data != NULL,
		.arg3.mode = 0755,
	};
	return exec_io_op(ops, &iop_mkdir, &args, 0);
}

static OpsResult
op_rmdir(ops_t *ops, void *data, const char src[], const char dst[])
{
	if(!ops_uses_syscalls(ops))
	{
#ifndef _WIN32
		char cmd[128 + PATH_MAX];
		char *escaped;

		escaped = shell_arg_escape(src, ops_shell_type(ops));
		snprintf(cmd, sizeof(cmd), "rmdir %s", escaped);
		free(escaped);
		LOG_INFO_MSG("Running rmdir command: \"%s\"", cmd);
		return run_operation_command(ops, cmd, 1);
#else
		wchar_t *const utf16_path = utf8_to_utf16(src);
		const BOOL success = RemoveDirectoryW(utf16_path);
		free(utf16_path);
		return result_from_code(!success);
#endif
	}

	io_args_t args = {
		.arg1.path = src,
	};
	return exec_io_op(ops, &iop_rmdir, &args, 0);
}

static OpsResult
op_mkfile(ops_t *ops, void *data, const char src[], const char dst[])
{
	if(!ops_uses_syscalls(ops))
	{
#ifndef _WIN32
		char cmd[128 + PATH_MAX];
		char *escaped;

		escaped = shell_arg_escape(src, ops_shell_type(ops));
		snprintf(cmd, sizeof(cmd), "touch %s", escaped);
		free(escaped);
		LOG_INFO_MSG("Running touch command: \"%s\"", cmd);
		return run_operation_command(ops, cmd, 1);
#else
		HANDLE hfile;

		wchar_t *const utf16_path = utf8_to_utf16(src);
		hfile = CreateFileW(utf16_path, 0, 0, NULL, CREATE_NEW,
				FILE_ATTRIBUTE_NORMAL, NULL);
		free(utf16_path);
		if(hfile == INVALID_HANDLE_VALUE)
		{
			return OPS_FAILED;
		}

		CloseHandle(hfile);
		return OPS_SUCCEEDED;
#endif
	}

	io_args_t args = {
		.arg1.path = src,
	};
	return exec_io_op(ops, &iop_mkfile, &args, 0);
}

/* Checks whether specific operation should use system calls.  Returns non-zero
 * if so, otherwise zero is returned. */
static int
ops_uses_syscalls(const ops_t *ops)
{
	return ops == NULL ? cfg.use_system_calls : ops->use_system_calls;
}

/* Retrieves shell type for the operation.  Returns the type. */
static ShellType
ops_shell_type(const ops_t *ops)
{
	return (ops == NULL ? curr_stats.shell_type : (ShellType)ops->shell_type);
}

/* Executes i/o operation with some predefined pre/post actions.  Returns
 * status. */
static OpsResult
exec_io_op(ops_t *ops, IoRes (*func)(io_args_t *), io_args_t *args,
		int cancellable)
{
	args->estim = (ops == NULL) ? NULL : ops->estim;

	if(ops != NULL)
	{
		if(!ops->bg)
		{
			args->confirm = &confirm_overwrite;
			args->result.errors_cb = &dispatch_error;
		}

		ioe_errlst_init(&args->result.errors);
	}

	if(cancellable)
	{
		if(ops_runs_in_bg(ops))
		{
			args->cancellation.arg = ops->bg_op;
			args->cancellation.hook = &bg_cancellation_hook;
		}
		else
		{
			/* ui_cancellation_push_off() should be called outside of this unit to
			 * allow bulking several operations together. */
			ui_cancellation_enable();
			args->cancellation.hook = &ui_cancellation_hook;
		}
	}

	curr_ops = ops;
	OpsResult result = OPS_FAILED;
	IoRes io_res = func(args);
	switch(io_res)
	{
		case IO_RES_SUCCEEDED: result = OPS_SUCCEEDED; break;
		case IO_RES_SKIPPED:   result = OPS_SKIPPED; break;
		case IO_RES_FAILED:    result = OPS_FAILED; break;
		case IO_RES_ABORTED:   result = OPS_FAILED; break;
	}
	curr_ops = NULL;

	if(cancellable && (ops == NULL || !ops->bg))
	{
		ui_cancellation_disable();
	}

	if(ops != NULL)
	{
		if(io_res == IO_RES_ABORTED)
		{
			ops->aborted = 1;
		}

		size_t len = (ops->errors == NULL) ? 0U : strlen(ops->errors);
		char *const suffix = ioe_errlst_to_str(&args->result.errors);

		if(len != 0U)
		{
			(void)strappend(&ops->errors, &len, "\n");
		}
		(void)strappend(&ops->errors, &len, suffix);

		free(suffix);
		ioe_errlst_free(&args->result.errors);
	}

	return result;
}

/* Asks user to confirm file overwrite.  Returns non-zero on positive user
 * answer, otherwise zero is returned. */
static int
confirm_overwrite(io_args_t *args, const char src[], const char dst[])
{
	/* TODO: think about adding "append" and "rename" options here. */
	static const response_variant responses[] = {
		{ .key = 'y', .descr = "[y]es/", },
		{ .key = 'Y', .descr = "[Y]es for all/", },
		{ .key = 'n', .descr = "[n]o/", },
		{ .key = 'N', .descr = "[N]o for all", },
		{ },
	};

	char *title;
	char *msg;
	char response;
	char *src_dir, *dst_dir;
	const char *fname = get_last_path_component(dst);

	if(curr_ops->crp != CRP_ASK)
	{
		return (curr_ops->crp == CRP_OVERWRITE_ALL) ? 1 : 0;
	}

	src_dir = pretty_dir_path(src);
	dst_dir = pretty_dir_path(dst);

	title = format_str("File overwrite while %s", curr_ops->descr);
	msg = format_str("Overwrite \"%s\" in\n%s\nwith \"%s\" from\n%s\n?", fname,
			dst_dir, fname, src_dir);

	free(dst_dir);
	free(src_dir);

	response = prompt_user(args, title, msg, responses);

	free(msg);
	free(title);

	switch(response)
	{
		case 'Y': curr_ops->crp = CRP_OVERWRITE_ALL; /* Fall through. */
		case 'y': return 1;

		case 'N': curr_ops->crp = CRP_SKIP_ALL; /* Fall through. */
		case 'n': return 0;

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

/* Prepares path to presenting to the user.  Returns newly allocated string,
 * which should be freed by the caller, or NULL if there is not enough
 * memory. */
static char *
pretty_dir_path(const char path[])
{
	char dir_only[strlen(path) + 1];
	char canonic[PATH_MAX + 1];

	copy_str(dir_only, sizeof(dir_only), path);
	remove_last_path_component(dir_only);
	canonicalize_path(dir_only, canonic, sizeof(canonic));

	return strdup(canonic);
}

/* Asks user what to do with encountered error.  Returns the response. */
static IoErrCbResult
dispatch_error(io_args_t *args, const ioe_err_t *err)
{
	static const response_variant responses[] = {
		{ .key = 'r', .descr = "[r]etry/", },
		{ .key = 'i', .descr = "[i]gnore/", },
		{ .key = 'I', .descr = "[I]gnore for all/", },
		{ .key = 'a', .descr = "[a]bort", },
		{ },
	};

	char *title;
	char *msg;
	char response;

	if(curr_ops->choose == NULL)
	{
		return IO_ECR_BREAK;
	}

	if(curr_ops->erp == ERP_IGNORE_ALL)
	{
		return IO_ECR_IGNORE;
	}

	title = format_str("Error while %s", curr_ops->descr);
	msg = ioe_err_to_str(err);

	response = prompt_user(args, title, msg, responses);

	free(msg);
	free(title);

	switch(response)
	{
		case 'r': return IO_ECR_RETRY;

		case 'I': curr_ops->erp = ERP_IGNORE_ALL; /* Fall through. */
		case 'i': return IO_ECR_IGNORE;

		case 'a': return IO_ECR_BREAK;

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

/* prompt_msg_custom() wrapper that takes care of interaction if cancellation is
 * active. */
static char
prompt_user(const io_args_t *args, const char title[], const char msg[],
		const response_variant variants[])
{
	/* Active cancellation conflicts with input processing by putting terminal in
	 * a cooked mode. */
	ui_cancellation_push_off();

	const custom_prompt_t prompt = {
		.title = title,
		.message = msg,
		.variants = variants,
	};
	char response = curr_ops->choose(&prompt);

	ui_cancellation_pop();

	return response;
}

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

#ifndef _WIN32

/* Runs command in background and displays its errors to a user.  To determine
 * an error uses both stderr stream and exit status.  Returns status. */
static OpsResult
run_operation_command(ops_t *ops, char cmd[], int cancellable)
{
	if(!cancellable)
	{
		return result_from_code(bg_and_wait_for_errors(cmd, &no_cancellation));
	}

	if(ops_runs_in_bg(ops))
	{
		const cancellation_t bg_cancellation_info = {
			.arg = ops->bg_op,
			.hook = &bg_cancellation_hook,
		};
		return result_from_code(bg_and_wait_for_errors(cmd, &bg_cancellation_info));
	}
	else
	{
		/* ui_cancellation_push_off() should be called outside this unit to allow
		 * bulking several operations together. */
		ui_cancellation_enable();
		OpsResult result =
			result_from_code(bg_and_wait_for_errors(cmd, &ui_cancellation_info));
		ui_cancellation_disable();
		return result;
	}
}

#endif

/* Checks whether operation is a background one.  The parameter can be NULL.
 * Returns non-zero for background operations. */
static int
ops_runs_in_bg(const ops_t *ops)
{
	return (ops != NULL && ops->bg);
}

/* Implementation of cancellation hook for background tasks. */
static int
bg_cancellation_hook(void *arg)
{
	return bg_op_cancelled(arg);
}

/* Turns exit code into OpsResult.  Returns OpsResult. */
static OpsResult
result_from_code(int exit_code)
{
	return (exit_code == 0 ? OPS_SUCCEEDED : OPS_FAILED);
}

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