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

#include <curses.h> /* werase() */

#include <sys/types.h> /* pid_t */
#include <sys/stat.h> /* S_IRWXU */
#ifndef _WIN32
#include <sys/wait.h> /* WEXITSTATUS() WIFEXITED() */
#endif
#include <unistd.h> /* unlink() */

#include <errno.h> /* errno ENOTDIR */
#include <stddef.h> /* NULL size_t */
#include <stdio.h> /* snprintf() fclose() */
#include <stdlib.h> /* EXIT_SUCCESS free() malloc() */
#include <string.h> /* memmove() strcpy() strlen() strcmp() */

#include "../cfg/config.h"
#include "../compat/fs_limits.h"
#include "../compat/os.h"
#include "../menus/menus.h"
#include "../modes/dialogs/msg_dialog.h"
#include "../ui/cancellation.h"
#include "../ui/statusbar.h"
#include "../ui/ui.h"
#include "../utils/cancellation.h"
#include "../utils/file_streams.h"
#include "../utils/fs.h"
#include "../utils/log.h"
#include "../utils/macros.h"
#include "../utils/path.h"
#include "../utils/str.h"
#include "../utils/test_helpers.h"
#include "../utils/utils.h"
#include "../background.h"
#include "../filelist.h"
#include "../flist_pos.h"
#include "../status.h"

/* Description of existing FUSE mounts. */
typedef struct fuse_mount_t
{
	char source_file_path[PATH_MAX + 1]; /* Full path to source file. */
	char source_file_dir[PATH_MAX + 1];  /* Full path to dir of source file. */
	char mount_point[PATH_MAX + 1];      /* Full path to mount point. */
	int mount_point_id;                  /* ID of mounts for unique dirs. */
	int needs_unmounting;                /* Whether unmount call is required. */
	struct fuse_mount_t *next;           /* Pointer to the next mount in chain. */
}
fuse_mount_t;

static int fuse_mount(view_t *view, char file_full_path[], const char param[],
		const char program[], char mount_point[]);
static int get_last_mount_point_id(const fuse_mount_t *mounts);
static void register_mount(fuse_mount_t **mounts, const char file_full_path[],
		const char mount_point[], int id, int needs_unmounting);
TSTATIC void format_mount_command(const char mount_point[],
		const char file_name[], const char param[], const char format[],
		size_t buf_size, char buf[], int *foreground);
static fuse_mount_t * get_mount_by_source(const char source[]);
static fuse_mount_t * get_mount_by_mount_point(const char dir[]);
static fuse_mount_t * get_mount_by_path(const char path[]);
static int run_fuse_command(char cmd[], int *cancelled, char **errors);
static char * read_proc_stream(pid_t pid, FILE *fp,
		const cancellation_t *cancellation);
static void kill_mount_point(const char mount_point[]);
static void updir_from_mount(view_t *view, fuse_mount_t *runner);

/* List of active mounts. */
static fuse_mount_t *fuse_mounts;

void
fuse_try_mount(view_t *view, const char program[])
{
	/* TODO: refactor this function fuse_try_mount() */

	fuse_mount_t *runner;
	char file_full_path[PATH_MAX + 1];
	char mount_point[PATH_MAX + 1];

	if(make_path(cfg.fuse_home, S_IRWXU) != 0)
	{
		show_error_msg("Unable to create FUSE mount home directory",
				cfg.fuse_home);
		return;
	}

	get_current_full_path(view, sizeof(file_full_path), file_full_path);

	/* Check if already mounted. */
	runner = get_mount_by_source(file_full_path);

	if(runner != NULL)
	{
		strcpy(mount_point, runner->mount_point);
	}
	else
	{
		char param[PATH_MAX + 1];
		param[0] = '\0';

		/* New file to be mounted. */
		if(starts_with(program, "FUSE_MOUNT2|"))
		{
			FILE *f;
			if((f = os_fopen(file_full_path, "r")) == NULL)
			{
				show_error_msg("FUSE mount failed", "Can't open file for reading");
				curr_stats.save_msg = 1;
				return;
			}

			if(fgets(param, sizeof(param), f) == NULL)
			{
				show_error_msg("FUSE mount failed", "Can't read file content");
				curr_stats.save_msg = 1;
				fclose(f);
				return;
			}
			fclose(f);

			chomp(param);
			if(param[0] == '\0')
			{
				show_error_msg("FUSE mount failed", "File is empty");
				curr_stats.save_msg = 1;
				return;
			}
		}

		if(fuse_mount(view, file_full_path, param, program, mount_point) != 0)
		{
			return;
		}
	}

	navigate_to(view, mount_point);
}

/* Searchers for mount record by source file path. */
static fuse_mount_t *
get_mount_by_source(const char source[])
{
	fuse_mount_t *runner = fuse_mounts;
	while(runner != NULL)
	{
		if(paths_are_equal(runner->source_file_path, source))
			break;
		runner = runner->next;
	}
	return runner;
}

/* mount_point should be an array of at least PATH_MAX characters
 * Returns non-zero on error. */
static int
fuse_mount(view_t *view, char file_full_path[], const char param[],
		const char program[], char mount_point[])
{
	/* TODO: refactor this function fuse_mount(). */

	int id;
	int mount_point_id;
	int foreground;

	id = get_last_mount_point_id(fuse_mounts);
	mount_point_id = id;
	do
	{
		snprintf(mount_point, PATH_MAX, "%s/%03d_%s", cfg.fuse_home,
				++mount_point_id, get_current_file_name(view));

		/* Make sure this is not an infinite loop, although practically this
		 * condition will always be false. */
		if(mount_point_id == id)
		{
			show_error_msg("Unable to create FUSE mount directory", mount_point);
			return -1;
		}

		errno = 0;
	}
	while(os_mkdir(mount_point, S_IRWXU) != 0 && errno == EEXIST);

	if(errno != 0)
	{
		show_error_msg("Unable to create FUSE mount directory", mount_point);
		return -1;
	}

	/* Just before running the mount,
		 I need to chdir out temporarily from any FUSE mounted
		 paths, Otherwise the fuse-zip command fails with
		 "fusermount: failed to open current directory: permission denied"
		 (this happens when mounting JARs from mounted JARs) */
	if(vifm_chdir(cfg.fuse_home) != 0)
	{
		show_error_msg("FUSE MOUNT ERROR", "Can't chdir() to FUSE home");
		return -1;
	}

	char mount_cmd[2*PATH_MAX];
	format_mount_command(mount_point, file_full_path, param, program,
			sizeof(mount_cmd), mount_cmd, &foreground);

	ui_sb_msg("FUSE mounting selected file, please stand by..");

	if(foreground)
	{
		ui_shutdown();
	}

	LOG_INFO_MSG("FUSE mount command: `%s`", mount_cmd);
	int cancelled = 0;
	char *mounter_errors = NULL;
	int status = run_fuse_command(mount_cmd, foreground ? NULL : &cancelled,
			&mounter_errors);

	ui_sb_clear();

	/* Check child process exit status. */
	if(!WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS)
	{
		if(!WIFEXITED(status))
		{
			LOG_ERROR_MSG("FUSE mounter didn't exit!");
		}
		else
		{
			LOG_ERROR_MSG("FUSE mount command exit status: %d", WEXITSTATUS(status));
		}

		if(!is_null_or_empty(mounter_errors))
		{
			show_error_msg("FUSE Mounter Errors", mounter_errors);
		}
		free(mounter_errors);

		werase(status_bar);

		if(cancelled)
		{
			ui_sb_msg("FUSE mount cancelled");
			curr_stats.save_msg = 1;
		}
		else
		{
			show_error_msgf("FUSE", "Failed to mount file: %s", file_full_path);
		}

		/* Remove the directory we created for the mount. */
		kill_mount_point(mount_point);

		(void)vifm_chdir(flist_get_dir(view));
		return -1;
	}
	ui_sb_msg("FUSE mount success");

	register_mount(&fuse_mounts, file_full_path, mount_point, mount_point_id,
			!starts_with(program, "FUSE_MOUNT3|"));

	return 0;
}

/* Gets last mount point id used.  Returns the id or 0 if list of mounts is
 * empty. */
static int
get_last_mount_point_id(const fuse_mount_t *mounts)
{
	/* As new entries are added at the front, first entry must have the largest
	 * value of the id. */
	return (mounts == NULL) ? 0 : mounts->mount_point_id;
}

/* Adds new entry to the list of *mounts. */
static void
register_mount(fuse_mount_t **mounts, const char file_full_path[],
		const char mount_point[], int id, int needs_unmounting)
{
	fuse_mount_t *fuse_mount = malloc(sizeof(*fuse_mount));
	if(fuse_mount == NULL)
	{
		return;
	}

	copy_str(fuse_mount->source_file_path, sizeof(fuse_mount->source_file_path),
			file_full_path);

	copy_str(fuse_mount->source_file_dir, sizeof(fuse_mount->source_file_dir),
			file_full_path);
	remove_last_path_component(fuse_mount->source_file_dir);

	canonicalize_path(mount_point, fuse_mount->mount_point,
			sizeof(fuse_mount->mount_point));

	fuse_mount->mount_point_id = id;
	fuse_mount->needs_unmounting = needs_unmounting;

	fuse_mount->next = *mounts;
	*mounts = fuse_mount;
}

/* Builds the mount command based on the file type program.
 * Accepted formats are:
 *   FUSE_MOUNT|some_mount_command %SOURCE_FILE %DESTINATION_DIR [%FOREGROUND]
 *   FUSE_MOUNT2|some_mount_command %PARAM %DESTINATION_DIR [%FOREGROUND]
 *   FUSE_MOUNT3|some_mount_command %SOURCE_FILE %DESTINATION_DIR [%FOREGROUND]
 * %CLEAR is an obsolete name of %FOREGROUND.
 * Always sets value of *foreground. */
TSTATIC void
format_mount_command(const char mount_point[], const char file_name[],
		const char param[], const char format[], size_t buf_size, char buf[],
		int *foreground)
{
	char *buf_pos;
	const char *prog_pos;
	char *escaped_path;
	char *escaped_mount_point;

	*foreground = 0;

	escaped_path = shell_arg_escape(file_name, curr_stats.shell_type);
	escaped_mount_point = shell_arg_escape(mount_point, curr_stats.shell_type);

	buf_pos = buf;
	buf_pos[0] = '\0';

	prog_pos = after_first(format, '|');
	while(*prog_pos != '\0')
	{
		if(*prog_pos == '%')
		{
			char cmd_buf[96];
			char *cmd_pos;

			cmd_pos = cmd_buf;
			while(*prog_pos != '\0' && *prog_pos != ' ')
			{
				*cmd_pos = *prog_pos;
				if((size_t)(cmd_pos - cmd_buf) < sizeof(cmd_buf))
				{
					++cmd_pos;
				}
				++prog_pos;
			}
			*cmd_pos = '\0';

			if(!strcmp(cmd_buf, "%SOURCE_FILE"))
			{
				copy_str(buf_pos, buf_size - (buf_pos - buf), escaped_path);
				buf_pos += strlen(buf_pos);
			}
			else if(!strcmp(cmd_buf, "%PARAM"))
			{
				copy_str(buf_pos, buf_size - (buf_pos - buf), param);
				buf_pos += strlen(buf_pos);
			}
			else if(!strcmp(cmd_buf, "%DESTINATION_DIR"))
			{
				copy_str(buf_pos, buf_size - (buf_pos - buf), escaped_mount_point);
				buf_pos += strlen(buf_pos);
			}
			else if(!strcmp(cmd_buf, "%FOREGROUND") || !strcmp(cmd_buf, "%CLEAR"))
			{
				*foreground = 1;
			}
		}
		else
		{
			*buf_pos = *prog_pos;
			if((size_t)(buf_pos - buf) < buf_size - 1)
			{
				++buf_pos;
			}
			++prog_pos;
		}
	}

	*buf_pos = '\0';
	free(escaped_mount_point);
	free(escaped_path);
}

void
fuse_unmount_all(void)
{
	if(fuse_mounts == NULL)
	{
		return;
	}

	if(vifm_chdir("/") != 0)
	{
		return;
	}

	fuse_mount_t *runner = fuse_mounts;
	fuse_mounts = NULL;

	while(runner != NULL)
	{
		if(runner->needs_unmounting)
		{
			char *escaped_filename =
				shell_arg_escape(runner->mount_point, curr_stats.shell_type);
			char buf[14 + PATH_MAX + 1];
			snprintf(buf, sizeof(buf), "%s %s", curr_stats.fuse_umount_cmd,
					escaped_filename);
			free(escaped_filename);

			(void)vifm_system(buf, SHELL_BY_APP);
		}

		kill_mount_point(runner->mount_point);

		fuse_mount_t *next = runner->next;
		free(runner);
		runner = next;
	}

	leave_invalid_dir(&lwin);
	leave_invalid_dir(&rwin);
}

int
fuse_try_updir_from_a_mount(const char path[], view_t *view)
{
	fuse_mount_t *const mount = get_mount_by_mount_point(path);
	if(mount == NULL)
	{
		return 0;
	}

	updir_from_mount(view, mount);
	return 1;
}

int
fuse_is_mount_point(const char path[])
{
	return get_mount_by_mount_point(path) != NULL;
}

/* Searches for mount record by path to mount point.  Returns mount point or
 * NULL on failure. */
static fuse_mount_t *
get_mount_by_mount_point(const char dir[])
{
	fuse_mount_t *runner = fuse_mounts;
	while(runner != NULL)
	{
		if(paths_are_equal(runner->mount_point, dir))
		{
			return runner;
		}
		runner = runner->next;
	}
	return NULL;
}

const char *
fuse_get_mount_file(const char path[])
{
	const fuse_mount_t *const mount = get_mount_by_path(path);
	return (mount == NULL) ? NULL : mount->source_file_path;
}

/* Searches for mount record by path inside one of mount points.  Picks the
 * longest match so that even nested mount points work.  Returns mount point or
 * NULL on failure. */
static fuse_mount_t *
get_mount_by_path(const char path[])
{
	size_t max_len = 0U;
	fuse_mount_t *mount = NULL;
	fuse_mount_t *runner = fuse_mounts;
	while(runner != NULL)
	{
		if(path_starts_with(path, runner->mount_point))
		{
			const size_t len = strlen(runner->mount_point);
			if(len > max_len)
			{
				max_len = len;
				mount = runner;
			}
		}
		runner = runner->next;
	}
	return mount;
}

int
fuse_try_unmount(view_t *view)
{
	fuse_mount_t *runner = fuse_mounts;
	fuse_mount_t *trailer = NULL;
	while(runner)
	{
		if(paths_are_equal(runner->mount_point, view->curr_dir))
		{
			break;
		}

		trailer = runner;
		runner = runner->next;
	}

	if(runner == NULL)
	{
		return 0;
	}

	/* We are exiting a top level dir. */

	if(runner->needs_unmounting)
	{
		char *escaped_mount_point =
			shell_arg_escape(runner->mount_point, curr_stats.shell_type);

		char unmount_cmd[14 + PATH_MAX + 1];
		snprintf(unmount_cmd, sizeof(unmount_cmd), "%s %s",
				curr_stats.fuse_umount_cmd, escaped_mount_point);
		LOG_INFO_MSG("FUSE unmount command: `%s`", unmount_cmd);
		free(escaped_mount_point);

		/* Have to chdir to parent temporarily, so that this DIR can be
		 * unmounted. */
		if(vifm_chdir(cfg.fuse_home) != 0)
		{
			show_error_msg("FUSE UMOUNT ERROR", "Can't chdir to FUSE home");
			return -1;
		}

		ui_sb_msg("FUSE unmounting selected file, please stand by...");
		int status = run_fuse_command(unmount_cmd, /*cancelled=*/NULL,
				/*errors=*/NULL);
		ui_sb_clear();
		/* Check child status. */
		if(!WIFEXITED(status) || WEXITSTATUS(status))
		{
			werase(status_bar);
			show_error_msgf("FUSE UMOUNT ERROR", "Can't unmount %s.  It may be busy.",
					runner->source_file_path);
			(void)vifm_chdir(flist_get_dir(view));
			return -1;
		}
	}

	/* Remove the directory we created for the mount. */
	kill_mount_point(runner->mount_point);

	/* Remove mount point from fuse_mount_t. */
	fuse_mount_t *sniffer = runner->next;
	if(trailer)
		trailer->next = sniffer ? sniffer : NULL;
	else
		fuse_mounts = sniffer;

	updir_from_mount(view, runner);
	free(runner);
	return 1;
}

/* Runs command in background keeping its stdin and stdout connected to the
 * terminal.  NULL in the cancelled parameter means that the operations is not
 * cancellable, otherwise *cancelled is set to the correct value even on error.
 * If the errors parameter isn't NULL, stderr output is returned, otherwise
 * it's captured and discarded.  Returns status on success, otherwise -1 is
 * returned. */
static int
run_fuse_command(char cmd[], int *cancelled, char **errors)
{
	const cancellation_t *cancellation = &no_cancellation;
	if(cancelled != NULL)
	{
		cancellation = &ui_cancellation_info;
		*cancelled = 0;
	}

	FILE *err;
	pid_t pid =
		bg_run_and_capture(cmd, /*user_sh=*/0, /*in=*/NULL, /*out=*/NULL, &err);
	if(pid == (pid_t)-1)
	{
		return -1;
	}

	if(cancelled != NULL)
	{
		ui_cancellation_push_on();
	}

	/* We're always reading the error stream, but dropping all that we've read
	 * unless errors parameter is non-NULL. */
	char *errors_buf = read_proc_stream(pid, err, cancellation);
	fclose(err);

	if(errors != NULL)
	{
		*errors = errors_buf;
		errors_buf = NULL;
	}
	free(errors_buf);

	/* The only bit that can't compile on Windows.  Also mind that
	 * bg_run_and_capture() doesn't actually return PID on Windows. */
#ifndef _WIN32
	int status = get_proc_exit_status(pid, cancellation);
#else
	int status = -1;
#endif

	if(cancelled != NULL)
	{
		*cancelled = cancellation_requested(cancellation);
		ui_cancellation_pop();
	}

	return status;
}

/* Reads redirected stream from the process while handling cancellation.
 * Returns read data */
static char *
read_proc_stream(pid_t pid, FILE *fp, const cancellation_t *cancellation)
{
	char *buf = NULL;
	size_t buf_len = 0;

	char *line = NULL;
	wait_for_data_from(pid, fp, /*fd=*/-1, cancellation);
	while((line = read_line(fp, line)) != NULL)
	{
		if(buf_len != 0)
		{
			(void)strappendch(&buf, &buf_len, '\n');
		}
		(void)strappend(&buf, &buf_len, line);
		wait_for_data_from(pid, fp, /*fd=*/-1, cancellation);
	}
	free(line);

	return buf;
}

/* Deletes mount point by its path. */
static void
kill_mount_point(const char mount_point[])
{
	/* rmdir() on some systems (FreeBSD at least) can resolve symbolic links if
	 * path ends with a slash and unlink() will also fail if there is a trailing
	 * slash. */
	char no_slash[strlen(mount_point) + 1];
	strcpy(no_slash, mount_point);
	chosp(no_slash);

	if(os_rmdir(no_slash) != 0 && errno == ENOTDIR)
	{
		/* FUSE mounter might replace directory with a symbolic link, account for
		 * this possibility. */
		(void)unlink(no_slash);
	}
}

static void
updir_from_mount(view_t *view, fuse_mount_t *runner)
{
	char *file;
	int pos;

	if(change_directory(view, runner->source_file_dir) < 0)
		return;

	load_dir_list(view, 0);

	file = runner->source_file_path;
	file += strlen(runner->source_file_dir) + 1;
	pos = fpos_find_by_name(view, file);
	fpos_set_pos(view, pos);
}

int
fuse_is_mount_string(const char string[])
{
	return starts_with(string, "FUSE_MOUNT|")
	    || starts_with(string, "FUSE_MOUNT2|")
	    || starts_with(string, "FUSE_MOUNT3|");
}

void
fuse_strip_mount_metadata(char string[])
{
	size_t prefix_len;
	if(starts_with(string, "FUSE_MOUNT|"))
	{
		prefix_len = ARRAY_LEN("FUSE_MOUNT|") - 1;
	}
	else if(starts_with(string, "FUSE_MOUNT2|"))
	{
		prefix_len = ARRAY_LEN("FUSE_MOUNT2|") - 1;
	}
	else if(starts_with(string, "FUSE_MOUNT3|"))
	{
		prefix_len = ARRAY_LEN("FUSE_MOUNT3|") - 1;
	}
	else
	{
		prefix_len = 0;
	}

	if(prefix_len != 0)
	{
		size_t new_len = strlen(string) - prefix_len;
		memmove(string, string + prefix_len, new_len + 1);
	}
}

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