/* Transmit Power computation */

/* (C) 2014 by Harald Welte <laforge@gnumonks.org>
 *
 * All Rights Reserved
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <stdint.h>
#include <limits.h>
#include <errno.h>

#include <osmocom/core/utils.h>

#include <osmo-bts/logging.h>
#include <osmo-bts/gsm_data.h>
#include <osmo-bts/bts_model.h>
#include <osmo-bts/tx_power.h>
#include <osmo-bts/bts_trx.h>

static int get_pa_drive_level_mdBm(const struct power_amp *pa,
		       int desired_p_out_mdBm, unsigned int arfcn)
{
	if (arfcn >= ARRAY_SIZE(pa->calib.delta_mdB))
		return INT_MIN;

	/* FIXME: temperature compensation */

	return desired_p_out_mdBm - pa->nominal_gain_mdB - pa->calib.delta_mdB[arfcn];
}

/* maximum output power of the system */
int get_p_max_out_mdBm(const struct gsm_bts_trx *trx)
{
	const struct trx_power_params *tpp = &trx->power_params;
	/* Add user gain, internal and external PA gain to TRX output power */
	return tpp->trx_p_max_out_mdBm + tpp->user_gain_mdB +
			tpp->pa.nominal_gain_mdB + tpp->user_pa.nominal_gain_mdB;
}

/* nominal output power, i.e. OML-reduced maximum output power */
int get_p_nominal_mdBm(const struct gsm_bts_trx *trx)
{
	/* P_max_out subtracted by OML maximum power reduction IE */
	return get_p_max_out_mdBm(trx) - to_mdB(trx->max_power_red);
}

/* calculate the target total output power required, reduced by both
 * OML and RSL, but ignoring the attenuation required for power ramping and
 * thermal management */
int get_p_target_mdBm(const struct gsm_bts_trx *trx, uint8_t bs_power_red)
{
	/* Pn subtracted by RSL BS Power Recudtion (in 1 dB steps) */
	return get_p_nominal_mdBm(trx) - to_mdB(bs_power_red);
}
int get_p_target_mdBm_lchan(const struct gsm_lchan *lchan)
{
	return get_p_target_mdBm(lchan->ts->trx, lchan->bs_power_ctrl.current);
}

/* calculate the actual total output power required, taking into account the
 * attenuation required for power ramping but not thermal management */
int get_p_actual_mdBm(const struct gsm_bts_trx *trx, int p_target_mdBm)
{
	const struct trx_power_params *tpp = &trx->power_params;

	/* P_target subtracted by ramp attenuation */
	return p_target_mdBm - tpp->ramp.attenuation_mdB;
}

/* calculate the effective total output power required, taking into account the
 * attenuation required for power ramping and thermal management */
int get_p_eff_mdBm(const struct gsm_bts_trx *trx, int p_target_mdBm)
{
	const struct trx_power_params *tpp = &trx->power_params;

	/* P_target subtracted by ramp attenuation */
	return p_target_mdBm - tpp->ramp.attenuation_mdB - tpp->thermal_attenuation_mdB;
}

/* calculate effect TRX output power required, taking into account the
 * attenuations required for power ramping and thermal management */
int get_p_trxout_eff_mdBm(const struct gsm_bts_trx *trx, int p_target_mdBm)
{
	const struct trx_power_params *tpp = &trx->power_params;
	int p_actual_mdBm, user_pa_drvlvl_mdBm, pa_drvlvl_mdBm;
	unsigned int arfcn = trx->arfcn;

	/* P_actual subtracted by any bulk gain added by the user */
	p_actual_mdBm = get_p_eff_mdBm(trx, p_target_mdBm) - tpp->user_gain_mdB;

	/* determine input drive level required at input to user PA */
	user_pa_drvlvl_mdBm = get_pa_drive_level_mdBm(&tpp->user_pa, p_actual_mdBm, arfcn);

	/* determine input drive level required at input to internal PA */
	pa_drvlvl_mdBm = get_pa_drive_level_mdBm(&tpp->pa, user_pa_drvlvl_mdBm, arfcn);

	/* internal PA input drive level is TRX output power */
	return pa_drvlvl_mdBm;
}

/* calculate target TRX output power required, ignoring the
 * attenuations required for power ramping but not thermal management */
int get_p_trxout_target_mdBm(const struct gsm_bts_trx *trx, uint8_t bs_power_red)
{
	const struct trx_power_params *tpp = &trx->power_params;
	int p_target_mdBm, user_pa_drvlvl_mdBm, pa_drvlvl_mdBm;
	unsigned int arfcn = trx->arfcn;

	/* P_target subtracted by any bulk gain added by the user */
	p_target_mdBm = get_p_target_mdBm(trx, bs_power_red) - tpp->user_gain_mdB;

	/* determine input drive level required at input to user PA */
	user_pa_drvlvl_mdBm = get_pa_drive_level_mdBm(&tpp->user_pa, p_target_mdBm, arfcn);

	/* determine input drive level required at input to internal PA */
	pa_drvlvl_mdBm = get_pa_drive_level_mdBm(&tpp->pa, user_pa_drvlvl_mdBm, arfcn);

	/* internal PA input drive level is TRX output power */
	return pa_drvlvl_mdBm;
}
int get_p_trxout_target_mdBm_lchan(const struct gsm_lchan *lchan)
{
	return get_p_trxout_target_mdBm(lchan->ts->trx, lchan->bs_power_ctrl.current);
}


/* output power ramping code */

/* The idea here is to avoid a hard switch from 0 to 100, but to actually
 * slowly and gradually ramp up or down the power.  This is needed on the
 * one hand side to avoid very fast dynamic load changes towards the PA power
 * supply, but is also needed in order to avoid a DoS by too many subscriber
 * attempting to register at the same time.  Rather, grow the cell slowly in
 * radius than start with the full radius at once.  */

static int we_are_ramping_up(const struct gsm_bts_trx *trx)
{
	const struct trx_power_params *tpp = &trx->power_params;

	if (tpp->p_total_tgt_mdBm > tpp->p_total_cur_mdBm)
		return 1;
	else
		return 0;
}

static void power_ramp_do_step(struct gsm_bts_trx *trx, int first);

/* timer call-back for the ramp timer */
static void power_ramp_timer_cb(void *_trx)
{
	struct gsm_bts_trx *trx = _trx;
	struct trx_power_params *tpp = &trx->power_params;
	int p_trxout_eff_mdBm;

	/* compute new actual total output power (= minus ramp attenuation) */
	tpp->p_total_cur_mdBm = get_p_actual_mdBm(trx, tpp->p_total_tgt_mdBm);

	/* compute new effective (= minus ramp and thermal attenuation) TRX output required */
	p_trxout_eff_mdBm = get_p_trxout_eff_mdBm(trx, tpp->p_total_tgt_mdBm);

	LOGPTRX(trx, DL1C, LOGL_DEBUG, "ramp_timer_cb(cur_pout=%d, tgt_pout=%d, "
		"ramp_att=%d, therm_att=%d, user_gain=%d)\n",
		tpp->p_total_cur_mdBm, tpp->p_total_tgt_mdBm,
		tpp->ramp.attenuation_mdB, tpp->thermal_attenuation_mdB,
		tpp->user_gain_mdB);

	LOGPTRX(trx, DL1C, LOGL_INFO,
		"ramping TRX board output power to %d mdBm.\n", p_trxout_eff_mdBm);

	/* Instruct L1 to apply new effective TRX output power required */
	bts_model_change_power(trx, p_trxout_eff_mdBm);
}

/* BTS model call-back once one a call to bts_model_change_power()
 * completes, indicating actual L1 transmit power */
void power_trx_change_compl(struct gsm_bts_trx *trx, int p_trxout_cur_mdBm)
{
	struct trx_power_params *tpp = &trx->power_params;
	int p_trxout_should_mdBm;

	p_trxout_should_mdBm = get_p_trxout_eff_mdBm(trx, tpp->p_total_tgt_mdBm);

	/* for now we simply write an error message, but in the future
	 * we might use the value (again) as part of our math? */
	if (p_trxout_cur_mdBm != p_trxout_should_mdBm) {
		LOGPTRX(trx, DL1C, LOGL_ERROR, "bts_model notifies us of %d mdBm TRX "
			"output power.  However, it should be %d mdBm!\n",
			p_trxout_cur_mdBm, p_trxout_should_mdBm);
	}

	/* and do another step... */
	power_ramp_do_step(trx, 0);
}

static void power_ramp_do_step(struct gsm_bts_trx *trx, int first)
{
	struct trx_power_params *tpp = &trx->power_params;

	/* we had finished in last loop iteration */
	if (!first && tpp->ramp.attenuation_mdB == 0) {
		if (tpp->ramp.compl_cb)
			tpp->ramp.compl_cb(trx);
		return;
	}

	if (we_are_ramping_up(trx)) {
		/* ramp up power -> ramp down attenuation */
		tpp->ramp.attenuation_mdB -= tpp->ramp.step_size_mdB;
		if (tpp->ramp.attenuation_mdB <= 0) {
			/* we are done */
			tpp->ramp.attenuation_mdB = 0;
		}
	} else {
		/* ramp down power -> ramp up attenuation */
		tpp->ramp.attenuation_mdB += tpp->ramp.step_size_mdB;
		if (tpp->ramp.attenuation_mdB >= 0) {
			/* we are done */
			tpp->ramp.attenuation_mdB = 0;
		}
	}

	/* schedule timer for the next step */
	tpp->ramp.step_timer.data = trx;
	tpp->ramp.step_timer.cb = power_ramp_timer_cb;
	osmo_timer_schedule(&tpp->ramp.step_timer, tpp->ramp.step_interval_sec, 0);
}

int _power_ramp_start(struct gsm_bts_trx *trx, int p_total_tgt_mdBm, int bypass, ramp_compl_cb_t ramp_compl_cb, bool skip_ramping)
{
	struct trx_power_params *tpp = &trx->power_params;

	/* The input to this function is the actual desired output power, i.e.
	 * the maximum total system power subtracted by OML as well as RSL
	 * reductions */

	LOGPTRX(trx, DL1C, LOGL_INFO, "power_ramp_start(cur=%d, tgt=%d%s)\n",
		tpp->p_total_cur_mdBm, p_total_tgt_mdBm, bypass ? ", bypass" : "");

	if (!bypass && (p_total_tgt_mdBm > get_p_nominal_mdBm(trx))) {
		LOGPTRX(trx, DL1C, LOGL_ERROR, "Asked to ramp power up to "
			"%d mdBm, which exceeds P_max_out (%d)\n",
			p_total_tgt_mdBm, get_p_nominal_mdBm(trx));
		return -ERANGE;
	}

	/* Cancel any pending request */
	power_ramp_abort(trx);

	/* set the new target */
	tpp->p_total_tgt_mdBm = p_total_tgt_mdBm;
	tpp->ramp.compl_cb = ramp_compl_cb;

	if (skip_ramping) {
		/* Jump straight to the target power */
		tpp->p_total_cur_mdBm = p_total_tgt_mdBm;
		tpp->ramp.attenuation_mdB = 0;
		power_ramp_timer_cb(trx);
	} else if (we_are_ramping_up(trx)) {
		if (tpp->p_total_tgt_mdBm <= tpp->ramp.max_initial_pout_mdBm) {
			LOGPTRX(trx, DL1C, LOGL_INFO,
				"target_power (%d mdBm) is below or equal to 'power ramp max-initial' power (%d mdBm)\n",
				tpp->p_total_tgt_mdBm, tpp->ramp.max_initial_pout_mdBm);
			/* new setting is below the maximum initial output
			 * power, so we can directly jump to this level */
			tpp->p_total_cur_mdBm = tpp->p_total_tgt_mdBm;
			tpp->ramp.attenuation_mdB = 0;
			power_ramp_timer_cb(trx);
		} else {
			/* We need to step it up. Start from the current value, shortcutting to max-initial. */
			/* Set attenuation to cause no power change right now */
			if (tpp->p_total_cur_mdBm + (int)tpp->ramp.step_size_mdB < tpp->ramp.max_initial_pout_mdBm)
				tpp->p_total_cur_mdBm = tpp->ramp.max_initial_pout_mdBm - tpp->ramp.step_size_mdB;
			tpp->ramp.attenuation_mdB = tpp->p_total_tgt_mdBm - tpp->p_total_cur_mdBm;

			/* start with the first step */
			power_ramp_do_step(trx, 1);
		}
	} else {
		/* Set ramp attenuation to negative value, and increase that by
		 * steps until it reaches 0 */
		tpp->ramp.attenuation_mdB = tpp->p_total_tgt_mdBm - tpp->p_total_cur_mdBm;

		/* start with the first step */
		power_ramp_do_step(trx, 1);
	}

	return 0;
}

/* Cancel any pending request */
void power_ramp_abort(struct gsm_bts_trx *trx)
{
	osmo_timer_del(&trx->power_params.ramp.step_timer);
}

/* determine the initial transceiver output power at start-up time */
int power_ramp_initial_power_mdBm(const struct gsm_bts_trx *trx)
{
	const struct trx_power_params *tpp = &trx->power_params;
	int pout_mdBm;

	/* this is the maximum initial output on the antenna connector
	 * towards the antenna */
	pout_mdBm = tpp->ramp.max_initial_pout_mdBm;

	/* use this as input to compute transceiver board power
	 * (reflecting gains in internal/external amplifiers */
	return get_p_trxout_eff_mdBm(trx, pout_mdBm);
}