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

import from PFCP_Types all;
import from PFCP_Templates 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;

	/* List of PFCP_ConnHdlr subscribed for broadcast */
	var PFCPEM_conns g_conns_bcast;
	/* List of PFCP_ConnHdlr subscribed for SEID */
	var PFCPEM_conns g_conns_seqnr;
	/* List of PFCP_ConnHdlr subscribed for SeqNr */
	var PFCPEM_conns g_conns_seid;

	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 seqnr optional
};

type record of PFCPEM_conn PFCPEM_conns;

private function f_PFCPEM_conn_by_seqnr(LIN3_BO_LAST seqnr)
runs on PFCP_Emulation_CT return PFCP_ConnHdlr {
	for (var integer i := 0; i < lengthof(g_conns_seqnr); i := i + 1) {
		if (seqnr == g_conns_seqnr[i].seqnr) {
			return g_conns_seqnr[i].vc_conn;
		}
	}
	return null;
};

private function f_PFCPEM_conn_by_seid(OCT8 seid)
runs on PFCP_Emulation_CT return PFCP_ConnHdlr {
	for (var integer i := 0; i < lengthof(g_conns_seid); i := i + 1) {
		if (seid == g_conns_seid[i].seid) {
			return g_conns_seid[i].vc_conn;
		}
	}
	return null;
};

private function f_PFCPEM_conn_for_pdu(in PDU_PFCP pdu)
runs on PFCP_Emulation_CT return PFCP_ConnHdlr {
	var PFCP_ConnHdlr vc_conn := null;

	vc_conn := f_PFCPEM_conn_by_seqnr(pdu.sequence_number);
	if (vc_conn != null) {
		f_PFCPEM_conns_seqnr_del(pdu.sequence_number);
		return vc_conn;
	}

	/* If there is a SEID, we can look it up */
	if (pdu.s_flag == '1'B) {
		vc_conn := f_PFCPEM_conn_by_seid(pdu.seid);
	}

	return vc_conn;
};

/* subscribe/unsubscribe for/from broadcast PDUs */
private function f_PFCPEM_conns_bcast_add(PFCP_ConnHdlr vc_conn)
runs on PFCP_Emulation_CT return boolean {
	for (var integer i := 0; i < lengthof(g_conns_bcast); i := i + 1) {
		if (g_conns_bcast[i].vc_conn == vc_conn) {
			log(__SCOPE__, "(): vc_conn ", vc_conn, " is already in the list");
			return false;
		}
	}
	g_conns_bcast := g_conns_bcast & {{vc_conn, omit, omit}};
	log(__SCOPE__, "(): vc_conn ", vc_conn, " subscribed for broadcast");
	return true;
}
private function f_PFCPEM_conns_bcast_del(PFCP_ConnHdlr vc_conn)
runs on PFCP_Emulation_CT return boolean {
	var PFCPEM_conns conns := { };

	for (var integer i := 0; i < lengthof(g_conns_bcast); i := i + 1) {
		if (g_conns_bcast[i].vc_conn != vc_conn) {
			conns := conns & { g_conns_bcast[i] };
		}
	}
	if (lengthof(conns) == lengthof(g_conns_bcast)) {
		log(__SCOPE__, "(): vc_conn ", vc_conn, " was not in the list");
		return false;
	}
	log(__SCOPE__, "(): vc_conn ", vc_conn, " unsubscribed from broadcast");
	g_conns_bcast := conns;
	return true;
}

/* subscribe/unsubscribe for/from PDUs with the given SeqNr value */
private function f_PFCPEM_conns_seqnr_add(PFCP_ConnHdlr vc_conn,
					  LIN3_BO_LAST seqnr)
runs on PFCP_Emulation_CT return boolean {
	for (var integer i := 0; i < lengthof(g_conns_seqnr); i := i + 1) {
		if (g_conns_seqnr[i].seqnr == seqnr) {
			log(__SCOPE__, "(): SeqNr ", seqnr, " is already claimed",
			    " by vc_conn ", g_conns_seqnr[i].vc_conn);
			return false;
		}
	}
	g_conns_seqnr := g_conns_seqnr & { {vc_conn, omit, seqnr} };
	return true;
}
private function f_PFCPEM_conns_seqnr_del(LIN3_BO_LAST seqnr)
runs on PFCP_Emulation_CT return boolean {
	var PFCPEM_conns conns := { };

	for (var integer i := 0; i < lengthof(g_conns_seqnr); i := i + 1) {
		if (g_conns_seqnr[i].seqnr != seqnr) {
			conns := conns & { g_conns_seqnr[i] };
		}
	}
	if (lengthof(conns) == lengthof(g_conns_seqnr)) {
		log(__SCOPE__, "(): SeqNr ", seqnr, " was not in the list");
		return false;
	}
	g_conns_seqnr := conns;
	return true;
}

/* subscribe/unsubscribe for/from PDUs with the given SEID value */
private function f_PFCPEM_conns_seid_add(PFCP_ConnHdlr vc_conn,
					 OCT8 seid)
runs on PFCP_Emulation_CT return boolean {
	for (var integer i := 0; i < lengthof(g_conns_seid); i := i + 1) {
		if (g_conns_seid[i].seid == seid) {
			log(__SCOPE__, "(): SEID ", seid, " is already claimed",
			    " by vc_conn ", g_conns_seid[i].vc_conn);
			return false;
		}
	}
	g_conns_seid := g_conns_seid & { {vc_conn, seid, omit} };
	return true;
}
private function f_PFCPEM_conns_seid_del(OCT8 seid)
runs on PFCP_Emulation_CT return boolean {
	var PFCPEM_conns conns := { };

	for (var integer i := 0; i < lengthof(g_conns_seid); i := i + 1) {
		if (g_conns_seid[i].seid != seid) {
			conns := conns & { g_conns_seid[i] };
		}
	}
	if (lengthof(conns) == lengthof(g_conns_seid)) {
		log(__SCOPE__, "(): SEID ", seid, " was not in the list");
		return false;
	}
	g_conns_seid := conns;
	return true;
}

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_bcast := { };
	g_conns_seqnr := { };
	g_conns_seid := { };

	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;
	var OCT8 seid;

	f_init(cfg);

	while (true) {
		alt {
		[] PFCP.receive(tr_PFCP_UD(tr_PFCP_Heartbeat_Req)) -> value ud {
				log("Rx Heartbeat Req: ", ud.pdu);
				pdu := valueof(ts_PFCP_Heartbeat_Resp(g_recovery_timestamp));
				pdu.sequence_number := ud.pdu.sequence_number;
				ud.pdu := pdu;
				PFCP.send(ud);
			}

		[] PFCP.receive(tr_PFCP_UD(?)) -> value ud {
				log("PFCP_Emulation main() PFCP.receive: ", ud);
				vc_conn := f_PFCPEM_conn_for_pdu(ud.pdu);
				if (vc_conn != null) {
					log("found destination ", vc_conn);
					CLIENT.send(ud.pdu) to vc_conn;
				} else {
					for (var integer i := 0; i < lengthof(g_conns_bcast); i := i + 1) {
						log("broadcasting to ", g_conns_bcast[i].vc_conn);
						CLIENT.send(ud.pdu) to g_conns_bcast[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
				};

				/* route the response back to this vc_conn */
				f_PFCPEM_conns_seqnr_add(vc_conn, pdu.sequence_number);

				PFCP.send(ud);
			}

		/* subscribe/unsubscribe for/from broadcast PDUs */
		[] CLIENT_PROC.getcall(PFCPEM_subscribe_bcast:{?}) -> sender vc_conn {
				var boolean res := f_PFCPEM_conns_bcast_add(vc_conn);
				CLIENT_PROC.reply(PFCPEM_subscribe_bcast:{res}) to vc_conn;
			}
		[] CLIENT_PROC.getcall(PFCPEM_unsubscribe_bcast:{?}) -> sender vc_conn {
				var boolean res := f_PFCPEM_conns_bcast_del(vc_conn);
				CLIENT_PROC.reply(PFCPEM_unsubscribe_bcast:{res}) to vc_conn;
			}

		/* subscribe/unsubscribe for/from PDUs with the given SEID value */
		[] CLIENT_PROC.getcall(PFCPEM_subscribe_seid:{?, ?}) -> param(seid := seid) sender vc_conn {
				var boolean res := f_PFCPEM_conns_seid_add(vc_conn, seid);
				CLIENT_PROC.reply(PFCPEM_subscribe_seid:{seid, res}) to vc_conn;
			}
		[] CLIENT_PROC.getcall(PFCPEM_unsubscribe_seid:{?, ?}) -> param(seid := seid) sender vc_conn {
				var boolean res := f_PFCPEM_conns_seid_del(seid);
				CLIENT_PROC.reply(PFCPEM_unsubscribe_seid:{seid, res}) to vc_conn;
			}

		[] CLIENT_PROC.getcall(PFCPEM_get_recovery_timestamp:{?}) -> sender vc_conn {
				CLIENT_PROC.reply(PFCPEM_get_recovery_timestamp:{g_recovery_timestamp}) to vc_conn;
			}
		}
	}
}


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

signature PFCPEM_subscribe_bcast(out boolean success);
signature PFCPEM_unsubscribe_bcast(out boolean success);
signature PFCPEM_subscribe_seid(in OCT8 seid, out boolean success);
signature PFCPEM_unsubscribe_seid(in OCT8 seid, out boolean success);
signature PFCPEM_get_recovery_timestamp(out integer rts);

type port PFCPEM_PROC_PT procedure {
	inout PFCPEM_subscribe_bcast;
	inout PFCPEM_unsubscribe_bcast;
	inout PFCPEM_subscribe_seid;
	inout PFCPEM_unsubscribe_seid;
	inout PFCPEM_get_recovery_timestamp;
} with { extension "internal" };

/***********************************************************************
 * Client Component
 ***********************************************************************/

type component PFCP_ConnHdlr {
	port PFCPEM_PT PFCP;
	port PFCPEM_PROC_PT PFCP_PROC;
};

/* subscribe/unsubscribe for/from broadcast PDUs */
function f_PFCPEM_subscribe_bcast() runs on PFCP_ConnHdlr {
	PFCP_PROC.call(PFCPEM_subscribe_bcast:{-}) {
		[] PFCP_PROC.getreply(PFCPEM_subscribe_bcast:{true});
		[] PFCP_PROC.getreply(PFCPEM_subscribe_bcast:{false}) {
			setverdict(fail, __SCOPE__, "() failed");
			Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
		}
	}
}
function f_PFCPEM_unsubscribe_bcast() runs on PFCP_ConnHdlr {
	PFCP_PROC.call(PFCPEM_unsubscribe_bcast:{-}) {
		[] PFCP_PROC.getreply(PFCPEM_unsubscribe_bcast:{true});
		[] PFCP_PROC.getreply(PFCPEM_unsubscribe_bcast:{false}) {
			setverdict(fail, __SCOPE__, "() failed");
			Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
		}
	}
}

/* subscribe/unsubscribe for/from PDUs with the given SEID value */
function f_PFCPEM_subscribe_seid(in OCT8 seid) runs on PFCP_ConnHdlr {
	PFCP_PROC.call(PFCPEM_subscribe_seid:{seid, -}) {
		[] PFCP_PROC.getreply(PFCPEM_subscribe_seid:{seid, true});
		[] PFCP_PROC.getreply(PFCPEM_subscribe_seid:{seid, false}) {
			setverdict(fail, __SCOPE__, "() failed");
			Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
		}
	}
}
function f_PFCPEM_unsubscribe_seid(in OCT8 seid) runs on PFCP_ConnHdlr {
	PFCP_PROC.call(PFCPEM_unsubscribe_seid:{seid, -}) {
		[] PFCP_PROC.getreply(PFCPEM_unsubscribe_seid:{seid, true});
		[] PFCP_PROC.getreply(PFCPEM_unsubscribe_seid:{seid, false}) {
			setverdict(fail, __SCOPE__, "() failed");
			Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
		}
	}
}

function f_PFCPEM_get_recovery_timestamp()
runs on PFCP_ConnHdlr return integer {
	var integer rts;

	PFCP_PROC.call(PFCPEM_get_recovery_timestamp:{-}) {
		[] PFCP_PROC.getreply(PFCPEM_get_recovery_timestamp:{?}) -> param(rts) {};
	}

	return rts;
}

altstep as_pfcp_ignore(PFCPEM_PT pt, template PDU_PFCP pfcp_expect := ?)
{
	var PDU_PFCP pdu;

	[] pt.receive(pfcp_expect) -> value pdu {
		log("Ignoring PFCP PDU (SeqNr := ", pdu.sequence_number, ")");
		repeat;
	}
}

}