/*
linphone
Copyright (C) 2012  Belledonne Communications SARL

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 2
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, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#include "upnp.h"
#include "private.h"
#include "linphone/lpconfig.h"
#include <ctype.h>

#define UPNP_STRINGIFY(x)     #x
#define UPNP_TOSTRING(x)      UPNP_STRINGIFY(x)

#define UPNP_ADD_MAX_RETRY    4
#define UPNP_REMOVE_MAX_RETRY 4
#define UPNP_SECTION_NAME     "uPnP"
#define UPNP_CORE_READY_CHECK 1
#define UPNP_CORE_RETRY_DELAY 10
#define UPNP_CALL_RETRY_DELAY 3
#define UPNP_UUID_LEN         128
#define UPNP_UUID_LEN_STR     UPNP_TOSTRING(UPNP_UUID_LEN)
/*
 * uPnP Definitions
 */

typedef struct _UpnpPortBinding {
	ms_mutex_t mutex;
	LinphoneUpnpState state;
	upnp_igd_ip_protocol protocol;
	char *device_id;
	char local_addr[LINPHONE_IPADDR_SIZE];
	int local_port;
	char external_addr[LINPHONE_IPADDR_SIZE];
	int external_port;
	int retry;
	int ref;
	bool_t to_remove;
	bool_t to_add;
	time_t last_update;
} UpnpPortBinding;

typedef struct _UpnpStream {
	UpnpPortBinding *rtp;
	UpnpPortBinding *rtcp;
	LinphoneUpnpState state;
} UpnpStream;

struct _UpnpSession {
	LinphoneCall *call;
	UpnpStream *audio;
	UpnpStream *video;
	LinphoneUpnpState state;
};

struct _UpnpContext {
	LinphoneCore *lc;
	upnp_igd_context *upnp_igd_ctxt;
	UpnpPortBinding *sip_tcp;
	UpnpPortBinding *sip_tls;
	UpnpPortBinding *sip_udp;
	LinphoneUpnpState state;
	bctbx_list_t *removing_configs;
	bctbx_list_t *adding_configs;
	bctbx_list_t *pending_bindings;

	ms_mutex_t mutex;
	ms_cond_t empty_cond;

	time_t last_ready_check;
	LinphoneUpnpState last_ready_state;
};


bool_t linphone_core_upnp_hook(void *data);
void linphone_upnp_update(UpnpContext *ctx);
bool_t linphone_upnp_is_blacklisted(UpnpContext *ctx);

UpnpPortBinding *linphone_upnp_port_binding_new(void);
UpnpPortBinding *linphone_upnp_port_binding_new_with_parameters(upnp_igd_ip_protocol protocol, int local_port, int external_port);
UpnpPortBinding *linphone_upnp_port_binding_new_or_collect(bctbx_list_t *list, upnp_igd_ip_protocol protocol, int local_port, int external_port);
UpnpPortBinding *linphone_upnp_port_binding_copy(const UpnpPortBinding *port);
void linphone_upnp_port_binding_set_device_id(UpnpPortBinding *port, const char * device_id);
bool_t linphone_upnp_port_binding_equal(const UpnpPortBinding *port1, const UpnpPortBinding *port2);
UpnpPortBinding *linphone_upnp_port_binding_equivalent_in_list(bctbx_list_t *list, const UpnpPortBinding *port);
UpnpPortBinding *linphone_upnp_port_binding_retain(UpnpPortBinding *port);
void linphone_upnp_update_port_binding(UpnpContext *lupnp, UpnpPortBinding **port_mapping, upnp_igd_ip_protocol protocol, int port, int retry_delay);
void linphone_upnp_port_binding_log(int level, const char *msg, const UpnpPortBinding *port);
void linphone_upnp_port_binding_release(UpnpPortBinding *port);
void linphone_upnp_update_config(UpnpContext *lupnp);
void linphone_upnp_update_proxy(UpnpContext *lupnp, bool_t force);

// Configuration
bctbx_list_t *linphone_upnp_config_list_port_bindings(struct _LpConfig *lpc, const char *device_id);
void linphone_upnp_config_add_port_binding(UpnpContext *lupnp, const UpnpPortBinding *port);
void linphone_upnp_config_remove_port_binding(UpnpContext *lupnp, const UpnpPortBinding *port);

// uPnP
int linphone_upnp_context_send_remove_port_binding(UpnpContext *lupnp, UpnpPortBinding *port, bool_t retry);
int linphone_upnp_context_send_add_port_binding(UpnpContext *lupnp, UpnpPortBinding *port, bool_t retry);

static int linphone_upnp_strncmpi(const char *str1, const char *str2, int len) {
	int i = 0;
	char char1, char2;
	while(i < len) {
		char1 = toupper(*str1);
		char2 = toupper(*str2);
		if(char1 == '\0' || char1 != char2) {
			return char1 - char2;
		}
		str1++;
		str2++;
		i++;
	}
	return 0;
}

static int linphone_upnp_str_min(const char *str1, const char *str2) {
	int len1 = strlen(str1);
	int len2 = strlen(str2);
	if(len1 > len2) {
		return len2;
	}
	return len1;
}

char * linphone_upnp_format_device_id(const char *device_id) {
	char *ret = NULL;
	char *tmp;
	char tchar;
	bool_t copy;
	if(device_id == NULL) {
		return ret;
	}
	ret = ms_new0(char, UPNP_UUID_LEN + 1);
	tmp = ret;
	if(linphone_upnp_strncmpi(device_id, "uuid:", linphone_upnp_str_min(device_id, "uuid:")) == 0) {
		device_id += strlen("uuid:");
	}
	while(*device_id != '\0' && tmp - ret < UPNP_UUID_LEN) {
		copy = FALSE;
		tchar = *device_id;
		if(tchar >= '0' && tchar <= '9')
			copy = TRUE;
		if(!copy && tchar >= 'A' && tchar <= 'Z')
			copy = TRUE;
		if(!copy && tchar >= 'a' && tchar <= 'z')
			copy = TRUE;
		if(copy) {
			*tmp = *device_id;
			tmp++;
		}
		device_id++;
	}
	*tmp = '\0';
	return ret;
}

/**
 * uPnP Callbacks
 */

/* Convert uPnP IGD logs to ortp logs */
void linphone_upnp_igd_print(void *cookie, upnp_igd_print_level level, const char *fmt, va_list list) {
	int ortp_level = ORTP_DEBUG;
	switch(level) {
	case UPNP_IGD_MESSAGE:
		ortp_level = ORTP_MESSAGE;
		break;
	case UPNP_IGD_WARNING:
		ortp_level = ORTP_DEBUG; // Too verbose otherwise
		break;
	case UPNP_IGD_ERROR:
		ortp_level = ORTP_DEBUG; // Too verbose otherwise
		break;
	default:
		break;
	}
	ortp_logv(ORTP_LOG_DOMAIN, ortp_level, fmt, list);
}

void linphone_upnp_igd_callback(void *cookie, upnp_igd_event event, void *arg) {
	UpnpContext *lupnp = (UpnpContext *)cookie;
	upnp_igd_port_mapping *mapping = NULL;
	UpnpPortBinding *port_mapping = NULL;
	const char *ip_address = NULL;
	const char *connection_status = NULL;
	bool_t nat_enabled = FALSE;
	bool_t blacklisted = FALSE;
	LinphoneUpnpState old_state;

	if(lupnp == NULL || lupnp->upnp_igd_ctxt == NULL) {
		ms_error("uPnP IGD: Invalid context in callback");
		return;
	}

	ms_mutex_lock(&lupnp->mutex);
	old_state = lupnp->state;

	switch(event) {
	case UPNP_IGD_DEVICE_ADDED:
	case UPNP_IGD_DEVICE_REMOVED:
	case UPNP_IGD_EXTERNAL_IPADDRESS_CHANGED:
	case UPNP_IGD_NAT_ENABLED_CHANGED:
	case UPNP_IGD_CONNECTION_STATUS_CHANGED:
		ip_address = upnp_igd_get_external_ipaddress(lupnp->upnp_igd_ctxt);
		connection_status = upnp_igd_get_connection_status(lupnp->upnp_igd_ctxt);
		nat_enabled = upnp_igd_get_nat_enabled(lupnp->upnp_igd_ctxt);
		blacklisted = linphone_upnp_is_blacklisted(lupnp);

		if(ip_address == NULL || connection_status == NULL) {
			ms_message("uPnP IGD: Pending");
			lupnp->state = LinphoneUpnpStatePending;
		} else if(strcasecmp(connection_status, "Connected")  || !nat_enabled) {
			ms_message("uPnP IGD: Not Available");
			lupnp->state = LinphoneUpnpStateNotAvailable;
		} else if(blacklisted) {
			ms_message("uPnP IGD: Router is blacklisted");
			lupnp->state = LinphoneUpnpStateBlacklisted;
		} else {
			ms_message("uPnP IGD: Connected");
			lupnp->state = LinphoneUpnpStateOk;
			if(old_state != LinphoneUpnpStateOk) {
				linphone_upnp_update(lupnp);
			}
		}

		break;

	case UPNP_IGD_PORT_MAPPING_ADD_SUCCESS:
		mapping = (upnp_igd_port_mapping *) arg;
		port_mapping = (UpnpPortBinding*) mapping->cookie;
		port_mapping->external_port = mapping->remote_port;
		port_mapping->state = LinphoneUpnpStateOk;
		linphone_upnp_port_binding_log(ORTP_MESSAGE, "Added port binding", port_mapping);
		linphone_upnp_config_add_port_binding(lupnp, port_mapping);

		break;

	case UPNP_IGD_PORT_MAPPING_ADD_FAILURE:
		mapping = (upnp_igd_port_mapping *) arg;
		port_mapping = (UpnpPortBinding*) mapping->cookie;
		port_mapping->external_port = -1; //Force random external port
		if(linphone_upnp_context_send_add_port_binding(lupnp, port_mapping, TRUE) != 0) {
			linphone_upnp_port_binding_log(ORTP_ERROR, "Can't add port binding", port_mapping);
		}

		break;

	case UPNP_IGD_PORT_MAPPING_REMOVE_SUCCESS:
		mapping = (upnp_igd_port_mapping *) arg;
		port_mapping = (UpnpPortBinding*) mapping->cookie;
		port_mapping->state = LinphoneUpnpStateIdle;
		linphone_upnp_port_binding_log(ORTP_MESSAGE, "Removed port binding", port_mapping);
		linphone_upnp_config_remove_port_binding(lupnp, port_mapping);

		break;

	case UPNP_IGD_PORT_MAPPING_REMOVE_FAILURE:
		mapping = (upnp_igd_port_mapping *) arg;
		port_mapping = (UpnpPortBinding*) mapping->cookie;
		if(linphone_upnp_context_send_remove_port_binding(lupnp, port_mapping, TRUE) != 0) {
			linphone_upnp_port_binding_log(ORTP_ERROR, "Can't remove port binding", port_mapping);
			linphone_upnp_config_remove_port_binding(lupnp, port_mapping);
		}

		break;

	default:
		break;
	}

	if(port_mapping != NULL) {
		/*
		 * Execute delayed actions
		 */
		if(port_mapping->to_remove) {
			if(port_mapping->state == LinphoneUpnpStateOk) {
				port_mapping->to_remove = FALSE;
				linphone_upnp_context_send_remove_port_binding(lupnp, port_mapping, FALSE);
			} else if(port_mapping->state == LinphoneUpnpStateKo) {
				port_mapping->to_remove = FALSE;
			}
		}
		if(port_mapping->to_add) {
			if(port_mapping->state == LinphoneUpnpStateIdle || port_mapping->state == LinphoneUpnpStateKo) {
				port_mapping->to_add = FALSE;
				linphone_upnp_context_send_add_port_binding(lupnp, port_mapping, FALSE);
			}
		}

		lupnp->pending_bindings = bctbx_list_remove(lupnp->pending_bindings, port_mapping);
		linphone_upnp_port_binding_release(port_mapping);
	}

	/*
	 * If there is no pending binding emit a signal
	 */
	if(lupnp->pending_bindings == NULL) {
		ms_cond_signal(&lupnp->empty_cond);
	}
	ms_mutex_unlock(&lupnp->mutex);
}


/**
 * uPnP Context
 */

UpnpContext* linphone_upnp_context_new(LinphoneCore *lc) {
	UpnpContext *lupnp = (UpnpContext *)ms_new0(UpnpContext,1);
	char address[LINPHONE_IPADDR_SIZE];
	const char*upnp_binding_address=address;
	if (linphone_core_get_local_ip_for(lc->sip_conf.ipv6_enabled ? AF_INET6 : AF_INET,NULL,address)) {
		ms_warning("Linphone core [%p] cannot guess local address for upnp, let's choice the lib",lc);
		upnp_binding_address=NULL;
	}
	ms_mutex_init(&lupnp->mutex, NULL);
	ms_cond_init(&lupnp->empty_cond, NULL);

	lupnp->last_ready_check = 0;
	lupnp->last_ready_state = LinphoneUpnpStateIdle;

	lupnp->lc = lc;
	lupnp->pending_bindings = NULL;
	lupnp->adding_configs = NULL;
	lupnp->removing_configs = NULL;
	lupnp->state = LinphoneUpnpStateIdle;
	ms_message("uPnP IGD: New %p for core %p bound to %s", lupnp, lc,upnp_binding_address);

	// Init ports
	lupnp->sip_udp = NULL;
	lupnp->sip_tcp = NULL;
	lupnp->sip_tls = NULL;

	linphone_core_add_iterate_hook(lc, linphone_core_upnp_hook, lupnp);

	lupnp->upnp_igd_ctxt = NULL;
	lupnp->upnp_igd_ctxt = upnp_igd_create(linphone_upnp_igd_callback, linphone_upnp_igd_print, address, lupnp);
	if(lupnp->upnp_igd_ctxt == NULL) {
		lupnp->state = LinphoneUpnpStateKo;
		ms_error("Can't create uPnP IGD context");
		return NULL;
	}

	lupnp->state = LinphoneUpnpStatePending;
	upnp_igd_start(lupnp->upnp_igd_ctxt);

	return lupnp;
}

void linphone_upnp_context_destroy(UpnpContext *lupnp) {
	linphone_core_remove_iterate_hook(lupnp->lc, linphone_core_upnp_hook, lupnp);

	ms_mutex_lock(&lupnp->mutex);

	if(lupnp->lc->sip_network_reachable) {
		/* Send port binding removes */
		if(lupnp->sip_udp != NULL) {
			linphone_upnp_context_send_remove_port_binding(lupnp, lupnp->sip_udp, TRUE);
		}
		if(lupnp->sip_tcp != NULL) {
			linphone_upnp_context_send_remove_port_binding(lupnp, lupnp->sip_tcp, TRUE);
		}
		if(lupnp->sip_tls != NULL) {
			linphone_upnp_context_send_remove_port_binding(lupnp, lupnp->sip_tls, TRUE);
		}
	}

	/* Wait all pending bindings are done */
	if(lupnp->pending_bindings != NULL) {
		ms_message("uPnP IGD: Wait all pending port bindings ...");
		ms_cond_wait(&lupnp->empty_cond, &lupnp->mutex);
	}
	ms_mutex_unlock(&lupnp->mutex);

	if(lupnp->upnp_igd_ctxt != NULL) {
		upnp_igd_destroy(lupnp->upnp_igd_ctxt);
		lupnp->upnp_igd_ctxt = NULL;
	}

	/* No more multi threading here */

	/* Run one more time configuration update and proxy */
	linphone_upnp_update_config(lupnp);
	linphone_upnp_update_proxy(lupnp, TRUE);

	/* Release port bindings */
	if(lupnp->sip_udp != NULL) {
		linphone_upnp_port_binding_release(lupnp->sip_udp);
		lupnp->sip_udp = NULL;
	}
	if(lupnp->sip_tcp != NULL) {
		linphone_upnp_port_binding_release(lupnp->sip_tcp);
		lupnp->sip_tcp = NULL;
	}
	if(lupnp->sip_tls != NULL) {
		linphone_upnp_port_binding_release(lupnp->sip_tls);
		lupnp->sip_tcp = NULL;
	}

	/* Release lists */
	bctbx_list_for_each(lupnp->adding_configs,(void (*)(void*))linphone_upnp_port_binding_release);
	lupnp->adding_configs = bctbx_list_free(lupnp->adding_configs);
	bctbx_list_for_each(lupnp->removing_configs,(void (*)(void*))linphone_upnp_port_binding_release);
	lupnp->removing_configs = bctbx_list_free(lupnp->removing_configs);
	bctbx_list_for_each(lupnp->pending_bindings,(void (*)(void*))linphone_upnp_port_binding_release);
	lupnp->pending_bindings = bctbx_list_free(lupnp->pending_bindings);

	ms_mutex_destroy(&lupnp->mutex);
	ms_cond_destroy(&lupnp->empty_cond);

	ms_message("uPnP IGD: destroy %p", lupnp);
	ms_free(lupnp);
}

LinphoneUpnpState linphone_upnp_context_get_state(UpnpContext *lupnp) {
	LinphoneUpnpState state = LinphoneUpnpStateKo;
	if(lupnp != NULL) {
		ms_mutex_lock(&lupnp->mutex);
		state = lupnp->state;
		ms_mutex_unlock(&lupnp->mutex);
	}
	return state;
}

bool_t _linphone_upnp_context_is_ready_for_register(UpnpContext *lupnp) {
	bool_t ready = TRUE;

	// 1 Check global uPnP state
	ready = (lupnp->state == LinphoneUpnpStateOk);

	// 2 Check external ip address
	if(ready) {
		if (upnp_igd_get_external_ipaddress(lupnp->upnp_igd_ctxt) == NULL) {
			ready = FALSE;
		}
	}

	// 3 Check sip ports bindings
	if(ready) {
		if(lupnp->sip_udp != NULL) {
			if(lupnp->sip_udp->state != LinphoneUpnpStateOk) {
				ready = FALSE;
			}
		} else if(lupnp->sip_tcp != NULL) {
			if(lupnp->sip_tcp->state != LinphoneUpnpStateOk) {
				ready = FALSE;
			}
		} else if(lupnp->sip_tls != NULL) {
			if(lupnp->sip_tls->state != LinphoneUpnpStateOk) {
				ready = FALSE;
			}
		} else {
			ready = FALSE;
		}
	}

	return ready;
}

bool_t linphone_upnp_context_is_ready_for_register(UpnpContext *lupnp) {
	bool_t ready = FALSE;
	if(lupnp != NULL) {
		ms_mutex_lock(&lupnp->mutex);
		ready = _linphone_upnp_context_is_ready_for_register(lupnp);
		ms_mutex_unlock(&lupnp->mutex);
	}
	return ready;
}

int linphone_upnp_context_get_external_port(UpnpContext *lupnp) {
	int port = -1;
	if(lupnp != NULL) {
		ms_mutex_lock(&lupnp->mutex);

		if(lupnp->sip_udp != NULL) {
			if(lupnp->sip_udp->state == LinphoneUpnpStateOk) {
				port = lupnp->sip_udp->external_port;
			}
		} else if(lupnp->sip_tcp != NULL) {
			if(lupnp->sip_tcp->state == LinphoneUpnpStateOk) {
				port = lupnp->sip_tcp->external_port;
			}
		} else if(lupnp->sip_tls != NULL) {
			if(lupnp->sip_tls->state == LinphoneUpnpStateOk) {
				port = lupnp->sip_tls->external_port;
			}
		}

		ms_mutex_unlock(&lupnp->mutex);
	}
	return port;
}

bool_t linphone_upnp_is_blacklisted(UpnpContext *lupnp) {
	const char * device_model_name = upnp_igd_get_device_model_name(lupnp->upnp_igd_ctxt);
	const char * device_model_number = upnp_igd_get_device_model_number(lupnp->upnp_igd_ctxt);
	const char * blacklist = lp_config_get_string(lupnp->lc->config, "net", "upnp_blacklist", NULL);
	bool_t blacklisted = FALSE;
	char *str;
	char *pch;
	char *model_name;
	char *model_number;

	// Sanity checks
	if(device_model_name == NULL || device_model_number == NULL || blacklist == NULL) {
		return FALSE;
	}

	// Find in the list
	str = strdup(blacklist);
	pch = strtok(str, ";");
	while (pch != NULL && !blacklisted) {
		// Extract model name & number
		model_name = pch;
		model_number = strstr(pch, ",");
		if(model_number != NULL) {
			*(model_number++) = '\0';
		}

		// Compare with current device
		if(strcmp(model_name, device_model_name) == 0) {
			if(model_number == NULL || strcmp(model_number, device_model_number) == 0) {
				blacklisted = TRUE;
			}
		}
		pch = strtok(NULL, ";");
	}
	free(str);

	return blacklisted;
}

void linphone_upnp_refresh(UpnpContext * lupnp) {
	upnp_igd_refresh(lupnp->upnp_igd_ctxt);
}

const char* linphone_upnp_context_get_external_ipaddress(UpnpContext *lupnp) {
	const char* addr = NULL;
	if(lupnp != NULL) {
		ms_mutex_lock(&lupnp->mutex);
		addr = upnp_igd_get_external_ipaddress(lupnp->upnp_igd_ctxt);
		ms_mutex_unlock(&lupnp->mutex);
	}
	return addr;
}

int linphone_upnp_context_send_add_port_binding(UpnpContext *lupnp, UpnpPortBinding *port, bool_t retry) {
	upnp_igd_port_mapping mapping;
	char description[128];
	int ret;

	if(lupnp->state != LinphoneUpnpStateOk) {
		return -2;
	}

	// Compute port binding state
	if(port->state != LinphoneUpnpStateAdding) {
		port->to_remove = FALSE;
		switch(port->state) {
			case LinphoneUpnpStateKo:
			case LinphoneUpnpStateIdle: {
				port->retry = 0;
				port->state = LinphoneUpnpStateAdding;
			}
			break;
			case LinphoneUpnpStateRemoving: {
				port->to_add = TRUE;
				return 0;
			}
			break;
			default:
				return 0;
		}
	}

	// No retry if specified
	if(port->retry != 0 && !retry) {
		return -1;
	}

	if(port->retry >= UPNP_ADD_MAX_RETRY) {
		ret = -1;
	} else {
		linphone_upnp_port_binding_set_device_id(port, upnp_igd_get_device_id(lupnp->upnp_igd_ctxt));
		mapping.cookie = linphone_upnp_port_binding_retain(port);
		lupnp->pending_bindings = bctbx_list_append(lupnp->pending_bindings, mapping.cookie);

		mapping.local_port = port->local_port;
		mapping.local_host = port->local_addr;
		if(port->external_port == -1)
			port->external_port = rand()%(0xffff - 1024) + 1024;
		mapping.remote_port = port->external_port;
		mapping.remote_host = "";
		snprintf(description, 128, "%s %s at %s:%d",
				"Linphone",
				(port->protocol == UPNP_IGD_IP_PROTOCOL_TCP)? "TCP": "UDP",
				port->local_addr, port->local_port);
		mapping.description = description;
		mapping.protocol = port->protocol;

		port->retry++;
		linphone_upnp_port_binding_log(ORTP_MESSAGE, "Try to add port binding", port);
		ret = upnp_igd_add_port_mapping(lupnp->upnp_igd_ctxt, &mapping);
	}
	if(ret != 0) {
		port->state = LinphoneUpnpStateKo;
	}
	return ret;
}

int linphone_upnp_context_send_remove_port_binding(UpnpContext *lupnp, UpnpPortBinding *port, bool_t retry) {
	upnp_igd_port_mapping mapping;
	int ret;

	if(lupnp->state != LinphoneUpnpStateOk) {
		return -2;
	}

	// Compute port binding state
	if(port->state != LinphoneUpnpStateRemoving) {
		port->to_add = FALSE;
		switch(port->state) {
			case LinphoneUpnpStateOk: {
				port->retry = 0;
				port->state = LinphoneUpnpStateRemoving;
			}
			break;
			case LinphoneUpnpStateAdding: {
				port->to_remove = TRUE;
				return 0;
			}
			break;
			default:
				return 0;
		}
	}

	// No retry if specified
	if(port->retry != 0 && !retry) {
		return 1;
	}

	if(port->retry >= UPNP_REMOVE_MAX_RETRY) {
		ret = -1;
	} else {
		linphone_upnp_port_binding_set_device_id(port, upnp_igd_get_device_id(lupnp->upnp_igd_ctxt));
		mapping.cookie = linphone_upnp_port_binding_retain(port);
		lupnp->pending_bindings = bctbx_list_append(lupnp->pending_bindings, mapping.cookie);

		mapping.remote_port = port->external_port;
		mapping.remote_host = "";
		mapping.protocol = port->protocol;
		port->retry++;
		linphone_upnp_port_binding_log(ORTP_MESSAGE, "Try to remove port binding", port);
		ret = upnp_igd_delete_port_mapping(lupnp->upnp_igd_ctxt, &mapping);
	}
	if(ret != 0) {
		port->state = LinphoneUpnpStateKo;
	}
	return ret;
}

/*
 * uPnP Core interfaces
 */

int linphone_call_update_upnp_audio_video(LinphoneCall *call, bool_t audio, bool_t video) {
	LinphoneCore *lc = call->core;
	UpnpContext *lupnp = lc->upnp;
	int ret = -1;

	if(lupnp == NULL) {
		return ret;
	}

	ms_mutex_lock(&lupnp->mutex);

	// Don't handle when the call
	if(lupnp->state == LinphoneUpnpStateOk && call->upnp_session != NULL) {
		ret = 0;

		/*
		 * Audio part
		 */
		linphone_upnp_update_port_binding(lupnp, &call->upnp_session->audio->rtp,
			UPNP_IGD_IP_PROTOCOL_UDP, (audio)? call->media_ports[call->main_audio_stream_index].rtp_port:0, UPNP_CALL_RETRY_DELAY);

		linphone_upnp_update_port_binding(lupnp, &call->upnp_session->audio->rtcp,
			UPNP_IGD_IP_PROTOCOL_UDP, (audio)? call->media_ports[call->main_audio_stream_index].rtcp_port:0, UPNP_CALL_RETRY_DELAY);

		/*
		 * Video part
		 */
		linphone_upnp_update_port_binding(lupnp, &call->upnp_session->video->rtp,
			UPNP_IGD_IP_PROTOCOL_UDP, (video)? call->media_ports[call->main_video_stream_index].rtp_port:0, UPNP_CALL_RETRY_DELAY);

		linphone_upnp_update_port_binding(lupnp, &call->upnp_session->video->rtcp,
			UPNP_IGD_IP_PROTOCOL_UDP, (video)? call->media_ports[call->main_video_stream_index].rtcp_port:0, UPNP_CALL_RETRY_DELAY);
	}

	ms_mutex_unlock(&lupnp->mutex);

	/*
	 * Update uPnP call state
	 */
	linphone_upnp_call_process(call);

	return ret;
}



int linphone_call_update_upnp_from_remote_media_description(LinphoneCall *call, const SalMediaDescription *md) {
	bool_t audio = FALSE;
	bool_t video = FALSE;
	int i;
	const SalStreamDescription *stream;

	for (i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
		stream = &md->streams[i];
		if (!sal_stream_description_active(stream)) continue;
		if(stream->type == SalAudio) {
			audio = TRUE;
		} else if(stream->type == SalVideo) {
			video = TRUE;
		}
	}

	return linphone_call_update_upnp_audio_video(call, audio, video);
}

int linphone_call_update_upnp(LinphoneCall *call) {
	return linphone_call_update_upnp_audio_video(call, call->audiostream!=NULL, call->videostream!=NULL);
}

void linphone_call_update_upnp_state_in_call_stats(LinphoneCall *call) {
	call->audio_stats->upnp_state = call->upnp_session->audio->state;
	call->video_stats->upnp_state = call->upnp_session->video->state;
}

void linphone_upnp_update_stream_state(UpnpStream *stream) {
	if((stream->rtp == NULL || stream->rtp->state == LinphoneUpnpStateOk || stream->rtp->state == LinphoneUpnpStateIdle) &&
	   (stream->rtcp == NULL || stream->rtcp->state == LinphoneUpnpStateOk || stream->rtcp->state == LinphoneUpnpStateIdle)) {
		stream->state = LinphoneUpnpStateOk;
	} else if((stream->rtp != NULL &&
	   (stream->rtp->state == LinphoneUpnpStateAdding || stream->rtp->state == LinphoneUpnpStateRemoving)) ||
		  (stream->rtcp != NULL &&
			 (stream->rtcp->state == LinphoneUpnpStateAdding || stream->rtcp->state == LinphoneUpnpStateRemoving))) {
		stream->state = LinphoneUpnpStatePending;
	} else if((stream->rtp != NULL && stream->rtp->state == LinphoneUpnpStateKo) ||
			(stream->rtcp != NULL && stream->rtcp->state == LinphoneUpnpStateKo)) {
		stream->state = LinphoneUpnpStateKo;
	} else {
		ms_error("Invalid stream %p state", stream);
	}
}

int linphone_upnp_call_process(LinphoneCall *call) {
	LinphoneCore *lc = call->core;
	UpnpContext *lupnp = lc->upnp;
	int ret = -1;
	LinphoneUpnpState oldState = 0, newState = 0;

	if(lupnp == NULL) {
		return ret;
	}

	ms_mutex_lock(&lupnp->mutex);

	// Don't handle when the call
	if(lupnp->state == LinphoneUpnpStateOk && call->upnp_session != NULL) {
		ret = 0;

		/*
		 * Update Audio state
		 */
		linphone_upnp_update_stream_state(call->upnp_session->audio);

		/*
		 * Update Video state
		 */
		linphone_upnp_update_stream_state(call->upnp_session->video);

		/*
		 * Update stat
		 */
		linphone_call_update_upnp_state_in_call_stats(call);

		/*
		 * Update session state
		 */
		oldState = call->upnp_session->state;
		if(call->upnp_session->audio->state == LinphoneUpnpStateOk &&
			call->upnp_session->video->state == LinphoneUpnpStateOk) {
			call->upnp_session->state = LinphoneUpnpStateOk;
		} else if(call->upnp_session->audio->state == LinphoneUpnpStatePending ||
				call->upnp_session->video->state == LinphoneUpnpStatePending) {
			call->upnp_session->state = LinphoneUpnpStatePending;
		} else if(call->upnp_session->audio->state == LinphoneUpnpStateKo ||
				call->upnp_session->video->state == LinphoneUpnpStateKo) {
			call->upnp_session->state = LinphoneUpnpStateKo;
		} else {
			call->upnp_session->state = LinphoneUpnpStateIdle;
		}
		newState = call->upnp_session->state;
	}

	ms_mutex_unlock(&lupnp->mutex);

	/* When change is done proceed update */
	if(oldState != LinphoneUpnpStateOk && oldState != LinphoneUpnpStateKo &&
			(newState == LinphoneUpnpStateOk || newState == LinphoneUpnpStateKo)) {
		if(call->upnp_session->state == LinphoneUpnpStateOk)
			ms_message("uPnP IGD: uPnP for Call %p is ok", call);
		else
			ms_message("uPnP IGD: uPnP for Call %p is ko", call);

		switch (call->state) {
			case LinphoneCallUpdating:
				linphone_call_start_update(call);
				break;
			case LinphoneCallUpdatedByRemote:
				linphone_call_start_accept_update(call, call->prevstate, linphone_call_state_to_string(call->prevstate));
				break;
			case LinphoneCallOutgoingInit:
				linphone_call_proceed_with_invite_if_ready(call, NULL);
				break;
			case LinphoneCallIdle:
				linphone_call_update_local_media_description_from_ice_or_upnp(call);
				sal_call_set_local_media_description(call->op,call->localdesc);
				linphone_core_notify_incoming_call(lc, call);
				break;
			default:
				break;
		}
	}

	return ret;
}

static const char *linphone_core_upnp_get_charptr_null(const char *str) {
	if(str != NULL) {
		return str;
	}
	return "(Null)";
}

void linphone_upnp_update(UpnpContext *lupnp) {
	bctbx_list_t *global_list = NULL;
	bctbx_list_t *list = NULL;
	bctbx_list_t *item;
	LinphoneCall *call;
	UpnpPortBinding *port_mapping, *port_mapping2;

	ms_message("uPnP IGD: Name:%s", linphone_core_upnp_get_charptr_null(upnp_igd_get_device_name(lupnp->upnp_igd_ctxt)));
	ms_message("uPnP IGD: Device:%s %s",
				   linphone_core_upnp_get_charptr_null(upnp_igd_get_device_model_name(lupnp->upnp_igd_ctxt)),
			   linphone_core_upnp_get_charptr_null(upnp_igd_get_device_model_number(lupnp->upnp_igd_ctxt)));
	ms_message("uPnP IGD: Refresh mappings");

	if(lupnp->sip_udp != NULL) {
		global_list = bctbx_list_append(global_list, lupnp->sip_udp);
	}
	if(lupnp->sip_tcp != NULL) {
		global_list = bctbx_list_append(global_list, lupnp->sip_tcp);
	}
	if(lupnp->sip_tls != NULL) {
		global_list = bctbx_list_append(global_list, lupnp->sip_tls);
	}

	list = lupnp->lc->calls;
	while(list != NULL) {
		call = (LinphoneCall *)list->data;
		if(call->upnp_session != NULL) {
			if(call->upnp_session->audio->rtp != NULL) {
				global_list = bctbx_list_append(global_list, call->upnp_session->audio->rtp);
			}
			if(call->upnp_session->audio->rtcp != NULL) {
				global_list = bctbx_list_append(global_list, call->upnp_session->audio->rtcp);
			}
			if(call->upnp_session->video->rtp != NULL) {
				global_list = bctbx_list_append(global_list, call->upnp_session->video->rtp);
			}
			if(call->upnp_session->video->rtcp != NULL) {
				global_list = bctbx_list_append(global_list, call->upnp_session->video->rtcp);
			}
		}
		list = list->next;
	}

	list = linphone_upnp_config_list_port_bindings(lupnp->lc->config, upnp_igd_get_device_id(lupnp->upnp_igd_ctxt));
	for(item = list;item != NULL; item = item->next) {
			port_mapping = (UpnpPortBinding *)item->data;
			port_mapping2 = linphone_upnp_port_binding_equivalent_in_list(global_list, port_mapping);
			if(port_mapping2 == NULL) {
				linphone_upnp_context_send_remove_port_binding(lupnp, port_mapping, TRUE);
			} else if(port_mapping2->state == LinphoneUpnpStateIdle){
				/* Force to remove */
				port_mapping2->state = LinphoneUpnpStateOk;
			}
	}
	bctbx_list_for_each(list, (void (*)(void*))linphone_upnp_port_binding_release);
	list = bctbx_list_free(list);


	// (Re)Add removed port bindings
	list = global_list;
	while(list != NULL) {
		port_mapping = (UpnpPortBinding *)list->data;
		linphone_upnp_context_send_remove_port_binding(lupnp, port_mapping, TRUE);
		linphone_upnp_context_send_add_port_binding(lupnp, port_mapping, TRUE);
		list = list->next;
	}
	global_list = bctbx_list_free(global_list);
}

void linphone_upnp_update_port_binding(UpnpContext *lupnp, UpnpPortBinding **port_mapping, upnp_igd_ip_protocol protocol, int port, int retry_delay) {
	const char *local_addr, *external_addr;
	time_t now = time(NULL);
	if(port != 0) {
		if(*port_mapping != NULL) {
			if(port != (*port_mapping)->local_port) {
				linphone_upnp_context_send_remove_port_binding(lupnp, *port_mapping, FALSE);
				*port_mapping = NULL;
			}
		}
		if(*port_mapping == NULL) {
			*port_mapping = linphone_upnp_port_binding_new_or_collect(lupnp->pending_bindings, protocol, port, port);
		}

		// Get addresses
		local_addr = upnp_igd_get_local_ipaddress(lupnp->upnp_igd_ctxt);
		external_addr = upnp_igd_get_external_ipaddress(lupnp->upnp_igd_ctxt);

		// Force binding update on local address change
		if(local_addr != NULL) {
			if(strncmp((*port_mapping)->local_addr, local_addr, sizeof((*port_mapping)->local_addr))) {
				linphone_upnp_context_send_remove_port_binding(lupnp, *port_mapping, FALSE);
				strncpy((*port_mapping)->local_addr, local_addr, sizeof((*port_mapping)->local_addr));
			}
		}
		if(external_addr != NULL) {
			strncpy((*port_mapping)->external_addr, external_addr, sizeof((*port_mapping)->external_addr));
		}

		// Add (if not already done) the binding
		if(now - (*port_mapping)->last_update >= retry_delay) {
			(*port_mapping)->last_update = now;
			linphone_upnp_context_send_add_port_binding(lupnp, *port_mapping, FALSE);
		}
	} else {
		if(*port_mapping != NULL) {
			linphone_upnp_context_send_remove_port_binding(lupnp, *port_mapping, FALSE);
			*port_mapping = NULL;
		}
	}
}

void linphone_upnp_update_config(UpnpContext* lupnp) {
	char key[64];
	const bctbx_list_t *item;
	UpnpPortBinding *port_mapping;

	/* Add configs */
	for(item = lupnp->adding_configs;item!=NULL;item=item->next) {
		port_mapping = (UpnpPortBinding *)item->data;
		snprintf(key, sizeof(key), "%s-%s-%d-%d",
					port_mapping->device_id,
					(port_mapping->protocol == UPNP_IGD_IP_PROTOCOL_TCP)? "TCP":"UDP",
					port_mapping->external_port,
					port_mapping->local_port);
		lp_config_set_string(lupnp->lc->config, UPNP_SECTION_NAME, key, "uPnP");
		linphone_upnp_port_binding_log(ORTP_DEBUG, "Configuration: Added port binding", port_mapping);
	}
	bctbx_list_for_each(lupnp->adding_configs,(void (*)(void*))linphone_upnp_port_binding_release);
	lupnp->adding_configs = bctbx_list_free(lupnp->adding_configs);

	/* Remove configs */
	for(item = lupnp->removing_configs;item!=NULL;item=item->next) {
		port_mapping = (UpnpPortBinding *)item->data;
		snprintf(key, sizeof(key), "%s-%s-%d-%d",
					port_mapping->device_id,
					(port_mapping->protocol == UPNP_IGD_IP_PROTOCOL_TCP)? "TCP":"UDP",
					port_mapping->external_port,
					port_mapping->local_port);
		lp_config_set_string(lupnp->lc->config, UPNP_SECTION_NAME, key, NULL);
		linphone_upnp_port_binding_log(ORTP_DEBUG, "Configuration: Removed port binding", port_mapping);
	}
	bctbx_list_for_each(lupnp->removing_configs,(void (*)(void*))linphone_upnp_port_binding_release);
	lupnp->removing_configs = bctbx_list_free(lupnp->removing_configs);
}

void linphone_upnp_update_proxy(UpnpContext* lupnp, bool_t force) {
	LinphoneUpnpState ready_state;
	const bctbx_list_t *item;
	time_t now = (force)? (lupnp->last_ready_check + UPNP_CORE_READY_CHECK) : time(NULL);

	/* Refresh registers if we are ready */
	if(now - lupnp->last_ready_check >= UPNP_CORE_READY_CHECK) {
		lupnp->last_ready_check = now;
		ready_state = (_linphone_upnp_context_is_ready_for_register(lupnp))? LinphoneUpnpStateOk: LinphoneUpnpStateKo;
		if(ready_state != lupnp->last_ready_state) {
			for(item=linphone_core_get_proxy_config_list(lupnp->lc);item!=NULL;item=item->next) {
				LinphoneProxyConfig *cfg=(LinphoneProxyConfig*)item->data;
				if (linphone_proxy_config_register_enabled(cfg)) {
					if (ready_state != LinphoneUpnpStateOk) {
						// Only reset ithe registration if we require that upnp should be ok
						if(lupnp->lc->sip_conf.register_only_when_upnp_is_ok) {
							linphone_proxy_config_set_state(cfg, LinphoneRegistrationNone, "Registration impossible (uPnP not ready)");
						} else {
							cfg->commit=TRUE;
						}
					} else {
						cfg->commit=TRUE;
					}
				}
			}
			lupnp->last_ready_state = ready_state;
		}
	}
}

bool_t linphone_core_upnp_hook(void *data) {
	LCSipTransports transport;
	UpnpContext *lupnp = (UpnpContext *)data;

	ms_mutex_lock(&lupnp->mutex);

	/* Update ports */
	if(lupnp->state == LinphoneUpnpStateOk) {
		linphone_core_get_sip_transports(lupnp->lc, &transport);
		linphone_upnp_update_port_binding(lupnp, &lupnp->sip_udp, UPNP_IGD_IP_PROTOCOL_UDP, transport.udp_port, UPNP_CORE_RETRY_DELAY);
		linphone_upnp_update_port_binding(lupnp, &lupnp->sip_tcp, UPNP_IGD_IP_PROTOCOL_TCP, transport.tcp_port, UPNP_CORE_RETRY_DELAY);
		linphone_upnp_update_port_binding(lupnp, &lupnp->sip_tls, UPNP_IGD_IP_PROTOCOL_TCP, transport.tls_port, UPNP_CORE_RETRY_DELAY);
	}

	linphone_upnp_update_proxy(lupnp, FALSE);
	linphone_upnp_update_config(lupnp);

	ms_mutex_unlock(&lupnp->mutex);
	return TRUE;
}

int linphone_call_update_local_media_description_from_upnp(SalMediaDescription *desc, UpnpSession *session) {
	int i;
	SalStreamDescription *stream;
	UpnpStream *upnpStream;

	for (i = 0; i < SAL_MEDIA_DESCRIPTION_MAX_STREAMS; i++) {
		stream = &desc->streams[i];
		if (!sal_stream_description_active(stream)) continue;
		upnpStream = NULL;
		if(stream->type == SalAudio) {
			upnpStream = session->audio;
		} else if(stream->type == SalVideo) {
			upnpStream = session->video;
		}
		if(upnpStream != NULL) {
			if(upnpStream->rtp != NULL && upnpStream->rtp->state == LinphoneUpnpStateOk) {
				strncpy(stream->rtp_addr, upnpStream->rtp->external_addr, LINPHONE_IPADDR_SIZE);
				stream->rtp_port = upnpStream->rtp->external_port;
			}
			if(upnpStream->rtcp != NULL && upnpStream->rtcp->state == LinphoneUpnpStateOk) {
				strncpy(stream->rtcp_addr, upnpStream->rtcp->external_addr, LINPHONE_IPADDR_SIZE);
				stream->rtcp_port = upnpStream->rtcp->external_port;
			}
		}
	}
	return 0;
}


/*
 * uPnP Port Binding
 */

UpnpPortBinding *linphone_upnp_port_binding_new(void) {
	UpnpPortBinding *port = NULL;
	port = ms_new0(UpnpPortBinding,1);
	ms_mutex_init(&port->mutex, NULL);
	port->state = LinphoneUpnpStateIdle;
	port->protocol = UPNP_IGD_IP_PROTOCOL_UDP;
	port->device_id = NULL;
	port->local_addr[0] = '\0';
	port->local_port = -1;
	port->external_addr[0] = '\0';
	port->external_port = -1;
	port->to_remove = FALSE;
	port->to_add = FALSE;
	port->ref = 1;
	port->last_update = 0;
	return port;
}

UpnpPortBinding *linphone_upnp_port_binding_new_with_parameters(upnp_igd_ip_protocol protocol, int local_port, int external_port) {
	UpnpPortBinding *port_binding = linphone_upnp_port_binding_new();
	port_binding->protocol = protocol;
	port_binding->local_port = local_port;
	port_binding->external_port = external_port;
	return port_binding;
}

UpnpPortBinding *linphone_upnp_port_binding_new_or_collect(bctbx_list_t *list, upnp_igd_ip_protocol protocol, int local_port, int external_port) {
	UpnpPortBinding *tmp_binding;
	UpnpPortBinding *end_binding;

	// Seek an binding with same protocol and local port
	end_binding = linphone_upnp_port_binding_new_with_parameters(protocol, local_port, -1);
	tmp_binding = linphone_upnp_port_binding_equivalent_in_list(list, end_binding);

	// Must be not attached to any struct
	if(tmp_binding != NULL && tmp_binding->ref == 1) {
		linphone_upnp_port_binding_release(end_binding);
		end_binding = linphone_upnp_port_binding_retain(tmp_binding);
	} else {
		end_binding->external_port = external_port;
	}
	return end_binding;
}

UpnpPortBinding *linphone_upnp_port_binding_copy(const UpnpPortBinding *port) {
	UpnpPortBinding *new_port = NULL;
	new_port = ms_new0(UpnpPortBinding,1);
	memcpy(new_port, port, sizeof(UpnpPortBinding));
	new_port->device_id = NULL;
	linphone_upnp_port_binding_set_device_id(new_port, port->device_id);
	ms_mutex_init(&new_port->mutex, NULL);
	new_port->ref = 1;
	return new_port;
}

void linphone_upnp_port_binding_set_device_id(UpnpPortBinding *port, const char *device_id) {
	char *formated_device_id = linphone_upnp_format_device_id(device_id);
	if(formated_device_id != NULL && port->device_id != NULL) {
		if(strcmp(formated_device_id, port->device_id) == 0) {
			ms_free(formated_device_id);
			return;
		}
	}
	if(port->device_id != NULL) {
		ms_free(port->device_id);
	}
	port->device_id = formated_device_id;
}

void linphone_upnp_port_binding_log(int level, const char *msg, const UpnpPortBinding *port) {
	if(strlen(port->local_addr)) {
		ortp_log(level, "uPnP IGD: %s %s|%d->%s:%d (retry %d)", msg,
							(port->protocol == UPNP_IGD_IP_PROTOCOL_TCP)? "TCP":"UDP",
									port->external_port,
									port->local_addr,
									port->local_port,
									port->retry - 1);
	} else {
		ortp_log(level, "uPnP IGD: %s %s|%d->%d (retry %d)", msg,
							(port->protocol == UPNP_IGD_IP_PROTOCOL_TCP)? "TCP":"UDP",
									port->external_port,
									port->local_port,
									port->retry - 1);
	}
}

// Return true if the binding are equivalent. (Note external_port == -1 means "don't care")
bool_t linphone_upnp_port_binding_equal(const UpnpPortBinding *port1, const UpnpPortBinding *port2) {
	return port1->protocol == port2->protocol &&
		   port1->local_port == port2->local_port &&
		   (port1->external_port == -1 || port2->external_port == -1 || port1->external_port == port2->external_port);
}

UpnpPortBinding *linphone_upnp_port_binding_equivalent_in_list(bctbx_list_t *list, const UpnpPortBinding *port) {
	UpnpPortBinding *port_mapping;
	while(list != NULL) {
		port_mapping = (UpnpPortBinding *)list->data;
		if(linphone_upnp_port_binding_equal(port, port_mapping)) {
			return port_mapping;
		}
		list = list->next;
	}

	return NULL;
}

UpnpPortBinding *linphone_upnp_port_binding_retain(UpnpPortBinding *port) {
	ms_mutex_lock(&port->mutex);
	port->ref++;
	ms_mutex_unlock(&port->mutex);
	return port;
}

void linphone_upnp_port_binding_release(UpnpPortBinding *port) {
	ms_mutex_lock(&port->mutex);
	if(--port->ref == 0) {
		if(port->device_id != NULL) {
			ms_free(port->device_id);
		}
		ms_mutex_unlock(&port->mutex);
		ms_mutex_destroy(&port->mutex);
		ms_free(port);
		return;
	}
	ms_mutex_unlock(&port->mutex);
}


/*
 * uPnP Stream
 */

UpnpStream* linphone_upnp_stream_new(void) {
	UpnpStream *stream = ms_new0(UpnpStream,1);
	stream->state = LinphoneUpnpStateIdle;
	stream->rtp = NULL;
	stream->rtcp = NULL;
	return stream;
}

void linphone_upnp_stream_destroy(UpnpStream* stream) {
	if(stream->rtp != NULL) {
		linphone_upnp_port_binding_release(stream->rtp);
		stream->rtp = NULL;
	}
	if(stream->rtcp != NULL) {
		linphone_upnp_port_binding_release(stream->rtcp);
		stream->rtcp = NULL;
	}
	ms_free(stream);
}


/*
 * uPnP Session
 */

UpnpSession* linphone_upnp_session_new(LinphoneCall* call) {
	UpnpSession *session = ms_new0(UpnpSession,1);
	session->call = call;
	session->state = LinphoneUpnpStateIdle;
	session->audio = linphone_upnp_stream_new();
	session->video = linphone_upnp_stream_new();
	return session;
}

void linphone_upnp_session_destroy(UpnpSession *session) {
	LinphoneCore *lc = session->call->core;

	if(lc->upnp != NULL) {
		/* Remove bindings */
		if(session->audio->rtp != NULL) {
			linphone_upnp_context_send_remove_port_binding(lc->upnp, session->audio->rtp, TRUE);
		}
		if(session->audio->rtcp != NULL) {
			linphone_upnp_context_send_remove_port_binding(lc->upnp, session->audio->rtcp, TRUE);
		}
		if(session->video->rtp != NULL) {
			linphone_upnp_context_send_remove_port_binding(lc->upnp, session->video->rtp, TRUE);
		}
		if(session->video->rtcp != NULL) {
			linphone_upnp_context_send_remove_port_binding(lc->upnp, session->video->rtcp, TRUE);
		}
	}

	session->call->audio_stats->upnp_state = LinphoneUpnpStateKo;
	session->call->video_stats->upnp_state = LinphoneUpnpStateKo;

	linphone_upnp_stream_destroy(session->audio);
	linphone_upnp_stream_destroy(session->video);
	ms_free(session);
}

LinphoneUpnpState linphone_upnp_session_get_state(UpnpSession *session) {
	return session->state;
}


/*
 * uPnP Config
 */

struct linphone_upnp_config_list_port_bindings_struct {
	struct _LpConfig *lpc;
	bctbx_list_t *retList;
	const char *device_id;
};

static void linphone_upnp_config_list_port_bindings_cb(const char *entry, struct linphone_upnp_config_list_port_bindings_struct *cookie) {
	char device_id[UPNP_UUID_LEN + 1];
	char protocol_str[4]; // TCP or UDP
	upnp_igd_ip_protocol protocol;
	int external_port;
	int local_port;
	int ret;
	bool_t valid = TRUE;
	UpnpPortBinding *port;

	ret = sscanf(entry, "%"UPNP_UUID_LEN_STR"[^-]-%3s-%i-%i", device_id, protocol_str, &external_port, &local_port);
	if(ret == 4) {
		// Handle only wanted device bindings
		if(device_id != NULL && strcmp(cookie->device_id, device_id) != 0) {
			return;
		}
		if(linphone_upnp_strncmpi(protocol_str, "TCP", 3) == 0) {
			protocol = UPNP_IGD_IP_PROTOCOL_TCP;
		} else if(linphone_upnp_strncmpi(protocol_str, "UDP", 3) == 0) {
			protocol = UPNP_IGD_IP_PROTOCOL_UDP;
		} else {
			valid = FALSE;
		}
		if(valid) {
			port = linphone_upnp_port_binding_new();
			linphone_upnp_port_binding_set_device_id(port, device_id);
			port->state = LinphoneUpnpStateOk;
			port->protocol = protocol;
			port->external_port = external_port;
			port->local_port = local_port;
			cookie->retList = bctbx_list_append(cookie->retList, port);
		}
	} else {
		valid = FALSE;
	}
	if(!valid) {
		ms_warning("uPnP configuration invalid line: %s", entry);
	}
}

bctbx_list_t *linphone_upnp_config_list_port_bindings(struct _LpConfig *lpc, const char *device_id) {
	char *formated_device_id = linphone_upnp_format_device_id(device_id);
	struct linphone_upnp_config_list_port_bindings_struct cookie = {lpc, NULL, formated_device_id};
	lp_config_for_each_entry(lpc, UPNP_SECTION_NAME, (void(*)(const char *, void*))linphone_upnp_config_list_port_bindings_cb, &cookie);
	ms_free(formated_device_id);
	return cookie.retList;
}

void linphone_upnp_config_add_port_binding(UpnpContext *lupnp, const UpnpPortBinding *port) {
	bctbx_list_t *list;
	UpnpPortBinding *list_port;

	if(port->device_id == NULL) {
		ms_error("Can't remove port binding without device_id");
		return;
	}

	list = lupnp->removing_configs;
	while(list != NULL) {
		list_port = (UpnpPortBinding *)list->data;
		if(linphone_upnp_port_binding_equal(list_port, port) == TRUE) {
			lupnp->removing_configs = bctbx_list_remove(lupnp->removing_configs, list_port);
			linphone_upnp_port_binding_release(list_port);
			return;
		}
		list = bctbx_list_next(list);
	}

	list = lupnp->adding_configs;
	while(list != NULL) {
		list_port = (UpnpPortBinding *)list->data;
		if(linphone_upnp_port_binding_equal(list_port, port) == TRUE) {
			return;
		}
		list = bctbx_list_next(list);
	}

	list_port = linphone_upnp_port_binding_copy(port);
	lupnp->adding_configs = bctbx_list_append(lupnp->adding_configs, list_port);
}

void linphone_upnp_config_remove_port_binding(UpnpContext *lupnp, const UpnpPortBinding *port) {
	bctbx_list_t *list;
	UpnpPortBinding *list_port;

	if(port->device_id == NULL) {
		ms_error("Can't remove port binding without device_id");
		return;
	}

	list = lupnp->adding_configs;
	while(list != NULL) {
		list_port = (UpnpPortBinding *)list->data;
		if(linphone_upnp_port_binding_equal(list_port, port) == TRUE) {
			lupnp->adding_configs = bctbx_list_remove(lupnp->adding_configs, list_port);
			linphone_upnp_port_binding_release(list_port);
			return;
		}
		list = bctbx_list_next(list);
	}

	list = lupnp->removing_configs;
	while(list != NULL) {
		list_port = (UpnpPortBinding *)list->data;
		if(linphone_upnp_port_binding_equal(list_port, port) == TRUE) {
			return;
		}
		list = bctbx_list_next(list);
	}

	list_port = linphone_upnp_port_binding_copy(port);
	lupnp->removing_configs = bctbx_list_append(lupnp->removing_configs, list_port);
}
