/* Session Description Protocol parsing and generator
 * This shall be simple and is incomplete.
 *
 * (C) 2019 by Andreas Eversberg <jolly@eversberg.eu>
 * All Rights Reserved
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/timer.h>
#include <osmocom/core/select.h>
#include <osmocom/cc/endpoint.h>
#include <osmocom/cc/sdp.h>

extern int DLCC;

#define strncat_printf(sdp, fmt, arg...) \
	{ \
		snprintf(sdp + strlen(sdp), sizeof(sdp) - strlen(sdp), fmt, ## arg); \
		sdp[sizeof(sdp) - 1] = '\0'; \
	}

/* generate SDP from session structure */
char *osmo_cc_session_gensdp(osmo_cc_session_t *session, int accepted_only)
{
	/* calc max size of SDP: quick an dirty (close to max UDP payload size) */
	static char sdp[65000];
	const char *username, *sess_id, *sess_version, *nettype, *addrtype, *unicast_address;
	const char *session_name;
	int individual_connection_data = 1; /* in case there is no media, there is no connection data */
	int individual_send_receive = 1; /* in case there is no media, there is no send/receive attribute */
	struct osmo_cc_session_media *media;
	struct osmo_cc_session_codec *codec;

	sdp[0] = 0;

	/* Version */
	strncat_printf(sdp, "v=0\r\n");

	/* Origin */
	username = session->origin_local.username;
	sess_id = session->origin_local.sess_id;
	sess_version = session->origin_local.sess_version;
	nettype = session->origin_local.nettype;
	addrtype = session->origin_local.addrtype;
	unicast_address = session->origin_local.unicast_address;
	strncat_printf(sdp, "o=%s %s %s %s %s %s\r\n", username, sess_id, sess_version, nettype, addrtype, unicast_address);

	/* Session */
	session_name = session->name;
	strncat_printf(sdp, "s=%s\r\n", session_name);

	/* Connection Data (if all media have the same data) */
	if (session->media_list) {
		osmo_cc_session_for_each_media(session->media_list->next, media) {
			if (accepted_only && !media->accepted)
				continue;
			if (session->media_list->connection_data_local.nettype != media->connection_data_local.nettype)
				break;
			if (session->media_list->connection_data_local.addrtype != media->connection_data_local.addrtype)
				break;
			if (!!strcmp(session->media_list->connection_data_local.address, media->connection_data_local.address))
				break;
		}
		if (!media)
			individual_connection_data = 0;
	}
	if (!individual_connection_data)
		strncat_printf(sdp, "c=%s %s %s\r\n", osmo_cc_session_nettype2string(session->media_list->connection_data_local.nettype), osmo_cc_session_addrtype2string(session->media_list->connection_data_local.addrtype), session->media_list->connection_data_local.address);

	/* timestamp */
	strncat_printf(sdp, "t=0 0\r\n");

	/* sendonly /recvonly (if all media have the same data) */
	if (session->media_list) {
		osmo_cc_session_for_each_media(session->media_list->next, media) {
			if (accepted_only && !media->accepted)
				continue;
			if (session->media_list->send != media->send)
				break;
			if (session->media_list->receive != media->receive)
				break;
		}
		if (!media)
			individual_send_receive = 0;
	}
	if (!individual_send_receive) {
		if (session->media_list->send && !session->media_list->receive)
			strncat_printf(sdp, "a=sendonly\r\n");
		if (!session->media_list->send && session->media_list->receive)
			strncat_printf(sdp, "a=recvonly\r\n");
		if (!session->media_list->send && !session->media_list->receive)
			strncat_printf(sdp, "a=inactive\r\n");
	}

	/* media */
	osmo_cc_session_for_each_media(session->media_list, media) {
		if (accepted_only && !media->accepted)
			continue;
		strncat_printf(sdp, "m=%s %u %s",
			osmo_cc_session_media_type2string(media->description.type) ? : media->description.type_name,
			media->description.port_local,
			osmo_cc_session_media_proto2string(media->description.proto) ? : media->description.proto_name);
		osmo_cc_session_for_each_codec(media->codec_list, codec) {
			if (accepted_only && !codec->accepted)
				continue;
			strncat_printf(sdp, " %u", codec->payload_type_local);
		}
		strncat_printf(sdp, "\r\n");
		/* don't list rtpmap when session was canceled by setting port to 0 */
		if (media->description.port_local == 0)
			continue;
		if (individual_connection_data)
			strncat_printf(sdp, "c=%s %s %s\r\n", osmo_cc_session_nettype2string(media->connection_data_local.nettype), osmo_cc_session_addrtype2string(media->connection_data_local.addrtype), media->connection_data_local.address);
		osmo_cc_session_for_each_codec(media->codec_list, codec) {
			if (accepted_only && !codec->accepted)
				continue;
			strncat_printf(sdp, "a=rtpmap:%u %s/%d/%d\r\n", codec->payload_type_local, codec->payload_name, codec->payload_rate, codec->payload_channels);
		}
		if (individual_send_receive) {
			if (media->send && !media->receive)
				strncat_printf(sdp, "a=sendonly\r\n");
			if (!media->send && media->receive)
				strncat_printf(sdp, "a=recvonly\r\n");
			if (!media->send && !media->receive)
				strncat_printf(sdp, "a=inactive\r\n");
		}
	}

	/* check for overflow and return */
	if (strlen(sdp) == sizeof(sdp) - 1) {
		LOGP(DLCC, LOGL_ERROR, "Fatal error: Allocated SDP buffer with %d bytes is too small, please fix!\n", (int)sizeof(sdp));
		return NULL;
	}
	return sdp;
}

/* separate a word from string that is delimited with one or more space characters */
static char *wordsep(char **text_p)
{
	char *text = *text_p;
	static char word[256];
	int i;

	/* no text */
	if (text == NULL || *text == '\0')
		return NULL;
	/* skip spaces before text */
	while (*text && *text <= ' ')
		text++;
	/* copy content */
	i = 0;
	while (*text > ' ' && i < (int)sizeof(word))
		word[i++] = *text++;
	word[i] = '\0';
	/* set next */
	*text_p = text;
	return word;
}

/*
 * codecs and their default values
 *
 * if format is -1, payload type is dynamic
 * if rate is 0, rate may be any rate
 */
struct codec_defaults {
	int fmt;
	char *name;
	uint32_t rate;
	int channels;
} codec_defaults[] = {
	{ 0,	"PCMU",	 	8000,	1 },
	{ 3,	"GSM",	 	8000,	1 },
	{ 4,	"G723",	 	8000,	1 },
	{ 5,	"DVI4",	 	8000,	1 },
	{ 6,	"DVI4",	 	16000,	1 },
	{ 7,	"LPC",	 	8000,	1 },
	{ 8,	"PCMA",	 	8000,	1 },
	{ 9,	"G722",	 	8000,	1 },
	{ 10,	"L16",	 	44100,	2 },
	{ 11,	"L16",	 	44100,	1 },
	{ 12,	"QCELP", 	8000,	1 },
	{ 13,	"CN",	 	8000,	1 },
	{ 14,	"MPA",	 	90000,	1 },
	{ 15,	"G728",	 	8000,	1 },
	{ 16,	"DVI4",	 	11025,	1 },
	{ 17,	"DVI4",	 	22050,	1 },
	{ 18,	"G729",	 	8000,	1 },
	{ 25,	"CELB",	 	90000,	0 },
	{ 26,	"JPEG",	 	90000,	0 },
	{ 28,	"nv",	 	90000,	0 },
	{ 31,	"H261",	 	90000,	0 },
	{ 32,	"MPV",	 	90000,	0 },
	{ 33,	"MP2T",	 	90000,	0 },
	{ 34,	"H263",	 	90000,	0 },
	{ -1, NULL, 0, 0 },
};

static void complete_codec_by_fmt(uint8_t fmt, const char **name, uint32_t *rate, int *channels)
{
	int i;

	for (i = 0; codec_defaults[i].name; i++) {
		if (codec_defaults[i].fmt == fmt)
			break;
	}
	if (!codec_defaults[i].name)
		return;

	free((char *)*name);
	*name = strdup(codec_defaults[i].name);
	*rate = codec_defaults[i].rate;
	*channels = codec_defaults[i].channels;
}

int osmo_cc_payload_type_by_attrs(uint8_t *fmt, const char *name, uint32_t *rate, int *channels)
{
	int i;

	for (i = 0; codec_defaults[i].name; i++) {
		if (!strcmp(codec_defaults[i].name, name)
		 && (*rate == 0 || codec_defaults[i].rate == *rate)
		 && (*channels == 0 || codec_defaults[i].channels == *channels))
			break;
	}
	if (!codec_defaults[i].name)
		return -EINVAL;

	*fmt = codec_defaults[i].fmt;
	*rate = codec_defaults[i].rate;
	*channels = codec_defaults[i].channels;

	return 0;
}

/* parses data and codec list from SDP
 *
 * sdp = given SDP text
 * return: SDP session description structure */
struct osmo_cc_session *osmo_cc_session_parsesdp(osmo_cc_session_config_t *conf, void *priv, const char *_sdp)
{
	char buffer[strlen(_sdp) + 1], *sdp = buffer;
	char *line, *p, *word, *next_word;
	int line_no = 0;
	struct osmo_cc_session_connection_data ccd, *cd;
	int csend = 1, creceive = 1; /* common default */
	struct osmo_cc_session *session = NULL;
	struct osmo_cc_session_media *media = NULL;
	struct osmo_cc_session_codec *codec = NULL;

	/* prepare data */
	strcpy(sdp, _sdp);
	memset(&ccd, 0, sizeof(ccd));

	/* create SDP session description */
	session = osmo_cc_new_session(conf, priv, NULL, NULL, NULL, 0, 0, NULL, NULL, 0);

	/* check every line of SDP and parse its data */
	while(*sdp) {
		if ((p = strchr(sdp, '\r'))) {
			*p++ = '\0';
			if (*p == '\n')
				p++;
			line = sdp;
			sdp = p;
		} else if ((p = strchr(sdp, '\n'))) {
			*p++ = '\0';
			line = sdp;
			sdp = p;
		} else {
			line = sdp;
			sdp = strchr(sdp, '\0');
		}
		next_word = line + 2;
		line_no++;

		if (line[0] == '\0')
			continue;

		if (line[1] != '=') {
			LOGP(DLCC, LOGL_NOTICE, "SDP line %d = '%s' is garbage, expecting '=' as second character.\n", line_no, line);
			continue;
		}

		switch(line[0]) {
		case 'v':
			LOGP(DLCC, LOGL_DEBUG, " -> Version: %s\n", next_word);
			if (atoi(next_word) != 0) {
				LOGP(DLCC, LOGL_NOTICE, "SDP line %d = '%s' describes unsupported version.\n", line_no, line);
				osmo_cc_free_session(session);
				return NULL;
			}
			break;
		case 'o':
			LOGP(DLCC, LOGL_DEBUG, " -> Originator: %s\n", next_word);
			/* Originator */
			word = wordsep(&next_word);
			if (!word)
				break;
			free((char *)session->origin_remote.username); // if already set
			session->origin_remote.username = strdup(word);
			word = wordsep(&next_word);
			if (!word)
				break;
			free((char *)session->origin_remote.sess_id); // if already set
			session->origin_remote.sess_id = strdup(word);
			word = wordsep(&next_word);
			if (!word)
				break;
			free((char *)session->origin_remote.sess_version); // if already set
			session->origin_remote.sess_version = strdup(word);
			word = wordsep(&next_word);
			if (!word)
				break;
			free((char *)session->origin_remote.nettype); // if already set
			session->origin_remote.nettype = strdup(word);
			word = wordsep(&next_word);
			if (!word)
				break;
			free((char *)session->origin_remote.addrtype); // if already set
			session->origin_remote.addrtype = strdup(word);
			word = wordsep(&next_word);
			if (!word)
				break;
			free((char *)session->origin_remote.unicast_address); // if already set
			session->origin_remote.unicast_address = strdup(word);
			break;
		case 's':
			/* Session Name */
			LOGP(DLCC, LOGL_DEBUG, " -> Session Name: %s\n", next_word);
			free((char *)session->name); // if already set
			session->name = strdup(next_word);
			break;
		case 'c': /* Connection Data */
			LOGP(DLCC, LOGL_DEBUG, " -> Connection Data: %s\n", next_word);
			if (media)
				cd = &media->connection_data_remote;
			else
				cd = &ccd;
			/* network type */
			if (!(word = wordsep(&next_word)))
				break;
			if (!strcmp(word, "IN"))
				cd->nettype = osmo_cc_session_nettype_inet;
			else {
				LOGP(DLCC, LOGL_NOTICE, "Unsupported network type '%s' in SDP line %d = '%s'\n", word, line_no, line);
				break;
			}
			/* address type */
			if (!(word = wordsep(&next_word)))
				break;
			if (!strcmp(word, "IP4")) {
				cd->addrtype = osmo_cc_session_addrtype_ipv4;
				LOGP(DLCC, LOGL_DEBUG, " -> Address Type = IPv4\n");
			} else
			if (!strcmp(word, "IP6")) {
				cd->addrtype = osmo_cc_session_addrtype_ipv6;
				LOGP(DLCC, LOGL_DEBUG, " -> Address Type = IPv6\n");
			} else {
				LOGP(DLCC, LOGL_NOTICE, "Unsupported address type '%s' in SDP line %d = '%s'\n", word, line_no, line);
				break;
			}
			/* connection address */
			if (!(word = wordsep(&next_word)))
				break;
			if ((p = strchr(word, '/')))
				*p++ = '\0';
			free((char *)cd->address); // in case of multiple lines of 'c'
			cd->address = strdup(word);
			LOGP(DLCC, LOGL_DEBUG, " -> Address = %s\n", word);
			break;
		case 'm': /* Media Description */
			LOGP(DLCC, LOGL_DEBUG, " -> Media Description: %s\n", next_word);
			/* add media description */
			media = osmo_cc_add_media(session, 0, 0, NULL, 0, 0, 0, csend, creceive, NULL, 0);
			/* copy common connection data from common connection, if exists */
			cd = &media->connection_data_remote;
			memcpy(cd, &ccd, sizeof(*cd));
			/* media type */
			if (!(word = wordsep(&next_word)))
				break;
			if (!strcmp(word, "audio"))
				media->description.type = osmo_cc_session_media_type_audio;
			else
			if (!strcmp(word, "video"))
				media->description.type = osmo_cc_session_media_type_video;
			else {
				media->description.type = osmo_cc_session_media_type_unknown;
				media->description.type_name = strdup(word);
				LOGP(DLCC, LOGL_DEBUG, "Unsupported media type in SDP line %d = '%s'\n", line_no, line);
			}
			/* port */
			if (!(word = wordsep(&next_word)))
				break;
			media->description.port_remote = atoi(word);
			/* proto */
			if (!(word = wordsep(&next_word)))
				break;
			if (!strcmp(word, "RTP/AVP"))
				media->description.proto = osmo_cc_session_media_proto_rtp;
			else {
				media->description.proto = osmo_cc_session_media_proto_unknown;
				media->description.proto_name = strdup(word);
				LOGP(DLCC, LOGL_NOTICE, "Unsupported protocol type in SDP line %d = '%s'\n", line_no, line);
				break;
			}
			/* create codec description for each codec and link */
			while ((word = wordsep(&next_word))) {
				/* create codec */
				codec = osmo_cc_add_codec(media, NULL, 0, 1, NULL, NULL, 0);
				/* fmt */
				codec->payload_type_remote = atoi(word);
				complete_codec_by_fmt(codec->payload_type_remote, &codec->payload_name, &codec->payload_rate, &codec->payload_channels);
				LOGP(DLCC, LOGL_DEBUG, " -> payload type = %d\n", codec->payload_type_remote);
				if (codec->payload_name)
					LOGP(DLCC, LOGL_DEBUG, " -> payload name = %s\n", codec->payload_name);
				if (codec->payload_rate)
					LOGP(DLCC, LOGL_DEBUG, " -> payload rate = %d\n", codec->payload_rate);
				if (codec->payload_channels)
					LOGP(DLCC, LOGL_DEBUG, " -> payload channels = %d\n", codec->payload_channels);
			}
			break;
		case 'a':
			LOGP(DLCC, LOGL_DEBUG, " -> Attribute: %s\n", next_word);
			word = wordsep(&next_word);
			if (!strcmp(word, "sendrecv")) {
				if (media) {
					media->receive = 1;
					media->send = 1;
				} else {
					creceive = 1;
					csend = 1;
				}
				break;
			} else
			if (!strcmp(word, "recvonly")) {
				if (media) {
					media->receive = 1;
					media->send = 0;
				} else {
					creceive = 1;
					csend = 0;
				}
				break;
			} else
			if (!strcmp(word, "sendonly")) {
				if (media) {
					media->receive = 0;
					media->send = 1;
				} else {
					creceive = 0;
					csend = 1;
				}
				break;
			} else
			if (!strcmp(word, "inactive")) {
				if (media) {
					media->receive = 0;
					media->send = 0;
				} else {
					creceive = 0;
					csend = 0;
				}
				break;
			} else
			if (!media) {
				LOGP(DLCC, LOGL_NOTICE, "Attribute without previously defined media in SDP line %d = '%s'\n", line_no, line);
				break;
			}
			if (!strncmp(word, "rtpmap:", 7)) {
				int fmt = atoi(word + 7);
				osmo_cc_session_for_each_codec(media->codec_list, codec) {
					if (codec->payload_type_remote == fmt)
						break;
				}
				if (!codec) {
					LOGP(DLCC, LOGL_NOTICE, "Attribute without previously defined codec in SDP line %d = '%s'\n", line_no, line);
					break;
				}
				LOGP(DLCC, LOGL_DEBUG, " -> (rtpmap) payload type = %d\n", codec->payload_type_remote);
				if (!(word = wordsep(&next_word))) {
rtpmap_broken:
					LOGP(DLCC, LOGL_NOTICE, "Broken 'rtpmap' definition in SDP line %d = '%s' Skipping codec!\n", line_no, line);
					osmo_cc_free_codec(codec);
					break;
				}
				if ((p = strchr(word, '/')))
					*p++ = '\0';
				free((char *)codec->payload_name); // in case it is already set above
				codec->payload_name = strdup(word);
				LOGP(DLCC, LOGL_DEBUG, " -> (rtpmap) payload name = %s\n", codec->payload_name);
				if (!(word = p))
					goto rtpmap_broken;
				if ((p = strchr(word, '/')))
					*p++ = '\0';
				codec->payload_rate = atoi(word);
				LOGP(DLCC, LOGL_DEBUG, " -> (rtpmap) payload rate = %d\n", codec->payload_rate);
				if (!(word = p)) {
					/* For audio streams, encoding-params indicates the number of audio channels.
					 * This parameter is OPTIONAL and may be omitted if the number of channels is
					 * one, provided that no additional parameters are needed. */
					codec->payload_channels = 1;
					LOGP(DLCC, LOGL_DEBUG, " -> (rtpmap) payload channels = %d (default)\n", codec->payload_channels);
					break;
				}
				codec->payload_channels = atoi(word);
				LOGP(DLCC, LOGL_DEBUG, " -> (rtpmap) payload channels = %d\n", codec->payload_channels);
				break;
			}
			break;
		}
	}

	/* if something is incomplete, abort here */
	if (osmo_cc_session_check(session, 1)) {
		LOGP(DLCC, LOGL_NOTICE, "Parsing SDP failed.\n");
		osmo_cc_free_session(session);
		return NULL;
	}

	return session;
}

void osmo_cc_debug_sdp(const char *_sdp)
{
	const unsigned char *sdp = (const unsigned char *)_sdp;
	char text[256];
	int i;

	while (*sdp) {
		for (i = 0; *sdp > 0 && *sdp >= 32 && i < (int)sizeof(text) - 1; i++)
			text[i] = *sdp++;
		text[i] = '\0';
		LOGP(DLCC, LOGL_DEBUG, " | %s\n", text);
		while (*sdp > 0 && *sdp < 32)
			sdp++;
	}
}

