/* vifm
 * Copyright (C) 2018 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 "media_menu.h"

#include <stddef.h> /* NULL */
#include <stdlib.h> /* free() */
#include <string.h> /* strdup() */
#include <wchar.h> /* wcscmp() */

#include "../cfg/config.h"
#include "../compat/fs_limits.h"
#include "../compat/reallocarray.h"
#include "../modes/dialogs/msg_dialog.h"
#include "../ui/ui.h"
#include "../utils/fs.h"
#include "../utils/path.h"
#include "../utils/str.h"
#include "../utils/string_array.h"
#include "../utils/utils.h"
#include "../running.h"
#include "menus.h"

/* Example state of the menu:
 *
 *    Items                              Data            Action
 *
 *    /dev/sdc                                           mount /dev/sda3
 *    `-- (not mounted)                  m/dev/sda3      mount /dev/sda3
 *
 *    /dev/sdd {text}
 *    |-- /                              u/              unmount /
 *    `-- /root/tmp                      u/root/tmp      unmount /root/tmp
 *
 *    /dev/sde                                           unmount /media
 *    `-- /media                         u/media         unmount /media
 *
 * Additional keys:
 *  - r -- reload list
 *  - [ -- previous device
 *  - ] -- next device
 *  - m -- mount/unmount
 *
 * Behaviour on Enter:
 *  - navigate inside mount-point or,
 *  - mount device if there are no mount-points or,
 *  - do nothing and keep menu open
 */

/* Description of a single device. */
typedef struct
{
	char *device;   /* Device name in the system. */
	char *text;     /* Arbitrary text next to device name (can be NULL). */
	char **paths;   /* List of paths at which the device is mounted. */
	int path_count; /* Number of elements in the paths array. */
	int has_info;   /* Set when text is set via info=. */
}
media_info_t;

static int execute_media_cb(struct view_t *view, menu_data_t *m);
static KHandlerResponse media_khandler(struct view_t *view, menu_data_t *m,
		const wchar_t keys[]);
static int reload_list(menu_data_t *m);
static void output_handler(const char line[], void *arg);
static void free_info_array(void);
static void position_cursor(menu_data_t *m);
static const char * get_selected_data(menu_data_t *m);
static void path_get_decors(const char path[], FileType type,
		const char **prefix, const char **suffix);
static int mediaprg_mount(const char data[], menu_data_t *m);

/* List of media devices. */
static media_info_t *infos;
/* Number of elements in infos array. */
static int info_count;

int
show_media_menu(struct view_t *view)
{
	static menu_data_t m;
	menus_init_data(&m, view, strdup("Media"), strdup("No media found"));
	m.execute_handler = &execute_media_cb;
	m.key_handler = &media_khandler;

	if(reload_list(&m) != 0)
	{
		return 0;
	}

	position_cursor(&m);

	return menus_enter(&m, view);
}

/* Callback that is called when menu item is selected.  Return non-zero to stay
 * in menu mode and zero otherwise. */
static int
execute_media_cb(struct view_t *view, menu_data_t *m)
{
	const char *data = get_selected_data(m);
	if(data != NULL)
	{
		if(*data == 'u')
		{
			const char *path = data + 1;
			menus_goto_dir(view, path);
			return 0;
		}
		else if(*data == 'm')
		{
			(void)mediaprg_mount(data, m);
		}
	}
	return 1;
}

/* Menu-specific shortcut handler.  Returns code that specifies both taken
 * actions and what should be done next. */
static KHandlerResponse
media_khandler(struct view_t *view, menu_data_t *m, const wchar_t keys[])
{
	if(wcscmp(keys, L"r") == 0)
	{
		reload_list(m);
		menus_set_pos(m->state, m->pos);
		menus_partial_redraw(m->state);
		return KHR_REFRESH_WINDOW;
	}
	else if(wcscmp(keys, L"[") == 0)
	{
		int i = m->pos;
		while(i-- > 0)
		{
			if(m->data[i] == NULL && m->data[i + 1] != NULL)
			{
				menus_set_pos(m->state, i);
				menus_partial_redraw(m->state);
				break;
			}
		}
		return KHR_REFRESH_WINDOW;
	}
	else if(wcscmp(keys, L"]") == 0)
	{
		int i = m->pos;
		while(++i < m->len - 1)
		{
			if(m->data[i] == NULL && m->data[i + 1] != NULL)
			{
				menus_set_pos(m->state, i);
				menus_partial_redraw(m->state);
				break;
			}
		}
		return KHR_REFRESH_WINDOW;
	}
	else if(wcscmp(keys, L"m") == 0)
	{
		const char *data = get_selected_data(m);
		(void)mediaprg_mount(data, m);
		return KHR_REFRESH_WINDOW;
	}
	return KHR_UNHANDLED;
}

/* (Re)loads list of media devices into the menu.  Returns zero on success,
 * otherwise non-zero is returned (empty menu is not considered an error). */
static int
reload_list(menu_data_t *m)
{
	free_string_array(m->data, m->len);
	free_string_array(m->items, m->len);

	m->data = NULL;
	m->items = NULL;
	m->len = 0;

	if(cfg.media_prg[0] == '\0')
	{
		show_error_msg("Listing media devices",
				"'mediaprg' option must not be empty");
		return 1;
	}

	char *cmd = format_str("%s list", cfg.media_prg);
	if(process_cmd_output("Listing media", cmd, NULL, 0, 0, &output_handler,
				&m) != 0)
	{
		show_error_msgf("Listing media devices", "Unable to run: %s", cmd);
		free(cmd);
		return 1;
	}
	free(cmd);

	int i;
	/* The NULL check is for tests. */
	const int menu_rows = (menu_win != NULL ? (getmaxy(menu_win) + 1) - 3 : 100);
	int max_rows_with_blanks = info_count       /* Device lines. */
		                       + (info_count - 1) /* Blank lines. */;
	for(i = 0; i < info_count; ++i)
	{
		max_rows_with_blanks += infos[i].path_count > 0
		                      ? infos[i].path_count /* Mount-points. */
		                      : 1;                  /* "Not mounted" line. */
	}

	for(i = 0; i < info_count; ++i)
	{
		const char *prefix, *suffix;
		media_info_t *info = &infos[i];

		put_into_string_array(&m->data, m->len, NULL);

		path_get_decors(info->device, FT_BLOCK_DEV, &prefix, &suffix);
		m->len = put_into_string_array(&m->items, m->len,
				format_str("%s%s%s %s", prefix, info->device, suffix,
						info->text ? info->text : ""));

		if(info->path_count == 0)
		{
			put_into_string_array(&m->data, m->len, format_str("m%s", info->device));
			m->len = add_to_string_array(&m->items, m->len, "`-- (not mounted)");
		}
		else
		{
			int j;
			for(j = 0; j < info->path_count; ++j)
			{
				put_into_string_array(&m->data, m->len,
						format_str("u%s", info->paths[j]));

				path_get_decors(info->paths[j], FT_DIR, &prefix, &suffix);
				const char *path = (is_root_dir(info->paths[j]) && suffix[0] == '/')
				                 ? ""
				                 : info->paths[j];
				m->len = put_into_string_array(&m->items, m->len,
						format_str("%c-- %s%s%s", j + 1 == info->path_count ? '`' : '|',
								prefix, path, suffix));
			}
		}

		if(i != info_count - 1 && max_rows_with_blanks <= menu_rows)
		{
			put_into_string_array(&m->data, m->len, NULL);
			m->len = add_to_string_array(&m->items, m->len, "");
		}
	}

	free_info_array();

	return 0;
}

/* Implements process_cmd_output() callback that builds array of devices. */
static void
output_handler(const char line[], void *arg)
{
	if(skip_prefix(&line, "device="))
	{
		media_info_t *new_infos = reallocarray(infos, ++info_count, sizeof(*infos));
		if(new_infos != NULL)
		{
			infos = new_infos;
			media_info_t *info = &infos[info_count - 1];
			info->device = strdup(line);
			info->text = NULL;
			info->paths = NULL;
			info->path_count = 0;
			info->has_info = 0;
		}
	}
	else if(info_count > 0)
	{
		media_info_t *info = &infos[info_count - 1];
		if(skip_prefix(&line, "info="))
		{
			replace_string(&info->text, line);
			info->has_info = 1;
		}
		else if(skip_prefix(&line, "label=") && !info->has_info)
		{
			put_string(&info->text, format_str("[%s]", line));
		}
		else if(skip_prefix(&line, "mount-point="))
		{
			info->path_count = add_to_string_array(&info->paths, info->path_count,
					line);
		}
	}
}

/* Releases global data that collects information about media. */
static void
free_info_array(void)
{
	int i;
	for(i = 0; i < info_count; ++i)
	{
		media_info_t *info = &infos[i];
		free(info->device);
		free(info->text);
		free_string_array(info->paths, info->path_count);
	}
	free(infos);
	infos = NULL;
	info_count = 0;
}

/* Puts cursor position to a mount point that corresponds to current path if
 * possible. */
static void
position_cursor(menu_data_t *m)
{
	int i;
	for(i = 0; i < m->len; ++i)
	{
		if(m->data[i] != NULL && *m->data[i] == 'u' &&
				is_in_subtree(m->cwd, m->data[i] + 1, 1))
		{
			m->pos = i;
			break;
		}
	}
}

/* Retrieves decorations for path.  See ui_get_decors(). */
static void
path_get_decors(const char path[], FileType type, const char **prefix,
		const char **suffix)
{
	dir_entry_t entry = {};

	entry.name = get_last_path_component(path);
	/* Avoid memory allocation. */
	if(entry.name != path)
	{
		entry.name[-1] = '\0';
		entry.origin = entry.name;
	}
	else
	{
		entry.origin = (type == FT_BLOCK_DEV ? "/dev" : "/");
	}
	entry.type = type;
	entry.name_dec_num = -1;

	ui_get_decors(&entry, prefix, suffix);

	/* Restore original path. */
	if(entry.name != path)
	{
		entry.name[-1] = '/';
	}
}

/* Retrieves appropriate data for selected item in a human-friendly way. */
static const char *
get_selected_data(menu_data_t *m)
{
	const char *data = m->data[m->pos];
	if(data == NULL)
	{
		char *next_data      = (m->pos + 1 < m->len ? m->data[m->pos + 1] : NULL);
		char *next_next_data = (m->pos + 2 < m->len ? m->data[m->pos + 2] : NULL);
		if(next_data != NULL && next_next_data == NULL)
		{
			data = next_data;
		}
	}
	return data;
}

/* Executes "mount" or "unmount" command on data.  Returns non-zero if program
 * executed successfully and zero otherwise. */
static int
mediaprg_mount(const char data[], menu_data_t *m)
{
	if(data == NULL || (*data != 'm' && *data != 'u') || cfg.media_prg[0] == '\0')
	{
		return 1;
	}

	int error = 0;
	int mount = (*data == 'm');
	const char *path = data + 1;

	if(!mount)
	{
		/* Try to get out of mount point before trying to unmount it. */
		char cwd[PATH_MAX + 1];
		if(get_cwd(cwd, sizeof(cwd)) == cwd && is_in_subtree(cwd, path, 1))
		{
			char out_of_mount_path[PATH_MAX + 1];
			snprintf(out_of_mount_path, sizeof(out_of_mount_path), "%s/..", path);
			(void)vifm_chdir(out_of_mount_path);
		}
	}

	const char *action = (mount ? "mount" : "unmount");
	const char *description = (mount ? "Mounting" : "Unmounting");

	char *escaped_path = shell_arg_escape(path, curr_stats.shell_type);
	char *cmd = format_str("%s %s %s", cfg.media_prg, action, escaped_path);
	if(rn_shell(cmd, PAUSE_NEVER, 0, SHELL_BY_APP) == 0)
	{
		reload_list(m);
	}
	else
	{
		error = 1;
		show_error_msgf("Media operation error", "%s has failed", description);
	}
	free(escaped_path);
	free(cmd);

	menus_partial_redraw(m->state);
	return error;
}

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