module GSUP_Emulation {

/* GSUP Emulation, runs on top of IPA_Emulation.  It multiplexes/demultiplexes
 * the individual calls, so there can be separate TTCN-3 components handling
 * each of the calls
 *
 * The GSUP_Emulation.main() function processes GSUP primitives from the IPA/GSUP
 * socket via the IPA_Emulation, and dispatches them to the per-connection components.
 *
 * Outbound GSUP connections are initiated by sending a FIXME primitive
 * to the component running the GSUP_Emulation.main() function.
 *
 * For each new inbound connections, the GsupOps.create_cb() is called.  It can create
 * or resolve a TTCN-3 component, and returns a component reference to which that inbound
 * connection is routed/dispatched.
 *
 * If a pre-existing component wants to register to handle a future inbound call, it can
 * do so by registering an "expect" with the expected destination phone number.  This is e.g. useful
 * if you are simulating BSC + HUL, and first trigger a connection from BSC side in a
 * component which then subsequently should also handle the GSUP emulation.
 *
 * (C) 2018 by Harald Welte <laforge@gnumonks.org>
 * All rights reserved.
 *
 * Released under the terms of GNU General Public License, Version 2 or
 * (at your option) any later version.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */


import from Osmocom_Types all;
import from GSUP_Types all;
import from IPA_Emulation all;

/* General "base class" component definition, of which specific implementations
 * derive themselves by means of the "extends" feature */
type component GSUP_ConnHdlr {
	/* ports towards GSUP Emulator core / call dispatchar */
	port GSUP_Conn_PT GSUP;
	port GSUPEM_PROC_PT GSUP_PROC;
}

/* port between individual per-connection components and this dispatcher */
type port GSUP_Conn_PT message {
	inout GSUP_PDU;
} with { extension "internal" };


/* represents a single GSUP call */
type record ConnectionData {
	/* reference to the instance of the per-connection component */
	GSUP_ConnHdlr	comp_ref,
	charstring	imsi
}

type component GSUP_Emulation_CT {
	/* UNIX DOMAIN socket on the bottom side, using primitives */
	port IPA_GSUP_PT GSUP;
	/* GSUP port to the per-connection clients */
	port GSUP_Conn_PT GSUP_CLIENT;

	var ConnectionData GsupImsiTable[256];

	/* pending expected incoming connections */
	var ExpectData GsupExpectTable[256];
	/* procedure based port to register for incoming connections */
	port GSUPEM_PROC_PT GSUP_PROC;
};

private function f_imsi_known(charstring imsi)
runs on GSUP_Emulation_CT return boolean {
	var integer i;
	for (i := 0; i < sizeof(GsupImsiTable); i := i+1) {
		if (GsupImsiTable[i].imsi == imsi) {
			return true;
		}
	}
	return false;
}

private function f_comp_known(GSUP_ConnHdlr client)
runs on GSUP_Emulation_CT return boolean {
	var integer i;
	for (i := 0; i < sizeof(GsupImsiTable); i := i+1) {
		if (GsupImsiTable[i].comp_ref == client) {
			return true;
		}
	}
	return false;
}

/* resolve component reference by connection ID */
private function f_comp_by_imsi(charstring imsi)
runs on GSUP_Emulation_CT return GSUP_ConnHdlr {
	var integer i;
	for (i := 0; i < sizeof(GsupImsiTable); i := i+1) {
		if (GsupImsiTable[i].imsi == imsi) {
			return GsupImsiTable[i].comp_ref;
		}
	}
	setverdict(fail, "GSUP IMSI table not found by IMSI ", imsi);
	mtc.stop;
}

/* resolve connection ID by component reference */
private function f_imsi_by_comp(GSUP_ConnHdlr client)
runs on GSUP_Emulation_CT return charstring {
	for (var integer i := 0; i < sizeof(GsupImsiTable); i := i+1) {
		if (GsupImsiTable[i].comp_ref == client) {
			return GsupImsiTable[i].imsi;
		}
	}
	setverdict(fail, "GSUP IMSI table not found by component ", client);
	mtc.stop;
}

private function f_imsi_table_init()
runs on GSUP_Emulation_CT {
	for (var integer i := 0; i < sizeof(GsupImsiTable); i := i+1) {
		GsupImsiTable[i].comp_ref := null;
		GsupImsiTable[i].imsi := "";
	}
}

private function f_expect_table_init()
runs on GSUP_Emulation_CT {
	for (var integer i := 0; i < sizeof(GsupExpectTable); i := i+1) {
		GsupExpectTable[i].vc_conn := null;
		GsupExpectTable[i].imsi := omit;
	}
}

private function f_imsi_table_add(GSUP_ConnHdlr comp_ref, charstring imsi)
runs on GSUP_Emulation_CT {
	for (var integer i := 0; i < sizeof(GsupImsiTable); i := i+1) {
		if (GsupImsiTable[i].imsi == "") {
			GsupImsiTable[i].comp_ref := comp_ref;
			GsupImsiTable[i].imsi := imsi;
			log("Added IMSI table entry ", i, comp_ref, imsi);
			return;
		}
	}
	testcase.stop("GSUP IMSI table full!");
}

private function f_imsi_table_del(charstring imsi)
runs on GSUP_Emulation_CT {
	for (var integer i := 0; i < sizeof(GsupImsiTable); i := i+1) {
		if (GsupImsiTable[i].imsi == imsi) {
			log("Deleted GSUP IMSI table entry ", i,
			    GsupImsiTable[i].comp_ref, imsi);
			GsupImsiTable[i].imsi := "";
			GsupImsiTable[i].comp_ref := null;
			return
		}
	}
	setverdict(fail, "GSUP IMSI table attempt to delete non-existant ", imsi);
	mtc.stop;
}


/* call-back type, to be provided by specific implementation; called when new SCCP connection
 * arrives */
type function GsupCreateCallback(GSUP_PDU gsup, charstring id)
runs on GSUP_Emulation_CT return GSUP_ConnHdlr;

type record GsupOps {
	GsupCreateCallback create_cb
}

function main(GsupOps ops, charstring id) runs on GSUP_Emulation_CT {

	f_imsi_table_init();
	f_expect_table_init();

	while (true) {
		var GSUP_ConnHdlr vc_conn;
		var GSUP_ConnHdlr vc_hdlr;
		var GSUP_PDU gsup;
		var charstring imsi;

		alt {

		[] GSUP.receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_ID_ACK)) { repeat; }
		[] GSUP.receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_UP)) { repeat; }
		[] GSUP.receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_DOWN)) {
			setverdict(fail, "GSUP Connection Lost");
			mtc.stop;
			}

		/* GSUP -> Client: call related messages */
		[] GSUP.receive(GSUP_PDU:?) -> value gsup {
			imsi := hex2str(gsup.ies[0].val.imsi);

			if (f_imsi_known(imsi)) {
				vc_conn := f_comp_by_imsi(imsi);
				GSUP_CLIENT.send(gsup) to vc_conn;
			} else {
				/* TODO: Only accept this for SETUP.req? */
				vc_conn := ops.create_cb.apply(gsup, id)
				/* store mapping between client components and SCCP connectionId */
				f_imsi_table_add(vc_conn, imsi);
				/* handle user payload */
				GSUP_CLIENT.send(gsup) to vc_conn;
			}
			}

		[] GSUP.receive { repeat; }

		/* Client -> GSUP Socket: Normal message */
		[] GSUP_CLIENT.receive(GSUP_PDU:?) -> value gsup sender vc_conn {
			/* forward to GSUP socket */
			GSUP.send(gsup);
			}


		/* Client -> us: procedure call to register expect */
		[] GSUP_PROC.getcall(GSUPEM_register_expect:{?,?}) -> param(imsi, vc_hdlr) {
			f_create_expect(imsi, vc_hdlr);
			GSUP_PROC.reply(GSUPEM_register_expect:{imsi, vc_hdlr}) to vc_hdlr;
			}
		/* Client -> us: procedure call to unregister expect */
		[] GSUP_PROC.getcall(GSUPEM_unregister_expect:{?,?}) -> param(imsi, vc_hdlr) {
			f_destroy_expect(imsi, vc_hdlr);
			GSUP_PROC.reply(GSUPEM_unregister_expect:{imsi, vc_hdlr}) to vc_hdlr;
			}

		[] GSUP_PROC.getcall(GSUPEM_unregister_connhdlr:{?}) -> param(imsi) sender vc_hdlr {
			f_imsi_table_del(imsi);
			GSUP_PROC.reply(GSUPEM_unregister_connhdlr:{imsi}) to vc_hdlr;
			}
		[] GSUP_PROC.getcall(GSUPEM_change_connhdlr:{?,?}) -> param(imsi, vc_hdlr) {
			f_imsi_table_del(imsi);
			f_imsi_table_add(vc_hdlr, imsi);
			GSUP_PROC.reply(GSUPEM_change_connhdlr:{imsi, vc_hdlr}) to vc_hdlr;
			}

		}
	}
}

/***********************************************************************
 * "Expect" Handling (mapping for expected incoming GSUP calls from IUT)
 ***********************************************************************/

/* data about an expected future incoming connection */
type record ExpectData {
	/* destination number based on which we can match it */
	charstring imsi optional,
	/* component reference for this connection */
	GSUP_ConnHdlr vc_conn
}

/* procedure based port to register for incoming calls */
signature GSUPEM_register_expect(in charstring imsi, in GSUP_ConnHdlr hdlr);
signature GSUPEM_unregister_expect(in charstring imsi, in GSUP_ConnHdlr hdlr);
signature GSUPEM_unregister_connhdlr(in charstring imsi);
signature GSUPEM_change_connhdlr(in charstring imsi, in GSUP_ConnHdlr hdlr);

type port GSUPEM_PROC_PT procedure {
	inout GSUPEM_register_expect, GSUPEM_unregister_expect,
	      GSUPEM_unregister_connhdlr, GSUPEM_change_connhdlr;
} with { extension "internal" };

/* CreateCallback that can be used as create_cb and will use the expectation table */
function ExpectedCreateCallback(GSUP_PDU gsup, charstring id)
runs on GSUP_Emulation_CT return GSUP_ConnHdlr {
	var GSUP_ConnHdlr ret := null;
	var charstring imsi;
	var integer i;

	imsi := hex2str(gsup.ies[0].val.imsi);

	for (i := 0; i < sizeof(GsupExpectTable); i:= i+1) {
		if (not ispresent(GsupExpectTable[i].imsi)) {
			continue;
		}
		if (imsi == GsupExpectTable[i].imsi) {
			ret := GsupExpectTable[i].vc_conn;
			/* release this entry to be used again */
			GsupExpectTable[i].imsi := omit;
			GsupExpectTable[i].vc_conn := null;
			log("Found GsupExpect[", i, "] for ", imsi, " handled at ", ret);
			/* return the component reference */
			return ret;
		}
	}
	setverdict(fail, "Couldn't find GsupExpect for incoming imsi ", imsi);
	mtc.stop;
	return ret;
}

/* server/emulation side function to create expect */
private function f_create_expect(charstring imsi, GSUP_ConnHdlr hdlr)
runs on GSUP_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(GsupExpectTable); i := i+1) {
		if (not ispresent(GsupExpectTable[i].imsi)) {
			GsupExpectTable[i].imsi := imsi;
			GsupExpectTable[i].vc_conn := hdlr;
			log("Created GsupExpect[", i, "] for ", imsi, " to be handled at ", hdlr);
			return;
		}
	}
	testcase.stop("No space left in GsupExpectTable");
}

/* server/emulation side function to destroy expect */
private function f_destroy_expect(charstring imsi, GSUP_ConnHdlr hdlr)
runs on GSUP_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(GsupExpectTable); i := i+1) {
		if (GsupExpectTable[i].imsi == imsi and GsupExpectTable[i].vc_conn == hdlr) {
			GsupExpectTable[i].imsi := omit;
			GsupExpectTable[i].vc_conn := null;
			log("Destroyed GsupExpect[", i, "] for ", imsi, " to be handled at ", hdlr);
			return;
		}
	}
	testcase.stop("No matching expect found to be destoyed");
}

/* client/conn_hdlr side function to use procedure port to create expect in emulation */
function f_create_gsup_expect(charstring imsi) runs on GSUP_ConnHdlr {
	GSUP_PROC.call(GSUPEM_register_expect:{imsi, self}) {
		[] GSUP_PROC.getreply(GSUPEM_register_expect:{?,?}) {};
	}
}

function f_destroy_gsup_expect(charstring imsi) runs on GSUP_ConnHdlr {
	GSUP_PROC.call(GSUPEM_unregister_expect:{imsi, self}) {
		[] GSUP_PROC.getreply(GSUPEM_unregister_expect:{?,?}) {};
	}
}

function f_unregister_gsup_imsi(charstring imsi) runs on GSUP_ConnHdlr {
	GSUP_PROC.call(GSUPEM_unregister_connhdlr:{imsi}) {
		[] GSUP_PROC.getreply(GSUPEM_unregister_connhdlr:{?}) {};
	}
}



/* Same as f_create_gsup_expect, but with explicit addressing. Needed when connecting multiple ports to GSUP_PROC. */
function f_create_gsup_expect_explicit(charstring imsi, GSUP_Emulation_CT ct) runs on GSUP_ConnHdlr {
	GSUP_PROC.call(GSUPEM_register_expect:{imsi, self}) to ct {
		[] GSUP_PROC.getreply(GSUPEM_register_expect:{?,?}) {};
	}
}

function f_gsup_change_connhdlr(charstring imsi) runs on GSUP_ConnHdlr {
	GSUP_PROC.call(GSUPEM_change_connhdlr:{imsi, self}) {
		[] GSUP_PROC.getreply(GSUPEM_change_connhdlr:{?,?}) {};
	}
}

}
