module MNCC_Emulation {

/* MNCC Emulation, runs on top of MNCC_CodecPort.  It multiplexes/demultiplexes
 * the individual calls, so there can be separate TTCN-3 components handling
 * each of the calls
 *
 * The MNCC_Emulation.main() function processes MNCC primitives from the MNCC
 * socket via the MNCC_CodecPort, and dispatches them to the per-connection components.
 *
 * Outbound MNCC connections are initiated by sending a MNCC_Call_Req primitive
 * to the component running the MNCC_Emulation.main() function.
 *
 * For each new inbound connections, the MnccOps.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 + MNCC, and first trigger a connection from BSC side in a
 * component which then subsequently should also handle the MNCC emulation.
 *
 * Inbound Unit Data messages (such as are dispatched to the MnccOps.unitdata_cb() callback,
 * which is registered with an argument to the main() function below.
 *
 * (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 MNCC_CodecPort all;
import from MNCC_Types all;
import from UD_Types all;

modulepar {
	int mp_mncc_version := 8;
}

/* General "base class" component definition, of which specific implementations
 * derive themselves by means of the "extends" feature */
type component MNCC_ConnHdlr {
	/* ports towards MNCC Emulator core / call dispatchar */
	port MNCC_Conn_PT MNCC;
	port MNCCEM_PROC_PT MNCC_PROC;
}

/* Auxiliary primitive that can happen on the port between per-connection client and this dispatcher */
type enumerated MNCC_Conn_Prim {
	/* MNCC tell us that connection was released */
	MNCC_CONN_PRIM_DISC_IND,
	/* we tell MNCC to release connection */
	MNCC_CONN_PRIM_DISC_REQ
}

type record MNCC_Conn_Req {
	MNCC_PDU		mncc
}

/* port between individual per-connection components and this dispatcher */
type port MNCC_Conn_PT message {
	inout MNCC_PDU, MNCC_Conn_Prim, MNCC_Conn_Req;
} with { extension "internal" };


/* represents a single MNCC call */
type record ConnectionData {
	/* reference to the instance of the per-connection component */
	MNCC_ConnHdlr	comp_ref,
	integer		mncc_call_id
}

type component MNCC_Emulation_CT {
	/* UNIX DOMAIN socket on the bottom side, using primitives */
	port MNCC_CODEC_PT MNCC;
	/* MNCC port to the per-connection clients */
	port MNCC_Conn_PT MNCC_CLIENT;

	/* use 16 as this is also the number of SCCP connections that SCCP_Emulation can handle */
	var ConnectionData MnccCallTable[16];

	/* pending expected incoming connections */
	var ExpectData MnccExpectTable[8];
	/* procedure based port to register for incoming connections */
	port MNCCEM_PROC_PT MNCC_PROC;

	var integer g_mncc_ud_id;
};

private function f_call_id_known(uint32_t mncc_call_id)
runs on MNCC_Emulation_CT return boolean {
	var integer i;
	for (i := 0; i < sizeof(MnccCallTable); i := i+1) {
		if (MnccCallTable[i].mncc_call_id == mncc_call_id){
			return true;
		}
	}
	return false;
}

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

/* resolve component reference by connection ID */
private function f_comp_by_call_id(uint32_t mncc_call_id)
runs on MNCC_Emulation_CT return MNCC_ConnHdlr {
	var integer i;
	for (i := 0; i < sizeof(MnccCallTable); i := i+1) {
		if (MnccCallTable[i].mncc_call_id == mncc_call_id) {
			return MnccCallTable[i].comp_ref;
		}
	}
	setverdict(fail, "MNCC Call table not found by MNCC Call ID ", mncc_call_id);
	mtc.stop;
}

/* resolve connection ID by component reference */
private function f_call_id_by_comp(MNCC_ConnHdlr client)
runs on MNCC_Emulation_CT return integer {
	for (var integer i := 0; i < sizeof(MnccCallTable); i := i+1) {
		if (MnccCallTable[i].comp_ref == client) {
			return MnccCallTable[i].mncc_call_id;
		}
	}
	setverdict(fail, "MNCC Call table not found by component ", client);
	mtc.stop;
}

private function f_gen_call_id()
runs on MNCC_Emulation_CT return integer {
	var uint32_t call_id;

	do {
		call_id := float2int(rnd()*4294967296.0);
	} while (f_call_id_known(call_id) == true);

	return call_id;
}

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

private function f_call_table_init()
runs on MNCC_Emulation_CT {
	for (var integer i := 0; i < sizeof(MnccCallTable); i := i+1) {
		MnccCallTable[i].comp_ref := null;
		MnccCallTable[i].mncc_call_id := -1;
	}
}

private function f_call_table_add(MNCC_ConnHdlr comp_ref, uint32_t mncc_call_id)
runs on MNCC_Emulation_CT {
	for (var integer i := 0; i < sizeof(MnccCallTable); i := i+1) {
		if (MnccCallTable[i].mncc_call_id == -1) {
			MnccCallTable[i].comp_ref := comp_ref;
			MnccCallTable[i].mncc_call_id := mncc_call_id;
			log("Added conn table entry ", i, comp_ref, mncc_call_id);
			return;
		}
	}
	testcase.stop("MNCC Call table full!");
}

private function f_call_table_del(uint32_t mncc_call_id)
runs on MNCC_Emulation_CT {
	for (var integer i := 0; i < sizeof(MnccCallTable); i := i+1) {
		if (MnccCallTable[i].mncc_call_id == mncc_call_id) {
			log("Deleted conn table entry ", i,
			    MnccCallTable[i].comp_ref, mncc_call_id);
			MnccCallTable[i].mncc_call_id := -1;
			MnccCallTable[i].comp_ref := null;
			return
		}
	}
	setverdict(fail, "MNCC Call table attempt to delete non-existant ", mncc_call_id);
	mtc.stop;
}


private function f_connect(charstring sock) runs on MNCC_Emulation_CT {
	var UD_connect_result res;
	timer T := 5.0;

	T.start;
	MNCC.send(UD_connect:{sock, -1});
	alt {
	[] MNCC.receive(UD_connect_result:?) -> value res {
		if (ispresent(res.result) and ispresent(res.result.result_code) and res.result.result_code == ERROR) {
			setverdict(fail, "Error connecting to MNCC socket", res);
			mtc.stop;
		} else {
			g_mncc_ud_id := res.id;
		}
		}
	[] T.timeout {
		setverdict(fail, "Timeout connecting to MNCC socket");
		mtc.stop;
		}
	}
}

private function f_listen(charstring sock) runs on MNCC_Emulation_CT {
	var UD_listen_result res;
	var UD_connected udc;
	timer T := 5.0;

	T.start;
	MNCC.send(UD_listen:{sock});
	alt {
	[] MNCC.receive(UD_listen_result:?) -> value res {
		if (ispresent(res.result) and ispresent(res.result.result_code) and res.result.result_code == ERROR) {
			setverdict(fail, "Error listening to MNCC socket", res);
			mtc.stop;
		} else {
			g_mncc_ud_id := res.id;
		}
		}
	[] T.timeout {
		setverdict(fail, "Timeout listening to MNCC socket");
		mtc.stop;
		}
	}

	T.start;
	alt {
	[] MNCC.receive(UD_connected:?) -> value udc {
		g_mncc_ud_id := res.id;
		}
	[] T.timeout {
		setverdict(fail, "Timeout waiting for MNCC connection");
		mtc.stop;
		}
	}
}

/* call-back type, to be provided by specific implementation; called when new SCCP connection
 * arrives */
type function MnccCreateCallback(MNCC_PDU conn_ind, charstring id)
runs on MNCC_Emulation_CT return MNCC_ConnHdlr;

type function MnccUnitdataCallback(MNCC_PDU mncc)
runs on MNCC_Emulation_CT return template MNCC_PDU;

type record MnccOps {
	MnccCreateCallback create_cb,
	MnccUnitdataCallback unitdata_cb
}

function main(MnccOps ops, charstring id, charstring sock, boolean role_server := false)
runs on MNCC_Emulation_CT {

	if (not set_MNCC_version(mp_mncc_version)) {
		setverdict(fail, "Failed configuring MNCC enc/dec to version ", mp_mncc_version);
		return;
	}

	if (role_server) {
		f_listen(sock);
		MNCC.send(t_SD_MNCC(g_mncc_ud_id, ts_MNCC_HELLO(version := mp_mncc_version)));
	} else {
		f_connect(sock);
	}
	f_expect_table_init();
	f_call_table_init();

	while (true) {
		var MNCC_send_data sd;
		var MNCC_Conn_Req creq;
		var MNCC_ConnHdlr vc_conn;
		var MNCC_PDU mncc;
		var MNCC_ConnHdlr vc_hdlr;
		var charstring dest_nr;
		var uint32_t mncc_call_id;

		alt {
		/* MNCC -> Client: UNIT-DATA (connectionless SCCP) from a BSC */
		[] MNCC.receive(t_SD_MNCC_MSGT(g_mncc_ud_id, MNCC_SOCKET_HELLO)) -> value sd {
			/* Connectionless Procedures like HELLO */
			var template MNCC_PDU resp;
			resp := ops.unitdata_cb.apply(sd.data);
			if (isvalue(resp)) {
				MNCC.send(t_SD_MNCC(g_mncc_ud_id, resp));
			}
			}

		/* MNCC -> Client: Release Indication / confirmation */
		[] MNCC.receive(t_SD_MNCC_MSGT(g_mncc_ud_id, (MNCC_REL_IND, MNCC_REL_CNF))) -> value sd {
			var uint32_t call_id := f_mncc_get_call_id(sd.data);
			/* forward to respective client */
			vc_conn := f_comp_by_call_id(call_id);
			MNCC_CLIENT.send(sd.data) to vc_conn;
			/* remove from call table */
			f_call_table_del(call_id);
			}

		/* MNCC -> Client: call related messages */
		[] MNCC.receive(t_SD_MNCC_MSGT(g_mncc_ud_id, ?)) -> value sd {
			var uint32_t call_id := f_mncc_get_call_id(sd.data);

			if (f_call_id_known(call_id)) {
				vc_conn := f_comp_by_call_id(call_id);
				MNCC_CLIENT.send(sd.data) to vc_conn;
			} else {
				/* TODO: Only accept this for SETUP.req? */
				vc_conn := ops.create_cb.apply(sd.data, id)
				/* store mapping between client components and SCCP connectionId */
				f_call_table_add(vc_conn, call_id);
				/* handle user payload */
				MNCC_CLIENT.send(sd.data) to vc_conn;
			}
			}

		/* Client -> MNCC Socket: RELEASE.ind or RELEASE.cnf: forward + drop call table entry */
		[] MNCC_CLIENT.receive(MNCC_PDU:{msg_type := (MNCC_REL_IND, MNCC_REL_CNF), u:=?}) -> value mncc sender vc_conn {
			var integer call_id := f_call_id_by_comp(vc_conn);
			/* forward to MNCC socket */
			MNCC.send(t_SD_MNCC(g_mncc_ud_id, mncc));
			/* remove from call table */
			f_call_table_del(call_id);
			}

		/* Client -> MNCC Socket: Normal message */
		[] MNCC_CLIENT.receive(MNCC_PDU:?) -> value mncc sender vc_conn {
			if (mncc.msg_type == MNCC_SETUP_REQ and not role_server) {
				/* ConnHdlr -> MNCC Server: SETUP.req: add to call table */
				f_call_table_add(vc_conn, f_mncc_get_call_id(mncc));
			} else if (mncc.msg_type == MNCC_SETUP_IND and role_server) {
				/* ConnHdlr -> MNCC Client: SETUP.ind: add to call table */
				f_call_table_add(vc_conn, f_mncc_get_call_id(mncc));
			}
			/* forward to MNCC socket */
			MNCC.send(t_SD_MNCC(g_mncc_ud_id, mncc));
			}

		[] MNCC_CLIENT.receive(MNCC_PDU:?) -> value mncc sender vc_conn {
			/* forward to MNCC socket */
			MNCC.send(t_SD_MNCC(g_mncc_ud_id, mncc));
			}


		/* Client -> us: procedure call to register expect */
		[] MNCC_PROC.getcall(MNCCEM_register:{?,?}) -> param(dest_nr, vc_hdlr) {
			f_create_expect(dest_nr, vc_hdlr);
			MNCC_PROC.reply(MNCCEM_register:{dest_nr, vc_hdlr}) to vc_hdlr;
			}

		[] MNCC_PROC.getcall(MNCCEM_change_connhdlr:{?,?}) -> param(mncc_call_id, vc_hdlr) {
			f_call_table_del(mncc_call_id);
			f_call_table_add(vc_hdlr, mncc_call_id);
			MNCC_PROC.reply(MNCCEM_change_connhdlr:{mncc_call_id, vc_hdlr}) to vc_hdlr;
			}
		}
	}
}

private function f_mgcp_ep_extract_cic(charstring inp) return integer {
	var charstring local_part := regexp(inp, "(*)@*", 0);
	return hex2int(str2hex(local_part));

}

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

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

/* procedure based port to register for incoming calls */
signature MNCCEM_register(in charstring dest_nr, in MNCC_ConnHdlr hdlr);
signature MNCCEM_change_connhdlr(in uint32_t mncc_call_id, in MNCC_ConnHdlr hdlr);

type port MNCCEM_PROC_PT procedure {
	inout MNCCEM_register, MNCCEM_change_connhdlr;
} with { extension "internal" };

/* CreateCallback that can be used as create_cb and will use the expectation table */
function ExpectedCreateCallback(MNCC_PDU conn_ind, charstring id)
runs on MNCC_Emulation_CT return MNCC_ConnHdlr {
	var MNCC_ConnHdlr ret := null;
	var charstring dest_number;
	var integer i;

	if (not ischosen(conn_ind.u.signal) or
	    (conn_ind.msg_type != MNCC_SETUP_IND and conn_ind.msg_type != MNCC_SETUP_REQ)) {
		setverdict(fail, "MNCC ExpectedCreateCallback needs MNCC_SETUP_{IND,REQ}");
		mtc.stop;
		return ret;
	}
	dest_number := conn_ind.u.signal.called.number;

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

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

/* client/conn_hdlr side function to use procedure port to create expect in emulation */
function f_create_mncc_expect(charstring dest_number) runs on MNCC_ConnHdlr {
	MNCC_PROC.call(MNCCEM_register:{dest_number, self}) {
		[] MNCC_PROC.getreply(MNCCEM_register:{?,?}) {};
	}
}

/* Move MNCC handling for a given call id to another MNCC_ConnHdlr test component. */
function f_mncc_change_connhdlr(uint32_t mncc_call_id) runs on MNCC_ConnHdlr {
	MNCC_PROC.call(MNCCEM_change_connhdlr:{mncc_call_id, self}) {
		[] MNCC_PROC.getreply(MNCCEM_change_connhdlr:{?,?}) {};
	}
}

function DummyUnitdataCallback(MNCC_PDU mncc)
runs on MNCC_Emulation_CT return template MNCC_PDU {
	log("Ignoring MNCC ", mncc);
	return omit;
}

}