module SIP_Emulation {

/* SIP Emulation, runs on top of SIPmsg_PT.  It multiplexes/demultiplexes
 * the individual calls, so there can be separate TTCN-3 components handling
 * each of the calls
 *
 * The SIP_Emulation.main() function processes SIP message from the SIPmsg
 * socket via the SIPmsg_PT, and dispatches them to the per-connection components.
 *
 * Outbound SIP calls are initiated by sending a PDU_SIP_Request messages
 * to the component running the SIP_Emulation.main() function.
 *
 * For each new inbound call, the SipOps.create_cb() is called.  It can create
 * or resolve a TTCN-3 component, and returns a component reference to which that inbound
 * call 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 MNCC + SIP, and first trigger a connection from MNCC side in a
 * component which then subsequently should also handle the SIP emulation.
 *
 * (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 SIPmsg_Types all;
import from SIPmsg_PortType all;

type component SIP_ConnHdlr {
	/* ports towards SIP Emulation core / call dispatcher */
	port SIP_Conn_PT SIP;
	port SIPEM_PROC_PT SIP_PROC;
}

/* port between individual per-call components and this dispatcher */
type port SIP_Conn_PT message {
	inout PDU_SIP_Request, PDU_SIP_Response, ASP_SIP_close
} with { extension "internal" };

/* represents a single SIP Call */
type record CallData {
	/* reference to the instance of the per-connection component */
	SIP_ConnHdlr	comp_ref,
	CallidString	call_id
}

type component SIP_Emulation_CT {
	/* SIP test port on bottom side */
	port SIPmsg_PT SIP;
	/* SIP port to the per-call clients */
	port SIP_Conn_PT CLIENT;

	var CallData SipCallTable[16];
	var ExpectData SipExpectTable[16];

	/* procedure based port to register for incoming connections */
	port SIPEM_PROC_PT CLIENT_PROC;
};

template RequestLine tr_ReqLine(template Method method) := {
	method := method,
	requestUri := ?,
	sipVersion := ?
}

private template PDU_SIP_Request tr_SIP_REGISTER := {
	requestLine := tr_ReqLine(REGISTER_E),
	msgHeader := t_SIP_msgHeader_any,
	messageBody := *,
	payload := *
}

private template PDU_SIP_Request tr_SIP_INVITE := {
	requestLine := tr_ReqLine(INVITE_E),
	msgHeader := t_SIP_msgHeader_any,
	messageBody := *,
	payload := *
}

template SipUrl tr_SIP_Url(template charstring user_or_num,
			   template charstring host := *,
			   template integer portField := *) := {
	scheme := "sip",
	userInfo := {
		userOrTelephoneSubscriber := user_or_num,
		password := *
	},
	hostPort := {
		host := host,
		portField := portField
	},
	urlParameters := *,
	headers := *
}
template (value) SipUrl ts_SIP_Url(charstring user_or_num,
			   template (omit) charstring host := omit,
			   template (omit) integer portField := omit) := {
	scheme := "sip",
	userInfo := {
		userOrTelephoneSubscriber := user_or_num,
		password := omit
	},
	hostPort := {
		host := host,
		portField := portField
	},
	urlParameters := omit,
	headers := omit
}

template Addr_Union tr_SIP_Addr(template SipUrl sip_url) := {
	nameAddr := {
		displayName := *,
		addrSpec := sip_url
	}
}
template (value) Addr_Union ts_SIP_Addr(template (value) SipUrl sip_url) := {
	nameAddr := {
		displayName := omit,
		addrSpec := sip_url
	}
}

template To tr_SIP_To(template Addr_Union addr) := {
	fieldName := TO_E,
	addressField := addr,
	toParams := *
}
template (value) To ts_SIP_To(template (value) Addr_Union addr) := {
	fieldName := TO_E,
	addressField := addr,
	toParams := omit
}

/* resolve component reference by connection ID */
private function f_call_id_known(CallidString call_id)
runs on SIP_Emulation_CT return boolean {
	var integer i;
	for (i := 0; i < sizeof(SipCallTable); i := i+1) {
		if (SipCallTable[i].call_id == call_id) {
			return true;
		}
	}
	return false;
}

/* resolve component reference by connection ID */
private function f_comp_by_call_id(CallidString call_id)
runs on SIP_Emulation_CT return SIP_ConnHdlr {
	var integer i;
	for (i := 0; i < sizeof(SipCallTable); i := i+1) {
		if (SipCallTable[i].call_id == call_id) {
			return SipCallTable[i].comp_ref;
		}
	}
	setverdict(fail, "SIP Call table not found by SIP Call ID ", call_id);
	mtc.stop;
}

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

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

private function f_call_table_init()
runs on SIP_Emulation_CT {
	for (var integer i := 0; i < sizeof(SipCallTable); i := i+1) {
		SipCallTable[i].comp_ref := null;
		SipCallTable[i].call_id := "";
	}
}

private function f_call_table_add(SIP_ConnHdlr comp_ref, CallidString call_id)
runs on SIP_Emulation_CT {
	for (var integer i := 0; i < sizeof(SipCallTable); i := i+1) {
		if (SipCallTable[i].call_id == "") {
			SipCallTable[i].comp_ref := comp_ref;
			SipCallTable[i].call_id := call_id;
			log("Added SIP Call Table entry [", i, "] for ", call_id, " at ", comp_ref);
			return;
		}
	}
	testcase.stop("SIP Call table full");
}

private function f_call_table_del(CallidString call_id)
runs on SIP_Emulation_CT {
	for (var integer i := 0; i < sizeof(SipCallTable); i := i+1) {
		if (SipCallTable[i].call_id == call_id) {
			SipCallTable[i].comp_ref := null;
			SipCallTable[i].call_id := "";
			log("Deleted SIP Call Table entry [", i, "] for ", call_id);
			return;
		}
	}
	setverdict(fail, "SIP Call table attempt to delete non-existant ", call_id);
	mtc.stop;
}

/* call-back type, to be provided by specific implementation; called when new call connection
 * arrives */
type function SipCreateCallback(PDU_SIP_Request sip_req, charstring id)
runs on SIP_Emulation_CT return SIP_ConnHdlr;

type record SipOps {
	SipCreateCallback create_cb
};

function f_init_sip(inout SIP_Emulation_CT ct, charstring id := "SIP_EMU") {
	var SipOps ops := {
		create_cb := refers(SIP_Emulation.ExpectedCreateCallback)
	};

	ct := SIP_Emulation_CT.create(id) alive;
	map(ct:SIP, system:SIP);
	ct.start(SIP_Emulation.main(ops, id));
}

function main(SipOps ops, charstring id)
runs on SIP_Emulation_CT {

	f_expect_table_init();
	f_call_table_init();

	while (true) {
		var SIP_ConnHdlr vc_hdlr, vc_conn;
		var PDU_SIP_Request sip_req;
		var PDU_SIP_Response sip_resp;
		var ASP_SIP_close sip_close;
		var SipUrl sip_to;

		alt {
		/* SIP REGISTER was received on SIP socket/port */
		[] SIP.receive(tr_SIP_REGISTER) -> value sip_req {
			var CallidString call_id := sip_req.msgHeader.callId.callid;
			if (f_call_id_known(call_id)) {
				/* re-register? */
				vc_conn := f_comp_by_call_id(call_id);
			} else {
				/* new REGISTER: check expect */
				vc_conn := ops.create_cb.apply(sip_req, id);
				f_call_table_add(vc_conn, call_id);
			}
			CLIENT.send(sip_req) to vc_conn;
			}
		/* SIP INVITE was received on SIP socket/port */
		[] SIP.receive(tr_SIP_INVITE) -> value sip_req {
			var CallidString call_id := sip_req.msgHeader.callId.callid;
			if (f_call_id_known(call_id)) {
				/* re-invite? */
				vc_conn := f_comp_by_call_id(call_id);
			} else {
				/* new INVITE: check expect */
				vc_conn := ops.create_cb.apply(sip_req, id);
				f_call_table_add(vc_conn, call_id);
			}
			CLIENT.send(sip_req) to vc_conn;
			}
		/* other SIP request was received on SIP socket/port */
		[] SIP.receive(PDU_SIP_Request:?) -> value sip_req {
			var CallidString call_id := sip_req.msgHeader.callId.callid;
			if (f_call_id_known(call_id)) {
				vc_conn := f_comp_by_call_id(call_id);
				CLIENT.send(sip_req) to vc_conn;
			} else {
				setverdict(fail, "SIP Request for unknown call ", call_id);
				mtc.stop;
			}
			}
		/* SIP response was received on SIP socket/port */
		[] SIP.receive(PDU_SIP_Response:?) -> value sip_resp {
			var CallidString call_id := sip_resp.msgHeader.callId.callid;
			if (f_call_id_known(call_id)) {
				vc_conn := f_comp_by_call_id(call_id);
				CLIENT.send(sip_resp) to vc_conn;
			} else {
				setverdict(fail, "SIP Response for unknown call ", call_id);
				mtc.stop;
			}
			}

		/* a ConnHdlr is sending us a SIP REGISTER: Forward to SIP port */
		[] CLIENT.receive(tr_SIP_REGISTER) -> value sip_req sender vc_conn {
			var CallidString call_id := sip_req.msgHeader.callId.callid;
			if (f_call_id_known(call_id)) {
				/* re-register */
				vc_conn := f_comp_by_call_id(call_id);
			} else {
				/* new REGISTER: add to table */
				f_call_table_add(vc_conn, call_id);
			}
			SIP.send(sip_req);
			}

		/* a ConnHdlr is sending us a SIP INVITE: Forward to SIP port */
		[] CLIENT.receive(tr_SIP_INVITE) -> value sip_req sender vc_conn {
			var CallidString call_id := sip_req.msgHeader.callId.callid;
			if (f_call_id_known(call_id)) {
				/* re-invite? */
				vc_conn := f_comp_by_call_id(call_id);
			} else {
				/* new INVITE: add to table */
				f_call_table_add(vc_conn, call_id);
			}
			SIP.send(sip_req);
			}
		/* a ConnHdlr is sending us a SIP request: Forward to SIP port */
		[] CLIENT.receive(PDU_SIP_Request:?) -> value sip_req sender vc_conn {
			SIP.send(sip_req);
			}
		/* a ConnHdlr is sending us a SIP request: Forward to SIP port */
		[] CLIENT.receive(PDU_SIP_Response:?) -> value sip_resp sender vc_conn {
			SIP.send(sip_resp);
			}
		/* a ConnHdlr is sending us a ASP_SIP_close: Forward to SIP port */
		[] CLIENT.receive(ASP_SIP_close:?) -> value sip_close sender vc_conn {
			SIP.send(sip_close);
			}

		[] CLIENT_PROC.getcall(SIPEM_register:{?,?}) -> param(sip_to, vc_hdlr) sender vc_conn {
			f_create_expect(sip_to, vc_hdlr);
			CLIENT_PROC.reply(SIPEM_register:{sip_to, vc_hdlr}) to vc_conn;
			}

		}
	}
}

/***********************************************************************
 * "Expect" Handling (mapping for expected incoming SIP callds from IUT)
 ***********************************************************************/

/* data about an expected future incoming connection */
type record ExpectData {
	/* SIP "To" (destination number) based on which we can match */
	SipUrl sip_to optional,
	/* component reference registered for the connection */
	SIP_ConnHdlr vc_conn
}

/* procedure based port to register for incoming calls */
signature SIPEM_register(SipUrl sip_to, SIP_ConnHdlr vc_conn);

type port SIPEM_PROC_PT procedure {
	inout SIPEM_register;
} with { extension "internal" };


/* CreateCallback that can be used as create_cb and will use the expect table */
function ExpectedCreateCallback(PDU_SIP_Request sip_req, charstring id)
runs on SIP_Emulation_CT return SIP_ConnHdlr {
	var SIP_ConnHdlr ret := null;
	var SipUrl sip_to;
	var integer i;

	if (sip_req.requestLine.method != INVITE_E and
	    sip_req.requestLine.method != REGISTER_E) {
		setverdict(fail, "SIP ExpectedCreateCallback needs REGISTER/INVITE");
		mtc.stop
		return ret;
	}
	sip_to := sip_req.msgHeader.toField.addressField.nameAddr.addrSpec;

	for (i := 0; i < sizeof(SipExpectTable); i := i+1) {
		if (not ispresent(SipExpectTable[i].sip_to)) {
			continue;
		}
		/* build a template, use '*' for all 'omit' values */
		var template SipUrl t_exp := SipExpectTable[i].sip_to;
		if (not ispresent(t_exp.hostPort.host)) {
			t_exp.hostPort.host := *;
		}
		if (not ispresent(t_exp.hostPort.portField)) {
			t_exp.hostPort.portField := *;
		} else if (valueof(t_exp.hostPort.portField) == 5060) {
			/* if the port number is 5060, it may be omitted */
			t_exp.hostPort.portField := 5060 ifpresent;
		}
		if (not ispresent(t_exp.urlParameters)) {
			t_exp.urlParameters := *;
		}
		if (not ispresent(t_exp.headers)) {
			t_exp.headers := *;
		}
		/* match against the constructed template */
		if (match(sip_to, t_exp)) {
			ret := SipExpectTable[i].vc_conn;
			/* release this entry to be used again */
			SipExpectTable[i].sip_to := omit;
			SipExpectTable[i].vc_conn := null;
			log("Found SipExpect[", i, "] for ", sip_to, " handled at ", ret);
			return ret;
		}
	}

	setverdict(fail, "Couldn't find SipExpect for incoming call ", sip_to);
	mtc.stop
	return ret;
}

/* server/emulation side function to create expect */
private function f_create_expect(SipUrl sip_to, SIP_ConnHdlr hdlr)
runs on SIP_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(SipExpectTable); i := i+1) {
		if (not ispresent(SipExpectTable[i].sip_to)) {
			SipExpectTable[i].sip_to := sip_to;
			SipExpectTable[i].vc_conn := hdlr;
			log("Created SipExpect[", i, "] for ", sip_to, " to be handled at ", hdlr);
			return;
		}
	}
	testcase.stop("No space left in SipExpectTable");
}

/* client/conn_hdlr side function to use procedure port to create expect in emulation */
function f_create_sip_expect(SipUrl sip_to) runs on SIP_ConnHdlr {
	SIP_PROC.call(SIPEM_register:{sip_to, self}) {
		[] SIP_PROC.getreply(SIPEM_register:{?,?}) {};
	}
}



}
