/* MSC RAN connection implementation */

/*
 * (C) 2016-2018 by sysmocom s.f.m.c. <info@sysmocom.de>
 * All Rights Reserved
 *
 * Author: 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/logging.h>
#include <osmocom/core/fsm.h>
#include <osmocom/core/signal.h>

#include <osmocom/msc/ran_conn.h>
#include <osmocom/vlr/vlr.h>
#include <osmocom/msc/debug.h>
#include <osmocom/msc/transaction.h>
#include <osmocom/msc/signal.h>
#include <osmocom/msc/sgs_iface.h>
#include <osmocom/msc/ran_peer.h>
#include <osmocom/msc/sccp_ran.h>
#include <osmocom/msc/ran_infra.h>
#include <osmocom/msc/msub.h>

struct ran_conn *ran_conn_create_incoming(struct ran_peer *ran_peer, uint32_t sccp_conn_id)
{
	struct ran_conn *conn;

	conn = talloc(ran_peer, struct ran_conn);
	OSMO_ASSERT(conn);

	*conn = (struct ran_conn){
		.ran_peer = ran_peer,
		.sccp_conn_id = sccp_conn_id,
	};

	llist_add(&conn->entry, &ran_peer->sri->ran_conns);
	return conn;
}

struct ran_conn *ran_conn_create_outgoing(struct ran_peer *ran_peer)
{
	/* FIXME use method being developed in gerrit id Ifd55c6b7ed2558ff072042079cf45f5068a971de */
	static uint32_t next_outgoing_conn_id = 2342;
	uint32_t conn_id = 0;
	int attempts = 1000;
	bool already_used = true;
	while (attempts--) {
		struct ran_conn *conn;

		conn_id = next_outgoing_conn_id;
		next_outgoing_conn_id++;

		already_used = false;
		llist_for_each_entry(conn, &ran_peer->sri->ran_conns, entry) {
			if (conn->sccp_conn_id == conn_id) {
				already_used = true;
				break;
			}
		}

		if (!already_used)
			break;
	}
	if (already_used)
		return NULL;
	LOG_RAN_PEER(ran_peer, LOGL_DEBUG, "Outgoing conn id: %u\n", conn_id);
	return ran_conn_create_incoming(ran_peer, conn_id);
}

/* Return statically allocated string of the ran_conn RAT type and id. */
const char *ran_conn_name(struct ran_conn *conn)
{
	static char id[42];
	int rc;
	const char *ran_peer_name;

	if (!conn)
		return "ran_conn==NULL";

	if (!conn->ran_peer || !conn->ran_peer->sri || !conn->ran_peer->sri->ran)
		ran_peer_name = "no-RAN-peer";
	else
		ran_peer_name = osmo_rat_type_name(conn->ran_peer->sri->ran->type);

	rc = snprintf(id, sizeof(id), "%s-%u", ran_peer_name, conn->sccp_conn_id);
	/* < 0 is error, == 0 is empty, >= size means truncation. Not really expecting this to catch on in any practical
	 * situation. */
	if (rc <= 0 || rc >= sizeof(id))
		return "conn-name-error";
	return id;
}

int ran_conn_down_l2_co(struct ran_conn *conn, struct msgb *l3, bool initial)
{
	struct ran_peer_ev_ctx co = {
		.conn_id = conn->sccp_conn_id,
		.conn = conn,
		.msg = l3,
	};
	if (!conn->ran_peer)
		return -EIO;
	return osmo_fsm_inst_dispatch(conn->ran_peer->fi,
				      initial ? RAN_PEER_EV_MSG_DOWN_CO_INITIAL : RAN_PEER_EV_MSG_DOWN_CO,
				      &co);
}

void ran_conn_msc_role_gone(struct ran_conn *conn, struct osmo_fsm_inst *msc_role)
{
	if (!conn)
		return;

	if (conn->msc_role != msc_role)
		return;

	conn->msc_role = NULL;
	ran_conn_close(conn);
}

/* Regularly close the conn */
void ran_conn_close(struct ran_conn *conn)
{
	if (!conn)
		return;
	if (conn->closing)
		return;
	conn->closing = true;
	LOG_RAN_PEER(conn->ran_peer, LOGL_DEBUG, "Closing %s\n", ran_conn_name(conn));

	if (conn->msc_role) {
		osmo_fsm_inst_dispatch(conn->msc_role, MSC_EV_FROM_RAN_CONN_RELEASED, NULL);
		conn->msc_role = NULL;
	}

	if (conn->ran_peer) {
		/* Todo: pass a useful SCCP cause? */
		sccp_ran_disconnect(conn->ran_peer->sri, conn->sccp_conn_id, 0);
		conn->ran_peer = NULL;
	}

	LOG_RAN_PEER(conn->ran_peer, LOGL_DEBUG, "Deallocating %s\n", ran_conn_name(conn));
	llist_del(&conn->entry);
	talloc_free(conn);
}

/* Same as ran_conn_close() but without sending any SCCP messages (e.g. after RESET) */
void ran_conn_discard(struct ran_conn *conn)
{
	if (!conn)
		return;
	/* Make sure to drop dead and don't dispatch things like DISCONNECT requests on SCCP. */
	conn->ran_peer = NULL;
	ran_conn_close(conn);
}
