// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2019 Oracle.  All Rights Reserved.
 * Author: Darrick J. Wong <darrick.wong@oracle.com>
 */
#include "libxfs.h"
#include "command.h"
#include "output.h"
#include "init.h"
#include "io.h"
#include "type.h"
#include "input.h"
#include "libfrog/convert.h"

struct btmap {
	const char	*tag;
	unsigned int	(*maxlevels)(void);
	unsigned int	(*maxrecs)(struct xfs_mount *mp, unsigned int blocklen,
				   bool leaf);
} maps[] = {
	{
		.tag		= "bnobt",
		.maxlevels	= libxfs_allocbt_maxlevels_ondisk,
		.maxrecs	= libxfs_allocbt_maxrecs,
	},
	{
		.tag		= "cntbt",
		.maxlevels	= libxfs_allocbt_maxlevels_ondisk,
		.maxrecs	= libxfs_allocbt_maxrecs,
	},
	{
		.tag		= "inobt",
		.maxlevels	= libxfs_iallocbt_maxlevels_ondisk,
		.maxrecs	= libxfs_inobt_maxrecs,
	},
	{
		.tag		= "finobt",
		.maxlevels	= libxfs_iallocbt_maxlevels_ondisk,
		.maxrecs	= libxfs_inobt_maxrecs,
	},
	{
		.tag		= "bmapbt",
		.maxlevels	= libxfs_bmbt_maxlevels_ondisk,
		.maxrecs	= libxfs_bmbt_maxrecs,
	},
	{
		.tag		= "refcountbt",
		.maxlevels	= libxfs_refcountbt_maxlevels_ondisk,
		.maxrecs	= libxfs_refcountbt_maxrecs,
	},
	{
		.tag		= "rmapbt",
		.maxlevels	= libxfs_rmapbt_maxlevels_ondisk,
		.maxrecs	= libxfs_rmapbt_maxrecs,
	},
	{
		.tag		= "rtrmapbt",
		.maxlevels	= libxfs_rtrmapbt_maxlevels_ondisk,
		.maxrecs	= libxfs_rtrmapbt_maxrecs,
	},
	{
		.tag		= "rtrefcountbt",
		.maxlevels	= libxfs_rtrefcountbt_maxlevels_ondisk,
		.maxrecs	= libxfs_rtrefcountbt_maxrecs,
	},
};

static void
btheight_help(void)
{
	struct btmap	*m;
	int		i;

	dbprintf(_(
"\n"
" For a given number of btree records and a btree type, report the number of\n"
" records and blocks for each level of the btree, and the total btree size.\n"
" The btree type must be given after the options.  A raw btree geometry can\n"
" be provided in the format \"record_bytes:key_bytes:ptr_bytes:header_type\"\n"
" where header_type is one of \"short\", \"long\", \"shortcrc\", or \"longcrc\".\n"
"\n"
" Options:\n"
"   -b -- Override the btree block size.\n"
"   -n -- Number of records we want to store.\n"
"   -w max -- Show only the best case scenario.\n"
"   -w min -- Show only the worst case scenario.\n"
"   -w absmax -- Print the maximum possible btree height for all filesystems.\n"
"\n"
" Supported btree types:\n"
"   all "
));
	for (i = 0, m = maps; i < ARRAY_SIZE(maps); i++, m++)
		printf("%s ", m->tag);
	printf("\n");
}

static void
calc_height(
	unsigned long long	nr_records,
	uint			*records_per_block)
{
	unsigned int		level = 0;
	unsigned long long	total_blocks = 0;
	unsigned long long	blocks;
	char			*levels_suffix = "s";
	char			*totblocks_suffix = "s";

	while (nr_records) {
		unsigned int	level_rpb = records_per_block[level != 0];
		char		*recs_suffix = "s";
		char		*blocks_suffix = "s";

		blocks = (nr_records + level_rpb - 1) / level_rpb;

		if (nr_records == 1)
			recs_suffix = "";
		if (blocks == 1)
			blocks_suffix = "";

		printf(_("level %u: %llu record%s, %llu block%s\n"),
				level, nr_records, recs_suffix, blocks,
				blocks_suffix);

		total_blocks += blocks;
		nr_records = blocks == 1 ? 0 : blocks;
		level++;
	}

	if (level == 1)
		levels_suffix = "";
	if (total_blocks == 1)
		totblocks_suffix = "";

	printf(_("%u level%s, %llu block%s total\n"), level, levels_suffix,
			total_blocks, totblocks_suffix);
}

static int
construct_records_per_block(
	const char	*tag,
	int		blocksize,
	unsigned int	*records_per_block)
{
	char		*toktag;
	struct btmap	*m;
	unsigned int	record_size, key_size, ptr_size;
	char		*p;
	int		i, ret;

	for (i = 0, m = maps; i < ARRAY_SIZE(maps); i++, m++) {
		if (!strcmp(m->tag, tag)) {
			records_per_block[0] = m->maxrecs(mp, blocksize, 1);
			records_per_block[1] = m->maxrecs(mp, blocksize, 0);
			return 0;
		}
	}

	p = strchr(tag, ':');
	if (!p) {
		fprintf(stderr, _("%s: expected a btree geometry specification.\n"), tag);
		return -1;
	}

	toktag = strdup(tag);
	ret = -1;

	p = strtok(toktag, ":");
	if (!p) {
		fprintf(stderr, _("%s: record size not found.\n"), tag);
		goto out;
	}
	record_size = cvt_u16(p, 0);
	if (errno) {
		perror(p);
		goto out;
	}
	if (record_size == 0) {
		fprintf(stderr, _("%s: record size cannot be zero.\n"), tag);
		goto out;
	}

	p = strtok(NULL, ":");
	if (!p) {
		fprintf(stderr, _("%s: key size not found.\n"), tag);
		goto out;
	}
	key_size = cvt_u16(p, 0);
	if (errno) {
		perror(p);
		goto out;
	}
	if (key_size == 0) {
		fprintf(stderr, _("%s: key size cannot be zero.\n"), tag);
		goto out;
	}

	p = strtok(NULL, ":");
	if (!p) {
		fprintf(stderr, _("%s: pointer size not found.\n"), tag);
		goto out;
	}
	ptr_size = cvt_u16(p, 0);
	if (errno) {
		perror(p);
		goto out;
	}
	if (ptr_size == 0) {
		fprintf(stderr, _("%s: pointer size cannot be zero.\n"), tag);
		goto out;
	}

	p = strtok(NULL, ":");
	if (!p) {
		fprintf(stderr, _("%s: header type not found.\n"), tag);
		goto out;
	}
	if (!strcmp(p, "short"))
		blocksize -= XFS_BTREE_SBLOCK_LEN;
	else if (!strcmp(p, "shortcrc"))
		blocksize -= XFS_BTREE_SBLOCK_CRC_LEN;
	else if (!strcmp(p, "long"))
		blocksize -= XFS_BTREE_LBLOCK_LEN;
	else if (!strcmp(p, "longcrc"))
		blocksize -= XFS_BTREE_LBLOCK_CRC_LEN;
	else {
		fprintf(stderr, _("%s: unrecognized btree header type."),
				p);
		goto out;
	}

	if (record_size > blocksize) {
		fprintf(stderr,
_("%s: record size must be less than selected block size (%u bytes).\n"),
			tag, blocksize);
		goto out;
	}

	if (key_size > blocksize) {
		fprintf(stderr,
_("%s: key size must be less than selected block size (%u bytes).\n"),
			tag, blocksize);
		goto out;
	}

	if (ptr_size > blocksize) {
		fprintf(stderr,
_("%s: pointer size must be less than selected block size (%u bytes).\n"),
			tag, blocksize);
		goto out;
	}

	p = strtok(NULL, ":");
	if (p) {
		fprintf(stderr,
			_("%s: unrecognized raw btree geometry."),
				tag);
		goto out;
	}

	records_per_block[0] = blocksize / record_size;
	records_per_block[1] = blocksize / (key_size + ptr_size);
	ret = 0;
out:
	free(toktag);
	return ret;
}

#define REPORT_DEFAULT	(-1U)
#define REPORT_MAX	(1 << 0)
#define REPORT_MIN	(1 << 1)
#define REPORT_ABSMAX	(1 << 2)
#define REPORT_AVG	(1 << 3)

static void
report_absmax(const char *tag)
{
	struct btmap	*m;
	int		i;

	for (i = 0, m = maps; i < ARRAY_SIZE(maps); i++, m++) {
		if (!strcmp(m->tag, tag)) {
			printf("%s: %u\n", tag, m->maxlevels());
			return;
		}
	}
	printf(_("%s: Don't know how to report max height.\n"), tag);
}

static void
report(
	const char		*tag,
	unsigned int		report_what,
	unsigned long long	nr_records,
	unsigned int		blocksize)
{
	unsigned int		records_per_block[2];
	int			ret;

	if (report_what == REPORT_ABSMAX) {
		report_absmax(tag);
		return;
	}

	ret = construct_records_per_block(tag, blocksize, records_per_block);
	if (ret)
		return;

	if (report_what & REPORT_MAX) {
		if (records_per_block[0] < 2) {
			fprintf(stderr,
_("%s: cannot calculate best case scenario due to leaf geometry underflow.\n"),
				tag);
			return;
		}

		if (records_per_block[1] < 4) {
			fprintf(stderr,
_("%s: cannot calculate best case scenario due to node geometry underflow.\n"),
				tag);
			return;
		}

		printf(
_("%s: best case per %u-byte block: %u records (leaf) / %u keyptrs (node)\n"),
				tag, blocksize, records_per_block[0],
				records_per_block[1]);

		calc_height(nr_records, records_per_block);
	}

	if (report_what & REPORT_AVG) {
		records_per_block[0] *= 3;
		records_per_block[0] /= 4;
		records_per_block[1] *= 3;
		records_per_block[1] /= 4;

		if (records_per_block[0] < 2) {
			fprintf(stderr,
_("%s: cannot calculate average case scenario due to leaf geometry underflow.\n"),
				tag);
			return;
		}

		if (records_per_block[1] < 4) {
			fprintf(stderr,
_("%s: cannot calculate average case scenario due to node geometry underflow.\n"),
				tag);
			return;
		}

		printf(
_("%s: average case per %u-byte block: %u records (leaf) / %u keyptrs (node)\n"),
				tag, blocksize, records_per_block[0],
				records_per_block[1]);

		calc_height(nr_records, records_per_block);
	}

	if (report_what & REPORT_MIN) {
		records_per_block[0] /= 2;
		records_per_block[1] /= 2;

		if (records_per_block[0] < 1) {
			fprintf(stderr,
_("%s: cannot calculate worst case scenario due to leaf geometry underflow.\n"),
				tag);
			return;
		}

		if (records_per_block[1] < 2) {
			fprintf(stderr,
_("%s: cannot calculate worst case scenario due to node geometry underflow.\n"),
				tag);
			return;
		}

		printf(
_("%s: worst case per %u-byte block: %u records (leaf) / %u keyptrs (node)\n"),
				tag, blocksize, records_per_block[0],
				records_per_block[1]);

		calc_height(nr_records, records_per_block);
	}
}

static void
report_all(
	unsigned int		report_what,
	unsigned long long	nr_records,
	unsigned int		blocksize)
{
	struct btmap		*m;
	int			i;

	for (i = 0, m = maps; i < ARRAY_SIZE(maps); i++, m++)
		report(m->tag, report_what, nr_records, blocksize);
}

static int
btheight_f(
	int		argc,
	char		**argv)
{
	long long	blocksize = mp->m_sb.sb_blocksize;
	uint64_t	nr_records = 0;
	int		report_what = REPORT_DEFAULT;
	int		i, c;

	while ((c = getopt(argc, argv, "b:n:w:")) != -1) {
		switch (c) {
		case 'b':
			errno = 0;
			blocksize = cvtnum(mp->m_sb.sb_blocksize,
					mp->m_sb.sb_sectsize,
					optarg);
			if (errno) {
				perror(optarg);
				return 0;
			}
			break;
		case 'n':
			nr_records = cvt_u64(optarg, 0);
			if (errno) {
				perror(optarg);
				return 0;
			}
			break;
		case 'w':
			if (!strcmp(optarg, "min"))
				report_what = REPORT_MIN;
			else if (!strcmp(optarg, "max"))
				report_what = REPORT_MAX;
			else if (!strcmp(optarg, "absmax"))
				report_what = REPORT_ABSMAX;
			else if (!strcmp(optarg, "avg"))
				report_what = REPORT_AVG;
			else {
				btheight_help();
				return 0;
			}
			break;
		default:
			btheight_help();
			return 0;
		}
	}

	if (report_what != REPORT_ABSMAX && nr_records == 0) {
		fprintf(stderr,
_("Number of records must be greater than zero.\n"));
		return 0;
	}

	if (report_what != REPORT_ABSMAX && blocksize > INT_MAX) {
		fprintf(stderr,
_("The largest block size this command will consider is %u bytes.\n"),
			INT_MAX);
		return 0;
	}

	if (report_what != REPORT_ABSMAX && blocksize < 128) {
		fprintf(stderr,
_("The smallest block size this command will consider is 128 bytes.\n"));
		return 0;
	}

	if (argc == optind) {
		btheight_help();
		return 0;
	}

	for (i = optind; i < argc; i++) {
		if (!strcmp(argv[i], "all")) {
			report_all(report_what, nr_records, blocksize);
			return 0;
		}
	}

	for (i = optind; i < argc; i++)
		report(argv[i], report_what, nr_records, blocksize);

	return 0;
}

static const cmdinfo_t btheight_cmd =
	{ "btheight", "b", btheight_f, 1, -1, 0,
	  "[-b blksz] [-n recs] [-w max|-w min] btree types...",
	  N_("compute btree heights"), btheight_help };

void
btheight_init(void)
{
	add_command(&btheight_cmd);
}
