/*
 * fincore - count pages of file contents in core
 *
 * Copyright (C) 2017 Red Hat, Inc. All rights reserved.
 * Written by Masatake YAMATO <yamato@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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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 <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <getopt.h>
#include <stdio.h>
#include <string.h>

#include "c.h"
#include "nls.h"
#include "closestream.h"
#include "xalloc.h"
#include "strutils.h"
#include "blkdev.h"

#include "libsmartcols.h"

/* For large files, mmap is called in iterative way.
   Window is the unit of vma prepared in each mmap
   calling.

   Window size depends on page size.
   e.g. 128MB on x86_64. ( = N_PAGES_IN_WINDOW * 4096 ). */
#define N_PAGES_IN_WINDOW ((size_t)(32 * 1024))

#ifndef HAVE_CACHESTAT

#ifndef SYS_cachestat
#if defined (__alpha__)
#define SYS_cachestat 561
#else
#define SYS_cachestat 451
#endif
#endif

struct cachestat_range {
	uint64_t off;
	uint64_t len;
};

struct cachestat {
	uint64_t nr_cache;
	uint64_t nr_dirty;
	uint64_t nr_writeback;
	uint64_t nr_evicted;
	uint64_t nr_recently_evicted;
};

static inline int cachestat(unsigned int fd,
			    const struct cachestat_range *cstat_range,
			    struct cachestat *cstat, unsigned int flags)
{
	return syscall(SYS_cachestat, fd, cstat_range, cstat, flags);
}

#endif // HAVE_CACHESTAT

struct colinfo {
	const char * const name;
	double whint;
	int flags;
	const char *help;
	unsigned int pages : 1;
};

enum {
	COL_PAGES,
	COL_SIZE,
	COL_FILE,
	COL_RES,
	COL_DIRTY_PAGES,
	COL_DIRTY,
	COL_WRITEBACK_PAGES,
	COL_WRITEBACK,
	COL_EVICTED_PAGES,
	COL_EVICTED,
	COL_RECENTLY_EVICTED_PAGES,
	COL_RECENTLY_EVICTED,
};

static const struct colinfo infos[] = {
	[COL_PAGES]                  = { "PAGES",                  1, SCOLS_FL_RIGHT, N_("file data resident in memory in pages"), 1},
	[COL_RES]                    = { "RES",                    5, SCOLS_FL_RIGHT, N_("file data resident in memory in bytes")},
	[COL_SIZE]                   = { "SIZE",                   5, SCOLS_FL_RIGHT, N_("size of the file")},
	[COL_FILE]                   = { "FILE",                   4, 0, N_("file name")},
	[COL_DIRTY_PAGES]            = { "DIRTY_PAGES",            1, SCOLS_FL_RIGHT, N_("number of dirty pages"), 1},
	[COL_DIRTY]                  = { "DIRTY",                  5, SCOLS_FL_RIGHT, N_("number of dirty bytes")},
	[COL_WRITEBACK_PAGES]        = { "WRITEBACK_PAGES",        1, SCOLS_FL_RIGHT, N_("number of pages marked for writeback"), 1},
	[COL_WRITEBACK]              = { "WRITEBACK",              5, SCOLS_FL_RIGHT, N_("number of bytes marked for writeback")},
	[COL_EVICTED_PAGES]          = { "EVICTED_PAGES",          1, SCOLS_FL_RIGHT, N_("number of evicted pages"), 1},
	[COL_EVICTED]                = { "EVICTED",                5, SCOLS_FL_RIGHT, N_("number of evicted bytes")},
	[COL_RECENTLY_EVICTED_PAGES] = { "RECENTLY_EVICTED_PAGES", 1, SCOLS_FL_RIGHT, N_("number of recently evicted pages"), 1},
	[COL_RECENTLY_EVICTED]       = { "RECENTLY_EVICTED",       5, SCOLS_FL_RIGHT, N_("number of recently evicted bytes")},
};

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

struct fincore_control {
	const size_t pagesize;

	struct libscols_table *tb;		/* output */

	unsigned int bytes : 1,
		     noheadings : 1,
		     raw : 1,
		     json : 1;

};

struct fincore_state {
	const char * const name;
	long long unsigned int file_size;

	struct cachestat cstat;
	struct {
		unsigned int dirty : 1,
			     writeback : 1,
			     evicted : 1,
			     recently_evicted : 1;
	} cstat_fields;
};


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 int get_column_id(int num)
{
	assert(num >= 0);
	assert((size_t) 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 get_cstat_value(const struct fincore_state *st, int column_id,
			   uint64_t *value)
{
	switch(column_id) {
	case COL_PAGES:
	case COL_RES:
		*value = st->cstat.nr_cache;
		return 1;
	case COL_DIRTY_PAGES:
	case COL_DIRTY:
		if (!st->cstat_fields.dirty)
			break;
		*value = st->cstat.nr_dirty;
		return 1;
	case COL_WRITEBACK_PAGES:
	case COL_WRITEBACK:
		if (!st->cstat_fields.writeback)
		*value = st->cstat.nr_writeback;
		return 1;
	case COL_EVICTED_PAGES:
	case COL_EVICTED:
		if (!st->cstat_fields.evicted)
			break;
		*value = st->cstat.nr_evicted;
		return 1;
	case COL_RECENTLY_EVICTED_PAGES:
	case COL_RECENTLY_EVICTED:
		if (!st->cstat_fields.recently_evicted)
			break;
		*value = st->cstat.nr_recently_evicted;
		return 1;
	default:
		assert(0);
	}
	return 0;
}

static int add_output_data(struct fincore_control *ctl,
			   struct fincore_state *st)
{
	size_t i;
	char *tmp;
	uint64_t value = 0;
	struct libscols_line *ln;

	assert(ctl);
	assert(ctl->tb);

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

	for (i = 0; i < ncolumns; i++) {
		int rc = 0;
		int column_id = get_column_id(i);
		int format_value = 0;

		switch(column_id) {
		case COL_FILE:
			rc = scols_line_set_data(ln, i, st->name);
			break;
		case COL_SIZE:
			if (ctl->bytes)
				xasprintf(&tmp, "%jd", (intmax_t) st->file_size);
			else
				tmp = size_to_human_string(SIZE_SUFFIX_1LETTER, st->file_size);
			rc = scols_line_refer_data(ln, i, tmp);
			break;
		case COL_PAGES:
		case COL_RES:
		case COL_DIRTY_PAGES:
		case COL_DIRTY:
		case COL_WRITEBACK_PAGES:
		case COL_WRITEBACK:
		case COL_EVICTED:
		case COL_EVICTED_PAGES:
		case COL_RECENTLY_EVICTED:
		case COL_RECENTLY_EVICTED_PAGES:
			format_value = get_cstat_value(st, column_id, &value);
			break;
		default:
			return -EINVAL;
		}

		if (format_value) {
			if (get_column_info(i)->pages) {
				xasprintf(&tmp, "%ju", (uintmax_t) value);
			} else {
				value *= ctl->pagesize;
				if (ctl->bytes)
					xasprintf(&tmp, "%ju", (uintmax_t) value);
				else
					tmp = size_to_human_string(SIZE_SUFFIX_1LETTER, value);
			}
			rc = scols_line_refer_data(ln, i, tmp);
		}

		if (rc)
			err(EXIT_FAILURE, _("failed to add output data"));
	}

	return 0;
}

static int do_mincore(struct fincore_control *ctl,
		      void *window, const size_t len,
		      struct fincore_state *st)
{
	static unsigned char vec[N_PAGES_IN_WINDOW];
	int n = (len / ctl->pagesize) + ((len % ctl->pagesize)? 1: 0);

	if (mincore (window, len, vec) < 0) {
		warn(_("failed to do mincore: %s"), st->name);
		return -errno;
	}

	while (n > 0)
	{
		if (vec[--n] & 0x1)
		{
			vec[n] = 0;
			st->cstat.nr_cache++;
		}
	}

	return 0;
}

static int mincore_fd (struct fincore_control *ctl,
		       int fd,
		       struct fincore_state *st)
{
	size_t window_size = N_PAGES_IN_WINDOW * ctl->pagesize;
	long long unsigned int file_offset, len;
	int rc = 0;

	for (file_offset = 0; file_offset < st->file_size; file_offset += len) {
		void  *window = NULL;

		len = st->file_size - file_offset;
		if (len >= window_size)
			len = window_size;

		/* PROT_NONE is enough for Linux, but qemu-user wants PROT_READ */
		window = mmap(window, len, PROT_READ, MAP_PRIVATE, fd, file_offset);
		if (window == MAP_FAILED) {
			rc = -EINVAL;
			warn(_("failed to do mmap: %s"), st->name);
			break;
		}

		rc = do_mincore(ctl, window, len, st);
		if (rc)
			break;

		munmap (window, len);
	}

	return rc;
}

static int fincore_fd (struct fincore_control *ctl,
		       int fd,
		       struct fincore_state *st)
{
	int rc;
	const struct cachestat_range cstat_range = { 0 };

	rc = cachestat(fd, &cstat_range, &st->cstat, 0);
	if (!rc) {
		st->cstat_fields.dirty = 1;
		st->cstat_fields.writeback = 1;
		st->cstat_fields.evicted = 1;
		st->cstat_fields.recently_evicted = 1;
		return 0;
	}

	if (errno != ENOSYS)
		warn(_("failed to do cachestat: %s"), st->name);

	return mincore_fd(ctl, fd, st);
}

/*
 * Returns: <0 on error, 0 success, 1 ignore.
 */
static int fincore_name(struct fincore_control *ctl,
			struct fincore_state *st)
{
	int fd;
	int rc = 0;
	struct stat sb;

	if ((fd = open (st->name, O_RDONLY)) < 0) {
		warn(_("failed to open: %s"), st->name);
		return -errno;
	}

	if (fstat (fd, &sb) < 0) {
		warn(_("failed to do fstat: %s"), st->name);
		close (fd);
		return -errno;
	}
	st->file_size = sb.st_size;

	if (S_ISBLK(sb.st_mode)) {
		rc = blkdev_get_size(fd, &st->file_size);
		if (rc)
			warn(_("failed ioctl to get size: %s"), st->name);
	} else if (S_ISREG(sb.st_mode)) {
		st->file_size = sb.st_size;
	} else {
		rc = 1;			/* ignore things like symlinks
					 * and directories*/
	}

	if (!rc)
		rc = fincore_fd(ctl, fd, st);

	close (fd);
	return rc;
}

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

	fputs(USAGE_HEADER, out);
	fprintf(out, _(" %s [options] file...\n"), program_invocation_short_name);

	fputs(USAGE_OPTIONS, out);
	fputs(_(" -J, --json            use JSON output format\n"), out);
	fputs(_(" -b, --bytes           print sizes in bytes rather than in human readable format\n"), out);
	fputs(_(" -n, --noheadings      don't print headings\n"), out);
	fputs(_(" -o, --output <list>   output columns\n"), out);
	fputs(_("     --output-all      output all columns\n"), out);
	fputs(_(" -r, --raw             use raw output format\n"), out);

	fputs(USAGE_SEPARATOR, out);
	fprintf(out, USAGE_HELP_OPTIONS(23));

	fputs(USAGE_COLUMNS, out);

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

	fprintf(out, USAGE_MAN_TAIL("fincore(1)"));

	exit(EXIT_SUCCESS);
}

int main(int argc, char ** argv)
{
	int c;
	size_t i;
	int rc = EXIT_SUCCESS;
	char *outarg = NULL;

	struct fincore_control ctl = {
		.pagesize = getpagesize()
	};

	enum {
		OPT_OUTPUT_ALL = CHAR_MAX + 1
	};
	static const struct option longopts[] = {
		{ "bytes",      no_argument, NULL, 'b' },
		{ "noheadings", no_argument, NULL, 'n' },
		{ "output",     required_argument, NULL, 'o' },
		{ "output-all",	no_argument,       NULL, OPT_OUTPUT_ALL },
		{ "version",    no_argument, NULL, 'V' },
		{ "help",	no_argument, NULL, 'h' },
		{ "json",       no_argument, NULL, 'J' },
		{ "raw",        no_argument, NULL, 'r' },
		{ NULL, 0, NULL, 0 },
	};

	setlocale(LC_ALL, "");
	bindtextdomain(PACKAGE, LOCALEDIR);
	textdomain(PACKAGE);
	close_stdout_atexit();

	while ((c = getopt_long (argc, argv, "bno:JrVh", longopts, NULL)) != -1) {
		switch (c) {
		case 'b':
			ctl.bytes = 1;
			break;
		case 'n':
			ctl.noheadings = 1;
			break;
		case 'o':
			outarg = optarg;
			break;
		case OPT_OUTPUT_ALL:
			for (ncolumns = 0; ncolumns < ARRAY_SIZE(infos); ncolumns++)
				columns[ncolumns] = ncolumns;
			break;
		case 'J':
			ctl.json = 1;
			break;
		case 'r':
			ctl.raw = 1;
			break;
		case 'V':
			print_version(EXIT_SUCCESS);
		case 'h':
			usage();
		default:
			errtryhelp(EXIT_FAILURE);
		}
	}

	if (optind == argc) {
		warnx(_("no file specified"));
		errtryhelp(EXIT_FAILURE);
	}

	if (!ncolumns) {
		columns[ncolumns++] = COL_RES;
		columns[ncolumns++] = COL_PAGES;
		columns[ncolumns++] = COL_SIZE;
		columns[ncolumns++] = COL_FILE;
	}

	if (outarg && string_add_to_idarray(outarg, columns, ARRAY_SIZE(columns),
					 &ncolumns, column_name_to_id) < 0)
		return EXIT_FAILURE;

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

	scols_table_enable_noheadings(ctl.tb, ctl.noheadings);
	scols_table_enable_raw(ctl.tb, ctl.raw);
	scols_table_enable_json(ctl.tb, ctl.json);
	if (ctl.json)
		scols_table_set_name(ctl.tb, "fincore");

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

		cl = scols_table_new_column(ctl.tb, col->name, col->whint, col->flags);
		if (!cl)
			err(EXIT_FAILURE, _("failed to allocate output column"));

		if (ctl.json) {
			int id = get_column_id(i);

			switch (id) {
			case COL_FILE:
				scols_column_set_json_type(cl, SCOLS_JSON_STRING);
				break;
			case COL_SIZE:
			case COL_RES:
				if (!ctl.bytes)
					break;
				/* fallthrough */
			default:
				scols_column_set_json_type(cl, SCOLS_JSON_NUMBER);
				break;
			}
		}
	}

	for(; optind < argc; optind++) {
		struct fincore_state st = {
			.name = argv[optind],
		};

		switch (fincore_name(&ctl, &st)) {
		case 0:
			add_output_data(&ctl, &st);
			break;
		case 1:
			break; /* ignore */
		default:
			rc = EXIT_FAILURE;
			break;
		}
	}

	scols_print_table(ctl.tb);
	scols_unref_table(ctl.tb);

	return rc;
}
