/* GStreamer
 * Copyright (C) 2017 Matthew Waters <matthew@centricular.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "nice.h"
#include "nicestream.h"
#include "niceutils.h"
/* libnice */
#include <agent.h>

#define HTTP_PROXY_PORT_DEFAULT 3128
#define MAX_CLOSING_TIME_MILLI_SECONDS 2 * 1000 /* limit closing procedure to 2s */

typedef struct
{
  GMutex mutex;                 /* Mutex for guarding count */
  GCond cond;                   /* Condition for signaling that all resolves have finished */
  guint count;
  gboolean cancelled;
} OutstandingResolves;

/* XXX:
 *
 * - are locally generated remote candidates meant to be readded to libnice?
 */

static GstUri *_validate_turn_server (GstWebRTCNice * ice, const gchar * s);

#define GST_CAT_DEFAULT gst_webrtc_nice_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);

enum
{
  PROP_0,
  PROP_AGENT,
  PROP_ICE_TCP,
  PROP_ICE_UDP,
  PROP_MIN_RTP_PORT,
  PROP_MAX_RTP_PORT,
};

struct _GstWebRTCNicePrivate
{
  NiceAgent *nice_agent;

  GArray *nice_stream_map;

  GThread *thread;
  GMainContext *main_context;
  GMainLoop *loop;
  GMutex lock;
  GCond cond;

  GstWebRTCICEOnCandidateFunc on_candidate;
  gpointer on_candidate_data;
  GDestroyNotify on_candidate_notify;

  GstUri *stun_server;
  GstUri *turn_server;

  GHashTable *turn_servers;

  GstUri *http_proxy;

  gchar *remote_ufrag;
  gchar *remote_pwd;

  GCancellable *resolve_cancellable;
  OutstandingResolves *outstanding_resolves;    /* keeps track of uncompleted resolve tasks */
};

#define gst_webrtc_nice_parent_class parent_class
G_DEFINE_TYPE_WITH_CODE (GstWebRTCNice, gst_webrtc_nice,
    GST_TYPE_WEBRTC_ICE, G_ADD_PRIVATE (GstWebRTCNice)
    GST_DEBUG_CATEGORY_INIT (gst_webrtc_nice_debug, "webrtcnice", 0,
        "webrtcnice"););

static OutstandingResolves *
outstanding_resolves_ref (OutstandingResolves * r)
{
  return g_atomic_rc_box_acquire (r);
}

static void
outstanding_resolves_free (OutstandingResolves * r)
{
  g_cond_clear (&r->cond);
  g_mutex_clear (&r->mutex);
}

static void
outstanding_resolves_unref (OutstandingResolves * r)
{
  g_atomic_rc_box_release_full (r, (GDestroyNotify) outstanding_resolves_free);
}

static void
outstanding_resolves_dec (OutstandingResolves * r)
{
  g_mutex_lock (&r->mutex);
  r->count--;
  if (r->count == 0)
    g_cond_signal (&r->cond);
  g_mutex_unlock (&r->mutex);
}

static gboolean
outstanding_resolves_try_inc (OutstandingResolves * r)
{
  gboolean ret = FALSE;
  g_mutex_lock (&r->mutex);
  if (!r->cancelled) {
    r->count++;
    ret = TRUE;
  }
  g_mutex_unlock (&r->mutex);

  return ret;
}

static void
outstanding_resolves_wait (OutstandingResolves * r)
{
  g_mutex_lock (&r->mutex);
  r->cancelled = TRUE;
  while (r->count != 0)
    g_cond_wait (&r->cond, &r->mutex);
  g_mutex_unlock (&r->mutex);
}

static gboolean
_unlock_pc_thread (GMutex * lock)
{
  g_mutex_unlock (lock);
  return G_SOURCE_REMOVE;
}

static gpointer
_gst_nice_thread (GstWebRTCNice * ice)
{
  g_mutex_lock (&ice->priv->lock);
  ice->priv->main_context = g_main_context_new ();
  ice->priv->loop = g_main_loop_new (ice->priv->main_context, FALSE);

  g_cond_broadcast (&ice->priv->cond);
  g_main_context_invoke (ice->priv->main_context,
      (GSourceFunc) _unlock_pc_thread, &ice->priv->lock);

  g_main_context_push_thread_default (ice->priv->main_context);
  g_main_loop_run (ice->priv->loop);
  g_main_context_pop_thread_default (ice->priv->main_context);

  g_mutex_lock (&ice->priv->lock);
  g_main_context_unref (ice->priv->main_context);
  ice->priv->main_context = NULL;
  g_main_loop_unref (ice->priv->loop);
  ice->priv->loop = NULL;
  g_cond_broadcast (&ice->priv->cond);
  g_mutex_unlock (&ice->priv->lock);

  return NULL;
}

static void
_start_thread (GstWebRTCNice * ice)
{
  g_mutex_lock (&ice->priv->lock);
  ice->priv->thread = g_thread_new (GST_OBJECT_NAME (ice),
      (GThreadFunc) _gst_nice_thread, ice);

  while (!ice->priv->loop)
    g_cond_wait (&ice->priv->cond, &ice->priv->lock);
  g_mutex_unlock (&ice->priv->lock);
}

static void
_stop_thread (GstWebRTCNice * ice)
{
  g_mutex_lock (&ice->priv->lock);
  g_main_loop_quit (ice->priv->loop);
  while (ice->priv->loop)
    g_cond_wait (&ice->priv->cond, &ice->priv->lock);
  g_mutex_unlock (&ice->priv->lock);

  g_thread_unref (ice->priv->thread);
}

struct NiceStreamItem
{
  guint session_id;
  guint nice_stream_id;
  GstWebRTCICEStream *stream;
};

/* TRUE to continue, FALSE to stop */
typedef gboolean (*NiceStreamItemForeachFunc) (struct NiceStreamItem * item,
    gpointer user_data);

static void
_nice_stream_item_foreach (GstWebRTCNice * ice, NiceStreamItemForeachFunc func,
    gpointer data)
{
  int i, len;

  len = ice->priv->nice_stream_map->len;
  for (i = 0; i < len; i++) {
    struct NiceStreamItem *item =
        &g_array_index (ice->priv->nice_stream_map, struct NiceStreamItem,
        i);

    if (!func (item, data))
      break;
  }
}

/* TRUE for match, FALSE otherwise */
typedef gboolean (*NiceStreamItemFindFunc) (struct NiceStreamItem * item,
    gpointer user_data);

struct nice_find
{
  NiceStreamItemFindFunc func;
  gpointer data;
  struct NiceStreamItem *ret;
};

static gboolean
_find_nice_item (struct NiceStreamItem *item, gpointer user_data)
{
  struct nice_find *f = user_data;
  if (f->func (item, f->data)) {
    f->ret = item;
    return FALSE;
  }
  return TRUE;
}

static struct NiceStreamItem *
_nice_stream_item_find (GstWebRTCNice * ice, NiceStreamItemFindFunc func,
    gpointer data)
{
  struct nice_find f;

  f.func = func;
  f.data = data;
  f.ret = NULL;

  _nice_stream_item_foreach (ice, _find_nice_item, &f);

  return f.ret;
}

#define NICE_MATCH_INIT { -1, -1, NULL }

static gboolean
_match (struct NiceStreamItem *item, struct NiceStreamItem *m)
{
  if (m->session_id != -1 && m->session_id != item->session_id)
    return FALSE;
  if (m->nice_stream_id != -1 && m->nice_stream_id != item->nice_stream_id)
    return FALSE;
  if (m->stream != NULL && m->stream != item->stream)
    return FALSE;

  return TRUE;
}

static struct NiceStreamItem *
_find_item (GstWebRTCNice * ice, guint session_id, guint nice_stream_id,
    GstWebRTCICEStream * stream)
{
  struct NiceStreamItem m = NICE_MATCH_INIT;

  m.session_id = session_id;
  m.nice_stream_id = nice_stream_id;
  m.stream = stream;

  return _nice_stream_item_find (ice, (NiceStreamItemFindFunc) _match, &m);
}

static struct NiceStreamItem *
_create_nice_stream_item (GstWebRTCNice * ice, guint session_id)
{
  struct NiceStreamItem item;

  item.session_id = session_id;
  item.nice_stream_id = nice_agent_add_stream (ice->priv->nice_agent, 1);
  item.stream =
      GST_WEBRTC_ICE_STREAM (gst_webrtc_nice_stream_new (GST_WEBRTC_ICE (ice),
          item.nice_stream_id)
      );

  g_array_append_val (ice->priv->nice_stream_map, item);

  return _find_item (ice, item.session_id, item.nice_stream_id, item.stream);
}

static void
_parse_userinfo (const gchar * userinfo, gchar ** user, gchar ** pass)
{
  const gchar *colon;

  if (!userinfo) {
    *user = NULL;
    *pass = NULL;
    return;
  }

  colon = g_strstr_len (userinfo, -1, ":");
  if (!colon) {
    *user = g_uri_unescape_string (userinfo, NULL);
    *pass = NULL;
    return;
  }

  /* Check that the first occurence is also the last occurence */
  if (colon != g_strrstr (userinfo, ":"))
    GST_WARNING ("userinfo %s contains more than one ':', will assume that the "
        "first ':' delineates user:pass. You should escape the user and pass "
        "before adding to the URI.", userinfo);

  *user = g_uri_unescape_segment (userinfo, colon, NULL);
  *pass = g_uri_unescape_string (&colon[1], NULL);
}

typedef void (*GstResolvedCallback)
  (GstWebRTCNice * nice, GList * addresses, GError * error, gpointer user_data);

struct resolve_host_data
{
  GWeakRef nice_weak;
  char *host;
  gboolean main_context_handled;
  GstResolvedCallback resolved_callback;
  gpointer user_data;
  GDestroyNotify notify;
  OutstandingResolves *outstanding_resolves;
};

static struct resolve_host_data *
resolve_host_data_new (GstWebRTCNice * ice, const char *host)
{
  struct resolve_host_data *rh =
      g_atomic_rc_box_new0 (struct resolve_host_data);

  g_weak_ref_init (&rh->nice_weak, ice);
  rh->host = g_strdup (host);

  return rh;
}

static struct resolve_host_data *
resolve_host_data_ref (struct resolve_host_data *rh)
{
  return (struct resolve_host_data *) g_atomic_rc_box_acquire (rh);
}

static void
resolve_host_data_clear (struct resolve_host_data *rh)
{
  GST_TRACE ("Freeing data %p for resolving host %s", rh, rh->host);

  if (rh->notify)
    rh->notify (rh->user_data);

  g_weak_ref_clear (&rh->nice_weak);
  g_free (rh->host);
}

static void
resolve_host_data_unref (struct resolve_host_data *rh)
{
  g_atomic_rc_box_release_full (rh, (GDestroyNotify) resolve_host_data_clear);
}

static void
on_resolve_host (GResolver * resolver, GAsyncResult * res, gpointer user_data)
{
  struct resolve_host_data *rh = user_data;
  GstWebRTCNice *nice = g_weak_ref_get (&rh->nice_weak);
  GError *error = NULL;
  GList *addresses;

  outstanding_resolves_dec (rh->outstanding_resolves);
  outstanding_resolves_unref (rh->outstanding_resolves);

  if (!nice) {
    error = g_error_new_literal (G_IO_ERROR, G_IO_ERROR_CANCELLED, "Cancelled");
    rh->resolved_callback (NULL, NULL, error, rh->user_data);
    resolve_host_data_unref (rh);
    g_error_free (error);

    return;
  }

  if (!(addresses = g_resolver_lookup_by_name_finish (resolver, res, &error))) {
    GST_ERROR ("failed to resolve: %s", error->message);

    rh->resolved_callback (nice, NULL, error, rh->user_data);
    gst_object_unref (nice);
    resolve_host_data_unref (rh);
    g_error_free (error);

    return;
  }

  GST_DEBUG_OBJECT (nice, "Resolved %d addresses for host %s with data %p",
      g_list_length (addresses), rh->host, rh);

  rh->resolved_callback (nice, addresses, error, rh->user_data);
  gst_object_unref (nice);
  resolve_host_data_unref (rh);
  g_resolver_free_addresses (addresses);
}

static gboolean
resolve_host_main_cb (gpointer user_data)
{
  GResolver *resolver = g_resolver_get_default ();
  struct resolve_host_data *rh = user_data;
  GstWebRTCNice *nice = g_weak_ref_get (&rh->nice_weak);

  if (nice && outstanding_resolves_try_inc (rh->outstanding_resolves)) {
    /* no need to error anymore if the main context disappears and this task is
     * not run */
    rh->main_context_handled = TRUE;

    GST_DEBUG_OBJECT (nice, "Resolving host %s", rh->host);
    g_resolver_lookup_by_name_async (resolver, rh->host,
        nice->priv->resolve_cancellable, (GAsyncReadyCallback) on_resolve_host,
        resolve_host_data_ref (rh));
    gst_object_unref (nice);
  } else {
    outstanding_resolves_unref (rh->outstanding_resolves);
  }

  return G_SOURCE_REMOVE;
}

static void
error_resolve_if_unhandled (struct resolve_host_data *rh)
{
  GstWebRTCNice *nice = g_weak_ref_get (&rh->nice_weak);

  if (!rh->main_context_handled) {
    if (nice) {
      GST_DEBUG_OBJECT (nice, "host resolve for %s with data %p was never "
          "executed, main context quit?", rh->host, rh);
    } else {
      GST_DEBUG ("host resolve for %s with data %p was never "
          "executed, main context quit?", rh->host, rh);
    }

    GError *error =
        g_error_new_literal (G_IO_ERROR, G_IO_ERROR_CANCELLED, "Cancelled");
    rh->resolved_callback (nice, NULL, error, rh->user_data);
    g_error_free (error);
  }

  if (nice)
    gst_object_unref (nice);
  resolve_host_data_unref (rh);
}

static void
resolve_host_async (GstWebRTCNice * nice, const gchar * host,
    GstResolvedCallback resolved_callback, gpointer user_data,
    GDestroyNotify notify)
{
  struct resolve_host_data *rh = resolve_host_data_new (nice, host);

  rh->resolved_callback = resolved_callback;
  rh->user_data = user_data;
  rh->notify = notify;
  rh->outstanding_resolves =
      outstanding_resolves_ref (nice->priv->outstanding_resolves);

  GST_TRACE_OBJECT (nice, "invoking main context for resolving host %s "
      "with data %p", host, rh);
  g_main_context_invoke_full (nice->priv->main_context, G_PRIORITY_DEFAULT,
      resolve_host_main_cb, rh, (GDestroyNotify) error_resolve_if_unhandled);
}

static void
_add_turn_server (GstWebRTCNice * ice, struct NiceStreamItem *item,
    GstUri * turn_server)
{
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  const gchar *host;
  NiceRelayType relays[4] = { 0, };
  gchar *user, *pass;
  const gchar *userinfo, *transport, *scheme;
  int i, relay_n = 0;

  host = gst_uri_get_host (turn_server);
  if (!host) {
    GST_ERROR_OBJECT (ice, "Turn server has no host");
    return;
  }

  scheme = gst_uri_get_scheme (turn_server);
  transport = gst_uri_get_query_value (turn_server, "transport");
  userinfo = gst_uri_get_userinfo (turn_server);
  _parse_userinfo (userinfo, &user, &pass);

  if (g_strcmp0 (scheme, "turns") == 0) {
    relays[relay_n++] = NICE_RELAY_TYPE_TURN_TLS;
  } else if (g_strcmp0 (scheme, "turn") == 0) {
    if (!transport || g_strcmp0 (transport, "udp") == 0)
      relays[relay_n++] = NICE_RELAY_TYPE_TURN_UDP;
    if (!transport || g_strcmp0 (transport, "tcp") == 0)
      relays[relay_n++] = NICE_RELAY_TYPE_TURN_TCP;
  }
  g_assert (relay_n < G_N_ELEMENTS (relays));

  for (i = 0; i < relay_n; i++) {
    if (!nice_agent_set_relay_info (nice->priv->nice_agent,
            item->nice_stream_id, NICE_COMPONENT_TYPE_RTP,
            gst_uri_get_host (turn_server), gst_uri_get_port (turn_server),
            user, pass, relays[i])) {
      gchar *uri_str = gst_uri_to_string (turn_server);
      GST_ERROR_OBJECT (ice, "Could not set TURN server %s on libnice",
          uri_str);
      g_free (uri_str);
    }
  }

  g_free (user);
  g_free (pass);

}

typedef struct
{
  GstWebRTCNice *ice;
  struct NiceStreamItem *item;
} AddTurnServerData;

static void
_add_turn_server_func (const gchar * uri, GstUri * turn_server,
    AddTurnServerData * data)
{
  _add_turn_server (data->ice, data->item, turn_server);
}

static void
_add_stun_server (GstWebRTCNice * ice, GstUri * stun_server)
{
  const gchar *msg = "must be of the form stun://<host>:<port>";
  const gchar *host;
  gchar *s = NULL;
  guint port;

  s = gst_uri_to_string (stun_server);
  GST_DEBUG_OBJECT (ice, "adding stun server, %s", s);

  host = gst_uri_get_host (stun_server);
  if (!host) {
    GST_ERROR_OBJECT (ice, "Stun server '%s' has no host, %s", s, msg);
    goto out;
  }

  port = gst_uri_get_port (stun_server);
  if (port == GST_URI_NO_PORT) {
    GST_INFO_OBJECT (ice, "Stun server '%s' has no port, assuming 3478", s);
    port = 3478;
    gst_uri_set_port (stun_server, port);
  }

  g_object_set (ice->priv->nice_agent, "stun-server", host,
      "stun-server-port", port, NULL);

out:
  g_free (s);
}

static GstWebRTCICEStream *
gst_webrtc_nice_add_stream (GstWebRTCICE * ice, guint session_id)
{
  struct NiceStreamItem m = NICE_MATCH_INIT;
  struct NiceStreamItem *item;
  AddTurnServerData add_data;
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);

  m.session_id = session_id;
  item = _nice_stream_item_find (nice, (NiceStreamItemFindFunc) _match, &m);
  if (item) {
    GST_ERROR_OBJECT (nice, "stream already added with session_id=%u",
        session_id);
    return 0;
  }

  if (nice->priv->stun_server) {
    _add_stun_server (nice, nice->priv->stun_server);
  }

  item = _create_nice_stream_item (nice, session_id);

  if (nice->priv->turn_server) {
    _add_turn_server (nice, item, nice->priv->turn_server);
  }

  add_data.ice = nice;
  add_data.item = item;

  g_hash_table_foreach (nice->priv->turn_servers,
      (GHFunc) _add_turn_server_func, &add_data);

  gst_object_ref (item->stream);
  return item->stream;
}

void
gst_webrtc_nice_fill_local_candidate_credentials (NiceAgent * agent,
    NiceCandidate * candidate)
{

  if (!candidate->username || !candidate->password) {
    gboolean got_credentials;
    gchar *ufrag, *password;

    got_credentials =
        nice_agent_get_local_credentials (agent, candidate->stream_id, &ufrag,
        &password);
    g_warn_if_fail (got_credentials);

    if (!candidate->username)
      candidate->username = ufrag;
    else
      g_free (ufrag);

    if (!candidate->password)
      candidate->password = password;
    else
      g_free (password);
  }
}

void
gst_webrtc_nice_fill_remote_candidate_credentials (GstWebRTCNice * nice,
    NiceCandidate * candidate)
{
  if (!candidate->username)
    candidate->username = g_strdup (nice->priv->remote_ufrag);

  if (!candidate->password)
    candidate->password = g_strdup (nice->priv->remote_pwd);
}

static void
_on_new_candidate (NiceAgent * agent, NiceCandidate * candidate,
    GstWebRTCNice * ice)
{
  struct NiceStreamItem *item;
  NiceCandidate *c;
  gchar *attr;

  item = _find_item (ice, -1, candidate->stream_id, NULL);
  if (!item) {
    GST_WARNING_OBJECT (ice, "received signal for non-existent stream %u",
        candidate->stream_id);
    return;
  }

  c = nice_candidate_copy (candidate);
  gst_webrtc_nice_fill_local_candidate_credentials (agent, c);

  attr = nice_agent_generate_local_candidate_sdp (agent, c);

  nice_candidate_free (c);

  if (ice->priv->on_candidate)
    ice->priv->on_candidate (GST_WEBRTC_ICE (ice), item->session_id, attr,
        ice->priv->on_candidate_data);

  g_free (attr);
}

static GstWebRTCICETransport *
gst_webrtc_nice_find_transport (GstWebRTCICE * ice, GstWebRTCICEStream * stream,
    GstWebRTCICEComponent component)
{
  struct NiceStreamItem *item;
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);

  item = _find_item (nice, -1, -1, stream);
  g_return_val_if_fail (item != NULL, NULL);

  return gst_webrtc_ice_stream_find_transport (item->stream, component);
}

/* parse the address for possible resolution */
static gboolean
get_candidate_address (const gchar * candidate, gchar ** prefix,
    gchar ** address, gchar ** postfix)
{
  char **tokens = NULL;
  char *tmp_address = NULL;

  if (!g_str_has_prefix (candidate, "a=candidate:")) {
    GST_ERROR ("candidate \"%s\" does not start with \"a=candidate:\"",
        candidate);
    goto failure;
  }

  if (!(tokens = g_strsplit (candidate, " ", 6))) {
    GST_ERROR ("candidate \"%s\" could not be tokenized", candidate);
    goto failure;
  }

  if (g_strv_length (tokens) < 6) {
    GST_ERROR ("candidate \"%s\" tokenization resulted in not enough tokens",
        candidate);
    goto failure;
  }

  tmp_address = tokens[4];
  if (address)
    *address = g_strdup (tmp_address);
  tokens[4] = NULL;

  if (prefix)
    *prefix = g_strjoinv (" ", tokens);
  if (postfix)
    *postfix = g_strdup (tokens[5]);

  tokens[4] = tmp_address;

  g_strfreev (tokens);
  return TRUE;

failure:
  if (tokens)
    g_strfreev (tokens);
  return FALSE;
}

struct resolve_candidate_data
{
  guint nice_stream_id;
  char *prefix;
  char *postfix;
  GstPromise *promise;
};

static void
free_resolve_candidate_data (struct resolve_candidate_data *rc)
{
  g_free (rc->prefix);
  g_free (rc->postfix);
  if (rc->promise)
    gst_promise_unref (rc->promise);
  g_free (rc);
}

static void
add_ice_candidate_to_libnice (GstWebRTCICE * ice, guint nice_stream_id,
    NiceCandidate * cand)
{
  GSList *candidates = NULL;
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);

  if (cand->component_id == 2) {
    /* we only support rtcp-mux so rtcp candidates are useless for us */
    GST_INFO_OBJECT (ice, "Dropping RTCP candidate");
    return;
  }

  candidates = g_slist_append (candidates, cand);

  nice_agent_set_remote_candidates (nice->priv->nice_agent, nice_stream_id,
      cand->component_id, candidates);

  g_slist_free (candidates);
}

static void
on_candidate_resolved (GstWebRTCNice * nice, GList * addresses,
    GError * error, gpointer user_data)
{
  struct resolve_candidate_data *rc = user_data;
  char *new_candv[4] = { NULL, };
  char *new_addr, *new_candidate;
  NiceCandidate *cand;

  if (!nice)
    error = g_error_new_literal (G_IO_ERROR, G_IO_ERROR_CANCELLED, "Cancelled");

  if (error) {
    if (rc->promise) {
      GstStructure *s = gst_structure_new ("application/x-gst-promise", "error",
          G_TYPE_ERROR, error, NULL);
      gst_promise_reply (rc->promise, s);
    } else if (nice) {
      GST_WARNING_OBJECT (nice, "Could not resolve candidate address: %s",
          error->message);
    } else {
      GST_WARNING ("Could not resolve candidate address: %s", error->message);
    }

    if (!nice)
      g_clear_error (&error);

    return;
  }

  GstWebRTCICE *ice = GST_WEBRTC_ICE (nice);

  new_addr = g_inet_address_to_string (addresses->data);

  new_candv[0] = rc->prefix;
  new_candv[1] = new_addr;
  new_candv[2] = rc->postfix;
  new_candv[3] = NULL;
  new_candidate = g_strjoinv (" ", new_candv);

  GST_DEBUG_OBJECT (ice, "resolved to candidate %s", new_candidate);

  cand =
      nice_agent_parse_remote_candidate_sdp (nice->priv->nice_agent,
      rc->nice_stream_id, new_candidate);

  g_free (new_addr);
  if (!cand) {
    if (rc->promise) {
      GError *error =
          g_error_new (GST_WEBRTC_ERROR, GST_WEBRTC_ERROR_INTERNAL_FAILURE,
          "Could not parse candidate \'%s\'", new_candidate);
      GstStructure *s = gst_structure_new ("application/x-gst-promise", "error",
          G_TYPE_ERROR, error, NULL);
      gst_promise_reply (rc->promise, s);
      g_clear_error (&error);
    } else {
      GST_WARNING_OBJECT (ice, "Could not parse candidate \'%s\'",
          new_candidate);
    }
    g_free (new_candidate);
    return;
  }

  g_free (new_candidate);
  add_ice_candidate_to_libnice (ice, rc->nice_stream_id, cand);
  nice_candidate_free (cand);
}

/* candidate must start with "a=candidate:" or be NULL*/
static void
gst_webrtc_nice_add_candidate (GstWebRTCICE * ice, GstWebRTCICEStream * stream,
    const gchar * candidate, GstPromise * promise)
{
  struct NiceStreamItem *item;
  NiceCandidate *cand;
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);

  item = _find_item (nice, -1, -1, stream);
  g_return_if_fail (item != NULL);

  if (candidate == NULL) {
    nice_agent_peer_candidate_gathering_done (nice->priv->nice_agent,
        item->nice_stream_id);
    if (promise) {
      gst_promise_reply (promise, NULL);
    }
    return;
  }

  cand =
      nice_agent_parse_remote_candidate_sdp (nice->priv->nice_agent,
      item->nice_stream_id, candidate);
  if (!cand) {
    /* might be a .local candidate */
    char *prefix = NULL, *address = NULL, *postfix = NULL;
    struct resolve_candidate_data *rc;

    if (!get_candidate_address (candidate, &prefix, &address, &postfix)) {
      if (promise) {
        GError *error =
            g_error_new (GST_WEBRTC_ERROR, GST_WEBRTC_ERROR_INTERNAL_FAILURE,
            "Failed to retrieve address from candidate %s",
            candidate);
        GstStructure *s = gst_structure_new ("application/x-gst-promise",
            "error", G_TYPE_ERROR, error, NULL);
        gst_promise_reply (promise, s);
        g_clear_error (&error);
      } else {
        GST_WARNING_OBJECT (nice,
            "Failed to retrieve address from candidate %s", candidate);
      }
      goto done;
    }

    if (!g_str_has_suffix (address, ".local")) {
      if (promise) {
        GError *error =
            g_error_new (GST_WEBRTC_ERROR, GST_WEBRTC_ERROR_INTERNAL_FAILURE,
            "candidate address \'%s\' does not end " "with \'.local\'",
            address);
        GstStructure *s = gst_structure_new ("application/x-gst-promise",
            "error", G_TYPE_ERROR, error, NULL);
        gst_promise_reply (promise, s);
        g_clear_error (&error);
      } else {
        GST_WARNING_OBJECT (nice,
            "candidate address \'%s\' does not end "
            "with \'.local\'", address);
      }
      goto done;
    }

    rc = g_new0 (struct resolve_candidate_data, 1);
    rc->nice_stream_id = item->nice_stream_id;
    rc->prefix = prefix;
    rc->postfix = postfix;
    rc->promise = promise ? gst_promise_ref (promise) : NULL;
    resolve_host_async (nice, address,
        on_candidate_resolved, rc,
        (GDestroyNotify) free_resolve_candidate_data);

    prefix = NULL;
    postfix = NULL;

  done:
    g_clear_pointer (&address, g_free);
    g_clear_pointer (&prefix, g_free);
    g_clear_pointer (&postfix, g_free);

    return;
  }

  add_ice_candidate_to_libnice (ice, item->nice_stream_id, cand);
  nice_candidate_free (cand);
  if (promise) {
    gst_promise_reply (promise, NULL);
  }
}

static gboolean
gst_webrtc_nice_set_remote_credentials (GstWebRTCICE * ice,
    GstWebRTCICEStream * stream, const gchar * ufrag, const gchar * pwd)
{
  struct NiceStreamItem *item;
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);

  g_return_val_if_fail (ufrag != NULL, FALSE);
  g_return_val_if_fail (pwd != NULL, FALSE);
  item = _find_item (nice, -1, -1, stream);
  g_return_val_if_fail (item != NULL, FALSE);

  GST_DEBUG_OBJECT (nice, "Setting remote ICE credentials on "
      "ICE stream %u ufrag:%s pwd:%s", item->nice_stream_id, ufrag, pwd);

  nice_agent_set_remote_credentials (nice->priv->nice_agent,
      item->nice_stream_id, ufrag, pwd);

  g_free (nice->priv->remote_ufrag);
  g_free (nice->priv->remote_pwd);
  nice->priv->remote_ufrag = g_strdup (ufrag);
  nice->priv->remote_pwd = g_strdup (pwd);

  return TRUE;
}

typedef struct
{
  GstWebRTCNice *ice;
  GstUri *turn_server;
} AddTurnServerToStreamData;

static gboolean
_add_turn_server_foreach_stream_func (struct NiceStreamItem *item,
    gpointer data)
{
  AddTurnServerToStreamData *add_data = (AddTurnServerToStreamData *) data;
  _add_turn_server (add_data->ice, item, add_data->turn_server);
  return TRUE;
}

static gboolean
gst_webrtc_nice_add_turn_server (GstWebRTCICE * ice, const gchar * uri)
{
  gboolean ret = FALSE;
  GstUri *valid_uri;
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  gboolean inserted;
  AddTurnServerToStreamData add_data;

  if (!(valid_uri = _validate_turn_server (nice, uri)))
    goto done;

  inserted =
      g_hash_table_insert (nice->priv->turn_servers, g_strdup (uri), valid_uri);

  /* add the turn server to any streams that were already created */
  if (inserted) {
    add_data.ice = nice;
    add_data.turn_server = valid_uri;
    _nice_stream_item_foreach (nice, _add_turn_server_foreach_stream_func,
        &add_data);
  }

  ret = TRUE;

done:
  return ret;
}

static gboolean
gst_webrtc_nice_add_local_ip_address (GstWebRTCNice * ice,
    const gchar * address)
{
  gboolean ret = FALSE;
  NiceAddress nice_addr;

  nice_address_init (&nice_addr);

  ret = nice_address_set_from_string (&nice_addr, address);

  if (ret) {
    ret = nice_agent_add_local_address (ice->priv->nice_agent, &nice_addr);
    if (!ret) {
      GST_ERROR_OBJECT (ice, "Failed to add local address to NiceAgent");
    }
  } else {
    GST_ERROR_OBJECT (ice, "Failed to initialize NiceAddress [%s]", address);
  }

  return ret;
}

static gboolean
gst_webrtc_nice_set_local_credentials (GstWebRTCICE * ice,
    GstWebRTCICEStream * stream, const gchar * ufrag, const gchar * pwd)
{
  struct NiceStreamItem *item;
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);

  g_return_val_if_fail (ufrag != NULL, FALSE);
  g_return_val_if_fail (pwd != NULL, FALSE);
  item = _find_item (nice, -1, -1, stream);
  g_return_val_if_fail (item != NULL, FALSE);

  GST_DEBUG_OBJECT (nice, "Setting local ICE credentials on "
      "ICE stream %u ufrag:%s pwd:%s", item->nice_stream_id, ufrag, pwd);

  nice_agent_set_local_credentials (nice->priv->nice_agent,
      item->nice_stream_id, ufrag, pwd);

  return TRUE;
}

static gboolean
gst_webrtc_nice_gather_candidates (GstWebRTCICE * ice,
    GstWebRTCICEStream * stream)
{
  struct NiceStreamItem *item;
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);

  item = _find_item (nice, -1, -1, stream);
  g_return_val_if_fail (item != NULL, FALSE);

  GST_DEBUG_OBJECT (nice, "gather candidates for stream %u",
      item->nice_stream_id);

  return gst_webrtc_ice_stream_gather_candidates (stream);
}

static void
gst_webrtc_nice_set_is_controller (GstWebRTCICE * ice, gboolean controller)
{
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  g_object_set (G_OBJECT (nice->priv->nice_agent), "controlling-mode",
      controller, NULL);
}

static gboolean
gst_webrtc_nice_get_is_controller (GstWebRTCICE * ice)
{
  gboolean ret;
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  g_object_get (G_OBJECT (nice->priv->nice_agent), "controlling-mode",
      &ret, NULL);
  return ret;
}

static void
gst_webrtc_nice_set_force_relay (GstWebRTCICE * ice, gboolean force_relay)
{
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  g_object_set (G_OBJECT (nice->priv->nice_agent), "force-relay", force_relay,
      NULL);
}

static void
gst_webrtc_nice_set_on_ice_candidate (GstWebRTCICE * ice,
    GstWebRTCICEOnCandidateFunc func, gpointer user_data, GDestroyNotify notify)
{
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  if (nice->priv->on_candidate_notify)
    nice->priv->on_candidate_notify (nice->priv->on_candidate_data);
  nice->priv->on_candidate = NULL;

  nice->priv->on_candidate = func;
  nice->priv->on_candidate_data = user_data;
  nice->priv->on_candidate_notify = notify;
}

static void
gst_webrtc_nice_set_tos (GstWebRTCICE * ice, GstWebRTCICEStream * stream,
    guint tos)
{
  struct NiceStreamItem *item;
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);

  item = _find_item (nice, -1, -1, stream);
  g_return_if_fail (item != NULL);

  nice_agent_set_stream_tos (nice->priv->nice_agent, item->nice_stream_id, tos);
}

const gchar *
gst_webrtc_nice_get_candidate_relay_protocol (GstUri * turn_server)
{
  const gchar *scheme;
  const gchar *transport;

  if (!turn_server)
    return "none";

  scheme = gst_uri_get_scheme (turn_server);
  transport = gst_uri_get_query_value (turn_server, "transport");

  if (g_strcmp0 (scheme, "turns") == 0) {
    return "tls";
  } else if (g_strcmp0 (scheme, "turn") == 0) {
    if (!transport || g_strcmp0 (transport, "udp") == 0)
      return "udp";
    if (!transport || g_strcmp0 (transport, "tcp") == 0)
      return "tcp";
  }

  return "none";
}

gchar *
gst_webrtc_nice_get_candidate_server_url (GstWebRTCNice * ice,
    NiceCandidate * cand)
{
  switch (cand->type) {
    case NICE_CANDIDATE_TYPE_RELAYED:{
      NiceAddress addr;
      gchar ipaddr[NICE_ADDRESS_STRING_LEN];
      nice_candidate_relay_address (cand, &addr);
      nice_address_to_string (&addr, ipaddr);
      return g_strdup (ipaddr);
    }
    case NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE:{
      NiceAddress addr;
      gchar ipaddr[NICE_ADDRESS_STRING_LEN];
      if (nice_candidate_stun_server_address (cand, &addr)) {
        nice_address_to_string (&addr, ipaddr);
        return g_strdup (ipaddr);
      } else {
        return g_strdup (gst_uri_get_host (ice->priv->stun_server));
      }
      return g_strdup (gst_uri_get_host (ice->priv->stun_server));
    }
    default:
      return g_strdup ("");
  }
}

static void
_populate_candidate_stats (GstWebRTCNice * ice, NiceCandidate * cand,
    GstWebRTCICEStream * stream, GstWebRTCICECandidateStats * stats,
    GstWebRTCNiceCandidateOrigin origin)
{
  gchar ipaddr[INET6_ADDRSTRLEN];

  g_assert (cand != NULL);

  nice_address_to_string (&cand->addr, ipaddr);
  GST_WEBRTC_ICE_CANDIDATE_STATS_PORT (stats) =
      nice_address_get_port (&cand->addr);
  GST_WEBRTC_ICE_CANDIDATE_STATS_ADDRESS (stats) = g_strdup (ipaddr);
  GST_WEBRTC_ICE_CANDIDATE_STATS_STREAM_ID (stats) = stream->stream_id;
  GST_WEBRTC_ICE_CANDIDATE_STATS_TYPE (stats) =
      nice_candidate_type_to_string (cand->type);
  GST_WEBRTC_ICE_CANDIDATE_STATS_PRIORITY (stats) = cand->priority;
  GST_WEBRTC_ICE_CANDIDATE_STATS_PROTOCOL (stats) =
      cand->transport == NICE_CANDIDATE_TRANSPORT_UDP ? "udp" : "tcp";
  if (origin == GST_WEBRTC_NICE_CANDIDATE_ORIGIN_LOCAL) {
    if (cand->type == NICE_CANDIDATE_TYPE_RELAYED) {
      NiceAddress relay_address;
      nice_candidate_relay_address (cand, &relay_address);

      GST_WEBRTC_ICE_CANDIDATE_STATS_RELATED_ADDRESS (stats) =
          nice_address_dup_string (&relay_address);
      GST_WEBRTC_ICE_CANDIDATE_STATS_RELATED_PORT (stats) =
          nice_address_get_port (&relay_address);

      GST_WEBRTC_ICE_CANDIDATE_STATS_RELAY_PROTOCOL (stats) =
          gst_webrtc_nice_get_candidate_relay_protocol (ice->priv->turn_server);
    }
    GST_WEBRTC_ICE_CANDIDATE_STATS_URL (stats) =
        gst_webrtc_nice_get_candidate_server_url (ice, cand);
  }

  GST_WEBRTC_ICE_CANDIDATE_STATS_FOUNDATION (stats) =
      g_strdup (cand->foundation);

  switch (cand->transport) {
    case NICE_CANDIDATE_TRANSPORT_UDP:
      GST_WEBRTC_ICE_CANDIDATE_STATS_TCP_TYPE (stats) =
          GST_WEBRTC_ICE_TCP_CANDIDATE_TYPE_NONE;
      break;
    case NICE_CANDIDATE_TRANSPORT_TCP_ACTIVE:
      GST_WEBRTC_ICE_CANDIDATE_STATS_TCP_TYPE (stats) =
          GST_WEBRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE;
      break;
    case NICE_CANDIDATE_TRANSPORT_TCP_PASSIVE:
      GST_WEBRTC_ICE_CANDIDATE_STATS_TCP_TYPE (stats) =
          GST_WEBRTC_ICE_TCP_CANDIDATE_TYPE_PASSIVE;
      break;
    case NICE_CANDIDATE_TRANSPORT_TCP_SO:
      GST_WEBRTC_ICE_CANDIDATE_STATS_TCP_TYPE (stats) =
          GST_WEBRTC_ICE_TCP_CANDIDATE_TYPE_SO;
      break;
  };

  GST_WEBRTC_ICE_CANDIDATE_STATS_USERNAME_FRAGMENT (stats) =
      g_strdup (cand->username);
}

static void
_populate_candidate_list_stats (GstWebRTCNice * ice, GSList * cands,
    GstWebRTCICEStream * stream, GPtrArray * result,
    GstWebRTCNiceCandidateOrigin origin)
{
  GSList *item;

  for (item = cands; item != NULL; item = item->next) {
    GstWebRTCICECandidateStats *stats =
        g_malloc0 (sizeof (GstWebRTCICECandidateStats));
    NiceCandidate *c = item->data;
    _populate_candidate_stats (ice, c, stream, stats, origin);
    g_ptr_array_add (result, stats);
  }

  g_ptr_array_add (result, NULL);
}

static GstWebRTCICECandidateStats **
gst_webrtc_nice_get_local_candidates (GstWebRTCICE * ice,
    GstWebRTCICEStream * stream)
{
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  GSList *cands = NULL;

  /* TODO: Use a g_ptr_array_new_null_terminated once when we depend on GLib 2.74 */
  GPtrArray *result = g_ptr_array_new ();

  cands = nice_agent_get_local_candidates (nice->priv->nice_agent,
      stream->stream_id, NICE_COMPONENT_TYPE_RTP);

  _populate_candidate_list_stats (nice, cands, stream, result,
      GST_WEBRTC_NICE_CANDIDATE_ORIGIN_LOCAL);
  g_slist_free_full (cands, (GDestroyNotify) nice_candidate_free);

  return (GstWebRTCICECandidateStats **) g_ptr_array_free (result, FALSE);
}

static GstWebRTCICECandidateStats **
gst_webrtc_nice_get_remote_candidates (GstWebRTCICE * ice,
    GstWebRTCICEStream * stream)
{
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  GSList *cands = NULL;

  /* TODO: Use a g_ptr_array_new_null_terminated once when we depend on GLib 2.74 */
  GPtrArray *result = g_ptr_array_new ();

  cands = nice_agent_get_remote_candidates (nice->priv->nice_agent,
      stream->stream_id, NICE_COMPONENT_TYPE_RTP);

  _populate_candidate_list_stats (nice, cands, stream, result,
      GST_WEBRTC_NICE_CANDIDATE_ORIGIN_REMOTE);
  g_slist_free_full (cands, (GDestroyNotify) nice_candidate_free);

  return (GstWebRTCICECandidateStats **) g_ptr_array_free (result, FALSE);
}

static void
_clear_ice_stream (struct NiceStreamItem *item)
{
  GstWebRTCNice *ice = NULL;

  if (!item)
    return;

  if (item->stream) {
    g_object_get (item->stream, "ice", &ice, NULL);

    if (ice != NULL) {
      g_signal_handlers_disconnect_by_data (ice->priv->nice_agent,
          item->stream);
      gst_object_unref (ice);
    }
    gst_object_unref (item->stream);
  }
}

static GstUri *
_validate_turn_server (GstWebRTCNice * ice, const gchar * s)
{
  GstUri *uri = gst_uri_from_string_escaped (s);
  const gchar *userinfo, *scheme;
  GList *keys = NULL, *l;
  gchar *user = NULL, *pass = NULL;
  gboolean turn_tls = FALSE;
  guint port;

  GST_DEBUG_OBJECT (ice, "validating turn server, %s", s);

  if (!uri) {
    GST_ERROR_OBJECT (ice, "Could not parse turn server '%s'", s);
    return NULL;
  }

  scheme = gst_uri_get_scheme (uri);
  if (g_strcmp0 (scheme, "turn") == 0) {
  } else if (g_strcmp0 (scheme, "turns") == 0) {
    turn_tls = TRUE;
  } else {
    GST_ERROR_OBJECT (ice, "unknown scheme '%s'", scheme);
    goto out;
  }

  keys = gst_uri_get_query_keys (uri);
  for (l = keys; l; l = l->next) {
    gchar *key = l->data;

    if (g_strcmp0 (key, "transport") == 0) {
      const gchar *transport = gst_uri_get_query_value (uri, "transport");
      if (!transport) {
      } else if (g_strcmp0 (transport, "udp") == 0) {
      } else if (g_strcmp0 (transport, "tcp") == 0) {
      } else {
        GST_ERROR_OBJECT (ice, "unknown transport value, '%s'", transport);
        goto out;
      }
    } else {
      GST_ERROR_OBJECT (ice, "unknown query key, '%s'", key);
      goto out;
    }
  }

  /* TODO: Implement error checking similar to the stun server below */
  userinfo = gst_uri_get_userinfo (uri);
  _parse_userinfo (userinfo, &user, &pass);
  if (!user) {
    GST_ERROR_OBJECT (ice, "No username specified in '%s'", s);
    goto out;
  }
  if (!pass) {
    GST_ERROR_OBJECT (ice, "No password specified in '%s'", s);
    goto out;
  }

  port = gst_uri_get_port (uri);

  if (port == GST_URI_NO_PORT) {
    if (turn_tls) {
      gst_uri_set_port (uri, 5349);
    } else {
      gst_uri_set_port (uri, 3478);
    }
  }

  g_list_free (keys);
  g_free (user);
  g_free (pass);

  return uri;

out:
  g_list_free (keys);
  g_free (user);
  g_free (pass);
  gst_uri_unref (uri);

  return NULL;
}

static void
on_http_proxy_resolved (GstWebRTCNice * nice, GList * addresses,
    GError * error, gpointer user_data)
{
  GstUri *uri = user_data;
  const gchar *userinfo;
  gchar *user = NULL;
  gchar *pass = NULL;
  const gchar *alpn = NULL;
  gchar *ip = NULL;
  guint port = GST_URI_NO_PORT;
  GHashTable *extra_headers;

  if (error) {
    if (nice) {
      GST_WARNING_OBJECT (nice, "Failed to resolve http proxy: %s",
          error->message);
    } else {
      GST_WARNING ("Failed to resolve http proxy: %s", error->message);
    }

    return;
  }

  if (!nice) {
    GST_WARNING ("Missing GstWebRTCNice instance");
    return;
  }

  GstWebRTCICE *ice = GST_WEBRTC_ICE (nice);

  /* XXX: only the first IP is used */
  ip = g_inet_address_to_string (addresses->data);

  if (!ip) {
    GST_ERROR_OBJECT (ice, "failed to resolve host for proxy");
    gst_uri_unref (uri);
    return;
  }

  port = gst_uri_get_port (uri);
  if (port == GST_URI_NO_PORT) {
    port = HTTP_PROXY_PORT_DEFAULT;
    GST_DEBUG_OBJECT (ice, "Proxy server has no port, assuming %u",
        HTTP_PROXY_PORT_DEFAULT);
  }

  userinfo = gst_uri_get_userinfo (uri);
  _parse_userinfo (userinfo, &user, &pass);

  alpn = gst_uri_get_query_value (uri, "alpn");
  if (!alpn) {
    alpn = "webrtc";
  }
  extra_headers = g_hash_table_new_full (g_str_hash,
      g_str_equal, g_free, g_free);
  g_hash_table_insert (extra_headers, g_strdup ("ALPN"), g_strdup (alpn));

  g_object_set (nice->priv->nice_agent,
      "proxy-ip", ip, "proxy-port", port, "proxy-type", NICE_PROXY_TYPE_HTTP,
      "proxy-username", user, "proxy-password", pass, "proxy-extra-headers",
      extra_headers, NULL);

  g_free (ip);
  g_free (user);
  g_free (pass);
  g_hash_table_unref (extra_headers);
}

static GstUri *
_set_http_proxy (GstWebRTCICE * ice, const gchar * s)
{
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  GstUri *uri = gst_uri_from_string_escaped (s);
  const gchar *msg =
      "must be of the form http://[username:password@]<host>[:<port>]";
  const gchar *host = NULL;
  const gchar *userinfo;
  gchar *user = NULL, *pass = NULL;

  GST_DEBUG_OBJECT (ice, "setting http proxy %s", s);

  if (!uri) {
    GST_ERROR_OBJECT (ice, "Couldn't parse http proxy uri '%s', %s", s, msg);
    return NULL;
  }

  if (g_strcmp0 (gst_uri_get_scheme (uri), "http") != 0) {
    GST_ERROR_OBJECT (ice,
        "Couldn't parse uri scheme for http proxy server '%s', %s", s, msg);
    gst_uri_unref (uri);
    return NULL;
  }

  host = gst_uri_get_host (uri);
  if (!host) {
    GST_ERROR_OBJECT (ice, "http proxy server '%s' has no host, %s", s, msg);
    gst_uri_unref (uri);
    return NULL;
  }

  userinfo = gst_uri_get_userinfo (uri);
  _parse_userinfo (userinfo, &user, &pass);
  if ((pass && pass[0] != '\0') && (!user || user[0] == '\0')) {
    GST_ERROR_OBJECT (ice,
        "Password specified without user for http proxy '%s', %s", s, msg);
    uri = NULL;
    goto out;
  }

  resolve_host_async (nice, host, on_http_proxy_resolved,
      gst_uri_ref (uri), (GDestroyNotify) gst_uri_unref);

out:
  g_free (user);
  g_free (pass);

  return uri;
}

static void
gst_webrtc_nice_set_stun_server (GstWebRTCICE * ice, const gchar * uri_s)
{
  GstUri *uri = gst_uri_from_string_escaped (uri_s);
  const gchar *msg = "must be of the form stun://<host>:<port>";
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);

  GST_DEBUG_OBJECT (nice, "setting stun server, %s", uri_s);

  if (!uri) {
    GST_ERROR_OBJECT (nice, "Couldn't parse stun server '%s', %s", uri_s, msg);
    return;
  }

  if (nice->priv->stun_server)
    gst_uri_unref (nice->priv->stun_server);
  nice->priv->stun_server = uri;
}

static gchar *
gst_webrtc_nice_get_stun_server (GstWebRTCICE * ice)
{
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  if (nice->priv->stun_server)
    return gst_uri_to_string (nice->priv->stun_server);
  else
    return NULL;
}

static void
gst_webrtc_nice_set_turn_server (GstWebRTCICE * ice, const gchar * uri_s)
{
  GstUri *uri;
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  uri = _validate_turn_server (nice, uri_s);

  if (uri) {
    if (nice->priv->turn_server)
      gst_uri_unref (nice->priv->turn_server);
    nice->priv->turn_server = uri;
  }
}

static gchar *
gst_webrtc_nice_get_turn_server (GstWebRTCICE * ice)
{
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  if (nice->priv->turn_server)
    return gst_uri_to_string (nice->priv->turn_server);
  else
    return NULL;
}

static void
gst_webrtc_nice_set_http_proxy (GstWebRTCICE * ice, const gchar * http_proxy)
{
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  GstUri *uri = _set_http_proxy (ice, http_proxy);

  if (uri) {
    if (nice->priv->http_proxy)
      gst_uri_unref (nice->priv->http_proxy);
    nice->priv->http_proxy = uri;
  }
}

static gchar *
gst_webrtc_nice_get_http_proxy (GstWebRTCICE * ice)
{
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);

  if (nice->priv->http_proxy)
    return gst_uri_to_string (nice->priv->http_proxy);
  else
    return NULL;
}

struct close_data
{
  GWeakRef nice_weak;
  GstPromise *promise;
  gboolean agent_closed;
};

static struct close_data *
close_data_new (GstWebRTCNice * ice, GstPromise * p)
{
  struct close_data *d = g_atomic_rc_box_new0 (struct close_data);
  g_weak_ref_init (&d->nice_weak, ice);
  d->promise = p ? gst_promise_ref (p) : NULL;
  d->agent_closed = FALSE;
  return d;
}

static void
close_data_clear (struct close_data *d)
{
  g_weak_ref_clear (&d->nice_weak);
  if (d->promise)
    gst_promise_unref (d->promise);
}

static struct close_data *
close_data_ref (struct close_data *d)
{
  return (struct close_data *) g_atomic_rc_box_acquire (d);
}

static void
close_data_unref (struct close_data *d)
{
  g_atomic_rc_box_release_full (d, (GDestroyNotify) close_data_clear);
}

static void
on_agent_closed (GObject * src, GAsyncResult * result, gpointer user_data)
{
  struct close_data *d = (struct close_data *) user_data;

  /* 9. Set the [[IceTransportState]] slot of each of connection's
   * RTCIceTransports to "closed". */
  /* FIXME: We don't expose IceTransportState yet. */

  if (d->promise) {
    gst_promise_reply (d->promise, NULL);
  }

  d->agent_closed = TRUE;
  close_data_unref (d);
}

static gboolean
close_main_cb (gpointer user_data)
{
  struct close_data *d = (struct close_data *) user_data;
  GstWebRTCNice *nice = g_weak_ref_get (&d->nice_weak);

  if (nice) {
    /* 8. Destroy connection's ICE Agent, abruptly ending any active ICE
     * processing and releasing any relevant resources (e.g. TURN permissions). */
    nice_agent_close_async (NICE_AGENT (nice->priv->nice_agent),
        on_agent_closed, close_data_ref (d));
    if (!d->promise) {
      while (!d->agent_closed) {
        g_main_context_iteration (nice->priv->main_context, TRUE);
      }
    }
    gst_object_unref (nice);
  }

  return G_SOURCE_REMOVE;
}

static void
gst_webrtc_nice_close (GstWebRTCICE * ice, GstPromise * promise)
{
  GstWebRTCNice *nice = GST_WEBRTC_NICE (ice);
  struct close_data *d = close_data_new (nice, promise);

  /* https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close */

  g_main_context_invoke_full (nice->priv->main_context, G_PRIORITY_DEFAULT,
      close_main_cb, d, (GDestroyNotify) close_data_unref);
}

static void
gst_webrtc_nice_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstWebRTCICE *ice = GST_WEBRTC_ICE (object);
  GstWebRTCNice *nice = GST_WEBRTC_NICE (object);

  switch (prop_id) {
    case PROP_ICE_TCP:
      g_object_set_property (G_OBJECT (nice->priv->nice_agent),
          "ice-tcp", value);
      break;
    case PROP_ICE_UDP:
      g_object_set_property (G_OBJECT (nice->priv->nice_agent),
          "ice-udp", value);
      break;
    case PROP_MIN_RTP_PORT:
      ice->min_rtp_port = g_value_get_uint (value);
      if (ice->min_rtp_port > ice->max_rtp_port)
        g_warning ("Set min-rtp-port to %u which is larger than"
            " max-rtp-port %u", ice->min_rtp_port, ice->max_rtp_port);
      break;
    case PROP_MAX_RTP_PORT:
      ice->max_rtp_port = g_value_get_uint (value);
      if (ice->min_rtp_port > ice->max_rtp_port)
        g_warning ("Set max-rtp-port to %u which is smaller than"
            " min-rtp-port %u", ice->max_rtp_port, ice->min_rtp_port);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_webrtc_nice_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstWebRTCICE *ice = GST_WEBRTC_ICE (object);
  GstWebRTCNice *nice = GST_WEBRTC_NICE (object);

  switch (prop_id) {
    case PROP_AGENT:
      g_value_set_object (value, nice->priv->nice_agent);
      break;
    case PROP_ICE_TCP:
      g_object_get_property (G_OBJECT (nice->priv->nice_agent),
          "ice-tcp", value);
      break;
    case PROP_ICE_UDP:
      g_object_get_property (G_OBJECT (nice->priv->nice_agent),
          "ice-udp", value);
      break;
    case PROP_MIN_RTP_PORT:
      g_value_set_uint (value, ice->min_rtp_port);
      break;
    case PROP_MAX_RTP_PORT:
      g_value_set_uint (value, ice->max_rtp_port);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
_agent_closed_cb (GObject * source_object, GAsyncResult * res,
    gpointer user_data)
{
  gboolean *agent_closed = user_data;

  *agent_closed = TRUE;
}

static gboolean
_agent_closed_timeout_cb (gpointer user_data)
{
  gboolean *agent_timeout = user_data;

  *agent_timeout = TRUE;
  return FALSE;
};

static void
_close_agent (GstWebRTCNice * ice)
{
  GMainContext *main_context = g_main_context_new ();
  gboolean agent_closed = FALSE;
  gboolean agent_timeout = FALSE;
  GSource *timeout_source;

  g_main_context_push_thread_default (main_context);
  timeout_source = g_timeout_source_new (MAX_CLOSING_TIME_MILLI_SECONDS);
  g_source_set_callback (timeout_source, _agent_closed_timeout_cb,
      &agent_timeout, NULL);
  g_source_attach (timeout_source, main_context);
  nice_agent_close_async (ice->priv->nice_agent, _agent_closed_cb,
      &agent_closed);
  while (!agent_closed && !agent_timeout) {
    g_main_context_iteration (main_context, TRUE);
  }
  if (agent_timeout) {
    GST_WARNING ("nice_agent_close_async() did not finish");
  }
  g_source_destroy (timeout_source);
  g_source_unref (timeout_source);
  g_main_context_pop_thread_default (main_context);
  g_main_context_unref (main_context);
}

static void
gst_webrtc_nice_finalize (GObject * object)
{
  GstWebRTCNice *ice = GST_WEBRTC_NICE (object);

  g_signal_handlers_disconnect_by_data (ice->priv->nice_agent, ice);

  g_cancellable_cancel (ice->priv->resolve_cancellable);
  _close_agent (ice);
  outstanding_resolves_wait (ice->priv->outstanding_resolves);

  _stop_thread (ice);

  g_clear_object (&ice->priv->resolve_cancellable);
  outstanding_resolves_unref (ice->priv->outstanding_resolves);

  if (ice->priv->on_candidate_notify)
    ice->priv->on_candidate_notify (ice->priv->on_candidate_data);
  ice->priv->on_candidate = NULL;
  ice->priv->on_candidate_notify = NULL;

  if (ice->priv->turn_server)
    gst_uri_unref (ice->priv->turn_server);
  if (ice->priv->stun_server)
    gst_uri_unref (ice->priv->stun_server);
  if (ice->priv->http_proxy)
    gst_uri_unref (ice->priv->http_proxy);

  g_mutex_clear (&ice->priv->lock);
  g_cond_clear (&ice->priv->cond);

  g_array_free (ice->priv->nice_stream_map, TRUE);

  g_object_unref (ice->priv->nice_agent);

  g_hash_table_unref (ice->priv->turn_servers);

  g_free (ice->priv->remote_ufrag);
  g_free (ice->priv->remote_pwd);

  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static void
gst_webrtc_nice_constructed (GObject * object)
{
  GstWebRTCNice *ice = GST_WEBRTC_NICE (object);
  NiceAgentOption options = 0;

  _start_thread (ice);

  options |= NICE_AGENT_OPTION_ICE_TRICKLE;
  options |= NICE_AGENT_OPTION_REGULAR_NOMINATION;
  options |= NICE_AGENT_OPTION_CLOSE_FORCED;
  options |= NICE_AGENT_OPTION_CONSENT_FRESHNESS;

  ice->priv->nice_agent = nice_agent_new_full (ice->priv->main_context,
      NICE_COMPATIBILITY_RFC5245, options);
  g_signal_connect (ice->priv->nice_agent, "new-candidate-full",
      G_CALLBACK (_on_new_candidate), ice);

  G_OBJECT_CLASS (parent_class)->constructed (object);
}

static void
gst_webrtc_nice_class_init (GstWebRTCNiceClass * klass)
{
  GstWebRTCICEClass *gst_webrtc_ice_class = GST_WEBRTC_ICE_CLASS (klass);
  GObjectClass *gobject_class = (GObjectClass *) klass;

  // override virtual functions
  gst_webrtc_ice_class->add_candidate = gst_webrtc_nice_add_candidate;
  gst_webrtc_ice_class->add_stream = gst_webrtc_nice_add_stream;
  gst_webrtc_ice_class->add_turn_server = gst_webrtc_nice_add_turn_server;
  gst_webrtc_ice_class->find_transport = gst_webrtc_nice_find_transport;
  gst_webrtc_ice_class->gather_candidates = gst_webrtc_nice_gather_candidates;
  gst_webrtc_ice_class->get_is_controller = gst_webrtc_nice_get_is_controller;
  gst_webrtc_ice_class->get_stun_server = gst_webrtc_nice_get_stun_server;
  gst_webrtc_ice_class->get_turn_server = gst_webrtc_nice_get_turn_server;
  gst_webrtc_ice_class->get_http_proxy = gst_webrtc_nice_get_http_proxy;
  gst_webrtc_ice_class->set_force_relay = gst_webrtc_nice_set_force_relay;
  gst_webrtc_ice_class->set_is_controller = gst_webrtc_nice_set_is_controller;
  gst_webrtc_ice_class->set_local_credentials =
      gst_webrtc_nice_set_local_credentials;
  gst_webrtc_ice_class->set_remote_credentials =
      gst_webrtc_nice_set_remote_credentials;
  gst_webrtc_ice_class->set_stun_server = gst_webrtc_nice_set_stun_server;
  gst_webrtc_ice_class->set_tos = gst_webrtc_nice_set_tos;
  gst_webrtc_ice_class->set_turn_server = gst_webrtc_nice_set_turn_server;
  gst_webrtc_ice_class->set_http_proxy = gst_webrtc_nice_set_http_proxy;
  gst_webrtc_ice_class->set_on_ice_candidate =
      gst_webrtc_nice_set_on_ice_candidate;
  gst_webrtc_ice_class->get_local_candidates =
      gst_webrtc_nice_get_local_candidates;
  gst_webrtc_ice_class->get_remote_candidates =
      gst_webrtc_nice_get_remote_candidates;
  gst_webrtc_ice_class->close = gst_webrtc_nice_close;

  gobject_class->constructed = gst_webrtc_nice_constructed;
  gobject_class->get_property = gst_webrtc_nice_get_property;
  gobject_class->set_property = gst_webrtc_nice_set_property;
  gobject_class->finalize = gst_webrtc_nice_finalize;

  g_object_class_install_property (gobject_class,
      PROP_AGENT,
      g_param_spec_object ("agent", "ICE agent",
          "ICE agent in use by this object. WARNING! Accessing this property "
          "may have disastrous consequences for the operation of webrtcbin. "
          "Other ICE implementations may not have the same interface.",
          NICE_TYPE_AGENT, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class,
      PROP_ICE_TCP,
      g_param_spec_boolean ("ice-tcp", "ICE TCP",
          "Whether the agent should use ICE-TCP when gathering candidates",
          TRUE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class,
      PROP_ICE_UDP,
      g_param_spec_boolean ("ice-udp", "ICE UDP",
          "Whether the agent should use ICE-UDP when gathering candidates",
          TRUE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_signal_override_class_handler ("add-local-ip-address",
      G_TYPE_FROM_CLASS (klass),
      G_CALLBACK (gst_webrtc_nice_add_local_ip_address));
}

static void
gst_webrtc_nice_init (GstWebRTCNice * ice)
{
  ice->priv = gst_webrtc_nice_get_instance_private (ice);

  g_mutex_init (&ice->priv->lock);
  g_cond_init (&ice->priv->cond);

  ice->priv->turn_servers =
      g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
      (GDestroyNotify) gst_uri_unref);

  ice->priv->nice_stream_map =
      g_array_new (FALSE, TRUE, sizeof (struct NiceStreamItem));
  g_array_set_clear_func (ice->priv->nice_stream_map,
      (GDestroyNotify) _clear_ice_stream);

  ice->priv->resolve_cancellable = g_cancellable_new ();
  ice->priv->outstanding_resolves = g_atomic_rc_box_new0 (OutstandingResolves);
  g_mutex_init (&ice->priv->outstanding_resolves->mutex);
  g_cond_init (&ice->priv->outstanding_resolves->cond);
}

GstWebRTCNice *
gst_webrtc_nice_new (const gchar * name)
{
  return g_object_new (GST_TYPE_WEBRTC_NICE, "name", name, NULL);
}
