/*
 * wocky-jingle-factory.c - Support for XEP-0166 (Jingle)
 *
 * Copyright (C) 2006-2008 Collabora Ltd.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser 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
 */

#include "config.h"
#include "wocky-jingle-factory.h"

#include <stdio.h>
#include <string.h>
#include <glib.h>

#define WOCKY_DEBUG_FLAG WOCKY_DEBUG_JINGLE

#include "wocky-debug-internal.h"
#include "wocky-signals-marshal.h"
#include "wocky-jingle-media-rtp.h"
#include "wocky-jingle-session.h"
#include "wocky-jingle-transport-google.h"
#include "wocky-jingle-transport-rawudp.h"
#include "wocky-jingle-transport-iceudp.h"
#include "wocky-namespaces.h"
#include "wocky-session.h"
#include "wocky-utils.h"

#include "wocky-google-relay.h"

G_DEFINE_TYPE(WockyJingleFactory, wocky_jingle_factory, G_TYPE_OBJECT);

/* signal enum */
enum
{
    NEW_SESSION,
    QUERY_CAP,
    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = {0};

/* properties */
enum
{
  PROP_SESSION = 1,
  LAST_PROPERTY
};

struct _WockyJingleFactoryPrivate
{
  WockySession *session;
  WockyPorter *porter;
  guint jingle_handler_id;
  GHashTable *content_types;
  GHashTable *transports;

  /* instances of SESSION_MAP_KEY_FORMAT => WockyJingleSession. */
  GHashTable *sessions;

  WockyJingleInfo *jingle_info;

  gboolean dispose_has_run;
};

static gboolean jingle_cb (
    WockyPorter *porter,
    WockyStanza *msg,
    gpointer user_data);
static WockyJingleSession *create_session (WockyJingleFactory *fac,
    const gchar *sid,
    const gchar *jid,
    WockyJingleDialect dialect,
    gboolean local_hold);

static gboolean session_query_cap_cb (
    WockyJingleSession *session,
    WockyContact *contact,
    const gchar *cap_or_quirk,
    gpointer user_data);
static void session_terminated_cb (WockyJingleSession *sess,
    gboolean local_terminator,
    WockyJingleReason reason,
    const gchar *text,
    WockyJingleFactory *fac);

static void attach_to_wocky_session (WockyJingleFactory *self);

static void
wocky_jingle_factory_init (WockyJingleFactory *obj)
{
  WockyJingleFactoryPrivate *priv =
     G_TYPE_INSTANCE_GET_PRIVATE (obj, WOCKY_TYPE_JINGLE_FACTORY,
         WockyJingleFactoryPrivate);
  obj->priv = priv;

  priv->sessions = g_hash_table_new_full (g_str_hash, g_str_equal,
      g_free, g_object_unref);

  priv->transports = g_hash_table_new_full (g_str_hash, g_str_equal,
      NULL, NULL);

  priv->content_types = g_hash_table_new_full (g_str_hash, g_str_equal,
      NULL, NULL);

  priv->dispose_has_run = FALSE;
}

static void
wocky_jingle_factory_dispose (GObject *object)
{
  WockyJingleFactory *fac = WOCKY_JINGLE_FACTORY (object);
  WockyJingleFactoryPrivate *priv = fac->priv;
  GHashTableIter iter;
  gpointer val;

  if (priv->dispose_has_run)
    return;

  DEBUG ("dispose called");
  priv->dispose_has_run = TRUE;

  wocky_jingle_factory_stop (fac);
  g_clear_object (&priv->session);
  g_clear_object (&priv->porter);

  g_hash_table_iter_init (&iter, priv->sessions);
  while (g_hash_table_iter_next (&iter, NULL, &val))
    g_signal_handlers_disconnect_by_func (val, session_query_cap_cb, fac);
  g_hash_table_unref (priv->sessions);
  priv->sessions = NULL;

  g_hash_table_unref (priv->content_types);
  priv->content_types = NULL;
  g_hash_table_unref (priv->transports);
  priv->transports = NULL;
  g_clear_object (&priv->jingle_info);

  if (G_OBJECT_CLASS (wocky_jingle_factory_parent_class)->dispose)
    G_OBJECT_CLASS (wocky_jingle_factory_parent_class)->dispose (object);
}

static void
wocky_jingle_factory_get_property (GObject *object,
                                   guint property_id,
                                   GValue *value,
                                   GParamSpec *pspec)
{
  WockyJingleFactory *chan = WOCKY_JINGLE_FACTORY (object);
  WockyJingleFactoryPrivate *priv = chan->priv;

  switch (property_id) {
    case PROP_SESSION:
      g_value_set_object (value, priv->session);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
  }
}

static void
wocky_jingle_factory_set_property (GObject *object,
                                   guint property_id,
                                   const GValue *value,
                                   GParamSpec *pspec)
{
  WockyJingleFactory *chan = WOCKY_JINGLE_FACTORY (object);
  WockyJingleFactoryPrivate *priv = chan->priv;

  switch (property_id) {
    case PROP_SESSION:
      priv->session = g_value_dup_object (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
  }
}

static void
wocky_jingle_factory_constructed (GObject *obj)
{
  WockyJingleFactory *self = WOCKY_JINGLE_FACTORY (obj);
  GObjectClass *parent = G_OBJECT_CLASS (wocky_jingle_factory_parent_class);

  if (parent->constructed != NULL)
    parent->constructed (obj);

  attach_to_wocky_session (self);

  jingle_media_rtp_register (self);
  jingle_transport_google_register (self);
  jingle_transport_rawudp_register (self);
  jingle_transport_iceudp_register (self);
}

static void
wocky_jingle_factory_class_init (WockyJingleFactoryClass *cls)
{
  GObjectClass *object_class = G_OBJECT_CLASS (cls);
  GParamSpec *param_spec;

  g_type_class_add_private (cls, sizeof (WockyJingleFactoryPrivate));

  object_class->constructed = wocky_jingle_factory_constructed;
  object_class->get_property = wocky_jingle_factory_get_property;
  object_class->set_property = wocky_jingle_factory_set_property;
  object_class->dispose = wocky_jingle_factory_dispose;

  param_spec = g_param_spec_object ("session", "WockySession object",
      "WockySession to listen for Jingle sessions on",
      WOCKY_TYPE_SESSION,
      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
  g_object_class_install_property (object_class, PROP_SESSION, param_spec);

  /* signal definitions */

  /*
   * @session: a fresh new Jingle session for your listening pleasure
   * @initiated_locally: %TRUE if this is a new outgoing session; %FALSE if it
   *  is a new incoming session
   */
  signals[NEW_SESSION] = g_signal_new ("new-session",
        G_TYPE_FROM_CLASS (cls), G_SIGNAL_RUN_LAST,
        0, NULL, NULL, _wocky_signals_marshal_VOID__OBJECT_BOOL,
        G_TYPE_NONE, 2, WOCKY_TYPE_JINGLE_SESSION, G_TYPE_BOOLEAN);

  /*
   * @contact: the peer in a call
   * @cap: the XEP-0115 feature string the session is interested in.
   *
   * Emitted when a Jingle session wants to check whether the peer has a
   * particular capability. The handler should return %TRUE if @contact has
   * @cap.
   */
  signals[QUERY_CAP] = g_signal_new ("query-cap",
        G_TYPE_FROM_CLASS (cls), G_SIGNAL_RUN_LAST,
        0, g_signal_accumulator_first_wins, NULL,
        _wocky_signals_marshal_BOOLEAN__OBJECT_STRING,
        G_TYPE_BOOLEAN, 2, WOCKY_TYPE_CONTACT, G_TYPE_STRING);
}

WockyJingleFactory *
wocky_jingle_factory_new (
    WockySession *session)
{
  return g_object_new (WOCKY_TYPE_JINGLE_FACTORY,
      "session", session,
      NULL);
}

static void
attach_to_wocky_session (WockyJingleFactory *self)
{
  WockyJingleFactoryPrivate *priv = self->priv;

  g_assert (priv->session != NULL);

  g_assert (priv->porter == NULL);
  priv->porter = g_object_ref (wocky_session_get_porter (priv->session));

  /* TODO: we could match different dialects here maybe? */
  priv->jingle_handler_id = wocky_porter_register_handler_from_anyone (
      priv->porter,
      WOCKY_STANZA_TYPE_IQ, WOCKY_STANZA_SUB_TYPE_SET,
      WOCKY_PORTER_HANDLER_PRIORITY_NORMAL, jingle_cb, self,
      NULL);

  priv->jingle_info = wocky_jingle_info_new (priv->porter);
}

void
wocky_jingle_factory_stop (WockyJingleFactory *self)
{
  WockyJingleFactoryPrivate *priv = self->priv;

  if (priv->porter != NULL &&
      priv->jingle_handler_id != 0)
    {
      wocky_porter_unregister_handler (priv->porter, priv->jingle_handler_id);
      priv->jingle_handler_id = 0;
    }
}

/* The 'session' map is keyed by:
 * "<peer's jid>\n<session id>"
 */
#define SESSION_MAP_KEY_FORMAT "%s\n%s"

static gchar *
make_session_map_key (
    const gchar *jid,
    const gchar *sid)
{
  return g_strdup_printf (SESSION_MAP_KEY_FORMAT, jid, sid);
}

static gchar *
get_unique_sid_for (WockyJingleFactory *factory,
    const gchar *jid,
    gchar **key)
{
  guint32 val;
  gchar *sid = NULL;
  gchar *key_ = NULL;

  do
    {
      val = g_random_int_range (1000000, G_MAXINT);

      g_free (sid);
      g_free (key_);
      sid = g_strdup_printf ("%u", val);
      key_ = make_session_map_key (jid, sid);
    }
  while (g_hash_table_lookup (factory->priv->sessions, key_) != NULL);

  *key = key_;
  return sid;
}

static WockyJingleSession *
ensure_session (WockyJingleFactory *self,
    const gchar *sid,
    const gchar *from,
    WockyJingleAction action,
    WockyJingleDialect dialect,
    gboolean *new_session,
    GError **error)
{
  WockyJingleFactoryPrivate *priv = self->priv;
  gchar *key;
  WockyJingleSession *sess;

  if (!wocky_decode_jid (from, NULL, NULL, NULL))
    {
      g_prefix_error (error, "Couldn't parse sender '%s': ", from);
      return NULL;
    }

  /* If we can ensure the handle, we can decode the jid */
  key = make_session_map_key (from, sid);
  sess = g_hash_table_lookup (priv->sessions, key);
  g_free (key);

  if (sess == NULL)
    {
      if (action == WOCKY_JINGLE_ACTION_SESSION_INITIATE)
        {
          sess = create_session (self, sid, from, dialect, FALSE);
          *new_session = TRUE;
        }
      else
        {
          g_set_error (error, WOCKY_JINGLE_ERROR,
              WOCKY_JINGLE_ERROR_UNKNOWN_SESSION,
              "session %s is unknown", sid);
          return NULL;
        }
    }
  else
    {
      *new_session = FALSE;
    }

  return sess;
}

static gboolean
jingle_cb (
    WockyPorter *porter,
    WockyStanza *msg,
    gpointer user_data)
{
  WockyJingleFactory *self = WOCKY_JINGLE_FACTORY (user_data);
  GError *error = NULL;
  const gchar *sid, *from;
  WockyJingleSession *sess;
  gboolean new_session = FALSE;
  WockyJingleAction action;
  WockyJingleDialect dialect;

  /* see if it's a jingle message and detect dialect */
  sid = wocky_jingle_session_detect (msg, &action, &dialect);
  from = wocky_stanza_get_from (msg);

  if (sid == NULL || from == NULL)
    return FALSE;

  sess = ensure_session (self, sid, from, action, dialect, &new_session,
      &error);

  if (sess == NULL)
    goto REQUEST_ERROR;
  else
    /* One of the possible outcomes of wocky_jingle_session_parse() is that
     * the Jingle session is terminated, which removes it from our
     * hash table, which could release its last ref. */
    g_object_ref (sess);

  /* now act on the message */
  if (!wocky_jingle_session_parse (sess, action, msg, &error))
    goto REQUEST_ERROR;

  /* This has to be after the call to parse(), not inside create_session():
   * until the session has parsed the session-initiate stanza, it does not know
   * about its own contents, and we don't even know if the content types are
   * something we understand. So it's essentially half-alive and useless to
   * signal listeners.
   */
  if (new_session)
    g_signal_emit (self, signals[NEW_SESSION], 0, sess, FALSE);

  /* all went well, we can acknowledge the IQ */
  wocky_jingle_session_acknowledge_iq (sess, msg);

  g_object_unref (sess);
  return TRUE;

REQUEST_ERROR:
  g_assert (error != NULL);
  DEBUG ("NAKing with error: %s", error->message);
  wocky_porter_send_iq_gerror (porter, msg, error);
  g_error_free (error);

  if (sess != NULL)
    {
      if (new_session)
        wocky_jingle_session_terminate (sess, WOCKY_JINGLE_REASON_UNKNOWN, NULL, NULL);

      g_object_unref (sess);
    }

  return TRUE;
}

static gboolean
session_query_cap_cb (
    WockyJingleSession *session,
    WockyContact *contact,
    const gchar *cap_or_quirk,
    gpointer user_data)
{
  WockyJingleFactory *self = WOCKY_JINGLE_FACTORY (user_data);
  gboolean ret;

  /* Propagate the query out to the application. We can't depend on the
   * application connecting to ::query-cap on the session because caps queries
   * may happen while parsing the session-initiate stanza, which must happen
   * before the session is announced to the application.
   */
  g_signal_emit (self, signals[QUERY_CAP], 0, contact, cap_or_quirk, &ret);
  return ret;
}

/*
 * If sid is set to NULL a unique sid is generated and
 * the "local-initiator" property of the newly created
 * WockyJingleSession is set to true.
 */
static WockyJingleSession *
create_session (WockyJingleFactory *fac,
    const gchar *sid,
    const gchar *jid,
    WockyJingleDialect dialect,
    gboolean local_hold)
{
  WockyJingleFactoryPrivate *priv = fac->priv;
  WockyJingleSession *sess;
  gboolean local_initiator;
  gchar *sid_, *key;
  gpointer contact;
  WockyContactFactory *factory;

  factory = wocky_session_get_contact_factory (priv->session);
  g_assert (jid != NULL);

  if (strchr (jid, '/') != NULL)
    contact = wocky_contact_factory_ensure_resource_contact (factory, jid);
  else
    contact = wocky_contact_factory_ensure_bare_contact (factory, jid);

  g_return_val_if_fail (contact != NULL, NULL);
  g_return_val_if_fail (WOCKY_IS_CONTACT (contact), NULL);

  if (sid != NULL)
    {
      key = make_session_map_key (jid, sid);
      sid_ = g_strdup (sid);

      local_initiator = FALSE;
    }
  else
    {
      sid_ = get_unique_sid_for (fac, jid, &key);

      local_initiator = TRUE;
    }

  /* Either we should have found the existing session when the IQ arrived, or
   * get_unique_sid_for should have ensured the key is fresh. */
  g_assert (NULL == g_hash_table_lookup (priv->sessions, key));

  sess = wocky_jingle_session_new (
      fac,
      priv->porter,
      sid_, local_initiator, contact, dialect, local_hold);
  g_signal_connect (sess, "terminated",
    (GCallback) session_terminated_cb, fac);

  /* Takes ownership of key and sess */
  g_hash_table_insert (priv->sessions, key, sess);

  DEBUG ("new session (%s, %s) @ %p", jid, sid_, sess);

  g_free (sid_);
  g_object_unref (contact);

  g_signal_connect (sess, "query-cap",
      (GCallback) session_query_cap_cb, (GObject *) fac);

  return sess;
}


/**
 * wocky_jingle_factory_create_session:
 * @fac: the factory
 * @jid: the full JID (typically including a resource) to establish a session
 *  with
 * @dialect: the variant of the Jingle protocol to use
 * @local_hold: whether the call should start out on hold; if in doubt, pass %FALSE
 *
 * Creates a new #WockyJingleSession to the specified contact. Note that the
 * session will not be initiated until at least one content is added with
 * wocky_jingle_session_add_content(), and those contents are ready.
 *
 * You would typically determine which @dialect to use from the peer's
 * capabilities.
 *
 * Returns: (transfer none): the new session, which will not be %NULL
 */
WockyJingleSession *
wocky_jingle_factory_create_session (WockyJingleFactory *fac,
    const gchar *jid,
    WockyJingleDialect dialect,
    gboolean local_hold)
{
  WockyJingleSession *session = create_session (fac, NULL, jid, dialect, local_hold);

  g_signal_emit (fac, signals[NEW_SESSION], 0, session, TRUE);
  return session;
}

void
wocky_jingle_factory_register_transport (WockyJingleFactory *self,
                                          gchar *xmlns,
                                          GType transport_type)
{
  g_return_if_fail (g_type_is_a (transport_type,
        WOCKY_TYPE_JINGLE_TRANSPORT_IFACE));

  g_hash_table_insert (self->priv->transports, xmlns,
      GSIZE_TO_POINTER (transport_type));
}

GType
wocky_jingle_factory_lookup_transport (WockyJingleFactory *self,
                                        const gchar *xmlns)
{
  return GPOINTER_TO_SIZE (g_hash_table_lookup (self->priv->transports,
        xmlns));
}

void
wocky_jingle_factory_register_content_type (WockyJingleFactory *self,
                                             gchar *xmlns,
                                             GType content_type)
{
  g_return_if_fail (g_type_is_a (content_type, WOCKY_TYPE_JINGLE_CONTENT));

  g_hash_table_insert (self->priv->content_types, xmlns,
      GSIZE_TO_POINTER (content_type));
}

GType
wocky_jingle_factory_lookup_content_type (WockyJingleFactory *self,
                                           const gchar *xmlns)
{
  return GPOINTER_TO_SIZE (g_hash_table_lookup (self->priv->content_types,
        xmlns));
}

static void
session_terminated_cb (WockyJingleSession *session,
                       gboolean local_terminator G_GNUC_UNUSED,
                       WockyJingleReason reason G_GNUC_UNUSED,
                       const gchar *text G_GNUC_UNUSED,
                       WockyJingleFactory *factory)
{
  gchar *key = make_session_map_key (
      wocky_jingle_session_get_peer_jid (session),
      wocky_jingle_session_get_sid (session));

  DEBUG ("removing terminated session with key %s", key);

  g_signal_handlers_disconnect_by_func (session, session_query_cap_cb, factory);
  g_warn_if_fail (g_hash_table_remove (factory->priv->sessions, key));

  g_free (key);
}

WockyJingleInfo *
wocky_jingle_factory_get_jingle_info (
    WockyJingleFactory *self)
{
  return self->priv->jingle_info;
}
