/* SPDX-License-Identifier: LGPL-2.1-only */
/*
 * Copyright (c) 2003-2011 Thomas Graf <tgraf@suug.ch>
 */

/**
 * @ingroup qdisc
 * @defgroup qdisc_tbf Token Bucket Filter (TBF)
 * @{
 */

#include <netlink-private/netlink.h>
#include <netlink-private/tc.h>
#include <netlink/netlink.h>
#include <netlink/cache.h>
#include <netlink/utils.h>
#include <netlink-private/route/tc-api.h>
#include <netlink/route/qdisc.h>
#include <netlink/route/class.h>
#include <netlink/route/link.h>
#include <netlink/route/qdisc/tbf.h>

/** @cond SKIP */
#define TBF_ATTR_LIMIT			0x01
#define TBF_ATTR_RATE			0x02
#define TBF_ATTR_PEAKRATE		0x10
/** @endcond */

static struct nla_policy tbf_policy[TCA_TBF_MAX+1] = {
	[TCA_TBF_PARMS]	= { .minlen = sizeof(struct tc_tbf_qopt) },
};

static int tbf_msg_parser(struct rtnl_tc *tc, void *data)
{
	struct nlattr *tb[TCA_TBF_MAX + 1];
	struct rtnl_tbf *tbf = data;
	int err;

	if ((err = tca_parse(tb, TCA_TBF_MAX, tc, tbf_policy)) < 0)
		return err;

	if (tb[TCA_TBF_PARMS]) {
		struct tc_tbf_qopt opts;
		int bufsize;

		nla_memcpy(&opts, tb[TCA_TBF_PARMS], sizeof(opts));
		tbf->qt_limit = opts.limit;

		rtnl_copy_ratespec(&tbf->qt_rate, &opts.rate);
		tbf->qt_rate_txtime = opts.buffer;
		bufsize = rtnl_tc_calc_bufsize64(nl_ticks2us(opts.buffer),
		                                 tbf->qt_rate.rs_rate64);
		tbf->qt_rate_bucket = bufsize;

		rtnl_copy_ratespec(&tbf->qt_peakrate, &opts.peakrate);
		tbf->qt_peakrate_txtime = opts.mtu;
		bufsize = rtnl_tc_calc_bufsize64(nl_ticks2us(opts.mtu),
		                                 tbf->qt_peakrate.rs_rate64);
		tbf->qt_peakrate_bucket = bufsize;

		rtnl_tc_set_mpu(tc, tbf->qt_rate.rs_mpu);
		rtnl_tc_set_overhead(tc, tbf->qt_rate.rs_overhead);

		tbf->qt_mask = (TBF_ATTR_LIMIT | TBF_ATTR_RATE | TBF_ATTR_PEAKRATE);
	}

	return 0;
}

static void tbf_dump_line(struct rtnl_tc *tc, void *data,
			  struct nl_dump_params *p)
{
	double r, rbit, lim;
	char *ru, *rubit, *limu;
	struct rtnl_tbf *tbf = data;

	if (!tbf)
		return;

	r = nl_cancel_down_bytes(tbf->qt_rate.rs_rate64, &ru);
	rbit = nl_cancel_down_bits(tbf->qt_rate.rs_rate64*8, &rubit);
	lim = nl_cancel_down_bytes(tbf->qt_limit, &limu);

	nl_dump(p, " rate %.2f%s/s (%.0f%s) limit %.2f%s",
		r, ru, rbit, rubit, lim, limu);
}

static void tbf_dump_details(struct rtnl_tc *tc, void *data,
			     struct nl_dump_params *p)
{
	struct rtnl_tbf *tbf = data;

	if (!tbf)
		return;

	if (1) {
		char *bu, *cu;
		double bs = nl_cancel_down_bytes(tbf->qt_rate_bucket, &bu);
		double cl = nl_cancel_down_bytes(1 << tbf->qt_rate.rs_cell_log,
						 &cu);

		nl_dump(p, "rate-bucket-size %1.f%s "
			   "rate-cell-size %.1f%s\n",
			bs, bu, cl, cu);

	}

	if (tbf->qt_mask & TBF_ATTR_PEAKRATE) {
		char *pru, *prbu, *bsu, *clu;
		double pr, prb, bs, cl;

		pr = nl_cancel_down_bytes(tbf->qt_peakrate.rs_rate64, &pru);
		prb = nl_cancel_down_bits(tbf->qt_peakrate.rs_rate64 * 8, &prbu);
		bs = nl_cancel_down_bits(tbf->qt_peakrate_bucket, &bsu);
		cl = nl_cancel_down_bits(1 << tbf->qt_peakrate.rs_cell_log,
					 &clu);

		nl_dump_line(p,
			     "    peak-rate %.2f%s/s (%.0f%s) "
			     "bucket-size %.1f%s cell-size %.1f%s",
			     pr, pru, prb, prbu, bs, bsu, cl, clu);
	}
}

static int tbf_msg_fill(struct rtnl_tc *tc, void *data, struct nl_msg *msg)
{
	uint32_t rtab[RTNL_TC_RTABLE_SIZE], ptab[RTNL_TC_RTABLE_SIZE];
	struct tc_tbf_qopt opts;
	struct rtnl_tbf *tbf = data;
	int required = TBF_ATTR_RATE | TBF_ATTR_LIMIT;

	if ((tbf->qt_mask & required) != required)
		return -NLE_MISSING_ATTR;

	memset(&opts, 0, sizeof(opts));
	opts.limit = tbf->qt_limit;
	opts.buffer = tbf->qt_rate_txtime;

	rtnl_tc_build_rate_table(tc, &tbf->qt_rate, rtab);
	rtnl_rcopy_ratespec(&opts.rate, &tbf->qt_rate);

	if (tbf->qt_mask & TBF_ATTR_PEAKRATE) {
		opts.mtu = tbf->qt_peakrate_txtime;
		rtnl_tc_build_rate_table(tc, &tbf->qt_peakrate, ptab);
		rtnl_rcopy_ratespec(&opts.peakrate, &tbf->qt_peakrate);

	}

	NLA_PUT(msg, TCA_TBF_PARMS, sizeof(opts), &opts);
	NLA_PUT(msg, TCA_TBF_RTAB, sizeof(rtab), rtab);

	if (tbf->qt_mask & TBF_ATTR_PEAKRATE)
		NLA_PUT(msg, TCA_TBF_PTAB, sizeof(ptab), ptab);

	return 0;

nla_put_failure:
	return -NLE_MSGSIZE;
}

/**
 * @name Attribute Access
 * @{
 */

/**
 * Set limit of TBF qdisc.
 * @arg qdisc		TBF qdisc to be modified.
 * @arg limit		New limit in bytes.
 * @return 0 on success or a negative error code.
 */
void rtnl_qdisc_tbf_set_limit(struct rtnl_qdisc *qdisc, int limit)
{
	struct rtnl_tbf *tbf;
	
	if (!(tbf = rtnl_tc_data(TC_CAST(qdisc))))
		BUG();

	tbf->qt_limit = limit;
	tbf->qt_mask |= TBF_ATTR_LIMIT;
}

static inline double calc_limit(struct rtnl_ratespec *spec, int latency,
				int bucket)
{
	double limit;

	limit = (double) spec->rs_rate64 * ((double) latency / 1000000.);
	limit += bucket;

	return limit;
}

/**
 * Set limit of TBF qdisc by latency.
 * @arg qdisc		TBF qdisc to be modified.
 * @arg latency		Latency in micro seconds.
 *
 * Calculates and sets the limit based on the desired latency and the
 * configured rate and peak rate. In order for this operation to succeed,
 * the rate and if required the peak rate must have been set in advance.
 *
 * @f[
 *   limit_n = \frac{{rate_n} \times {latency}}{10^6}+{bucketsize}_n
 * @f]
 * @f[
 *   limit = min(limit_{rate},limit_{peak})
 * @f]
 * 
 * @return 0 on success or a negative error code.
 */
int rtnl_qdisc_tbf_set_limit_by_latency(struct rtnl_qdisc *qdisc, int latency)
{
	struct rtnl_tbf *tbf;
	double limit, limit2;

	if (!(tbf = rtnl_tc_data(TC_CAST(qdisc))))
		BUG();

	if (!(tbf->qt_mask & TBF_ATTR_RATE))
		return -NLE_MISSING_ATTR;

	limit = calc_limit(&tbf->qt_rate, latency, tbf->qt_rate_bucket);

	if (tbf->qt_mask & TBF_ATTR_PEAKRATE) {
		limit2 = calc_limit(&tbf->qt_peakrate, latency,
				    tbf->qt_peakrate_bucket);

		if (limit2 < limit)
			limit = limit2;
	}

	rtnl_qdisc_tbf_set_limit(qdisc, (int) limit);

	return 0;
}

/**
 * Get limit of TBF qdisc.
 * @arg qdisc		TBF qdisc.
 * @return Limit in bytes or a negative error code.
 */
int rtnl_qdisc_tbf_get_limit(struct rtnl_qdisc *qdisc)
{
	struct rtnl_tbf *tbf;
	
	if (!(tbf = rtnl_tc_data(TC_CAST(qdisc))))
		BUG();

	if (tbf->qt_mask & TBF_ATTR_LIMIT)
		return tbf->qt_limit;
	else
		return -NLE_NOATTR;
}

static inline int calc_cell_log(int cell, int bucket)
{
		cell = rtnl_tc_calc_cell_log(cell);
	return cell;
}

/**
 * Set rate of TBF qdisc.
 * @arg qdisc		TBF qdisc to be modified.
 * @arg rate		New rate in bytes per second.
 * @arg bucket		Size of bucket in bytes.
 * @arg cell		Size of a rate cell or 0 to get default value.
 * @return 0 on success or a negative error code.
 */
void rtnl_qdisc_tbf_set_rate(struct rtnl_qdisc *qdisc, int rate, int bucket,
			    int cell)
{
	struct rtnl_tbf *tbf;
	int cell_log;

	if (!(tbf = rtnl_tc_data(TC_CAST(qdisc))))
		BUG();

	if (!cell)
		cell_log = UINT8_MAX;
	else
		cell_log = rtnl_tc_calc_cell_log(cell);

	tbf->qt_rate.rs_rate64 = (uint32_t)rate;
	tbf->qt_rate_bucket = bucket;
	tbf->qt_rate.rs_cell_log = cell_log;
	tbf->qt_rate_txtime = nl_us2ticks(rtnl_tc_calc_txtime64(bucket, tbf->qt_rate.rs_rate64));
	tbf->qt_mask |= TBF_ATTR_RATE;
}

/**
 * Get rate of TBF qdisc.
 * @arg qdisc		TBF qdisc.
 * @return Rate in bytes per seconds or a negative error code.
 */
int rtnl_qdisc_tbf_get_rate(struct rtnl_qdisc *qdisc)
{
	struct rtnl_tbf *tbf;

	if (!(tbf = rtnl_tc_data(TC_CAST(qdisc))))
		BUG();

	if (tbf->qt_mask & TBF_ATTR_RATE)
		return tbf->qt_rate.rs_rate64;
	else
		return -1;
}

/**
 * Get rate bucket size of TBF qdisc.
 * @arg qdisc		TBF qdisc.
 * @return Size of rate bucket or a negative error code.
 */
int rtnl_qdisc_tbf_get_rate_bucket(struct rtnl_qdisc *qdisc)
{
	struct rtnl_tbf *tbf;

	if (!(tbf = rtnl_tc_data(TC_CAST(qdisc))))
		BUG();

	if (tbf->qt_mask & TBF_ATTR_RATE)
		return tbf->qt_rate_bucket;
	else
		return -1;
}

/**
 * Get rate cell size of TBF qdisc.
 * @arg qdisc		TBF qdisc.
 * @return Size of rate cell in bytes or a negative error code.
 */
int rtnl_qdisc_tbf_get_rate_cell(struct rtnl_qdisc *qdisc)
{
	struct rtnl_tbf *tbf;

	if (!(tbf = rtnl_tc_data(TC_CAST(qdisc))))
		BUG();

	if (tbf->qt_mask & TBF_ATTR_RATE)
		return (1 << tbf->qt_rate.rs_cell_log);
	else
		return -1;
}

/**
 * Set peak rate of TBF qdisc.
 * @arg qdisc		TBF qdisc to be modified.
 * @arg rate		New peak rate in bytes per second.
 * @arg bucket		Size of peakrate bucket.
 * @arg cell		Size of a peakrate cell or 0 to get default value.
 * @return 0 on success or a negative error code.
 */
int rtnl_qdisc_tbf_set_peakrate(struct rtnl_qdisc *qdisc, int rate, int bucket,
				int cell)
{
	struct rtnl_tbf *tbf;
	int cell_log;

	if (!(tbf = rtnl_tc_data(TC_CAST(qdisc))))
		BUG();

	cell_log = calc_cell_log(cell, bucket);
	if (cell_log < 0)
		return cell_log;

	tbf->qt_peakrate.rs_rate64 = (uint32_t)rate;
	tbf->qt_peakrate_bucket = bucket;
	tbf->qt_peakrate.rs_cell_log = cell_log;
	tbf->qt_peakrate_txtime = nl_us2ticks(rtnl_tc_calc_txtime64(bucket, tbf->qt_peakrate.rs_rate64));

	tbf->qt_mask |= TBF_ATTR_PEAKRATE;

	return 0;
}

/**
 * Get peak rate of TBF qdisc.
 * @arg qdisc		TBF qdisc.
 * @return Peak rate in bytes per seconds or a negative error code.
 */
int rtnl_qdisc_tbf_get_peakrate(struct rtnl_qdisc *qdisc)
{
	struct rtnl_tbf *tbf;

	if (!(tbf = rtnl_tc_data(TC_CAST(qdisc))))
		BUG();

	if (tbf->qt_mask & TBF_ATTR_PEAKRATE)
		return tbf->qt_peakrate.rs_rate64;
	else
		return -1;
}

/**
 * Get peak rate bucket size of TBF qdisc.
 * @arg qdisc		TBF qdisc.
 * @return Size of peak rate bucket or a negative error code.
 */
int rtnl_qdisc_tbf_get_peakrate_bucket(struct rtnl_qdisc *qdisc)
{
	struct rtnl_tbf *tbf;

	if (!(tbf = rtnl_tc_data(TC_CAST(qdisc))))
		BUG();

	if (tbf->qt_mask & TBF_ATTR_PEAKRATE)
		return tbf->qt_peakrate_bucket;
	else
		return -1;
}

/**
 * Get peak rate cell size of TBF qdisc.
 * @arg qdisc		TBF qdisc.
 * @return Size of peak rate cell in bytes or a negative error code.
 */
int rtnl_qdisc_tbf_get_peakrate_cell(struct rtnl_qdisc *qdisc)
{
	struct rtnl_tbf *tbf;

	if (!(tbf = rtnl_tc_data(TC_CAST(qdisc))))
		BUG();

	if (tbf->qt_mask & TBF_ATTR_PEAKRATE)
		return (1 << tbf->qt_peakrate.rs_cell_log);
	else
		return -1;
}

/** @} */

static struct rtnl_tc_ops tbf_tc_ops = {
	.to_kind		= "tbf",
	.to_type		= RTNL_TC_TYPE_QDISC,
	.to_size		= sizeof(struct rtnl_tbf),
	.to_msg_parser		= tbf_msg_parser,
	.to_dump = {
	    [NL_DUMP_LINE]	= tbf_dump_line,
	    [NL_DUMP_DETAILS]	= tbf_dump_details,
	},
	.to_msg_fill		= tbf_msg_fill,
};

static void __init tbf_init(void)
{
	rtnl_tc_register(&tbf_tc_ops);
}

static void __exit tbf_exit(void)
{
	rtnl_tc_unregister(&tbf_tc_ops);
}

/** @} */
