/* Select a suitable lchan from a given cell.
 *
 * (C) 2008 by Harald Welte <laforge@gnumonks.org>
 * (C) 2008, 2009 by Holger Hans Peter Freyther <zecke@selfish.org>
 * (C) 2018-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 *
 * 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 <stdlib.h>

#include <osmocom/bsc/debug.h>

#include <osmocom/bsc/gsm_data.h>
#include <osmocom/bsc/timeslot_fsm.h>
#include <osmocom/bsc/lchan_fsm.h>

#include <osmocom/bsc/lchan_select.h>
#include <osmocom/bsc/bts.h>

struct lchan_select_ts_list {
	struct gsm_bts_trx_ts **list;
	unsigned int num;
};

const struct value_string lchan_select_reason_names[] = {
	OSMO_VALUE_STRING(SELECT_FOR_MS_CHAN_REQ),
	OSMO_VALUE_STRING(SELECT_FOR_ASSIGNMENT),
	OSMO_VALUE_STRING(SELECT_FOR_HANDOVER),
	OSMO_VALUE_STRING(SELECT_FOR_VGCS),
	{0, NULL}
};

static struct gsm_lchan *pick_better_lchan(struct gsm_lchan *a, struct gsm_lchan *b)
{
	if (!a)
		return b;
	if (!b)
		return a;
	/* comparing negative dBm values: smaller value means less interference. */
	if (b->interf_dbm < a->interf_dbm)
		return b;
	return a;
}

static struct gsm_lchan *_lc_find(struct lchan_select_ts_list *ts_list,
				  enum gsm_phys_chan_config pchan,
				  enum gsm_phys_chan_config as_pchan,
				  bool allow_pchan_switch, bool log)
{
	struct gsm_lchan *lchan;
	struct gsm_lchan *found_lchan = NULL;

#define LOGPLCHANALLOC(fmt, args...) do { \
	if (log) \
		LOGP(DRLL, LOGL_DEBUG, "looking for lchan %s%s%s%s: " fmt, \
		     gsm_pchan_name(pchan), \
		     pchan == as_pchan ? "" : " as ", \
		     pchan == as_pchan ? "" : gsm_pchan_name(as_pchan), \
		     ((pchan != as_pchan) && !allow_pchan_switch) ? " without pchan switch" : "", \
		     ## args); \
	} while (0)

	for (unsigned int tn = 0; tn < ts_list->num; tn++) {
		struct gsm_bts_trx_ts *ts = ts_list->list[tn];
		int lchans_as_pchan;

		/* The caller first selects what kind of TS to search in, e.g. looking for exact
		 * GSM_PCHAN_TCH_F, or maybe among dynamic GSM_PCHAN_OSMO_DYN... */
		if (ts->pchan_on_init != pchan) {
			LOGPLCHANALLOC("%s is != %s\n", gsm_ts_and_pchan_name(ts),
				       gsm_pchan_name(pchan));
			continue;
		}
		/* Next, is this timeslot in or can it be switched to the pchan we want to use it for? */
		if (!ts_usable_as_pchan(ts, as_pchan, allow_pchan_switch)) {
			LOGPLCHANALLOC("%s is not usable as %s%s\n", gsm_ts_and_pchan_name(ts),
				       gsm_pchan_name(as_pchan),
				       allow_pchan_switch ? "" : " without pchan switch");
			continue;
		}

		/* TS is (going to be) in desired pchan mode. Go ahead and check for an available lchan. */
		lchans_as_pchan = pchan_subslots(as_pchan);
		ts_for_n_lchans(lchan, ts, lchans_as_pchan) {
			struct gsm_lchan *was = found_lchan;

			if (lchan->fi->state != LCHAN_ST_UNUSED) {
				LOGPLCHANALLOC("%s ss=%d in type=%s,state=%s not suitable\n",
					       gsm_ts_and_pchan_name(ts), lchan->nr,
					       gsm_chan_t_name(lchan->type),
					       osmo_fsm_inst_state_name(lchan->fi));
				continue;
			}

			found_lchan = pick_better_lchan(found_lchan, lchan);
			if (found_lchan != was)
				LOGPLCHANALLOC("%s ss=%d interf=%u=%ddBm is %s%s\n",
					       gsm_ts_and_pchan_name(ts), lchan->nr,
					       lchan->interf_band, lchan->interf_dbm,
					       was == NULL ? "available" : "better",
					       ts->pchan_is != as_pchan ? ", after dyn PCHAN change" : "");
			else
				LOGPLCHANALLOC("%s ss=%d interf=%u=%ddBm is also available but not better\n",
					       gsm_ts_and_pchan_name(ts), lchan->nr,
					       lchan->interf_band, lchan->interf_dbm);

			/* When picking an lchan with least interference, continue to loop across all lchans. When
			 * ignoring interference levels, return the first match. */
			if (found_lchan && !ts->trx->bts->chan_alloc_avoid_interf)
				return found_lchan;
		}
	}

	if (found_lchan)
		LOGPLCHANALLOC("%s ss=%d interf=%ddBm%s is the best pick\n",
			       gsm_ts_and_pchan_name(found_lchan->ts), found_lchan->nr,
			       found_lchan->interf_dbm,
			       found_lchan->ts->pchan_is != as_pchan ? ", after dyn PCHAN change," : "");
	else
		LOGPLCHANALLOC("Nothing found\n");
	return found_lchan;
#undef LOGPLCHANALLOC
}

static struct gsm_lchan *lc_dyn_find(struct lchan_select_ts_list *ts_list,
				     enum gsm_phys_chan_config pchan,
				     enum gsm_phys_chan_config dyn_as_pchan,
				     bool log)
{
	struct gsm_lchan *lchan;

	/* First find an lchan that needs no change in its timeslot pchan mode.
	 * In particular, this ensures that handover to a dynamic timeslot in TCH/H favors timeslots that are currently
	 * using only one of two TCH/H, so that we don't switch more dynamic timeslots to TCH/H than necessary.
	 * For non-dynamic timeslots, it is not necessary to do a second pass with allow_pchan_switch ==
	 * true, because they never switch anyway. */
	if ((lchan = _lc_find(ts_list, pchan, dyn_as_pchan, false, log)))
		return lchan;
	if ((lchan = _lc_find(ts_list, pchan, dyn_as_pchan, true, log)))
		return lchan;

	return NULL;
}

static struct gsm_lchan *lc_find(struct lchan_select_ts_list *ts_list,
				 enum gsm_phys_chan_config pchan,
				 bool log)
{
	return _lc_find(ts_list, pchan, pchan, false, log);
}

enum gsm_chan_t chan_mode_to_chan_type(enum gsm48_chan_mode chan_mode, enum channel_rate chan_rate)
{
	switch (gsm48_chan_mode_to_non_vamos(chan_mode)) {
	case GSM48_CMODE_SIGN:
		switch (chan_rate) {
		case CH_RATE_SDCCH:
			return GSM_LCHAN_SDCCH;
		case CH_RATE_HALF:
			return GSM_LCHAN_TCH_H;
		case CH_RATE_FULL:
			return GSM_LCHAN_TCH_F;
		default:
			return GSM_LCHAN_NONE;
		}
	case GSM48_CMODE_SPEECH_EFR:
	case GSM48_CMODE_DATA_14k5:
	case GSM48_CMODE_DATA_12k0:
		/* these rates work over full-rate channels only */
		if (chan_rate != CH_RATE_FULL)
			return GSM_LCHAN_NONE;
		/* fall through */
	case GSM48_CMODE_SPEECH_V1:
	case GSM48_CMODE_SPEECH_AMR:
	case GSM48_CMODE_DATA_6k0:
	case GSM48_CMODE_DATA_3k6:
		switch (chan_rate) {
		case CH_RATE_HALF:
			return GSM_LCHAN_TCH_H;
		case CH_RATE_FULL:
			return GSM_LCHAN_TCH_F;
		default:
			return GSM_LCHAN_NONE;
		}
	default:
		return GSM_LCHAN_NONE;
	}
}

static int qsort_func(const void *_a, const void *_b)
{
	const struct gsm_bts_trx *trx_a = *(const struct gsm_bts_trx **)_a;
	const struct gsm_bts_trx *trx_b = *(const struct gsm_bts_trx **)_b;

	int pwr_a = trx_a->nominal_power - trx_a->max_power_red;
	int pwr_b = trx_b->nominal_power - trx_b->max_power_red;

	/* Sort in descending order */
	return pwr_b - pwr_a;
}

static void populate_ts_list(struct lchan_select_ts_list *ts_list,
			     struct gsm_bts *bts,
			     bool chan_alloc_reverse,
			     bool sort_by_trx_power,
			     bool log)
{
	struct gsm_bts_trx **trx_list;
	struct gsm_bts_trx *trx;
	unsigned int num = 0;

	/* Allocate an array with pointers to all TRX instances of a BTS */
	trx_list = talloc_array_ptrtype(bts, trx_list, bts->num_trx);
	OSMO_ASSERT(trx_list != NULL);

	llist_for_each_entry(trx, &bts->trx_list, list)
		trx_list[trx->nr] = trx;

	/* Sort by TRX power in descending order (if needed) */
	if (sort_by_trx_power)
		qsort(&trx_list[0], bts->num_trx, sizeof(trx), &qsort_func);

	for (unsigned int trxn = 0; trxn < bts->num_trx; trxn++) {
		trx = trx_list[trxn];
		for (unsigned int tn = 0; tn < ARRAY_SIZE(trx->ts); tn++) {
			struct gsm_bts_trx_ts *ts = &trx->ts[tn];
			if (ts_is_usable(ts))
				ts_list->list[num++] = ts;
			else if (log)
				LOGP(DRLL, LOGL_DEBUG, "%s is not usable\n", gsm_ts_name(ts));
		}
	}

	talloc_free(trx_list);
	ts_list->num = num;

	/* Reverse the timeslot list if required */
	if (chan_alloc_reverse) {
		for (unsigned int tn = 0; tn < num / 2; tn++) {
			struct gsm_bts_trx_ts *temp = ts_list->list[tn];
			ts_list->list[tn] = ts_list->list[num - tn - 1];
			ts_list->list[num - tn - 1] = temp;
		}
	}
}

static bool chan_alloc_ass_dynamic_reverse(struct gsm_bts *bts,
					   void *ctx, bool log)
{
	const struct load_counter *ll = &bts->c0->lchan_load;
	const struct gsm_lchan *old_lchan = ctx;
	unsigned int lchan_load;
	int avg_ul_rxlev;

	OSMO_ASSERT(old_lchan != NULL);
	OSMO_ASSERT(old_lchan->ts->trx->bts == bts);

#define LOG_COND(fmt, args...) do { \
	if (log) \
		LOG_LCHAN(old_lchan, LOGL_DEBUG, fmt, ## args); \
	} while (0)

	/* Condition a) Channel load on the C0 (BCCH carrier) */
	lchan_load = ll->total ? ll->used * 100 / ll->total : 0;
	if (lchan_load < bts->chan_alloc_dyn_params.c0_chan_load_thresh) {
		LOG_COND("C0 Channel Load %u%% < thresh %u%% => using ascending order\n",
			 lchan_load, bts->chan_alloc_dyn_params.c0_chan_load_thresh);
		return false;
	}

	/* Condition b) average Uplink RxLev */
	avg_ul_rxlev = get_meas_rep_avg(old_lchan, TDMA_MEAS_FIELD_RXLEV,
					TDMA_MEAS_DIR_UL, TDMA_MEAS_SET_AUTO,
					bts->chan_alloc_dyn_params.ul_rxlev_avg_num);
	if (avg_ul_rxlev < 0) {
		LOG_COND("Unknown AVG UL RxLev => using ascending order\n");
		return false;
	}
	if (avg_ul_rxlev < bts->chan_alloc_dyn_params.ul_rxlev_thresh) {
		LOG_COND("AVG UL RxLev %u < thresh %u => using ascending order\n",
			 avg_ul_rxlev, bts->chan_alloc_dyn_params.ul_rxlev_thresh);
		return false;
	}

	LOG_COND("C0 Channel Load %u%% >= thresh %u%% and "
		 "AVG UL RxLev %u >= thresh %u => using descending order\n",
		 lchan_load, bts->chan_alloc_dyn_params.c0_chan_load_thresh,
		 avg_ul_rxlev, bts->chan_alloc_dyn_params.ul_rxlev_thresh);

#undef LOG_COND

	return true;
}

struct gsm_lchan *lchan_select_by_chan_mode(struct gsm_bts *bts,
					    enum gsm48_chan_mode chan_mode,
					    enum channel_rate chan_rate,
					    enum lchan_select_reason reason,
					    void *ctx)
{
	enum gsm_chan_t type = chan_mode_to_chan_type(chan_mode, chan_rate);
	if (type == GSM_LCHAN_NONE)
		return NULL;
	return lchan_select_by_type(bts, type, reason, ctx);
}

struct gsm_lchan *lchan_avail_by_type(struct gsm_bts *bts,
				      enum gsm_chan_t type,
				      enum lchan_select_reason reason,
				      void *ctx, bool log)
{
	struct gsm_lchan *lchan = NULL;
	enum gsm_phys_chan_config first, first_cbch, second, second_cbch;
	struct lchan_select_ts_list ts_list;
	bool sort_by_trx_power = false;
	bool chan_alloc_reverse = false;

	if (log) {
		LOG_BTS(bts, DRLL, LOGL_DEBUG, "lchan_avail_by_type(type=%s, reason=%s)\n",
			gsm_chan_t_name(type), lchan_select_reason_name(reason));
	}

	switch (reason) {
	case SELECT_FOR_MS_CHAN_REQ:
		chan_alloc_reverse = bts->chan_alloc_chan_req_reverse;
		break;
	case SELECT_FOR_ASSIGNMENT:
		if (bts->chan_alloc_assignment_dynamic) {
			chan_alloc_reverse = chan_alloc_ass_dynamic_reverse(bts, ctx, log);
			sort_by_trx_power = bts->chan_alloc_dyn_params.sort_by_trx_power;
		} else {
			chan_alloc_reverse = bts->chan_alloc_assignment_reverse;
		}
		break;
	case SELECT_FOR_HANDOVER:
		chan_alloc_reverse = bts->chan_alloc_handover_reverse;
		break;
	case SELECT_FOR_VGCS:
		chan_alloc_reverse = bts->chan_alloc_vgcs_reverse;
		break;
	}

	/* Allocate an array with pointers to all timeslots of a BTS */
	ts_list.list = talloc_array_ptrtype(bts, ts_list.list, bts->num_trx * 8);
	if (OSMO_UNLIKELY(ts_list.list == NULL))
		return NULL;

	/* Populate this array with the actual pointers */
	populate_ts_list(&ts_list, bts, chan_alloc_reverse, sort_by_trx_power, log);

	switch (type) {
	case GSM_LCHAN_SDCCH:
		if (chan_alloc_reverse) {
			first = GSM_PCHAN_SDCCH8_SACCH8C;
			first_cbch = GSM_PCHAN_SDCCH8_SACCH8C_CBCH;
			second = GSM_PCHAN_CCCH_SDCCH4;
			second_cbch = GSM_PCHAN_CCCH_SDCCH4_CBCH;
		} else {
			first = GSM_PCHAN_CCCH_SDCCH4;
			first_cbch = GSM_PCHAN_CCCH_SDCCH4_CBCH;
			second = GSM_PCHAN_SDCCH8_SACCH8C;
			second_cbch = GSM_PCHAN_SDCCH8_SACCH8C_CBCH;
		}

		lchan = lc_find(&ts_list, first, log);
		if (lchan == NULL)
			lchan = lc_find(&ts_list, first_cbch, log);
		if (lchan == NULL)
			lchan = lc_find(&ts_list, second, log);
		if (lchan == NULL)
			lchan = lc_find(&ts_list, second_cbch, log);
		/* No dedicated SDCCH available -- try fully dynamic
		 * TCH/F_TCH/H_SDCCH8_PDCH if BTS supports it: */
		if (lchan == NULL && osmo_bts_has_feature(&bts->features, BTS_FEAT_DYN_TS_SDCCH8))
			lchan = lc_dyn_find(&ts_list, GSM_PCHAN_OSMO_DYN,
						      GSM_PCHAN_SDCCH8_SACCH8C, log);
		break;
	case GSM_LCHAN_TCH_F:
		lchan = lc_find(&ts_list, GSM_PCHAN_TCH_F, log);
		/* If we don't have TCH/F available, try dynamic TCH/F_PDCH */
		if (!lchan)
			lchan = lc_dyn_find(&ts_list, GSM_PCHAN_TCH_F_PDCH,
						      GSM_PCHAN_TCH_F, log);

		/* Try fully dynamic TCH/F_TCH/H_PDCH as TCH/F... */
		if (!lchan && bts->network->dyn_ts_allow_tch_f)
			lchan = lc_dyn_find(&ts_list, GSM_PCHAN_OSMO_DYN,
						      GSM_PCHAN_TCH_F, log);
		break;
	case GSM_LCHAN_TCH_H:
		lchan = lc_find(&ts_list, GSM_PCHAN_TCH_H, log);
		/* No dedicated TCH/x available -- try fully dynamic
		 * TCH/F_TCH/H_PDCH */
		if (!lchan)
			lchan = lc_dyn_find(&ts_list, GSM_PCHAN_OSMO_DYN,
						      GSM_PCHAN_TCH_H, log);
		break;
	default:
		LOG_BTS(bts, DRLL, LOGL_ERROR, "Unknown gsm_chan_t %u\n", type);
	}

	talloc_free(ts_list.list);

	return lchan;
}

/* Return a matching lchan from a specific BTS that is currently available. The next logical step is
 * lchan_activate() on it, which would possibly cause dynamic timeslot pchan switching, taken care of by
 * the lchan and timeslot FSMs. */
struct gsm_lchan *lchan_select_by_type(struct gsm_bts *bts,
				       enum gsm_chan_t type,
				       enum lchan_select_reason reason,
				       void *ctx)
{
	struct gsm_lchan *lchan = NULL;

	LOG_BTS(bts, DRLL, LOGL_DEBUG, "lchan_select_by_type(type=%s, reason=%s)\n",
		gsm_chan_t_name(type), lchan_select_reason_name(reason));

	lchan = lchan_avail_by_type(bts, type, reason, ctx, true);

	if (!lchan) {
		LOG_BTS(bts, DRLL, LOGL_NOTICE, "Failed to select %s channel (%s)\n",
			gsm_chan_t_name(type), lchan_select_reason_name(reason));
		return NULL;
	}

	lchan_select_set_type(lchan, type);
	return lchan;
}

/* Set available lchan to given type. Usually used on lchan obtained with
 * lchan_avail_by_type. The next logical step is lchan_activate() on it, which
 * would possibly cause dynamic timeslot pchan switching, taken care of by the
 * lchan and timeslot FSMs. */
void lchan_select_set_type(struct gsm_lchan *lchan, enum gsm_chan_t type)
{
	lchan->type = type;
	LOG_LCHAN(lchan, LOGL_INFO, "Selected\n");
}