// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (c) 2000-2001,2005 Silicon Graphics, Inc.
 * Copyright (c) 2012 Red Hat, Inc.
 * Copyright (c) 2017 Oracle.
 * All Rights Reserved.
 */

#include "libxfs.h"
#include <linux/fiemap.h>
#include <linux/fsmap.h>
#include "libfrog/fsgeom.h"
#include "command.h"
#include "init.h"
#include "libfrog/paths.h"
#include "space.h"
#include "input.h"

struct histent
{
	long long	low;
	long long	high;
	long long	count;
	long long	blocks;
};

static int		agcount;
static xfs_agnumber_t	*aglist;
static struct histent	*hist;
static int		dumpflag;
static long long	equalsize;
static long long	multsize;
static int		histcount;
static int		seen1;
static int		summaryflag;
static int		gflag;
static bool		rtflag;
static long long	totblocks;
static long long	totexts;

static cmdinfo_t freesp_cmd;

static void
addhistent(
	long long	h)
{
	if (histcount == INT_MAX) {
		printf(_("Too many histogram buckets.\n"));
		return;
	}
	hist = realloc(hist, (histcount + 1) * sizeof(*hist));
	if (h == 0)
		h = 1;
	hist[histcount].low = h;
	hist[histcount].count = hist[histcount].blocks = 0;
	histcount++;
	if (h == 1)
		seen1 = 1;
}

static void
addtohist(
	xfs_agnumber_t	agno,
	xfs_agblock_t	agbno,
	off64_t		len)
{
	long		i;

	if (dumpflag)
		printf("%8d %8d %8"PRId64"\n", agno, agbno, len);
	totexts++;
	totblocks += len;
	for (i = 0; i < histcount; i++) {
		if (hist[i].high >= len) {
			hist[i].count++;
			hist[i].blocks += len;
			break;
		}
	}
}

static int
hcmp(
	const void	*a,
	const void	*b)
{
	return ((struct histent *)a)->low - ((struct histent *)b)->low;
}

static void
histinit(
	long long	maxlen)
{
	long long	i;

	if (equalsize) {
		for (i = 1; i < maxlen; i += equalsize)
			addhistent(i);
	} else if (multsize) {
		for (i = 1; i < maxlen; i *= multsize)
			addhistent(i);
	} else {
		if (!seen1)
			addhistent(1);
		qsort(hist, histcount, sizeof(*hist), hcmp);
	}
	for (i = 0; i < histcount; i++) {
		if (i < histcount - 1)
			hist[i].high = hist[i + 1].low - 1;
		else
			hist[i].high = maxlen;
	}
}

static void
printhist(void)
{
	int	i;

	printf("%7s %7s %7s %7s %6s\n",
		_("from"), _("to"), _("extents"), _("blocks"), _("pct"));
	for (i = 0; i < histcount; i++) {
		if (hist[i].count)
			printf("%7lld %7lld %7lld %7lld %6.2f\n", hist[i].low,
				hist[i].high, hist[i].count, hist[i].blocks,
				hist[i].blocks * 100.0 / totblocks);
	}
}

static int
inaglist(
	xfs_agnumber_t	agno)
{
	int		i;

	if (agcount == 0)
		return 1;
	for (i = 0; i < agcount; i++)
		if (aglist[i] == agno)
			return 1;
	return 0;
}

#define NR_EXTENTS 128

static void
scan_ag(
	xfs_agnumber_t		agno)
{
	struct fsmap_head	*fsmap;
	struct fsmap		*extent;
	struct fsmap		*l, *h;
	struct fsmap		*p;
	struct xfs_fd		*xfd = &file->xfd;
	off64_t			aglen;
	xfs_agblock_t		agbno;
	unsigned long long	freeblks = 0;
	unsigned long long	freeexts = 0;
	int			ret;
	int			i;

	fsmap = malloc(fsmap_sizeof(NR_EXTENTS));
	if (!fsmap) {
		fprintf(stderr, _("%s: fsmap malloc failed.\n"), progname);
		exitcode = 1;
		return;
	}

	memset(fsmap, 0, sizeof(*fsmap));
	fsmap->fmh_count = NR_EXTENTS;
	l = fsmap->fmh_keys;
	h = fsmap->fmh_keys + 1;
	if (agno != NULLAGNUMBER) {
		l->fmr_physical = cvt_agbno_to_b(xfd, agno, 0);
		h->fmr_physical = cvt_agbno_to_b(xfd, agno + 1, 0);
		l->fmr_device = h->fmr_device = file->fs_path.fs_datadev;
	} else {
		l->fmr_physical = 0;
		h->fmr_physical = ULLONG_MAX;
		l->fmr_device = h->fmr_device = file->fs_path.fs_rtdev;
	}
	h->fmr_owner = ULLONG_MAX;
	h->fmr_flags = UINT_MAX;
	h->fmr_offset = ULLONG_MAX;

	while (true) {
		ret = ioctl(file->xfd.fd, FS_IOC_GETFSMAP, fsmap);
		if (ret < 0) {
			fprintf(stderr, _("%s: FS_IOC_GETFSMAP [\"%s\"]: %s\n"),
				progname, file->name, strerror(errno));
			free(fsmap);
			exitcode = 1;
			return;
		}

		/* No more extents to map, exit */
		if (!fsmap->fmh_entries)
			break;

		for (i = 0, extent = fsmap->fmh_recs;
		     i < fsmap->fmh_entries;
		     i++, extent++) {
			if (!(extent->fmr_flags & FMR_OF_SPECIAL_OWNER) ||
			    extent->fmr_owner != XFS_FMR_OWN_FREE)
				continue;
			agbno = cvt_b_to_agbno(xfd, extent->fmr_physical);
			aglen = cvt_b_to_off_fsbt(xfd, extent->fmr_length);
			freeblks += aglen;
			freeexts++;

			addtohist(agno, agbno, aglen);
		}

		p = &fsmap->fmh_recs[fsmap->fmh_entries - 1];
		if (p->fmr_flags & FMR_OF_LAST)
			break;
		fsmap_advance(fsmap);
	}

	if (gflag) {
		if (agno == NULLAGNUMBER)
			printf(_("     rtdev %10llu %10llu\n"), freeexts,
					freeblks);
		else
			printf(_("%10u %10llu %10llu\n"), agno, freeexts,
					freeblks);
	}
	free(fsmap);
}

static void
aglistadd(
	char		*a)
{
	xfs_agnumber_t	x;

	aglist = realloc(aglist, (agcount + 1) * sizeof(*aglist));
	x = cvt_u32(a, 0);
	if (errno) {
		printf(_("Unrecognized AG number: %s\n"), a);
		return;
	}
	aglist[agcount] = x;
	agcount++;
}

static int
init(
	int			argc,
	char			**argv)
{
	struct xfs_fsop_geom	*fsgeom = &file->xfd.fsgeom;
	long long		x;
	int			c;
	int			speced = 0;	/* only one of -b -e -h or -m */

	agcount = dumpflag = equalsize = multsize = optind = gflag = 0;
	histcount = seen1 = summaryflag = 0;
	totblocks = totexts = 0;
	aglist = NULL;
	hist = NULL;
	rtflag = false;

	while ((c = getopt(argc, argv, "a:bde:gh:m:rs")) != EOF) {
		switch (c) {
		case 'a':
			aglistadd(optarg);
			break;
		case 'b':
			if (speced)
				goto many_spec;
			multsize = 2;
			speced = 1;
			break;
		case 'd':
			dumpflag = 1;
			break;
		case 'e':
			if (speced)
				goto many_spec;
			equalsize = cvt_s64(optarg, 0);
			if (errno)
				return command_usage(&freesp_cmd);
			speced = 1;
			break;
		case 'g':
			histcount = 0;
			gflag++;
			break;
		case 'h':
			if (speced && !histcount)
				goto many_spec;
			/* addhistent increments histcount */
			x = cvt_s64(optarg, 0);
			if (errno)
				return command_usage(&freesp_cmd);
			addhistent(x);
			speced = 1;
			break;
		case 'm':
			if (speced)
				goto many_spec;
			multsize = cvt_s64(optarg, 0);
			if (errno)
				return command_usage(&freesp_cmd);
			speced = 1;
			break;
		case 'r':
			rtflag = true;
			break;
		case 's':
			summaryflag = 1;
			break;
		default:
			return command_usage(&freesp_cmd);
		}
	}
	if (optind != argc)
		return 0;
	if (!speced)
		multsize = 2;
	histinit(fsgeom->agblocks);
	return 1;
many_spec:
	return command_usage(&freesp_cmd);
}

/*
 * Report on freespace usage in xfs filesystem.
 */
static int
freesp_f(
	int			argc,
	char			**argv)
{
	struct xfs_fsop_geom	*fsgeom = &file->xfd.fsgeom;
	xfs_agnumber_t		agno;

	if (!init(argc, argv))
		return 0;
	if (gflag)
		printf(_("        AG    extents     blocks\n"));
	if (rtflag)
		scan_ag(NULLAGNUMBER);
	for (agno = 0; !rtflag && agno < fsgeom->agcount; agno++) {
		if (inaglist(agno))
			scan_ag(agno);
	}
	if (histcount && !gflag)
		printhist();
	if (summaryflag) {
		printf(_("total free extents %lld\n"), totexts);
		printf(_("total free blocks %lld\n"), totblocks);
		printf(_("average free extent size %g\n"),
			(double)totblocks / (double)totexts);
	}
	if (aglist)
		free(aglist);
	if (hist)
		free(hist);
	return 0;
}

static void
freesp_help(void)
{
	printf(_(
"\n"
"Examine filesystem free space\n"
"\n"
" -a agno  -- Scan only the given AG agno.\n"
" -b       -- binary histogram bin size\n"
" -d       -- debug output\n"
" -e bsize -- Use fixed histogram bin size of bsize\n"
" -g       -- Print only a per-AG summary.\n"
" -h hbsz  -- Use custom histogram bin size of h1.\n"
"             Multiple specifications are allowed.\n"
" -m bmult -- Use histogram bin size multiplier of bmult.\n"
" -r       -- Display realtime device free space information.\n"
" -s       -- Emit freespace summary information.\n"
"\n"
"Only one of -b, -e, -h, or -m may be specified.\n"
"\n"));

}

void
freesp_init(void)
{
	freesp_cmd.name = "freesp";
	freesp_cmd.altname = "fsp";
	freesp_cmd.cfunc = freesp_f;
	freesp_cmd.argmin = 0;
	freesp_cmd.argmax = -1;
	freesp_cmd.args = "[-dgrs] [-a agno]... [ -b | -e bsize | -h h1... | -m bmult ]";
	freesp_cmd.flags = CMD_FLAG_ONESHOT;
	freesp_cmd.oneline = _("Examine filesystem free space");
	freesp_cmd.help = freesp_help;

	add_command(&freesp_cmd);
}

