/**
 * @file sdp/media.c  SDP Media
 *
 * Copyright (C) 2010 Creytiv.com
 */
#include <stdlib.h>
#include <string.h>
#include <re_types.h>
#include <re_fmt.h>
#include <re_mem.h>
#include <re_mbuf.h>
#include <re_list.h>
#include <re_sa.h>
#include <re_sdp.h>
#include "sdp.h"


static void destructor(void *arg)
{
	struct sdp_media *m = arg;
	unsigned i;

	list_flush(&m->lfmtl);
	list_flush(&m->rfmtl);
	list_flush(&m->rattrl);
	list_flush(&m->lattrl);

	if (m->le.list) {
		m->disabled = true;
		m->ench     = NULL;
		mem_ref(m);
		return;
	}

	for (i=0; i<ARRAY_SIZE(m->protov); i++)
		mem_deref(m->protov[i]);

	list_unlink(&m->le);
	mem_deref(m->name);
	mem_deref(m->proto);
	mem_deref(m->uproto);
}


static int media_alloc(struct sdp_media **mp, struct list *list)
{
	struct sdp_media *m;
	int i;

	m = mem_zalloc(sizeof(*m), destructor);
	if (!m)
		return ENOMEM;

	list_append(list, &m->le, m);

	m->ldir  = SDP_SENDRECV;
	m->rdir  = SDP_SENDRECV;
	m->dynpt = RTP_DYNPT_START;

	sa_init(&m->laddr, AF_INET);
	sa_init(&m->raddr, AF_INET);
	sa_init(&m->laddr_rtcp, AF_INET);
	sa_init(&m->raddr_rtcp, AF_INET);

	for (i=0; i<SDP_BANDWIDTH_MAX; i++) {
		m->lbwv[i] = -1;
		m->rbwv[i] = -1;
	}

	*mp = m;

	return 0;
}


/**
 * Add a media line to an SDP Session
 *
 * @param mp    Pointer to allocated SDP Media line object
 * @param sess  SDP Session
 * @param name  Media name
 * @param port  Port number
 * @param proto Transport protocol
 *
 * @return 0 if success, otherwise errorcode
 */
int sdp_media_add(struct sdp_media **mp, struct sdp_session *sess,
		  const char *name, uint16_t port, const char *proto)
{
	struct sdp_media *m;
	int err;

	if (!sess || !name || !proto)
		return EINVAL;

	err = media_alloc(&m, &sess->lmedial);
	if (err)
		return err;

	err  = str_dup(&m->name, name);
	err |= str_dup(&m->proto, proto);
	if (err)
		goto out;

	sa_set_port(&m->laddr, port);

 out:
	if (err)
		mem_deref(m);
	else if (mp)
		*mp = m;

	return err;
}


/**
 * Add a remote SDP media line to an SDP Session
 *
 * @param mp    Pointer to allocated SDP Media line object
 * @param sess  SDP Session
 * @param name  Media name
 * @param proto Transport protocol
 *
 * @return 0 if success, otherwise errorcode
 */
int sdp_media_radd(struct sdp_media **mp, struct sdp_session *sess,
		   const struct pl *name, const struct pl *proto)
{
	struct sdp_media *m;
	int err;

	if (!mp || !sess || !name || !proto)
		return EINVAL;

	err = media_alloc(&m, &sess->medial);
	if (err)
		return err;

	m->disabled = true;

	err  = pl_strdup(&m->name, name);
	err |= pl_strdup(&m->proto, proto);

	if (err)
		mem_deref(m);
	else
		*mp = m;

	return err;
}


/**
 * Reset the remote part of an SDP Media line
 *
 * @param m SDP Media line
 */
void sdp_media_rreset(struct sdp_media *m)
{
	int i;

	if (!m)
		return;

	sa_init(&m->raddr, AF_INET);
	sa_init(&m->raddr_rtcp, AF_INET);

	list_flush(&m->rfmtl);
	list_flush(&m->rattrl);

	m->rdir = SDP_SENDRECV;

	for (i=0; i<SDP_BANDWIDTH_MAX; i++)
		m->rbwv[i] = -1;
}


/**
 * Compare media line protocols
 *
 * @param m      SDP Media line
 * @param proto  Transport protocol
 * @param update Update media protocol if match is found in alternate set
 *
 * @return True if matching, False if not
 */
bool sdp_media_proto_cmp(struct sdp_media *m, const struct pl *proto,
			 bool update)
{
	unsigned i;

	if (!m || !proto)
		return false;

	if (!pl_strcmp(proto, m->proto))
		return true;

	for (i=0; i<ARRAY_SIZE(m->protov); i++) {

		if (!m->protov[i] || pl_strcmp(proto, m->protov[i]))
			continue;

		if (update) {
			mem_deref(m->proto);
			m->proto = mem_ref(m->protov[i]);
		}

		return true;
	}

	return false;
}


/**
 * Find an SDP Media line from name and transport protocol
 *
 * @param sess  SDP Session
 * @param name  Media name
 * @param proto Transport protocol
 * @param update_proto Update media transport protocol
 *
 * @return Matching media line if found, NULL if not found
 */
struct sdp_media *sdp_media_find(const struct sdp_session *sess,
				 const struct pl *name,
				 const struct pl *proto,
				 bool update_proto)
{
	struct le *le;

	if (!sess || !name || !proto)
		return NULL;

	for (le=sess->lmedial.head; le; le=le->next) {

		struct sdp_media *m = le->data;

		if (pl_strcmp(name, m->name))
			continue;

		if (!sdp_media_proto_cmp(m, proto, update_proto))
			continue;

		return m;
	}

	return NULL;
}


/**
 * Align the locate/remote formats of an SDP Media line
 *
 * @param m     SDP Media line
 * @param offer True if SDP Offer, False if SDP Answer
 */
void sdp_media_align_formats(struct sdp_media *m, bool offer)
{
	struct sdp_format *rfmt, *lfmt;
	struct le *rle, *lle;

	if (!m || m->disabled || !sa_port(&m->raddr) || m->fmt_ignore)
		return;

	for (lle=m->lfmtl.head; lle; lle=lle->next) {

		lfmt = lle->data;

		lfmt->rparams = mem_deref(lfmt->rparams);
		lfmt->sup = false;
	}

	for (rle=m->rfmtl.head; rle; rle=rle->next) {

		rfmt = rle->data;

		for (lle=m->lfmtl.head; lle; lle=lle->next) {

			lfmt = lle->data;

			if (sdp_format_cmp(lfmt, rfmt))
				break;
		}

		if (!lle) {
			rfmt->sup = false;
			continue;
		}

		mem_deref(lfmt->rparams);
		lfmt->rparams = mem_ref(rfmt->params);

		lfmt->sup = true;
		rfmt->sup = true;

		if (rfmt->ref)
			rfmt->data = mem_deref(rfmt->data);
		else
			rfmt->data = NULL;

		if (lfmt->ref)
			rfmt->data = mem_ref(lfmt->data);
		else
			rfmt->data = lfmt->data;

		rfmt->ref = lfmt->ref;

		if (offer) {
			mem_deref(lfmt->id);
			lfmt->id = mem_ref(rfmt->id);
			lfmt->pt = atoi(lfmt->id ? lfmt->id : "");

			list_unlink(&lfmt->le);
			list_append(&m->lfmtl, &lfmt->le, lfmt);
		}
	}

	if (offer) {

		for (lle=m->lfmtl.tail; lle; ) {

			lfmt = lle->data;

			lle = lle->prev;

			if (!lfmt->sup) {
				list_unlink(&lfmt->le);
				list_append(&m->lfmtl, &lfmt->le, lfmt);
			}
		}
	}
}


/**
 * Set alternative protocols for an SDP Media line
 *
 * @param m      SDP Media line
 * @param protoc Number of alternative protocols
 *
 * @return 0 if success, otherwise errorcode
 */
int sdp_media_set_alt_protos(struct sdp_media *m, unsigned protoc, ...)
{
	const char *proto;
	int err = 0;
	unsigned i;
	va_list ap;

	if (!m)
		return EINVAL;

	va_start(ap, protoc);

	for (i=0; i<ARRAY_SIZE(m->protov); i++) {

		m->protov[i] = mem_deref(m->protov[i]);

		if (i >= protoc)
			continue;

		proto = va_arg(ap, const char *);
		if (proto)
			err |= str_dup(&m->protov[i], proto);
	}

	va_end(ap);

	return err;
}


/**
 * Set SDP Media line encode handler
 *
 * @param m    SDP Media line
 * @param ench Encode handler
 * @param arg  Encode handler argument
 */
void sdp_media_set_encode_handler(struct sdp_media *m, sdp_media_enc_h *ench,
				  void *arg)
{
	if (!m)
		return;

	m->ench = ench;
	m->arg  = arg;
}


/**
 * Set an SDP Media line to ignore formats
 *
 * @param m          SDP Media line
 * @param fmt_ignore True for ignore formats, otherwise false
 */
void sdp_media_set_fmt_ignore(struct sdp_media *m, bool fmt_ignore)
{
	if (!m)
		return;

	m->fmt_ignore = fmt_ignore;
}


/**
 * Set an SDP Media line to enabled/disabled
 *
 * @param m        SDP Media line
 * @param disabled True for disabled, False for enabled
 */
void sdp_media_set_disabled(struct sdp_media *m, bool disabled)
{
	if (!m)
		return;

	m->disabled = disabled;
}


/**
 * Check if an SDP Media line is disabled
 *
 * @param  m SDP Media line
 * @return True if disabled, otherwise false

 */
bool sdp_media_disabled(struct sdp_media *m)
{
	if (!m)
		return true;

	return m->disabled;
}


/**
 * Set the local port number of an SDP Media line
 *
 * @param m    SDP Media line
 * @param port Port number
 */
void sdp_media_set_lport(struct sdp_media *m, uint16_t port)
{
	if (!m)
		return;

	sa_set_port(&m->laddr, port);
}


/**
 * Set the local network address of an SDP media line
 *
 * @param m     SDP Media line
 * @param laddr Local network address
 */
void sdp_media_set_laddr(struct sdp_media *m, const struct sa *laddr)
{
	if (!m || !laddr)
		return;

	m->laddr = *laddr;
}


/**
 * Set a local bandwidth of an SDP Media line
 *
 * @param m    SDP Media line
 * @param type Bandwidth type
 * @param bw   Bandwidth value
 */
void sdp_media_set_lbandwidth(struct sdp_media *m, enum sdp_bandwidth type,
			      int32_t bw)
{
	if (!m || type >= SDP_BANDWIDTH_MAX)
		return;

	m->lbwv[type] = bw;
}


/**
 * Set the local RTCP port number of an SDP Media line
 *
 * @param m    SDP Media line
 * @param port RTCP Port number
 */
void sdp_media_set_lport_rtcp(struct sdp_media *m, uint16_t port)
{
	if (!m)
		return;

	sa_set_port(&m->laddr_rtcp, port);
}


/**
 * Set the local RTCP network address of an SDP media line
 *
 * @param m     SDP Media line
 * @param laddr Local RTCP network address
 */
void sdp_media_set_laddr_rtcp(struct sdp_media *m, const struct sa *laddr)
{
	if (!m || !laddr)
		return;

	m->laddr_rtcp = *laddr;
}


/**
 * Set the local direction flag of an SDP Media line
 *
 * @param m   SDP Media line
 * @param dir Media direction flag
 */
void sdp_media_set_ldir(struct sdp_media *m, enum sdp_dir dir)
{
	if (!m)
		return;

	m->ldir = dir;
}


/**
 * Set a local attribute of an SDP Media line
 *
 * @param m       SDP Media line
 * @param replace True to replace attribute, False to append
 * @param name    Attribute name
 * @param value   Formatted attribute value
 *
 * @return 0 if success, otherwise errorcode
 */
int sdp_media_set_lattr(struct sdp_media *m, bool replace,
			const char *name, const char *value, ...)
{
	va_list ap;
	int err;

	if (!m || !name)
		return EINVAL;

	if (replace)
		sdp_attr_del(&m->lattrl, name);

	va_start(ap, value);
	err = sdp_attr_addv(&m->lattrl, name, value, ap);
	va_end(ap);

	return err;
}


/**
 * Delete a local attribute of an SDP Media line
 *
 * @param m    SDP Media line
 * @param name Attribute name
 */
void sdp_media_del_lattr(struct sdp_media *m, const char *name)
{
	if (!m || !name)
		return;

	sdp_attr_del(&m->lattrl, name);
}


const char *sdp_media_proto(const struct sdp_media *m)
{
	return m ? m->proto : NULL;
}


/**
 * Get the remote port number of an SDP Media line
 *
 * @param m SDP Media line
 *
 * @return Remote port number
 */
uint16_t sdp_media_rport(const struct sdp_media *m)
{
	return m ? sa_port(&m->raddr) : 0;
}


/**
 * Get the remote network address of an SDP Media line
 *
 * @param m SDP Media line
 *
 * @return Remote network address
 */
const struct sa *sdp_media_raddr(const struct sdp_media *m)
{
	return m ? &m->raddr : NULL;
}


/**
 * Get the local network address of an SDP Media line
 *
 * @param m SDP Media line
 *
 * @return Local network address
 */
const struct sa *sdp_media_laddr(const struct sdp_media *m)
{
	return m ? &m->laddr : NULL;
}


/**
 * Get the remote RTCP network address of an SDP Media line
 *
 * @param m     SDP Media line
 * @param raddr On return, contains remote RTCP network address
 */
void sdp_media_raddr_rtcp(const struct sdp_media *m, struct sa *raddr)
{
	if (!m || !raddr)
		return;

	if (sa_isset(&m->raddr_rtcp, SA_ALL)) {
		*raddr = m->raddr_rtcp;
	}
	else if (sa_isset(&m->raddr_rtcp, SA_PORT)) {
		*raddr = m->raddr;
		sa_set_port(raddr, sa_port(&m->raddr_rtcp));
	}
	else {
		uint16_t port = sa_port(&m->raddr);

		*raddr = m->raddr;
		sa_set_port(raddr, port ? port + 1 : 0);
	}
}


/**
 * Get a remote bandwidth of an SDP Media line
 *
 * @param m    SDP Media line
 * @param type Bandwidth type
 *
 * @return Remote bandwidth value
 */
int32_t sdp_media_rbandwidth(const struct sdp_media *m,
			      enum sdp_bandwidth type)
{
	if (!m || type >= SDP_BANDWIDTH_MAX)
		return 0;

	return m->rbwv[type];
}


/**
 * Get the local media direction of an SDP Media line
 *
 * @param m SDP Media line
 *
 * @return Local media direction
 */
enum sdp_dir sdp_media_ldir(const struct sdp_media *m)
{
	return m ? m->ldir : SDP_INACTIVE;
}


/**
 * Get the remote media direction of an SDP Media line
 *
 * @param m SDP Media line
 *
 * @return Remote media direction
 */
enum sdp_dir sdp_media_rdir(const struct sdp_media *m)
{
	return m ? m->rdir : SDP_INACTIVE;
}


/**
 * Get the combined media direction of an SDP Media line
 *
 * @param m SDP Media line
 *
 * @return Combined media direction
 */
enum sdp_dir sdp_media_dir(const struct sdp_media *m)
{
	return m ? (enum sdp_dir)(m->ldir & m->rdir) : SDP_INACTIVE;
}


/**
 * Find a local SDP format from a payload type
 *
 * @param m  SDP Media line
 * @param pt Payload type
 *
 * @return Local SDP format if found, NULL if not found
 */
const struct sdp_format *sdp_media_lformat(const struct sdp_media *m, int pt)
{
	struct le *le;

	if (!m)
		return NULL;

	for (le=m->lfmtl.head; le; le=le->next) {

		const struct sdp_format *fmt = le->data;

		if (pt == fmt->pt)
			return fmt;
	}

	return NULL;
}


/**
 * Find a remote SDP format from a format name
 *
 * @param m    SDP Media line
 * @param name Format name
 *
 * @return Remote SDP format if found, NULL if not found
 */
const struct sdp_format *sdp_media_rformat(const struct sdp_media *m,
					   const char *name)
{
	struct le *le;

	if (!m || !sa_port(&m->raddr))
		return NULL;

	for (le=m->rfmtl.head; le; le=le->next) {

		const struct sdp_format *fmt = le->data;

		if (!fmt->sup)
			continue;

		if (name && str_casecmp(name, fmt->name))
			continue;

		return fmt;
	}

	return NULL;
}


/**
 * Find an SDP Format of an SDP Media line
 *
 * @param m     SDP Media line
 * @param local True if local media, False if remote
 * @param id    SDP format id
 * @param pt    Payload type
 * @param name  Format name
 * @param srate Sampling rate
 * @param ch    Number of channels
 *
 * @return SDP Format if found, NULL if not found
 */
struct sdp_format *sdp_media_format(const struct sdp_media *m,
				    bool local, const char *id,
				    int pt, const char *name,
				    int32_t srate, int8_t ch)
{
	return sdp_media_format_apply(m, local, id, pt, name, srate, ch,
				      NULL, NULL);
}


/**
 * Apply a function handler to all matching SDP formats
 *
 * @param m     SDP Media line
 * @param local True if local media, False if remote
 * @param id    SDP format id
 * @param pt    Payload type
 * @param name  Format name
 * @param srate Sampling rate
 * @param ch    Number of channels
 * @param fmth  SDP Format handler
 * @param arg   Handler argument
 *
 * @return SDP Format if found, NULL if not found
 */
struct sdp_format *sdp_media_format_apply(const struct sdp_media *m,
					  bool local, const char *id,
					  int pt, const char *name,
					  int32_t srate, int8_t ch,
					  sdp_format_h *fmth, void *arg)
{
	struct le *le;

	if (!m)
		return NULL;

	le = local ? m->lfmtl.head : m->rfmtl.head;

	while (le) {

		struct sdp_format *fmt = le->data;

		le = le->next;

		if (id && (!fmt->id || strcmp(id, fmt->id)))
			continue;

		if (pt >= 0 && pt != fmt->pt)
			continue;

		if (name && str_casecmp(name, fmt->name))
			continue;

		if (srate >= 0 && (uint32_t)srate != fmt->srate)
			continue;

		if (ch >= 0 && (uint8_t)ch != fmt->ch)
			continue;

		if (!fmth || fmth(fmt, arg))
			return fmt;
	}

	return NULL;
}


/**
 * Get the list of SDP Formats
 *
 * @param m     SDP Media line
 * @param local True if local, False if remote
 *
 * @return List of SDP Formats
 */
const struct list *sdp_media_format_lst(const struct sdp_media *m, bool local)
{
	if (!m)
		return NULL;

	return local ? &m->lfmtl : &m->rfmtl;
}


/**
 * Get a remote attribute from an SDP Media line
 *
 * @param m     SDP Media line
 * @param name  Attribute name
 *
 * @return Attribute value, NULL if not found
 */
const char *sdp_media_rattr(const struct sdp_media *m, const char *name)
{
	if (!m || !name)
		return NULL;

	return sdp_attr_apply(&m->rattrl, name, NULL, NULL);
}


/**
 * Get a remote attribute from an SDP Media line or the SDP session
 *
 * @param m     SDP Media line
 * @param sess  SDP Session
 * @param name  Attribute name
 *
 * @return Attribute value, NULL if not found
 */
const char *sdp_media_session_rattr(const struct sdp_media *m,
				    const struct sdp_session *sess,
				    const char *name)
{
	const char *val;

	val = sdp_media_rattr(m, name);
	if (!val)
		val = sdp_session_rattr(sess, name);

	return val;
}


/**
 * Apply a function handler to all matching remote attributes
 *
 * @param m     SDP Media line
 * @param name  Attribute name
 * @param attrh Attribute handler
 * @param arg   Handler argument
 *
 * @return Attribute value if match
 */
const char *sdp_media_rattr_apply(const struct sdp_media *m, const char *name,
				  sdp_attr_h *attrh, void *arg)
{
	if (!m)
		return NULL;

	return sdp_attr_apply(&m->rattrl, name, attrh, arg);
}


/**
 * Get the name of an SDP Media line
 *
 * @param m SDP Media line
 *
 * @return SDP Media line name
 */
const char *sdp_media_name(const struct sdp_media *m)
{
	return m ? m->name : NULL;
}


/**
 * Print SDP Media line debug information
 *
 * @param pf Print function for output
 * @param m  SDP Media line
 *
 * @return 0 if success, otherwise errorcode
 */
int sdp_media_debug(struct re_printf *pf, const struct sdp_media *m)
{
	struct le *le;
	int err;

	if (!m)
		return 0;

	err  = re_hprintf(pf, "%s %s\n", m->name, m->proto);

	err |= re_hprintf(pf, "  local formats:\n");

	for (le=m->lfmtl.head; le; le=le->next)
		err |= re_hprintf(pf, "    %H\n", sdp_format_debug, le->data);

	err |= re_hprintf(pf, "  remote formats:\n");

	for (le=m->rfmtl.head; le; le=le->next)
		err |= re_hprintf(pf, "    %H\n", sdp_format_debug, le->data);

	err |= re_hprintf(pf, "  local attributes:\n");

	for (le=m->lattrl.head; le; le=le->next)
		err |= re_hprintf(pf, "    %H\n", sdp_attr_debug, le->data);

	err |= re_hprintf(pf, "  remote attributes:\n");

	for (le=m->rattrl.head; le; le=le->next)
		err |= re_hprintf(pf, "    %H\n", sdp_attr_debug, le->data);

	return err;
}
