/* (C) 2020 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
 * All Rights Reserved
 *
 * Authors: Philipp Maier, Neels Hofmeyr
 *
 * 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 <osmocom/core/fsm.h>
#include <osmocom/core/tdef.h>
#include <osmocom/bsc/debug.h>
#include <osmocom/bsc/bssmap_reset.h>
#include <osmocom/bsc/gsm_data.h>

static struct osmo_fsm bssmap_reset_fsm;

enum bssmap_reset_fsm_state {
	BSSMAP_RESET_ST_DISC,
	BSSMAP_RESET_ST_CONN,
};

static const struct value_string bssmap_reset_fsm_event_names[] = {
	OSMO_VALUE_STRING(BSSMAP_RESET_EV_RX_RESET),
	OSMO_VALUE_STRING(BSSMAP_RESET_EV_RX_RESET_ACK),
	OSMO_VALUE_STRING(BSSMAP_RESET_EV_CONN_CFM_FAILURE),
	OSMO_VALUE_STRING(BSSMAP_RESET_EV_CONN_CFM_SUCCESS),
	{}
};

static const struct osmo_tdef_state_timeout bssmap_reset_timeouts[32] = {
	[BSSMAP_RESET_ST_DISC] = { .T = 4 },
};

#define bssmap_reset_fsm_state_chg(FI, STATE) \
	osmo_tdef_fsm_inst_state_chg(FI, STATE, \
				     bssmap_reset_timeouts, \
				     (bsc_gsmnet)->T_defs, \
				     -1)

struct bssmap_reset *bssmap_reset_alloc(void *ctx, const char *label, const struct bssmap_reset_cfg *cfg)
{
	struct bssmap_reset *bssmap_reset;
	struct osmo_fsm_inst *fi;

	fi = osmo_fsm_inst_alloc(&bssmap_reset_fsm, ctx, NULL, LOGL_DEBUG, label);
	OSMO_ASSERT(fi);

	bssmap_reset = talloc_zero(fi, struct bssmap_reset);
	OSMO_ASSERT(bssmap_reset);
	*bssmap_reset = (struct bssmap_reset){
		.fi = fi,
		.cfg = *cfg,
	};
	fi->priv = bssmap_reset;

	/* Immediately (1ms) kick off reset sending mechanism */
	osmo_fsm_inst_state_chg_ms(fi, BSSMAP_RESET_ST_DISC, 1, 0);
	return bssmap_reset;
}

void bssmap_reset_term_and_free(struct bssmap_reset *bssmap_reset)
{
	if (!bssmap_reset)
		return;
	osmo_fsm_inst_term(bssmap_reset->fi, OSMO_FSM_TERM_REQUEST, NULL);
	talloc_free(bssmap_reset);
}

static void link_up(struct bssmap_reset *bssmap_reset)
{
	LOGPFSML(bssmap_reset->fi, LOGL_NOTICE, "link up\n");
	bssmap_reset->conn_cfm_failures = 0;
	if (bssmap_reset->cfg.ops.link_up)
		bssmap_reset->cfg.ops.link_up(bssmap_reset->cfg.data);
}

static void link_lost(struct bssmap_reset *bssmap_reset)
{
	LOGPFSML(bssmap_reset->fi, LOGL_NOTICE, "link lost\n");
	if (bssmap_reset->cfg.ops.link_lost)
		bssmap_reset->cfg.ops.link_lost(bssmap_reset->cfg.data);
}

static void tx_reset(struct bssmap_reset *bssmap_reset)
{
	if (bssmap_reset->cfg.ops.tx_reset)
		bssmap_reset->cfg.ops.tx_reset(bssmap_reset->cfg.data);
}

static void tx_reset_ack(struct bssmap_reset *bssmap_reset)
{
	if (bssmap_reset->cfg.ops.tx_reset_ack)
		bssmap_reset->cfg.ops.tx_reset_ack(bssmap_reset->cfg.data);
}

static void bssmap_reset_disc_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
{
	struct bssmap_reset *bssmap_reset = (struct bssmap_reset*)fi->priv;
	if (prev_state == BSSMAP_RESET_ST_CONN)
		link_lost(bssmap_reset);
}

static void bssmap_reset_disc_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct bssmap_reset *bssmap_reset = (struct bssmap_reset*)fi->priv;
	switch (event) {

	case BSSMAP_RESET_EV_RX_RESET:
		tx_reset_ack(bssmap_reset);
		bssmap_reset_fsm_state_chg(fi, BSSMAP_RESET_ST_CONN);
		break;

	case BSSMAP_RESET_EV_RX_RESET_ACK:
		bssmap_reset_fsm_state_chg(fi, BSSMAP_RESET_ST_CONN);
		break;

	case BSSMAP_RESET_EV_CONN_CFM_FAILURE:
		/* ignore */
		break;

	case BSSMAP_RESET_EV_CONN_CFM_SUCCESS:
		/* A connection succeeded before we managed to do a RESET handshake?
		 * Then the calling code is not taking care to check bssmap_reset_is_conn_ready().
		 */
		LOGPFSML(fi, LOGL_ERROR, "Connection success confirmed, but we have not seen a RESET-ACK; bug?\n");
		break;

	default:
		OSMO_ASSERT(false);
	}
}

static void bssmap_reset_conn_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
{
	struct bssmap_reset *bssmap_reset = (struct bssmap_reset*)fi->priv;
	if (prev_state != BSSMAP_RESET_ST_CONN)
		link_up(bssmap_reset);
}

static void bssmap_reset_conn_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct bssmap_reset *bssmap_reset = (struct bssmap_reset*)fi->priv;

	switch (event) {

	case BSSMAP_RESET_EV_RX_RESET:
		/* We were connected, but the remote side has restarted. */
		link_lost(bssmap_reset);
		tx_reset_ack(bssmap_reset);
		link_up(bssmap_reset);
		break;

	case BSSMAP_RESET_EV_RX_RESET_ACK:
		LOGPFSML(fi, LOGL_INFO, "Link is already up, ignoring RESET ACK\n");
		break;

	case BSSMAP_RESET_EV_CONN_CFM_FAILURE:
		bssmap_reset->conn_cfm_failures++;
		if (bssmap_reset->conn_cfm_failures > bssmap_reset->cfg.conn_cfm_failure_threshold)
			bssmap_reset_fsm_state_chg(fi, BSSMAP_RESET_ST_DISC);
		break;

	case BSSMAP_RESET_EV_CONN_CFM_SUCCESS:
		bssmap_reset->conn_cfm_failures = 0;
		break;

	default:
		OSMO_ASSERT(false);
	}
}

static int bssmap_reset_fsm_timer_cb(struct osmo_fsm_inst *fi)
{
	struct bssmap_reset *bssmap_reset = (struct bssmap_reset*)fi->priv;

	tx_reset(bssmap_reset);

	/* (re-)enter disconnect state to resend RESET after timeout. */
	bssmap_reset_fsm_state_chg(fi, BSSMAP_RESET_ST_DISC);

	/* Return 0 to not terminate the fsm */
	return 0;
}

#define S(x) (1 << (x))

static struct osmo_fsm_state bssmap_reset_fsm_states[] = {
	[BSSMAP_RESET_ST_DISC] = {
		     .name = "DISCONNECTED",
		     .in_event_mask = 0
			     | S(BSSMAP_RESET_EV_RX_RESET)
			     | S(BSSMAP_RESET_EV_RX_RESET_ACK)
			     | S(BSSMAP_RESET_EV_CONN_CFM_FAILURE)
			     | S(BSSMAP_RESET_EV_CONN_CFM_SUCCESS)
			     ,
		     .out_state_mask = 0
			     | S(BSSMAP_RESET_ST_DISC)
			     | S(BSSMAP_RESET_ST_CONN)
			     ,
		     .onenter = bssmap_reset_disc_onenter,
		     .action = bssmap_reset_disc_action,
		     },
	[BSSMAP_RESET_ST_CONN] = {
		     .name = "CONNECTED",
		     .in_event_mask = 0
			     | S(BSSMAP_RESET_EV_RX_RESET)
			     | S(BSSMAP_RESET_EV_RX_RESET_ACK)
			     | S(BSSMAP_RESET_EV_CONN_CFM_FAILURE)
			     | S(BSSMAP_RESET_EV_CONN_CFM_SUCCESS)
			     ,
		     .out_state_mask = 0
			     | S(BSSMAP_RESET_ST_DISC)
			     | S(BSSMAP_RESET_ST_CONN)
			     ,
		     .onenter = bssmap_reset_conn_onenter,
		     .action = bssmap_reset_conn_action,
		     },
};

static struct osmo_fsm bssmap_reset_fsm = {
	.name = "bssmap_reset",
	.states = bssmap_reset_fsm_states,
	.num_states = ARRAY_SIZE(bssmap_reset_fsm_states),
	.log_subsys = DRESET,
	.timer_cb = bssmap_reset_fsm_timer_cb,
	.event_names = bssmap_reset_fsm_event_names,
};

bool bssmap_reset_is_conn_ready(const struct bssmap_reset *bssmap_reset)
{
	/* bssmap_reset is NULL if SMLC is not enabled in the config */
	if (bssmap_reset == NULL)
		return false;
	return bssmap_reset->fi->state == BSSMAP_RESET_ST_CONN;
}

void bssmap_reset_resend_reset(struct bssmap_reset *bssmap_reset)
{
	OSMO_ASSERT(bssmap_reset);

	/* Immediately (1ms) kick off reset sending mechanism */
	osmo_fsm_inst_state_chg_ms(bssmap_reset->fi, BSSMAP_RESET_ST_DISC, 1, 0);
}

void bssmap_reset_set_disconnected(struct bssmap_reset *bssmap_reset)
{
	/* Go to disconnected state, with the normal RESET timeout to re-send RESET. */
	bssmap_reset_fsm_state_chg(bssmap_reset->fi, BSSMAP_RESET_ST_DISC);
}

static __attribute__((constructor)) void bssmap_reset_fsm_init(void)
{
	OSMO_ASSERT(osmo_fsm_register(&bssmap_reset_fsm) == 0);
}
