/*
 * zramctl - control compressed block devices in RAM
 *
 * Copyright (c) 2014 Timofey Titovets <Nefelim4ag@gmail.com>
 * Copyright (C) 2014 Karel Zak <kzak@redhat.com>
 *
 * 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.
 *
 * This program is distributed in the hope that it would 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 <getopt.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <assert.h>
#include <sys/types.h>
#include <dirent.h>

#include <libsmartcols.h>

#include "c.h"
#include "nls.h"
#include "closestream.h"
#include "strutils.h"
#include "xalloc.h"
#include "sysfs.h"
#include "optutils.h"
#include "ismounted.h"
#include "strv.h"
#include "path.h"
#include "pathnames.h"

/*#define CONFIG_ZRAM_DEBUG*/

#ifdef CONFIG_ZRAM_DEBUG
# define DBG(x)	 do { fputs("zram: ", stderr); x; fputc('\n', stderr); } while(0)
#else
# define DBG(x)
#endif

/* status output columns */
struct colinfo {
	const char *name;
	double whint;
	int flags;
	const char *help;
};

enum {
	COL_NAME = 0,
	COL_DISKSIZE,
	COL_ORIG_SIZE,
	COL_COMP_SIZE,
	COL_ALGORITHM,
	COL_STREAMS,
	COL_ZEROPAGES,
	COL_MEMTOTAL,
	COL_MEMLIMIT,
	COL_MEMUSED,
	COL_MIGRATED,
	COL_MOUNTPOINT
};

static const struct colinfo infos[] = {
	[COL_NAME]      = { "NAME",      0.25, 0, N_("zram device name") },
	[COL_DISKSIZE]  = { "DISKSIZE",     5, SCOLS_FL_RIGHT, N_("limit on the uncompressed amount of data") },
	[COL_ORIG_SIZE] = { "DATA",         5, SCOLS_FL_RIGHT, N_("uncompressed size of stored data") },
	[COL_COMP_SIZE] = { "COMPR",        5, SCOLS_FL_RIGHT, N_("compressed size of stored data") },
	[COL_ALGORITHM] = { "ALGORITHM",    3, 0, N_("the selected compression algorithm") },
	[COL_STREAMS]   = { "STREAMS",      3, SCOLS_FL_RIGHT, N_("number of concurrent compress operations") },
	[COL_ZEROPAGES] = { "ZERO-PAGES",   3, SCOLS_FL_RIGHT, N_("empty pages with no allocated memory") },
	[COL_MEMTOTAL]  = { "TOTAL",        5, SCOLS_FL_RIGHT, N_("all memory including allocator fragmentation and metadata overhead") },
	[COL_MEMLIMIT]  = { "MEM-LIMIT",    5, SCOLS_FL_RIGHT, N_("memory limit used to store compressed data") },
	[COL_MEMUSED]   = { "MEM-USED",     5, SCOLS_FL_RIGHT, N_("memory zram have been consumed to store compressed data") },
	[COL_MIGRATED]  = { "MIGRATED",     5, SCOLS_FL_RIGHT, N_("number of objects migrated by compaction") },
	[COL_MOUNTPOINT]= { "MOUNTPOINT",0.10, SCOLS_FL_TRUNC, N_("where the device is mounted") },
};

static int columns[ARRAY_SIZE(infos) * 2] = {-1};
static int ncolumns;

enum {
	MM_ORIG_DATA_SIZE = 0,
	MM_COMPR_DATA_SIZE,
	MM_MEM_USED_TOTAL,
	MM_MEM_LIMIT,
	MM_MEM_USED_MAX,
	MM_ZERO_PAGES,
	MM_NUM_MIGRATED
};

static const char *mm_stat_names[] = {
	[MM_ORIG_DATA_SIZE]  = "orig_data_size",
	[MM_COMPR_DATA_SIZE] = "compr_data_size",
	[MM_MEM_USED_TOTAL]  = "mem_used_total",
	[MM_MEM_LIMIT]       = "mem_limit",
	[MM_MEM_USED_MAX]    = "mem_used_max",
	[MM_ZERO_PAGES]      = "zero_pages",
	[MM_NUM_MIGRATED]    = "num_migrated"
};

struct zram {
	char	devname[32];
	struct sysfs_cxt sysfs;
	char	**mm_stat;

	unsigned int mm_stat_probed : 1,
		     control_probed : 1,
		     has_control : 1;	/* has /sys/class/zram-control/ */
};

static unsigned int raw, no_headings, inbytes;

static int get_column_id(int num)
{
	assert(num < ncolumns);
	assert(columns[num] < (int) ARRAY_SIZE(infos));
	return columns[num];
}

static const struct colinfo *get_column_info(int num)
{
	return &infos[ get_column_id(num) ];
}

static int column_name_to_id(const char *name, size_t namesz)
{
	size_t i;

	for (i = 0; i < ARRAY_SIZE(infos); i++) {
		const char *cn = infos[i].name;

		if (!strncasecmp(name, cn, namesz) && !*(cn + namesz))
			return i;
	}
	warnx(_("unknown column: %s"), name);
	return -1;
}

static void zram_reset_stat(struct zram *z)
{
	if (z) {
		strv_free(z->mm_stat);
		z->mm_stat = NULL;
		z->mm_stat_probed = 0;
	}
}

static void zram_set_devname(struct zram *z, const char *devname, size_t n)
{
	assert(z);

	if (!devname)
		snprintf(z->devname, sizeof(z->devname), "/dev/zram%zu", n);
	else {
		strncpy(z->devname, devname, sizeof(z->devname));
		z->devname[sizeof(z->devname) - 1] = '\0';
	}

	DBG(fprintf(stderr, "set devname: %s", z->devname));
	sysfs_deinit(&z->sysfs);
	zram_reset_stat(z);
}

static int zram_get_devnum(struct zram *z)
{
	int n;

	assert(z);

	if (sscanf(z->devname, "/dev/zram%d", &n) == 1)
		return n;
	return -EINVAL;
}

static struct zram *new_zram(const char *devname)
{
	struct zram *z = xcalloc(1, sizeof(struct zram));

	DBG(fprintf(stderr, "new: %p", z));
	if (devname)
		zram_set_devname(z, devname, 0);
	return z;
}

static void free_zram(struct zram *z)
{
	if (!z)
		return;
	DBG(fprintf(stderr, "free: %p", z));
	sysfs_deinit(&z->sysfs);
	zram_reset_stat(z);
	free(z);
}

static struct sysfs_cxt *zram_get_sysfs(struct zram *z)
{
	assert(z);

	if (!z->sysfs.devno) {
		dev_t devno = sysfs_devname_to_devno(z->devname, NULL);
		if (!devno)
			return NULL;
		if (sysfs_init(&z->sysfs, devno, NULL))
			return NULL;
		if (*z->devname != '/') {
			/* canonicalize the device name according to /sys */
			char name[PATH_MAX];
			if (sysfs_get_devname(&z->sysfs, name, sizeof(name)))
				snprintf(z->devname, sizeof(z->devname), "/dev/%s", name);
		}
	}

	return &z->sysfs;
}

static inline int zram_exist(struct zram *z)
{
	assert(z);

	errno = 0;
	if (zram_get_sysfs(z) == NULL) {
		errno = ENODEV;
		return 0;
	}

	DBG(fprintf(stderr, "%s exists", z->devname));
	return 1;
}

static int zram_set_u64parm(struct zram *z, const char *attr, uint64_t num)
{
	struct sysfs_cxt *sysfs = zram_get_sysfs(z);
	if (!sysfs)
		return -EINVAL;
	DBG(fprintf(stderr, "%s writing %ju to %s", z->devname, num, attr));
	return sysfs_write_u64(sysfs, attr, num);
}

static int zram_set_strparm(struct zram *z, const char *attr, const char *str)
{
	struct sysfs_cxt *sysfs = zram_get_sysfs(z);
	if (!sysfs)
		return -EINVAL;
	DBG(fprintf(stderr, "%s writing %s to %s", z->devname, str, attr));
	return sysfs_write_string(sysfs, attr, str);
}


static int zram_used(struct zram *z)
{
	uint64_t size;
	struct sysfs_cxt *sysfs = zram_get_sysfs(z);

	if (sysfs &&
	    sysfs_read_u64(sysfs, "disksize", &size) == 0 &&
	    size > 0) {

		DBG(fprintf(stderr, "%s used", z->devname));
		return 1;
	}
	DBG(fprintf(stderr, "%s unused", z->devname));
	return 0;
}

static int zram_has_control(struct zram *z)
{
	if (!z->control_probed) {
		z->has_control = access(_PATH_SYS_CLASS "/zram-control/", F_OK) == 0 ? 1 : 0;
		z->control_probed = 1;
		DBG(fprintf(stderr, "zram-control: %s", z->has_control ? "yes" : "no"));
	}

	return z->has_control;
}

static int zram_control_add(struct zram *z)
{
	int n;

	if (!zram_has_control(z))
		return -ENOSYS;

	n = path_read_s32(_PATH_SYS_CLASS "/zram-control/hot_add");
	if (n < 0)
		return n;

	DBG(fprintf(stderr, "hot-add: %d", n));
	zram_set_devname(z, NULL, n);
	return 0;
}

static int zram_control_remove(struct zram *z)
{
	char str[sizeof stringify_value(INT_MAX)];
	int n;

	if (!zram_has_control(z))
		return -ENOSYS;

	n = zram_get_devnum(z);
	if (n < 0)
		return n;

	DBG(fprintf(stderr, "hot-remove: %d", n));
	snprintf(str, sizeof(str), "%d", n);
	return path_write_str(str, _PATH_SYS_CLASS "/zram-control/hot_remove");
}

static struct zram *find_free_zram(void)
{
	struct zram *z = new_zram(NULL);
	size_t i;
	int isfree = 0;

	for (i = 0; isfree == 0; i++) {
		DBG(fprintf(stderr, "find free: checking zram%zu", i));
		zram_set_devname(z, NULL, i);
		if (!zram_exist(z) && zram_control_add(z) != 0)
			break;
		isfree = !zram_used(z);
	}
	if (!isfree) {
		free_zram(z);
		z = NULL;
	}
	return z;
}

static char *get_mm_stat(struct zram *z, size_t idx, int bytes)
{
	struct sysfs_cxt *sysfs;
	const char *name;
	uint64_t num;

	assert(idx < ARRAY_SIZE(mm_stat_names));
	assert(z);

	sysfs = zram_get_sysfs(z);
	if (!sysfs)
		return NULL;

	/* Linux >= 4.1 uses /sys/block/zram<id>/mm_stat */
	if (!z->mm_stat && !z->mm_stat_probed) {
		char *str;

		str = sysfs_strdup(sysfs, "mm_stat");
		if (str) {
			z->mm_stat = strv_split(str, " ");

			/* make sure kernel provides mm_stat as expected */
			if (strv_length(z->mm_stat) < ARRAY_SIZE(mm_stat_names)) {
				strv_free(z->mm_stat);
				z->mm_stat = NULL;
			}
		}
		z->mm_stat_probed = 1;
		free(str);

	}

	if (z->mm_stat) {
		if (bytes)
			return xstrdup(z->mm_stat[idx]);

		num = strtou64_or_err(z->mm_stat[idx], _("Failed to parse mm_stat"));
		return size_to_human_string(SIZE_SUFFIX_1LETTER, num);
	}

	/* Linux < 4.1 uses /sys/block/zram<id>/<attrname> */
	name = mm_stat_names[idx];
	if (bytes)
		return sysfs_strdup(sysfs, name);
	else if (sysfs_read_u64(sysfs, name, &num) == 0)
		return size_to_human_string(SIZE_SUFFIX_1LETTER, num);
	return NULL;
}

static void fill_table_row(struct libscols_table *tb, struct zram *z)
{
	static struct libscols_line *ln;
	struct sysfs_cxt *sysfs;
	size_t i;
	uint64_t num;

	assert(tb);
	assert(z);

	DBG(fprintf(stderr, "%s: filling status table", z->devname));

	sysfs = zram_get_sysfs(z);
	if (!sysfs)
		return;

	ln = scols_table_new_line(tb, NULL);
	if (!ln)
		err(EXIT_FAILURE, _("failed to allocate output line"));

	for (i = 0; i < (size_t) ncolumns; i++) {
		char *str = NULL;

		switch (get_column_id(i)) {
		case COL_NAME:
			str = xstrdup(z->devname);
			break;
		case COL_DISKSIZE:
			if (inbytes)
				str = sysfs_strdup(sysfs, "disksize");
			else if (sysfs_read_u64(sysfs, "disksize", &num) == 0)
				str = size_to_human_string(SIZE_SUFFIX_1LETTER, num);
			break;
		case COL_ALGORITHM:
		{
			char *alg = sysfs_strdup(sysfs, "comp_algorithm");
			if (!alg)
				break;
			if (strstr(alg, "[lzo]") == NULL) {
				if (strstr(alg, "[lz4]") == NULL)
					;
				else
					str = xstrdup("lz4");
			} else
				str = xstrdup("lzo");
			free(alg);
			break;
		}
		case COL_MOUNTPOINT:
		{
			char path[PATH_MAX] = { '\0' };
			int fl;

			check_mount_point(z->devname, &fl, path, sizeof(path));
			if (*path)
				str = xstrdup(path);
			break;
		}
		case COL_STREAMS:
			str = sysfs_strdup(sysfs, "max_comp_streams");
			break;
		case COL_ZEROPAGES:
			str = get_mm_stat(z, MM_ZERO_PAGES, 1);
			break;
		case COL_ORIG_SIZE:
			str = get_mm_stat(z, MM_ORIG_DATA_SIZE, inbytes);
			break;
		case COL_COMP_SIZE:
			str = get_mm_stat(z, MM_COMPR_DATA_SIZE, inbytes);
			break;
		case COL_MEMTOTAL:
			str = get_mm_stat(z, MM_MEM_USED_TOTAL, inbytes);
			break;
		case COL_MEMLIMIT:
			str = get_mm_stat(z, MM_MEM_LIMIT, inbytes);
			break;
		case COL_MEMUSED:
			str = get_mm_stat(z, MM_MEM_USED_MAX, inbytes);
			break;
		case COL_MIGRATED:
			str = get_mm_stat(z, MM_NUM_MIGRATED, inbytes);
			break;
		}
		if (str && scols_line_refer_data(ln, i, str))
			err(EXIT_FAILURE, _("failed to add output data"));
	}
}

static void status(struct zram *z)
{
	struct libscols_table *tb;
	size_t i;
	DIR *dir;
	struct dirent *d;

	scols_init_debug(0);

	tb = scols_new_table();
	if (!tb)
		err(EXIT_FAILURE, _("failed to allocate output table"));

	scols_table_enable_raw(tb, raw);
	scols_table_enable_noheadings(tb, no_headings);

	for (i = 0; i < (size_t) ncolumns; i++) {
		const struct colinfo *col = get_column_info(i);

		if (!scols_table_new_column(tb, col->name, col->whint, col->flags))
			err(EXIT_FAILURE, _("failed to initialize output column"));
	}

	if (z) {
		/* just one device specified */
		fill_table_row(tb, z);
		goto print_table;
	}

	/* list all used devices */
	z = new_zram(NULL);
	if (!(dir = opendir(_PATH_DEV)))
		err(EXIT_FAILURE, _("cannot open %s"), _PATH_DEV);

	while ((d = readdir(dir))) {
		int n;
		if (sscanf(d->d_name, "zram%d", &n) != 1)
			continue;
		zram_set_devname(z, NULL, n);
		if (zram_exist(z) && zram_used(z))
			fill_table_row(tb, z);
	}
	closedir(dir);
	free_zram(z);

print_table:
	scols_print_table(tb);
	scols_unref_table(tb);
}

static void __attribute__((__noreturn__)) usage(void)
{
	FILE *out = stdout;
	size_t i;

	fputs(USAGE_HEADER, out);
	fprintf(out, _(	" %1$s [options] <device>\n"
			" %1$s -r <device> [...]\n"
			" %1$s [options] -f | <device> -s <size>\n"),
			program_invocation_short_name);

	fputs(USAGE_SEPARATOR, out);
	fputs(_("Set up and control zram devices.\n"), out);

	fputs(USAGE_OPTIONS, out);
	fputs(_(" -a, --algorithm lzo|lz4   compression algorithm to use\n"), out);
	fputs(_(" -b, --bytes               print sizes in bytes rather than in human readable format\n"), out);
	fputs(_(" -f, --find                find a free device\n"), out);
	fputs(_(" -n, --noheadings          don't print headings\n"), out);
	fputs(_(" -o, --output <list>       columns to use for status output\n"), out);
	fputs(_("     --raw                 use raw status output format\n"), out);
	fputs(_(" -r, --reset               reset all specified devices\n"), out);
	fputs(_(" -s, --size <size>         device size\n"), out);
	fputs(_(" -t, --streams <number>    number of compression streams\n"), out);

	fputs(USAGE_SEPARATOR, out);
	printf(USAGE_HELP_OPTIONS(27));

	fputs(USAGE_COLUMNS, out);
	for (i = 0; i < ARRAY_SIZE(infos); i++)
		fprintf(out, " %11s  %s\n", infos[i].name, _(infos[i].help));

	printf(USAGE_MAN_TAIL("zramctl(8)"));
	exit(EXIT_SUCCESS);
}

/* actions */
enum {
	A_NONE = 0,
	A_STATUS,
	A_CREATE,
	A_FINDONLY,
	A_RESET
};

int main(int argc, char **argv)
{
	uintmax_t size = 0, nstreams = 0;
	char *algorithm = NULL;
	int rc = 0, c, find = 0, act = A_NONE;
	struct zram *zram = NULL;

	enum { OPT_RAW = CHAR_MAX + 1 };

	static const struct option longopts[] = {
		{ "algorithm", required_argument, NULL, 'a' },
		{ "bytes",     no_argument, NULL, 'b' },
		{ "find",      no_argument, NULL, 'f' },
		{ "help",      no_argument, NULL, 'h' },
		{ "output",    required_argument, NULL, 'o' },
		{ "noheadings",no_argument, NULL, 'n' },
		{ "reset",     no_argument, NULL, 'r' },
		{ "raw",       no_argument, NULL, OPT_RAW },
		{ "size",      required_argument, NULL, 's' },
		{ "streams",   required_argument, NULL, 't' },
		{ "version",   no_argument, NULL, 'V' },
		{ NULL, 0, NULL, 0 }
	};

	static const ul_excl_t excl[] = {
		{ 'f', 'o', 'r' },
		{ 'o', 'r', 's' },
		{ 0 }
	};
	int excl_st[ARRAY_SIZE(excl)] = UL_EXCL_STATUS_INIT;

	setlocale(LC_ALL, "");
	bindtextdomain(PACKAGE, LOCALEDIR);
	textdomain(PACKAGE);
	atexit(close_stdout);

	while ((c = getopt_long(argc, argv, "a:bfho:nrs:t:V", longopts, NULL)) != -1) {

		err_exclusive_options(c, longopts, excl, excl_st);

		switch (c) {
		case 'a':
			if (strcmp(optarg,"lzo") && strcmp(optarg,"lz4"))
				errx(EXIT_FAILURE, _("unsupported algorithm: %s"),
					     optarg);
			algorithm = optarg;
			break;
		case 'b':
			inbytes = 1;
			break;
		case 'f':
			find = 1;
			break;
		case 'o':
			ncolumns = string_to_idarray(optarg,
						     columns, ARRAY_SIZE(columns),
						     column_name_to_id);
			if (ncolumns < 0)
				return EXIT_FAILURE;
			break;
		case 's':
			size = strtosize_or_err(optarg, _("failed to parse size"));
			act = A_CREATE;
			break;
		case 't':
			nstreams = strtou64_or_err(optarg, _("failed to parse streams"));
			break;
		case 'r':
			act = A_RESET;
			break;
		case OPT_RAW:
			raw = 1;
			break;
		case 'n':
			no_headings = 1;
			break;
		case 'V':
			printf(UTIL_LINUX_VERSION);
			return EXIT_SUCCESS;
		case 'h':
			usage();
		default:
			errtryhelp(EXIT_FAILURE);
		}
	}

	if (find && optind < argc)
		errx(EXIT_FAILURE, _("option --find is mutually exclusive "
				     "with <device>"));
	if (act == A_NONE)
		act = find ? A_FINDONLY : A_STATUS;

	if (act != A_RESET && optind + 1 < argc)
		errx(EXIT_FAILURE, _("only one <device> at a time is allowed"));

	if ((act == A_STATUS || act == A_FINDONLY) && (algorithm || nstreams))
		errx(EXIT_FAILURE, _("options --algorithm and --streams "
				     "must be combined with --size"));

	switch (act) {
	case A_STATUS:
		if (!ncolumns) {		/* default columns */
			columns[ncolumns++] = COL_NAME;
			columns[ncolumns++] = COL_ALGORITHM;
			columns[ncolumns++] = COL_DISKSIZE;
			columns[ncolumns++] = COL_ORIG_SIZE;
			columns[ncolumns++] = COL_COMP_SIZE;
			columns[ncolumns++] = COL_MEMTOTAL;
			columns[ncolumns++] = COL_STREAMS;
			columns[ncolumns++] = COL_MOUNTPOINT;
		}
		if (optind < argc) {
			zram = new_zram(argv[optind++]);
			if (!zram_exist(zram))
				err(EXIT_FAILURE, "%s", zram->devname);
		}
		status(zram);
		free_zram(zram);
		break;
	case A_RESET:
		if (optind == argc)
			errx(EXIT_FAILURE, _("no device specified"));
		while (optind < argc) {
			zram = new_zram(argv[optind]);
			if (!zram_exist(zram)
			    || zram_set_u64parm(zram, "reset", 1)) {
				warn(_("%s: failed to reset"), zram->devname);
				rc = 1;
			}
			zram_control_remove(zram);
			free_zram(zram);
			optind++;
		}
		break;
	case A_FINDONLY:
		zram = find_free_zram();
		if (!zram)
			errx(EXIT_FAILURE, _("no free zram device found"));
		printf("%s\n", zram->devname);
		free_zram(zram);
		break;
	case A_CREATE:
		if (find) {
			zram = find_free_zram();
			if (!zram)
				errx(EXIT_FAILURE, _("no free zram device found"));
		} else if (optind == argc)
			errx(EXIT_FAILURE, _("no device specified"));
		else {
			zram = new_zram(argv[optind]);
			if (!zram_exist(zram))
				err(EXIT_FAILURE, "%s", zram->devname);
		}

		if (zram_set_u64parm(zram, "reset", 1))
			err(EXIT_FAILURE, _("%s: failed to reset"), zram->devname);

		if (nstreams &&
		    zram_set_u64parm(zram, "max_comp_streams", nstreams))
			err(EXIT_FAILURE, _("%s: failed to set number of streams"), zram->devname);

		if (algorithm &&
		    zram_set_strparm(zram, "comp_algorithm", algorithm))
			err(EXIT_FAILURE, _("%s: failed to set algorithm"), zram->devname);

		if (zram_set_u64parm(zram, "disksize", size))
			err(EXIT_FAILURE, _("%s: failed to set disksize (%ju bytes)"),
				zram->devname, size);
		if (find)
			printf("%s\n", zram->devname);
		free_zram(zram);
		break;
	}

	return rc ? EXIT_FAILURE : EXIT_SUCCESS;
}
