/* PFCP Emulation in TTCN-3
 *
 * (C) 2022 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
 */

module PFCP_Emulation {

import from IPL4asp_Types all;
import from General_Types all;
import from Osmocom_Types all;
import from PFCP_Types all;
import from PFCP_CodecPort all;
import from PFCP_CodecPort_CtrlFunct all;

/***********************************************************************
 * Main Emulation Component
 ***********************************************************************/

const integer PFCP_PORT := 8805;

type enumerated PFCP_Role {
	CPF,
	UPF
};

type record PFCP_Emulation_Cfg {
	HostName pfcp_bind_ip,
	PortNumber pfcp_bind_port,
	HostName pfcp_remote_ip,
	PortNumber pfcp_remote_port,
	PFCP_Role role
};

type component PFCP_Emulation_CT {
	/* Communication with underlying PFCP CodecPort */
	port PFCP_PT PFCP;

	/* Communication with Clients */
	port PFCPEM_PT CLIENT;
	port PFCPEM_PROC_PT CLIENT_PROC;

	/* Configuration by the user */
	var PFCP_Emulation_Cfg g_pfcp_cfg;

	/* State */
	var integer g_pfcp_conn_id;
	var integer g_recovery_timestamp;

	var PFCPEM_conns g_conns;

	var integer g_next_sequence_nr_state;
};

private function f_PFCPEM_next_sequence_nr() runs on PFCP_Emulation_CT return integer {
	g_next_sequence_nr_state := g_next_sequence_nr_state + 1;
	if (g_next_sequence_nr_state > 16777215) {
		g_next_sequence_nr_state := 1;
	}
	return g_next_sequence_nr_state;
}

type record PFCPEM_conn {
	PFCP_ConnHdlr vc_conn,
	OCT8 seid optional,
	LIN3_BO_LAST pfcp_msg_sequence_number optional
};

type record of PFCPEM_conn PFCPEM_conns;

private function f_PFCPEM_conn_by_seid_or_seqnr(OCT8 seid, LIN3_BO_LAST seqnr) runs on PFCP_Emulation_CT return PFCP_ConnHdlr {
	log("looking for seid ", seid, " seqnr ", seqnr, " in conns ", g_conns);
	for (var integer i := 0; i < lengthof(g_conns); i := i + 1) {
		if (isbound(g_conns[i].pfcp_msg_sequence_number)
		    and seqnr == g_conns[i].pfcp_msg_sequence_number) {
			return g_conns[i].vc_conn;
		}
		if (isbound(g_conns[i].seid)
		    and seid == g_conns[i].seid) {
			return g_conns[i].vc_conn;
		}
	}
	return null;
};

private function f_PFCPEM_add_conn(PFCP_ConnHdlr vc_conn) runs on PFCP_Emulation_CT {
	for (var integer i := 0; i < lengthof(g_conns); i := i + 1) {
		if (g_conns[i].vc_conn == vc_conn) {
			return;
		}
	}
	/* Not in the list yet, add. */
	var PFCPEM_conn conn := { vc_conn := vc_conn };
	g_conns := g_conns & { conn };
}

private function f_init(PFCP_Emulation_Cfg cfg) runs on PFCP_Emulation_CT {
	var Result res;

	map(self:PFCP, system:PFCP);
	res := PFCP_CodecPort_CtrlFunct.f_IPL4_listen(PFCP, cfg.pfcp_bind_ip, cfg.pfcp_bind_port, {udp:={}});
	g_pfcp_conn_id := res.connId;

	g_recovery_timestamp := f_rnd_int(4294967296);
	g_pfcp_cfg := cfg;

	g_conns := {};

	g_next_sequence_nr_state := (1 + f_rnd_int(1000)) * 10000;
}

function main(PFCP_Emulation_Cfg cfg) runs on PFCP_Emulation_CT {
	var PFCP_ConnHdlr vc_conn;
	var PFCP_Unitdata ud;
	var PDU_PFCP pdu;

	f_init(cfg);

	while (true) {
		alt {
		[] PFCP.receive(PFCP_Unitdata:?) -> value ud {
				log("PFCP_Emulation main() PFCP.receive: ", ud);
				vc_conn := null;
				if (ud.pdu.s_flag == '1'B) {
					/* There is a SEID */
					vc_conn := f_PFCPEM_conn_by_seid_or_seqnr(ud.pdu.seid, ud.pdu.sequence_number);
				}
				if (vc_conn != null) {
					log("found destination ", vc_conn);
					CLIENT.send(ud.pdu) to vc_conn;
				} else {
					log("sending to all conns: ", g_conns);
					for (var integer i := 0; i < lengthof(g_conns); i := i + 1) {
						CLIENT.send(ud.pdu) to g_conns[i].vc_conn;
					}
				}
			}

		[] CLIENT.receive(PDU_PFCP:?) -> value pdu sender vc_conn {
				log("PFCP_Emulation main() CLIENT.receive from ", vc_conn, ": ", pdu);
				if (pdu.sequence_number == 0) {
					pdu.sequence_number := f_PFCPEM_next_sequence_nr();
				}
				ud := {
					peer := {
						conn_id := g_pfcp_conn_id,
						remote_name := g_pfcp_cfg.pfcp_remote_ip,
						remote_port := g_pfcp_cfg.pfcp_remote_port
					},
					pdu := pdu
				};

				f_PFCPEM_add_conn(vc_conn);

				PFCP.send(ud);
			}

		[] CLIENT_PROC.getcall(PFCPEM_register:{}) -> sender vc_conn {
				log("PFCP_Emulation main() CLIENT_PROC.getcall(PFCPEM_register)");
				f_PFCPEM_add_conn(vc_conn);
				CLIENT_PROC.reply(PFCPEM_register:{}) to vc_conn;
			}
		}
	}
}


/***********************************************************************
 * Interaction between Main and Client Components
 ***********************************************************************/
type port PFCPEM_PT message {
	inout PDU_PFCP;
} with { extension "internal" };

signature PFCPEM_register();

type port PFCPEM_PROC_PT procedure {
	inout PFCPEM_register;
} with { extension "internal" };

/***********************************************************************
 * Client Compoennt
 ***********************************************************************/

type component PFCP_ConnHdlr {
	port PFCPEM_PT PFCP;
	port PFCPEM_PROC_PT PFCP_PROC;
	var PFCP_Emulation_CT vc_PFCP;
};

function f_pfcp_register() runs on PFCP_ConnHdlr {
	PFCP_PROC.call(PFCPEM_register:{}) {
		[] PFCP_PROC.getreply(PFCPEM_register:{});
	}
}

}
