module NGAP_Emulation {

/* NGAP Emulation, runs on top of NGAP_CodecPort. It multiplexes/demultiplexes
 * the individual subscribers by their UE association (AMF_UE_NGAP_ID/
 * RAN_UE_NGAP_ID identifiers), so there can be separate TTCN-3 components
 * handling each of them.
 *
 * The NGAP_Emulation.main() function processes NGAP primitives from the NGAP
 * socket via the NGAP_CodecPort, and dispatches them to the per-subscriber
 * components.
 *
 * For each new subscriber, the NGapOps.create_cb() is called. It can create
 * or resolve a TTCN-3 component, and returns a component reference to which
 * that subscriber traffic is routed/dispatched.
 *
 * If a pre-existing component wants to register to handle a future inbound UE
 * association, it can do so by registering an "expect" with the expected
 * AMF_UE_NGAP_ID/RAN_UE_NGAP_ID identifiers. It is also possible to register
 * an expect for a specific procedureCode, in case the expected message is non
 * UE related (unit-data).
 *
 * Inbound non-UE related NGAP messages (such as RESET, SETUP, OVERLOAD) are
 * dispatched to the NGapOps.unitdata_cb() callback, which is registered with
 * an argument to the main() function below.
 *
 * (C) 2019 by Harald Welte <laforge@gnumonks.org>
 * (C) 2025 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 * 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 General_Types all;
import from Osmocom_Types all;
import from IPL4asp_Types all;
import from Misc_Helpers all;
import from DNS_Helpers all;

import from SCTP_Templates all;

import from NGAP_CodecPort all;
import from NGAP_CodecPort_CtrlFunct all;
import from NGAP_CommonDataTypes all;
import from NGAP_Types all;
import from NGAP_Constants all;
import from NGAP_PDU_Contents all;
import from NGAP_PDU_Descriptions all;
import from NGAP_IEs all;
import from NGAP_Templates all;
import from NGAP_Functions all;

import from NG_NAS_MsgContainers all;
import from NG_NAS_Functions all;

import from NG_NAS_Osmo_Templates all;
import from NG_CryptoFunctions all;

type component NGAP_ConnHdlr {
	port NGAP_Conn_PT NGAP;
	/* procedure based port to register for incoming connections */
	port NGAPEM_PROC_PT NGAP_PROC;
}

/* port between individual per-connection components and this dispatcher */
type port NGAP_Conn_PT message {
	inout NGAP_PDU, NG_NAS_UL_Message_Type, NG_NAS_DL_Message_Type, NGAPEM_Config;
} with { extension "internal" };

type record NG_NAS_Keys {
	OCT32 k_nas_int,
	OCT32 k_nas_enc
};
type record NG_ResetNAScounts {
/* empty */
};
type union NGAPEM_Config {
	NG_NAS_Keys set_nas_keys,
	NG_ResetNAScounts reset_nas_counts,
	NG_NAS_ALG_INT set_nas_alg_int,
	NG_NAS_ALG_ENC set_nas_alg_enc
};

type enumerated NGAPEM_EventUpDown {
	NGAPEM_EVENT_DOWN,
	NGAPEM_EVENT_UP
}

/* an event indicating us whether or not a connection is physically up or down,
 * and whether we have received an ID_ACK */
type union NGAPEM_Event {
	NGAPEM_EventUpDown	up_down
}

/* global test port e.g. for non-imsi/conn specific messages */
type port NGAP_PT message {
	inout NGAP_PDU, NGAPEM_Event;
} with { extension "internal" };

/* "Expect" Handling */

type record ExpectData {
	AMF_UE_NGAP_ID amf_id optional,
	RAN_UE_NGAP_ID ran_id optional,
	NGAP_ConnHdlr vc_conn
}

/* represents a single NGAP PDU that we expect. When a matching PDU is seen, it is forwarded to the registered
 * component */
type record ExpectDataProc {
	integer procedureCode optional,
	NGAP_ConnHdlr vc_conn				/* component handling this UE connection */
};

signature NGAPEM_register(in AMF_UE_NGAP_ID amf_id, in RAN_UE_NGAP_ID ran_id, in NGAP_ConnHdlr hdlr);
signature NGAPEM_register_proc(in integer procedureCode, in NGAP_ConnHdlr hdlr);
signature NGAPEM_obtain_amf_id() return AMF_UE_NGAP_ID;
//signature NGAPEM_derive_nas_token(in octetstring kasme, in NGAP_ConnHdlr hdlr, out OCT32 nas_token);

type port NGAPEM_PROC_PT procedure {
	inout NGAPEM_register;
	inout NGAPEM_register_proc;
	inout NGAPEM_obtain_amf_id;
	//inout NGAPEM_derive_nas_token;
} with { extension "internal" };

/* Function that can be used as create_cb and will use the expect table */
function ExpectedCreateCallback(NGAP_PDU msg,
				template (omit) AMF_UE_NGAP_ID amf_id,
				template (omit) RAN_UE_NGAP_ID ran_id,
				charstring id)
runs on NGAP_Emulation_CT return NGAP_ConnHdlr {
	var NGAP_ConnHdlr ret := null;
	var integer i;

	for (i := 0; i < sizeof(NGapExpectTable); i := i+1) {
		if (not ispresent(NGapExpectTable[i].amf_id) and not ispresent(NGapExpectTable[i].ran_id)) {
			continue;
		}

		if (valueof(amf_id) == NGapExpectTable[i].amf_id and valueof(ran_id) == NGapExpectTable[i].ran_id) {
			ret := NGapExpectTable[i].vc_conn;
			/* Release this entry */
			NGapExpectTable[i].amf_id := omit;
			NGapExpectTable[i].ran_id := omit;
			NGapExpectTable[i].vc_conn := null;
			//log("Found Expect[", i, "] for ", msg, " handled at ", ret);
			return ret;
		}
	}
	setverdict(fail, "Couldn't find Expect for ", msg);
	mtc.stop;
}

/* represents a single NGAP Association */
type record AssociationData {
	NGAP_ConnHdlr	comp_ref,			/* component handling this UE connection */
	uint32_t	ran_ue_ngap_id optional,	/* eNB side NGAP ID */
	uint40_t	amf_ue_ngap_id optional,	/* AMF side NGAP ID */
	UserLocationInformation	uli optional,
	NG_NAS_UE_State	nus
};

type component NGAP_Emulation_CT {
	/* Port facing to the UDP SUT */
	port NGAP_CODEC_PT NGAP;
	/* All NGAP_ConnHdlr NGAP ports connect here
	 * NGAP_Emulation_CT.main needs to figure out what messages
	 * to send where with CLIENT.send() to vc_conn */
	port NGAP_Conn_PT NGAP_CLIENT;
	/* currently tracked connections */
	var AssociationData NGapAssociationTable[256];
	/* pending expected NGAP Association (UE oriented) */
	var ExpectData NGapExpectTable[256];
	/* pending expected NGAP PDU */
	var ExpectDataProc NGapExpectTableProc[256];
	/* procedure based port to register for incoming connections */
	port NGAPEM_PROC_PT NGAP_PROC;
	/* test port for unit data messages */
	port NGAP_PT NGAP_UNIT;

	var NGAP_conn_parameters g_pars;
	var charstring g_ngap_id;
	var integer g_ngap_conn_id := -1;
}

type function NGAPCreateCallback(NGAP_PDU msg,
				template (omit) AMF_UE_NGAP_ID amf_id,
				template (omit) RAN_UE_NGAP_ID ran_id,
				charstring id)
runs on NGAP_Emulation_CT return NGAP_ConnHdlr;

type function NGAPUnitdataCallback(NGAP_PDU msg)
runs on NGAP_Emulation_CT return template NGAP_PDU;

type record NGAPOps {
	NGAPCreateCallback create_cb,
	NGAPUnitdataCallback unitdata_cb
}

type record NGAP_conn_parameters {
	HostName remote_ip,
	IPL4asp_Types.PortNumber remote_sctp_port,
	HostName local_ip,
	IPL4asp_Types.PortNumber local_sctp_port,
	NG_NAS_Role role
}

function tr_NGAP_RecvFrom_R(template NGAP_PDU msg)
runs on NGAP_Emulation_CT return template NGAP_RecvFrom {
	var template NGAP_RecvFrom mrf := {
		connId := g_ngap_conn_id,
		remName := ?,
		remPort := ?,
		locName := ?,
		locPort := ?,
		msg := msg
	}
	return mrf;
}

private function f_ngap_ids_known(template (omit) AMF_UE_NGAP_ID amf_id,
				  template (omit) RAN_UE_NGAP_ID ran_id)
runs on NGAP_Emulation_CT return boolean {
	var integer i;
	//log("f_ngap_ids_known(",amf_id,", ",ran_id,")");
	for (i := 0; i < sizeof(NGapAssociationTable); i := i+1) {
		//log("tbl[",i,"]: amf=", NGapAssociationTable[i].amf_ue_ngap_id,
		//    ", ran=", NGapAssociationTable[i].ran_ue_ngap_id);
		/* skip empty records */
		if (NGapAssociationTable[i].amf_ue_ngap_id == omit and
		    NGapAssociationTable[i].ran_ue_ngap_id == omit) {
			//log("skipping empty ", i);
			continue;
		}
		if (NGapAssociationTable[i].amf_ue_ngap_id == omit) {
			//log("entry ", i, " has no AMF ID yet (ran=", NGapAssociationTable[i].ran_ue_ngap_id);
			/* Table doesn't yet know the AMF side ID, let's look-up only
			 * based on the eNB side ID */
			if (match(NGapAssociationTable[i].ran_ue_ngap_id, ran_id)) {
				/* update table with AMF side ID */
				NGapAssociationTable[i].amf_ue_ngap_id := valueof(amf_id);
				return true;
			}
		} else if (match(NGapAssociationTable[i].ran_ue_ngap_id, ran_id) and
			match(NGapAssociationTable[i].amf_ue_ngap_id, amf_id)) {
			return true;
		}
	}
	return false;
}
private function f_comp_known(NGAP_ConnHdlr client)
runs on NGAP_Emulation_CT return boolean {
	var integer i;
	for (i := 0; i < sizeof(NGapAssociationTable); i := i+1) {
		if (NGapAssociationTable[i].comp_ref == client) {
			return true;
		}
	}
	return false;
}

private function f_assoc_id_by_ngap_ids(template (omit) AMF_UE_NGAP_ID amf_id,
				        template (omit) RAN_UE_NGAP_ID ran_id)
runs on NGAP_Emulation_CT return integer {
	if (istemplatekind(ran_id, "omit") and istemplatekind(amf_id, "omit")) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Unexpected call f_assoc_id_by_ngap_ids(omit, omit)");
	}
	for (var integer i := 0; i < sizeof(NGapAssociationTable); i := i+1) {
		if (not isvalue(NGapAssociationTable[i].ran_ue_ngap_id)) {
			continue;
		}
		if (not istemplatekind(ran_id, "omit") and
		    not match(NGapAssociationTable[i].ran_ue_ngap_id, ran_id)) {
			continue;
		}
		/* ran_id matched, check amf_id now: */
		if (not istemplatekind(amf_id, "omit") and
		    not match(NGapAssociationTable[i].amf_ue_ngap_id, amf_id)) {
			continue;
		}
		return i;
	}
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
		log2str("NGAP Association Table not found by RAN-ID=", ran_id, " AMF-ID=", amf_id));
	return -1; /* make ttcn3 compiler happy */
}

private function f_assoc_id_by_comp(NGAP_ConnHdlr client)
runs on NGAP_Emulation_CT return integer {
	var integer i;
	for (i := 0; i < sizeof(NGapAssociationTable); i := i+1) {
		if (NGapAssociationTable[i].comp_ref == client) {
			return i;
		}
	}
	setverdict(fail, "NGAP Association Table not found by component ", client);
	mtc.stop;
}

private function f_assoc_by_comp(NGAP_ConnHdlr client)
runs on NGAP_Emulation_CT return AssociationData {
	var integer i := f_assoc_id_by_comp(client);
	return NGapAssociationTable[i];
}

private function f_ngap_id_table_add(NGAP_ConnHdlr comp_ref,
				     template (omit) AMF_UE_NGAP_ID amf_id,
				     RAN_UE_NGAP_ID ran_id)
runs on NGAP_Emulation_CT return integer {
	var integer i;
	for (i := 0; i < sizeof(NGapAssociationTable); i := i+1) {
		if (not isvalue(NGapAssociationTable[i].ran_ue_ngap_id)) {
			NGapAssociationTable[i].ran_ue_ngap_id := ran_id;
			if (istemplatekind(amf_id, "omit")) {
				NGapAssociationTable[i].amf_ue_ngap_id := omit;
			} else {
				NGapAssociationTable[i].amf_ue_ngap_id := valueof(amf_id);
			}
			NGapAssociationTable[i].comp_ref := comp_ref;
			return i;
		}
	}
	testcase.stop("NGAP Association Table full!");
	return -1;
}

private function f_ngap_id_table_del(NGAP_ConnHdlr comp_ref, RAN_UE_NGAP_ID ran_id)
runs on NGAP_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(NGapAssociationTable); i := i+1) {
		if (NGapAssociationTable[i].comp_ref == comp_ref and
		    NGapAssociationTable[i].ran_ue_ngap_id == ran_id) {
			NGapAssociationTable[i].ran_ue_ngap_id := omit;
			NGapAssociationTable[i].amf_ue_ngap_id := omit;
			NGapAssociationTable[i].comp_ref := null;
			return;
		}
	}
	setverdict(fail, "NGAP Association Table: Couldn't find to-be-deleted entry!");
	mtc.stop;
}


private function f_ngap_id_table_init()
runs on NGAP_Emulation_CT {
	for (var integer i := 0; i < sizeof(NGapAssociationTable); i := i+1) {
		NGapAssociationTable[i].amf_ue_ngap_id := omit;
		NGapAssociationTable[i].ran_ue_ngap_id := omit;
		NGapAssociationTable[i].uli := omit;
		NGapAssociationTable[i].nus := valueof(t_NG_NAS_UE_State(g_pars.role));
		NGapAssociationTable[i].comp_ref := null;
	}
}

private function f_ngap_xceive(template (value) NGAP_PDU tx,
				template NGAP_PDU rx_t := ?)
runs on NGAP_Emulation_CT  return NGAP_PDU {
	timer T := 10.0;
	var NGAP_RecvFrom mrf;

	NGAP.send(t_NGAP_Send(g_ngap_conn_id, tx));
	alt {
	[] NGAP.receive(tr_NGAP_RecvFrom_R(rx_t)) -> value mrf { }
	[] NGAP.receive(tr_SctpAssocChange) { repeat; }
	[] NGAP.receive(tr_SctpPeerAddrChange)  { repeat; }
	[] T.timeout {
		setverdict(fail, "Timeout waiting for ", rx_t);
		mtc.stop;
		}
	}
	return mrf.msg;
}

function handle_NGAP_UeContextReleaseCmd(template (present) NGAP_PDU rel_cmd) runs on NGAP_Emulation_CT {
	if (ispresent(rel_cmd.initiatingMessage.value_.uEContextReleaseCommand.protocolIEs[0].value_.uE_NGAP_IDs.uE_NGAP_ID_pair)) {
		var UE_NGAP_ID_pair ids := valueof(rel_cmd.initiatingMessage.value_.uEContextReleaseCommand.protocolIEs[0].value_.uE_NGAP_IDs.uE_NGAP_ID_pair);
		var template AMF_UE_NGAP_ID amf_ue_id;
		var template RAN_UE_NGAP_ID ran_ue_id;
		var integer assoc_id;
		var NGAP_ConnHdlr vc_conn

		amf_ue_id := ids.aMF_UE_NGAP_ID;
		ran_ue_id := ids.rAN_UE_NGAP_ID;

		assoc_id := f_assoc_id_by_ngap_ids(amf_ue_id, ran_ue_id);
		vc_conn := NGapAssociationTable[assoc_id].comp_ref;

		f_ngap_id_table_del(vc_conn, valueof(ran_ue_id));
	} else {
		/* TODO: The UE CONTEXT RELEASE COMMAND (see also: 3GPP TS 36.413, section 9.1.4.6), may identify the
		 * context by either an uE_NGAP_ID_pair (AMF_UE_NGAP_ID and RAN_UE_NGAP_ID) or an AMF_UE_NGAP_ID alone.
		 * The latter case is not implemented here yet. */
		setverdict(fail, "complete implementation of UeContextReleaseCmd handling");
		mtc.stop;
	}
}

private function SendToNGapExpectTableProc(NGAP_PDU msg) runs on NGAP_Emulation_CT {
	var integer procedureCode;
	var NGAP_ConnHdlr vc_conn;

	if (ispresent(msg.initiatingMessage.procedureCode)) {
		procedureCode := msg.initiatingMessage.procedureCode;
	} else if (ispresent(msg.unsuccessfulOutcome.procedureCode)) {
		procedureCode := msg.unsuccessfulOutcome.procedureCode;
	} else if (ispresent(msg.successfulOutcome.procedureCode)) {
		procedureCode := msg.successfulOutcome.procedureCode;
	} else {
		return;
	}

	for (var integer i := 0; i < sizeof(NGapExpectTableProc); i := i+1) {
		if (NGapExpectTableProc[i].procedureCode == procedureCode) {
			vc_conn := NGapExpectTableProc[i].vc_conn;
			if (vc_conn != null) {
				NGAP_CLIENT.send(msg) to vc_conn;
			}
		}
	}

	return;
}

template ProcedureCode tr_NGAP_ProcedureCode_non_UErelated :=
(id_NGSetup, id_RANConfigurationUpdate, id_AMFStatusIndication, id_NGReset, id_ErrorIndication, id_OverloadStart, id_OverloadStop);
template (present) NGAP_PDU
tr_NGAP_nonUErelated :=	(mw_ngap_initMsg({procedureCode := tr_NGAP_ProcedureCode_non_UErelated,
					  criticality   := ?,
					  value_        := ?}),
			mw_ngap_succMsg({procedureCode := tr_NGAP_ProcedureCode_non_UErelated,
					 criticality   := ?,
					 value_        := ?}),
			mw_ngap_unsuccMsg({procedureCode := tr_NGAP_ProcedureCode_non_UErelated,
					   criticality   := ?,
					   value_        := ?}));
template (present) NGAP_PDU
tr_NGAP_DownlinkNASTransport := mw_ngap_initMsg({procedureCode := id_DownlinkNASTransport,
						 criticality   := ?,
						 value_        := { DownlinkNASTransport := ? }
						});

function main(NGAPOps ops, NGAP_conn_parameters p, charstring id) runs on NGAP_Emulation_CT {
	var Result res;
	g_pars := p;
	g_ngap_id := id;
	f_ngap_id_table_init();
	f_expect_table_init();

	map(self:NGAP, system:NGAP_CODEC_PT);
	if (p.remote_sctp_port == -1) {
		res := NGAP_CodecPort_CtrlFunct.f_IPL4_listen(NGAP, p.local_ip, p.local_sctp_port,
							      { sctp := valueof(ts_SctpTuple(60)) });
	} else {
		res := NGAP_CodecPort_CtrlFunct.f_IPL4_connect(NGAP, p.remote_ip, p.remote_sctp_port,
							       p.local_ip, p.local_sctp_port, -1,
							       { sctp := valueof(ts_SctpTuple(60)) });
	}
	if (not ispresent(res.connId)) {
		setverdict(fail, "Could not connect NGAP socket, check your configuration");
		mtc.stop;
	}
	g_ngap_conn_id := res.connId;

	/* notify user about SCTP establishment */
	if (p.remote_sctp_port != -1) {
		NGAP_UNIT.send(NGAPEM_Event:{up_down:=NGAPEM_EVENT_UP})
	}

	while (true) {
		var NGAP_ConnHdlr vc_conn;
		var NG_NAS_UL_Message_Type ul_nas_msg;
		var AMF_UE_NGAP_ID amf_id;
		var RAN_UE_NGAP_ID ran_id;
		var integer procedureCode;
		var NGAP_RecvFrom mrf;
		var NGAP_PDU msg;
		var NGAPEM_Config ngcfg;
		var charstring vlr_name, amf_name;
		var integer ai;
		var octetstring kasme;

		alt {
		/* Configuration primitive from client */
		[] NGAP_CLIENT.receive(NGAPEM_Config:{set_nas_keys:=?}) -> value ngcfg sender vc_conn {
			var integer assoc_id := f_assoc_id_by_comp(vc_conn);
			NGapAssociationTable[assoc_id].nus.k_nas_int := ngcfg.set_nas_keys.k_nas_int;
			NGapAssociationTable[assoc_id].nus.k_nas_enc := ngcfg.set_nas_keys.k_nas_enc;
			}
		/* Configuration primitive from client */
		[] NGAP_CLIENT.receive(NGAPEM_Config:{reset_nas_counts:=?}) -> value ngcfg sender vc_conn {
			var integer assoc_id := f_assoc_id_by_comp(vc_conn);
			NGapAssociationTable[assoc_id].nus.rx_count := 0;
			NGapAssociationTable[assoc_id].nus.tx_count := 0;
			}
		/* Configuration primitive from client */
		[] NGAP_CLIENT.receive(NGAPEM_Config:{set_nas_alg_int:=?}) -> value ngcfg sender vc_conn {
			var integer assoc_id := f_assoc_id_by_comp(vc_conn);
			NGapAssociationTable[assoc_id].nus.alg_int := ngcfg.set_nas_alg_int;
			/* Mark ciphering even if using NIA0: */
			NGapAssociationTable[assoc_id].nus.use_int := true;
			}
		/* Configuration primitive from client */
		[] NGAP_CLIENT.receive(NGAPEM_Config:{set_nas_alg_enc:=?}) -> value ngcfg sender vc_conn {
			var integer assoc_id := f_assoc_id_by_comp(vc_conn);
			NGapAssociationTable[assoc_id].nus.alg_enc := ngcfg.set_nas_alg_enc;
			/* Mark ciphering even if using NEA0: */
			NGapAssociationTable[assoc_id].nus.use_enc := true;
			}
		/* NGAP from client: InitialUE */
		[] NGAP_CLIENT.receive(mw_ngap_initMsg(mw_n2_initialUeMessage)) -> value msg sender vc_conn {
			/* create a table entry about this connection */
			ai := f_ngap_id_table_add(vc_conn, omit, valueof(f_NGAP_get_RAN_UE_NGAP_ID(msg)));
			/* Store ULI so we can use it for generating UlNasTransport from NAS */
			NGapAssociationTable[ai].uli := msg.initiatingMessage.value_.InitialUEMessage.protocolIEs[2].value_.userLocationInformation;
			/* Pass message through */
			NGAP.send(t_NGAP_Send(g_ngap_conn_id, msg));
			}
		/* NAS from client: Wrap in NGAP Uplink NAS Transport */
		[] NGAP_CLIENT.receive(NG_NAS_UL_Message_Type:?) -> value ul_nas_msg sender vc_conn {
			var integer assoc_id := f_assoc_id_by_comp(vc_conn);
			var AssociationData ad := NGapAssociationTable[assoc_id];
			ul_nas_msg := f_NG_NAS_encaps_ul(NGapAssociationTable[assoc_id].nus, ul_nas_msg);
			var NAS_PDU nas_enc := enc_NG_NAS_UL_Message_Type(ul_nas_msg);
			NGAP.send(t_NGAP_Send(g_ngap_conn_id,
					      m_ngap_initMsg(
						m_n2_UplinkNASTransport(ad.amf_ue_ngap_id,
									ad.ran_ue_ngap_id,
									nas_enc,
									ad.uli))));
			}
		/* NGAP from client: pass on transparently */
		[] NGAP_CLIENT.receive(NGAP_PDU:?) -> value msg sender vc_conn {
			/* Pass message through */
			/* FIXME: validate NGAP_IDs ? */
			NGAP.send(t_NGAP_Send(g_ngap_conn_id, msg));
			}

		/* non-UE related NGAP: pass through unmodified/unverified */
		[] NGAP_UNIT.receive(NGAP_PDU:?) -> value msg sender vc_conn {
			/* Pass message through */
			NGAP.send(t_NGAP_Send(g_ngap_conn_id, msg));
			}

		/* NGAP received from peer (AMF) */
		[] NGAP.receive(tr_NGAP_RecvFrom_R(?)) -> value mrf {
			if (match(mrf.msg, tr_NGAP_nonUErelated)) {
				/* non-UE-related NGAP message */
				SendToNGapExpectTableProc(mrf.msg);
				var template NGAP_PDU resp := ops.unitdata_cb.apply(mrf.msg);
				if (isvalue(resp)) {
					NGAP.send(t_NGAP_Send(g_ngap_conn_id, valueof(resp)));
				}
			} else {
				/* Ue-related NGAP message */
				/* obtain AMF + NGRAN UE NGAP ID */
				var template (omit) AMF_UE_NGAP_ID amf_ue_id := f_NGAP_get_AMF_UE_NGAP_ID(mrf.msg);
				var template (omit) RAN_UE_NGAP_ID ran_ue_id := f_NGAP_get_RAN_UE_NGAP_ID(mrf.msg);
				/* check if those IDs are known in our table */
				if (f_ngap_ids_known(amf_ue_id, ran_ue_id)) {
					/* if yes, dispatch to the ConnHdlr for this Ue-Connection */
					var template (omit) octetstring nas_enc;
					var integer assoc_id := f_assoc_id_by_ngap_ids(amf_ue_id, ran_ue_id);
					vc_conn := NGapAssociationTable[assoc_id].comp_ref;
					nas_enc := f_NGAP_get_NAS_PDU(mrf.msg);
					if (isvalue(nas_enc)) {
						if (g_pars.role == NG_NAS_ROLE_UE) {
							var NG_NAS_DL_Message_Type dl_nas := dec_NG_NAS_DL_Message_Type(valueof(nas_enc));
							if (match(dl_nas, cr_NG_SECURITY_PROTECTED_NAS_MESSAGE)) {
								dl_nas := f_NG_NAS_try_decaps_dl(NGapAssociationTable[assoc_id].nus, dl_nas);
							}
							/* DL/UlNasTransport are not interesting, don't send them */
							if (not match(mrf.msg, tr_NGAP_DownlinkNASTransport)) {
								/* send raw NGAP */
								NGAP_CLIENT.send(mrf.msg) to vc_conn;
							}
							/* send decoded NAS */
							NGAP_CLIENT.send(dl_nas) to vc_conn;
						} else {
							log("Incoming NAS HANDLING in AMF role not supported!");
							var NG_NAS_UL_Message_Type ul_nas := dec_NG_NAS_UL_Message_Type(valueof(nas_enc));
						}
					} else {
						/* send raw NGAP */
						NGAP_CLIENT.send(mrf.msg) to vc_conn;
					}
				} else {
					/* if not, call create_cb so it can create new ConnHdlr */
					vc_conn := ops.create_cb.apply(mrf.msg, amf_ue_id, ran_ue_id, id);
					f_ngap_id_table_add(vc_conn, amf_ue_id, valueof(ran_ue_id));
					NGAP_CLIENT.send(mrf.msg) to vc_conn;
				}
				if (match(mrf.msg, mw_ngap_initMsg(mw_n2_UEContextReleaseCommand))) {
					handle_NGAP_UeContextReleaseCmd(mrf.msg);
				}
			}
		}
		[] NGAP.receive(tr_SctpAssocChange) { }
		[] NGAP.receive(tr_SctpPeerAddrChange)  { }
		[] NGAP_PROC.getcall(NGAPEM_register:{?,?,?}) -> param(amf_id, ran_id, vc_conn) {
			f_create_expect(amf_id, ran_id, vc_conn);
			NGAP_PROC.reply(NGAPEM_register:{amf_id, ran_id, vc_conn}) to vc_conn;
			}
		[] NGAP_PROC.getcall(NGAPEM_register_proc:{?,?}) -> param(procedureCode, vc_conn) {
			f_create_expect_proc(procedureCode, vc_conn);
			NGAP_PROC.reply(NGAPEM_register_proc:{procedureCode, vc_conn}) to vc_conn;
			}
		[] NGAP_PROC.getcall(NGAPEM_obtain_amf_id:{}) -> sender vc_conn {
			var integer i := f_assoc_id_by_comp(vc_conn);
			amf_id := NGapAssociationTable[i].amf_ue_ngap_id;
			NGAP_PROC.reply(NGAPEM_obtain_amf_id:{} value amf_id) to vc_conn;
			}
//		[] NGAP_PROC.getcall(NGAPEM_derive_nas_token:{?, ?, -}) -> param(kasme, vc_conn) {
//			var integer assoc_id := f_assoc_id_by_comp(vc_conn);
//			var OCT32 nas_token := f_kdf_nas_token(kasme, NGapAssociationTable[assoc_id].nus.tx_count)
//			NGapAssociationTable[assoc_id].nus.tx_count := NGapAssociationTable[assoc_id].nus.tx_count + 1;
//			NGAP_PROC.reply(NGAPEM_derive_nas_token:{kasme, vc_conn, nas_token}) to vc_conn;
//			}
		}
	}
}


private function f_create_expect(template (omit) AMF_UE_NGAP_ID amf_id,
				 template (omit) RAN_UE_NGAP_ID ran_id,
				 NGAP_ConnHdlr hdlr)
runs on NGAP_Emulation_CT {
	var integer i;

	/* Check an entry like this is not already presnt */
	for (i := 0; i < sizeof(NGapExpectTable); i := i+1) {
		if (not ispresent(NGapExpectTable[i].amf_id) and not ispresent(NGapExpectTable[i].ran_id)) {
			continue;
		}
		if (valueof(amf_id) == NGapExpectTable[i].amf_id and valueof(ran_id) == NGapExpectTable[i].ran_id) {
			setverdict(fail, "UE AMF id / UE RAN id pair already present", amf_id, ran_id);
			mtc.stop;
		}
	}
	for (i := 0; i < sizeof(NGapExpectTable); i := i+1) {
		if (not ispresent(NGapExpectTable[i].amf_id) and not ispresent(NGapExpectTable[i].ran_id)) {
			NGapExpectTable[i].amf_id := valueof(amf_id);
			NGapExpectTable[i].ran_id := valueof(ran_id);
			NGapExpectTable[i].vc_conn := hdlr;
			log("Created Expect[", i, "] for UE AMF id:", amf_id, ", UE RAN id:", ran_id, " to be handled at ", hdlr);
			return;
		}
	}
	testcase.stop("No space left in NGapExpectTable")
}

/* client/conn_hdlr side function to use procedure port to create expect in emulation */
function f_create_ngap_expect(template (omit) AMF_UE_NGAP_ID amf_id,
			      template (omit) RAN_UE_NGAP_ID ran_id) runs on NGAP_ConnHdlr {
	NGAP_PROC.call(NGAPEM_register:{amf_id, ran_id, self}) {
		[] NGAP_PROC.getreply(NGAPEM_register:{?,?,?}) {};
	}
}

private function f_create_expect_proc(integer procedureCode,NGAP_ConnHdlr hdlr) runs on NGAP_Emulation_CT {
	var integer i;

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

/* client/conn_hdlr side function to use procedure port to create expect (PDU) in emulation */
function f_create_ngap_expect_proc(integer procedureCode, NGAP_ConnHdlr hdlr) runs on NGAP_ConnHdlr
{
	NGAP_PROC.call(NGAPEM_register_proc:{procedureCode, self}) {
		[] NGAP_PROC.getreply(NGAPEM_register_proc:{?,?}) {};
	}

	log(procedureCode);
}

private function f_expect_table_init()
runs on NGAP_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(NGapExpectTable); i := i + 1) {
		NGapExpectTable[i].amf_id := omit;
		NGapExpectTable[i].ran_id := omit;
		NGapExpectTable[i].vc_conn := null;
	}

	for (i := 0; i < sizeof(NGapExpectTableProc); i := i + 1) {
		NGapExpectTableProc[i].procedureCode := omit;
		NGapExpectTableProc[i].vc_conn := null;
	}
}

/* ConnHdlr function to obtain AMF_ID internally stored in association table.
 * This is needed because we don't receive NGAP layer of DownlinkNasTransport in
 * ConnHdlr from Emulation, so there's no direct way to obtain it early during
 * registration procedure. */
function f_ngap_obtain_amf_id() runs on NGAP_ConnHdlr return AMF_UE_NGAP_ID {
	var AMF_UE_NGAP_ID amf_id;
	NGAP_PROC.call(NGAPEM_obtain_amf_id:{}) {
		[] NGAP_PROC.getreply(NGAPEM_obtain_amf_id:{}) -> value amf_id {
			return amf_id;
		}
	}
}

}
