module MGCP_Emulation {

/* MGCP Emulation, runs on top of MGCP_CodecPort.  It multiplexes/demultiplexes
 * the individual connections, so there can be separate TTCN-3 components handling
 * each of the connections.
 *
 * The MGCP_Emulation.main() function processes MGCP primitives from the MGCP
 * socket via the MGCP_CodecPort, and dispatches them to the per-connection components.
 *
 * For each new inbound connection, the MgcpOps.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 + MGCP, and first trigger a connection from BSC side in a
 * component which then subsequently should also handle the MGCP emulation.
 *
 * Inbound Unit Data messages (such as are dispatched to the MgcpOps.unitdata_cb() callback,
 * which is registered with an argument to the main() function below.
 *
 * (C) 2017-2018 by Harald Welte <laforge@gnumonks.org>
 * (C) 2018 by sysmocom - s.f.m.c. GmbH, Author: Daniel Willmann
 * 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 MGCP_CodecPort all;
import from MGCP_CodecPort_CtrlFunct all;
import from MGCP_Types all;
import from MGCP_Templates all;
import from Osmocom_Types all;
import from IPL4asp_Types all;

type component MGCP_ConnHdlr {
	/* Simple send/recv without caring about peer addr+port. Used with multi_conn_mode=false. */
	port MGCP_Conn_PT MGCP;
	/* Handle multiple connections concurrently. Used with multi_conn_mode=true. */
	port MGCP_Conn_Multi_PT MGCP_MULTI;
	/* procedure based port to register for incoming connections. */
	port MGCPEM_PROC_PT MGCP_PROC;
}

/* port between individual per-connection components and this dispatcher */
type port MGCP_Conn_PT message {
	inout MgcpCommand, MgcpResponse;
} with { extension "internal" };

/* port between individual per-connection components and this dispatcher */
type port MGCP_Conn_Multi_PT message {
	inout MGCP_RecvFrom, MGCP_SendTo;
} with { extension "internal" };

/* represents a single MGCP Endpoint */
type record EndpointData {
	MGCP_ConnHdlr	comp_ref,
	MgcpEndpoint	endpoint optional
};

/* pending CRCX with their transaction ID */
type set of MgcpTransId MgcpTransIds;

type component MGCP_Emulation_CT {
	/* Port facing to the UDP SUT */
	port MGCP_CODEC_PT MGCP;
	/* All MGCP_ConnHdlr MGCP ports connect here
	 * MGCP_Emulation_CT.main needs to figure out what messages
	 * to send where with CLIENT.send() to vc_conn */
	port MGCP_Conn_PT MGCP_CLIENT;
	/* This one is used with multi_conn_mode=true and allows differentiating UDP sockets */
	port MGCP_Conn_Multi_PT MGCP_CLIENT_MULTI;
	/* currently tracked connections */
	var EndpointData MgcpEndpointTable[16];
	var MgcpTransIds MgcpPendingTrans := {};
	/* pending expected CRCX */
	var ExpectData MgcpExpectTable[8];
	/* procedure based port to register for incoming connections */
	port MGCPEM_PROC_PT MGCP_PROC;

	var charstring g_mgcp_id;
	var integer g_mgcp_conn_id := -1;

	var MGCP_conn_parameters g_pars;
}

type function MGCPCreateCallback(MgcpCommand cmd, charstring id)
runs on MGCP_Emulation_CT return MGCP_ConnHdlr;

type function MGCPUnitdataCallback(MgcpMessage msg)
runs on MGCP_Emulation_CT return template MgcpMessage;

type record MGCPOps {
	MGCPCreateCallback create_cb,
	MGCPUnitdataCallback unitdata_cb
}

type record MGCP_conn_parameters {
	HostName callagent_ip,
	PortNumber callagent_udp_port,
	HostName mgw_ip,
	PortNumber mgw_udp_port,
	boolean multi_conn_mode
}

function tr_MGCP_RecvFrom_R(template MgcpMessage msg)
runs on MGCP_Emulation_CT return template MGCP_RecvFrom {
	var template MGCP_RecvFrom mrf := {
		connId := g_mgcp_conn_id,
		remName := ?,
		remPort := ?,
		locName := ?,
		locPort := ?,
		msg := msg
	}
	return mrf;
}

private function f_ep_known(MgcpEndpoint ep)
runs on MGCP_Emulation_CT return boolean {
	var integer i;
	for (i := 0; i < sizeof(MgcpEndpointTable); i := i+1) {
		if (MgcpEndpointTable[i].endpoint == ep) {
			return true;
		}
	}
	return false;
}

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

private function f_comp_by_ep(MgcpEndpoint ep)
runs on MGCP_Emulation_CT return MGCP_ConnHdlr {
	var integer i;
	for (i := 0; i < sizeof(MgcpEndpointTable); i := i+1) {
		if (MgcpEndpointTable[i].endpoint == ep) {
			return MgcpEndpointTable[i].comp_ref;
		}
	}
	setverdict(fail, "MGCP Endpoint Table not found by Endpoint", ep);
	mtc.stop;
}

private function f_ep_by_comp(MGCP_ConnHdlr client)
runs on MGCP_Emulation_CT return MgcpEndpoint {
	var integer i;
	for (i := 0; i < sizeof(MgcpEndpointTable); i := i+1) {
		if (MgcpEndpointTable[i].comp_ref == client) {
			return MgcpEndpointTable[i].endpoint;
		}
	}
	setverdict(fail, "MGCP Endpoint Table not found by component ", client);
	mtc.stop;
}

private function f_ep_table_add(MGCP_ConnHdlr comp_ref, MgcpEndpoint ep)
runs on MGCP_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(MgcpEndpointTable); i := i+1) {
		if (not isvalue(MgcpEndpointTable[i].endpoint)) {
			MgcpEndpointTable[i].endpoint := ep;
			MgcpEndpointTable[i].comp_ref := comp_ref;
			return;
		}
	}
	testcase.stop("MGCP Endpoint Table full!");
}

private function f_ep_table_del(MGCP_ConnHdlr comp_ref, MgcpEndpoint ep)
runs on MGCP_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(MgcpEndpointTable); i := i+1) {
		if (MgcpEndpointTable[i].comp_ref == comp_ref and
		    MgcpEndpointTable[i].endpoint == ep) {
			MgcpEndpointTable[i].endpoint := omit;
			MgcpEndpointTable[i].comp_ref := null;
			return;
		}
	}
	setverdict(fail, "MGCP Endpoint Table: Couldn't find to-be-deleted entry!");
	mtc.stop;
}

private function f_ep_table_change_connhdlr(MGCP_ConnHdlr comp_ref, MgcpEndpoint ep)
runs on MGCP_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(MgcpEndpointTable); i := i+1) {
		if (MgcpEndpointTable[i].endpoint == ep) {
			MgcpEndpointTable[i].comp_ref := comp_ref;
			log("MGCP_Emulation_CT: MgcpEndpointTable[", i, "] now sends to ", comp_ref);
			return;
		}
	}
	setverdict(fail, "MGCP Endpoint Table: Couldn't find entry to move to ", comp_ref);
	mtc.stop;
}

/* Check if the given transaction ID is a pending CRCX. If yes, return true + remove */
private function f_trans_id_was_pending(MgcpTransId trans_id)
runs on MGCP_Emulation_CT return boolean {
	for (var integer i := 0; i < lengthof(MgcpPendingTrans); i := i+1) {
		if (MgcpPendingTrans[i] == trans_id) {
			/* Remove from list */
			var MgcpTransIds OldPendingTrans := MgcpPendingTrans;
			MgcpPendingTrans := {}
			for (var integer j := 0; j < lengthof(OldPendingTrans); j := j+1) {
				if (j != i) {
					MgcpPendingTrans := MgcpPendingTrans & {OldPendingTrans[j]};
				}
			}
			return true;
		}
	}
	return false;
}

/* TODO: move this to MGCP_Types? */
function f_mgcp_ep(MgcpMessage msg) return MgcpEndpoint {
	var MgcpParameterList params;
	var integer i;
	if (ischosen(msg.command)) {
		return msg.command.line.ep;
	} else {
		var MgcpEndpoint ep;
		if (f_mgcp_find_param(msg, "Z", ep) == false) {
			setverdict(fail, "No SpecificEndpointName in MGCP response", msg);
			mtc.stop;
		}
		return ep;
	}
}

private function f_ep_table_init()
runs on MGCP_Emulation_CT {
	for (var integer i := 0; i < sizeof(MgcpEndpointTable); i := i+1) {
		MgcpEndpointTable[i].comp_ref := null;
		MgcpEndpointTable[i].endpoint := omit;
	}
}

private function f_forward_to_client(MGCP_RecvFrom mrf, MGCP_ConnHdlr vc_conn) runs on MGCP_Emulation_CT {
	if (g_pars.multi_conn_mode) {
		if (MGCP_CLIENT_MULTI.checkstate("Connected") and vc_conn.running) {
			MGCP_CLIENT_MULTI.send(mrf) to vc_conn;
		} else {
			log("MGCP_CLIENT_MULTI: Discarding msg due to peer unavailable: ", mrf);
		}
	} else {
		if (MGCP_CLIENT.checkstate("Connected") and vc_conn.running) {
			MGCP_CLIENT.send(mrf.msg.command) to vc_conn;
		} else {
			log("MGCP_CLIENT: Discarding msg due to peer unavailable: ", mrf);
		}
	}
}

function main(MGCPOps ops, MGCP_conn_parameters p, charstring id) runs on MGCP_Emulation_CT {
	var Result res;
	g_pars := p;
	g_mgcp_id := id;
	f_ep_table_init();
	f_expect_table_init();

	map(self:MGCP, system:MGCP_CODEC_PT);
	if (p.multi_conn_mode or p.callagent_udp_port == -1) {
		res := MGCP_CodecPort_CtrlFunct.f_IPL4_listen(MGCP, p.mgw_ip, p.mgw_udp_port, { udp:={} });
	} else {
		res := MGCP_CodecPort_CtrlFunct.f_IPL4_connect(MGCP, p.callagent_ip, p.callagent_udp_port, p.mgw_ip, p.mgw_udp_port, -1, { udp:={} });
	}
	if (not ispresent(res.connId)) {
		setverdict(fail, "Could not connect MGCP socket, check your configuration");
		mtc.stop;
	}
	g_mgcp_conn_id := res.connId;

	while (true) {
		var MGCP_ConnHdlr vc_conn;
		var ExpectCriteria crit;
		var MGCP_RecvFrom mrf;
		var MGCP_SendTo mst;
		var MgcpMessage msg;
		var MgcpCommand cmd;
		var MgcpResponse resp;
		var MgcpEndpoint ep;

		alt {
		/* MGCP from client */
		[not p.multi_conn_mode] MGCP_CLIENT.receive(MgcpResponse:?) -> value resp sender vc_conn {
			msg := {
				response := resp
			};
			/* If this is the response to a pending CRCX, extract Endpoint and store in table */
			if (f_trans_id_was_pending(resp.line.trans_id)) {
				f_ep_table_add(vc_conn, f_mgcp_ep(msg));
			}
			/* Pass message through */
			/* TODO: check which ConnectionID client has allocated + store in table? */
			MGCP.send(t_MGCP_Send(g_mgcp_conn_id, msg));
			}

		/* MGCP from client in Multi Conn mode */
		[p.multi_conn_mode] MGCP_CLIENT_MULTI.receive(MGCP_SendTo:?) -> value mst sender vc_conn {
			/* If this is the response to a pending CRCX, extract Endpoint and store in table */
			if (f_trans_id_was_pending(mst.msg.response.line.trans_id)) {
				f_ep_table_add(vc_conn, f_mgcp_ep(mst.msg));
			}
			/* Pass message through */
			/* TODO: check which ConnectionID client has allocated + store in table? */
			MGCP.send(mst);
			}
		[] MGCP.receive(tr_MGCP_RecvFrom_R(?)) -> value mrf {
			if (not p.multi_conn_mode and p.callagent_udp_port == -1) {
				/* we aren't yet connected to the remote side
				   port, let's fix this. This way upper layers
				   can use Send/Recv without caring about UDP
				   src/dst addr + port */
				p.callagent_udp_port := mrf.remPort;
				res := MGCP_CodecPort_CtrlFunct.f_IPL4_connect(MGCP, p.callagent_ip, p.callagent_udp_port, p.mgw_ip, p.mgw_udp_port, g_mgcp_conn_id, { udp:={} });
				if (not ispresent(res.connId)) {
					setverdict(fail, "Could not connect MGCP socket, check your configuration");
					mtc.stop;
				}
			}
			if (ischosen(mrf.msg.command)) {
				cmd := mrf.msg.command;
				if (f_ep_known(cmd.line.ep)) {
					vc_conn := f_comp_by_ep(cmd.line.ep);
					f_forward_to_client(mrf, vc_conn);
				} else {
					if (cmd.line.verb == "CRCX") {
						vc_conn := ops.create_cb.apply(cmd, id);
						if (not match(cmd.line.ep, t_MGCP_EP_wildcard)) {
							/* non-wildcard EP, use directly */
							f_ep_table_add(vc_conn, cmd.line.ep);
						} else {
							/* add this transaction to list of pending transactions */
							MgcpPendingTrans := MgcpPendingTrans & {cmd.line.trans_id};
						}
						f_forward_to_client(mrf, vc_conn);
					} else {
						/* connectionless MGCP, i.e. messages without ConnectionId */
						var template MgcpMessage r := ops.unitdata_cb.apply(mrf.msg);
						if (isvalue(r)) {
							MGCP.send(t_MGCP_SendToMrf(mrf, r));
						}
					}
				}
			} else {
				setverdict(fail, "Received unexpected MGCP response: ", mrf.msg.response);
				mtc.stop;
			}
			}
		[] MGCP_PROC.getcall(MGCPEM_register:{?,?}) -> param(crit, vc_conn) {
			f_create_expect(crit, vc_conn);
			MGCP_PROC.reply(MGCPEM_register:{crit, vc_conn}) to vc_conn;
			}
		[] MGCP_PROC.getcall(MGCPEM_delete_ep:{?,?}) -> param(ep, vc_conn) {
			f_ep_table_del(vc_conn, ep);
			MGCP_PROC.reply(MGCPEM_delete_ep:{ep, vc_conn}) to vc_conn;
			}
		[] MGCP_PROC.getcall(MGCPEM_change_connhdlr:{?,?}) -> param(ep, vc_conn) {
			f_ep_table_change_connhdlr(vc_conn, ep);
			MGCP_PROC.reply(MGCPEM_change_connhdlr:{ep, vc_conn}) to vc_conn;
			}
		}

	}
}

/* "Expect" Handling */

/*  */
type record ExpectCriteria {
	MgcpConnectionId connid optional,
	MgcpEndpoint endpoint optional,
	MgcpTransId transid optional
}

type record ExpectData {
	ExpectCriteria crit optional,
	MGCP_ConnHdlr vc_conn
}

signature MGCPEM_register(in ExpectCriteria cmd, in MGCP_ConnHdlr hdlr);
signature MGCPEM_delete_ep(in MgcpEndpoint ep, in MGCP_ConnHdlr hdlr);
signature MGCPEM_change_connhdlr(in MgcpEndpoint ep, in MGCP_ConnHdlr hdlr);

type port MGCPEM_PROC_PT procedure {
	inout MGCPEM_register, MGCPEM_delete_ep, MGCPEM_change_connhdlr;
} with { extension "internal" };

function f_get_mgcp_by_crit(ExpectCriteria crit)
return template MgcpCommand {
	var template MgcpCommand ret := {
		line := {
			verb := ?,
			trans_id := ?,
			ep := ?,
			ver := ?
		},
		params := *,
		sdp := *
	}
	if (ispresent(crit.connid)) {
		ret.params := { *, ts_MgcpParConnectionId(crit.connid), * };
	}
	if (ispresent(crit.endpoint)) {
		ret.line.ep := crit.endpoint;
	}
	if (ispresent(crit.transid)) {
		ret.line.trans_id := crit.transid;
	}

	return ret;
}

/* Function that can be used as create_cb and will use the expect table */
function ExpectedCreateCallback(MgcpCommand cmd, charstring id)
runs on MGCP_Emulation_CT return MGCP_ConnHdlr {
	var MGCP_ConnHdlr ret := null;
	var template MgcpCommand mgcpcmd;
	var integer i;

	for (i := 0; i < sizeof(MgcpExpectTable); i := i+1) {
		if (not ispresent(MgcpExpectTable[i].crit)) {
			continue;
		}
		/* FIXME: Ignore criteria for now */
		mgcpcmd := f_get_mgcp_by_crit(MgcpExpectTable[i].crit);
		if (match(cmd, mgcpcmd)) {
			ret := MgcpExpectTable[i].vc_conn;
			/* Release this entry */
			MgcpExpectTable[i].crit := omit;
			MgcpExpectTable[i].vc_conn := null;
			log("Found Expect[", i, "] for ", cmd, " handled at ", ret);
			return ret;
		}
	}
	setverdict(fail, "Couldn't find Expect for CRCX", cmd);
	mtc.stop;
}

private function f_create_expect(ExpectCriteria crit, MGCP_ConnHdlr hdlr)
runs on MGCP_Emulation_CT {
	var integer i;

	/* Check an entry like this is not already present */
	for (i := 0; i < sizeof(MgcpExpectTable); i := i+1) {
		if (crit == MgcpExpectTable[i].crit) {
			setverdict(fail, "Crit already present", crit);
			mtc.stop;
		}
	}
	for (i := 0; i < sizeof(MgcpExpectTable); i := i+1) {
		if (not ispresent(MgcpExpectTable[i].crit)) {
			MgcpExpectTable[i].crit := crit;
			MgcpExpectTable[i].vc_conn := hdlr;
			log("Created Expect[", i, "] for ", crit, " to be handled at ", hdlr);
			return;
		}
	}
	testcase.stop("No space left in MgcpExpectTable")
}

/* client/conn_hdlr side function to use procedure port to create expect in emulation */
function f_create_mgcp_expect(ExpectCriteria dest_number) runs on MGCP_ConnHdlr {
	MGCP_PROC.call(MGCPEM_register:{dest_number, self}) {
		[] MGCP_PROC.getreply(MGCPEM_register:{?,?}) {};
	}
}

/* client/conn_hdlr side function to use procedure port to delete expect in emulation */
function f_create_mgcp_delete_ep(MgcpEndpoint ep) runs on MGCP_ConnHdlr {
	MGCP_PROC.call(MGCPEM_delete_ep:{ep, self}) {
		[] MGCP_PROC.getreply(MGCPEM_delete_ep:{?,?}) {};
	}
}

/* Move MGCP handling for a given MGW endpoint to a different MGCP_ConnHdlr component. */
function f_mgcp_change_connhdlr(MgcpEndpoint ep) runs on MGCP_ConnHdlr {
	MGCP_PROC.call(MGCPEM_change_connhdlr:{ep, self}) {
		[] MGCP_PROC.getreply(MGCPEM_change_connhdlr:{?,?}) {};
	}
}


private function f_expect_table_init()
runs on MGCP_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(MgcpExpectTable); i := i + 1) {
		MgcpExpectTable[i].crit := omit;
	}
}

function DummyUnitdataCallback(MgcpMessage msg)
runs on MGCP_Emulation_CT return template MgcpMessage {
	log("Ignoring MGCP ", msg);
	return omit;
}

/* Determine encoding name for a specified payload type number */
function f_encoding_name_from_pt(SDP_FIELD_PayloadType pt) return charstring {
	if (pt == PT_PCMU) {
		return "PCMU";
	} else if (pt == PT_GSM) {
		return "GSM";
	} else if (pt == PT_PCMA) {
		return "PCMA";
	} else if (pt == PT_GSMEFR) {
		return "GSM-EFR";
	} else if (pt == PT_GSMHR) {
		return "GSM-HR-08";
	} else if (pt == PT_AMR) {
		return "AMR";
	} else if (pt == PT_AMRWB) {
		return "AMR-WB";
	}

	setverdict(fail, "Unknown payload type ", pt);
	mtc.stop;
}

}