/*
 * This file is part of the Sofia-SIP package
 *
 * Copyright (C) 2006 Nokia Corporation.
 *
 * Contact: Kai Vehmanen <kai.vehmanen@nokia.com>
 *
 * * 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
 *
 */

/*
 * Status:
 *  - skeleton, not yet fully working
 *
 * Todo:
 *  - see comments marked with 'XXX'
 *
 * Notes:
 *  - see test-sscm.c
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#if HAVE_FARSIGHT

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

#include <glib.h>
#include <gst/gst.h>
#include <farsight/farsight.h>
#include <farsight/farsight-transport.h>

#include "sdp_utils.h"
#include "ssc_media.h"
#include "ssc_media_farsight.h"
#include "ssc_media_gst_utils.h"

#if !HAVE_G_DEBUG
#include "replace_g_debug.h"
#endif

/* Signals */
enum {
  SIGNAL_LAST
};

/* props */
enum {
  PROP_LAST, 
};

static void ssc_media_farsight_class_init    (SscMediaFarsightClass *klass);
static void ssc_media_farsight_init          (SscMediaFarsight *sscm);
static void ssc_media_farsight_dispose       (GObject *object);
static void ssc_media_farsight_finalize      (GObject *object);

static void ssc_media_farsight_set_property (GObject      *object,
					guint         prop_id,
					const GValue *value,
					GParamSpec   *pspec);
static void ssc_media_farsight_get_property (GObject    *object,
					guint       prop_id,
					GValue     *value,
					GParamSpec *pspec);

static int priv_activate_farsight(SscMedia *sscm);
static int priv_deactivate_farsight(SscMedia *sscm);
static const char *priv_fs_media_type_str(FarsightMediaType arg);
static int priv_refresh_farsight(SscMedia *sscm);
static int priv_static_capabilities_farsight(SscMedia *sscm, char **dst);

static gboolean priv_cb_pipeline_bus (GstBus *bus, GstMessage *message, gpointer data);
static int priv_update_local_sdp(SscMediaFarsight *self);
static int priv_update_rx_elements(SscMediaFarsight *self);
static int priv_update_tx_elements(SscMediaFarsight *self);
static gboolean priv_verify_required_elements(void);
static void priv_new_native_candidate (FarsightStream *stream, const gchar *candidate_id, gpointer data);


static GObjectClass *parent_class = NULL;
/* static guint ssc_media_farsight_signals[SIGNAL_LAST] = { 0 }; */

static void
priv_stream_error (FarsightStream *stream,
		   FarsightStreamError error,
		   const gchar *debug)
{
  g_print ("%s: stream error: stream=%p error=%s\n", __FUNCTION__, stream, debug);
}

static void
priv_session_error (FarsightSession *stream,
		    FarsightSessionError error,
		    const gchar *debug,
		    gpointer data)
{
  SscMedia *parent = SSC_MEDIA(data);
  g_print ("%s: session error: session=%p error=%s\n", __FUNCTION__, stream, debug);
  ssc_media_signal_state_change (parent, sm_error);
}


static void
priv_new_active_candidate_pair (FarsightStream *stream, gchar* native_candidate, gchar *remote_candidate)
{
  /* XXX: ignore for now, only one candidate supported */
  g_print ("%s: new-native-candidate-pair: stream=%p\n", __FUNCTION__, stream);
}

static void
priv_codec_changed (FarsightStream *stream, gint codec_id)
{
  g_print ("%s: codec-changed: codec_id=%d, stream=%p\n", __FUNCTION__, codec_id, stream);
}

static void
priv_native_candidates_prepared (FarsightStream *stream, gpointer data)
{
  /* XXX: this is never called in the libjingle implementation */
  g_print ("%s: preparation-complete: stream=%p\n", __FUNCTION__, stream);
}

static void
priv_new_native_candidate (FarsightStream *stream, const gchar *candidate_id, gpointer data)
{
  SscMediaFarsight *self = SSC_MEDIA_FARSIGHT(data);
  SscMedia *parent = SSC_MEDIA(data);
  GList *list, *li;
  FarsightTransportInfo *info;
  gboolean regen_lsdp = FALSE;

  g_debug(__func__);

  list = farsight_stream_get_native_candidate(stream, candidate_id);
  for (li = list; li; li = g_list_next(li)) {
    info = (FarsightTransportInfo*)li->data;
    g_message ("New candidate added: %s %d %s %s %s:%d, pref %f", 
	       info->candidate_id, info->component, (info->proto == FARSIGHT_NETWORK_PROTOCOL_TCP)?"TCP":"UDP",
	       info->proto_subtype, info->ip, info->port, (double) info->preference);
    
    if (info->proto == FARSIGHT_NETWORK_PROTOCOL_UDP &&
	/* info->type == FARSIGHT_CANDIDATE_TYPE_DERIVED && */
	self->fs_local_transport == NULL) {
      g_message("Candidate selected as active.");
      self->fs_local_transport = info;
      regen_lsdp = TRUE;
    }
  }

  if (regen_lsdp) {
    priv_update_local_sdp(self);
    ssc_media_signal_state_change (parent, sm_local_ready);
  }
}

static void 
priv_state_changed (FarsightStream *stream, 
		    FarsightStreamState state,
		    FarsightStreamDirection dir,
		    gpointer data)
{
  SscMediaFarsight *self = SSC_MEDIA_FARSIGHT(data);
  SscMedia *parent = SSC_MEDIA(self);
  
  g_debug(__func__);

  switch (state) {
  case FARSIGHT_STREAM_STATE_STOPPED:
    g_message ("%s: %p stopped\n", __FUNCTION__, stream);
    ssc_media_signal_state_change (parent, sm_error);
    break;
  case FARSIGHT_STREAM_STATE_PLAYING: 
    g_print ("%s: %p playing\n", __FUNCTION__, stream);
    ssc_media_signal_state_change (parent, sm_active);
    break;
  }
}

GType
ssc_media_farsight_get_type (void)
{
  static GType type = 0;

  if (type == 0) {
    static const GTypeInfo info = {
      sizeof (SscMediaFarsightClass),
      NULL,
      NULL,
      (GClassInitFunc) ssc_media_farsight_class_init,
      NULL,
      NULL,
      sizeof (SscMediaFarsight),
      0,
      (GInstanceInitFunc) ssc_media_farsight_init
    };
    
    type = g_type_register_static (SSC_MEDIA_TYPE,
				   "SscMediaFarsightType",
				   &info, 0);
  }
  
  return type;
}

static void ssc_media_farsight_class_init (SscMediaFarsightClass *klass)
{
  GObjectClass *gobject_class;
  SscMediaClass *parent_class = SSC_MEDIA_CLASS(klass);
  
  g_debug("%s:%d", __func__, __LINE__);
  
  gobject_class = (GObjectClass *) klass;
  gobject_class->dispose = ssc_media_farsight_dispose;
  gobject_class->finalize = ssc_media_farsight_finalize;
  gobject_class->set_property = ssc_media_farsight_set_property;
  gobject_class->get_property = ssc_media_farsight_get_property;
  
  /* assign default methods */
  parent_class->activate = priv_activate_farsight;
  parent_class->deactivate = priv_deactivate_farsight;
  parent_class->refresh = priv_refresh_farsight;
  parent_class->static_capabilities = priv_static_capabilities_farsight;
}

static void ssc_media_farsight_init (SscMediaFarsight *object)
{
  SscMediaFarsight *self = SSC_MEDIA_FARSIGHT (object);

  self->fs_session = farsight_session_factory_make ("rtp");
  g_print ("protocol details:\n name: %s\n description: %s\n author: %s\n",
	   farsight_plugin_get_name (self->fs_session->plugin),
	   farsight_plugin_get_description (self->fs_session->plugin),
	   farsight_plugin_get_author (self->fs_session->plugin));

  g_signal_connect (G_OBJECT (self->fs_session), "error", 
		    G_CALLBACK (priv_session_error), NULL);
 
}

static void ssc_media_farsight_finalize (GObject *object)
{
  SscMediaFarsight *self = SSC_MEDIA_FARSIGHT (object);
  
  g_debug(__func__);
  g_assert(self);
}

static void ssc_media_farsight_dispose (GObject *object)
{
  SscMediaFarsight *self = SSC_MEDIA_FARSIGHT (object);

  if (!self->dispose_run) {
    self->dispose_run = TRUE;
    
    if (ssc_media_is_initialized(SSC_MEDIA(self)))
      priv_deactivate_farsight(SSC_MEDIA(self));

    if (self->fs_stream) 
	g_object_unref (self->fs_stream);
    /* handled by farsight? */
#if 0
    if (audiosink) 
	gst_object_unref(audiosink);
    if (audiosrc) 
	gst_object_unref(audiosrc);
#endif
  }
}

static void
ssc_media_farsight_set_property (GObject      *object, 
			    guint         prop_id,
			    const GValue *value, 
			    GParamSpec   *pspec)
{
  SscMediaFarsight *self;
  
  g_return_if_fail (SSC_IS_MEDIA_FARSIGHT (object));
  self = SSC_MEDIA_FARSIGHT (object);
  
  switch (prop_id) {
    default:
      g_debug("Unknown object property %s:%u.", G_OBJECT_TYPE_NAME(object), prop_id);
  }
}

static void
ssc_media_farsight_get_property (GObject    *object, 
			    guint       prop_id, 
			    GValue     *value,
			    GParamSpec *pspec)
{
  SscMediaFarsight *self;
  
  g_return_if_fail (SSC_IS_MEDIA_FARSIGHT (object));
  
  self = SSC_MEDIA_FARSIGHT (object);
  
  switch (prop_id) {
    default:
      g_debug("Unknown object property %s:%u.", G_OBJECT_TYPE_NAME(object), prop_id);
  }
}

static int priv_activate_farsight(SscMedia *parent)
{
  SscMediaFarsight *self = SSC_MEDIA_FARSIGHT (parent);
  int len = 0, res = 0;
  const GList *possible_codecs, *lp;
  FarsightCodec *codec;
  GstElement *audiosink, *audiosrc;
    
  g_debug(__func__);

  self->fs_stream = farsight_session_create_stream (self->fs_session,
						    FARSIGHT_MEDIA_TYPE_AUDIO,
						    FARSIGHT_STREAM_DIRECTION_BOTH);
  if (self->fs_stream) {
    g_signal_connect (G_OBJECT (self->fs_stream), "error", 
		      G_CALLBACK (priv_stream_error), NULL);
    g_signal_connect (G_OBJECT (self->fs_stream), "new-active-candidate-pair", 
		      G_CALLBACK (priv_new_active_candidate_pair), NULL);
    g_signal_connect (G_OBJECT (self->fs_stream), "codec-changed", 
		      G_CALLBACK (priv_codec_changed), NULL);
    g_signal_connect (G_OBJECT (self->fs_stream), "native-candidates-prepared", 
		      G_CALLBACK (priv_native_candidates_prepared), self);
    g_signal_connect (G_OBJECT (self->fs_stream), "new-native-candidate", 
		    G_CALLBACK (priv_new_native_candidate), self);
    g_signal_connect (G_OBJECT (self->fs_stream), "state-changed", 
		      G_CALLBACK (priv_state_changed), self);

    possible_codecs = farsight_stream_get_local_codecs (self->fs_stream);

    for (lp = possible_codecs; lp; lp = g_list_next (lp)) {
      codec = (FarsightCodec*) lp->data;
      g_message ("codec: %d: %s/%d found", codec->id, codec->encoding_name, 
		 codec->clock_rate);
    }
  }
  else 
    res = -1;

  /* step: source element */
  audiosrc = ssc_media_create_audio_src(SOFSIP_DEFAULT_AUDIO);
  if (audiosrc) {
    g_object_set (G_OBJECT (audiosrc), "device", "default", NULL);
    g_object_set (G_OBJECT (audiosrc), "latency-time", G_GINT64_CONSTANT (20000), NULL);
    g_object_set (G_OBJECT (audiosrc), "blocksize", 320, NULL); /* 320 octets, 20msec at L16/1 */
    farsight_stream_set_source (self->fs_stream, audiosrc);
  }
  else {
    res = -1;
    g_message("Unable to create gstreamer audio src '%s'.", SOFSIP_DEFAULT_AUDIO);
  }

  /* step: sink element */
  audiosink = ssc_media_create_audio_sink(SOFSIP_DEFAULT_AUDIO);
  if (audiosink) {
    g_object_set (G_OBJECT (audiosink), "device", "default", NULL);
    g_object_set (G_OBJECT (audiosink), "latency-time", G_GINT64_CONSTANT (20000), NULL);
    g_object_set (G_OBJECT (audiosink), "buffer-time", G_GINT64_CONSTANT (160000), NULL);
    farsight_stream_set_sink (self->fs_stream, audiosink);
  }
  else {
    g_message("Unable to create gstreamer audio sink '%s'.", SOFSIP_DEFAULT_AUDIO);
    res = -1;
  }

  if (res < 0) {
    if (audiosink) gst_object_unref(audiosink);
    if (audiosrc) gst_object_unref(audiosrc);
    if (self->fs_stream) g_object_unref (self->fs_stream);
  }
  else {
      farsight_stream_prepare_transports(self->fs_stream);
      parent->sm_state = sm_active;
  }

  return res;
}

static const char *priv_fs_media_type_str(FarsightMediaType arg)
{
  return (arg == FARSIGHT_MEDIA_TYPE_AUDIO ? "audio" : "video");
}

/**
 * Refreshes the local SDP based on Farsight stream, and current
 * object, state.
 */
static int priv_update_local_sdp(SscMediaFarsight *self)
{
  const char *c_sdp_version = "v=0\r\n"; 
  const char *c_crlf = "\r\n";
  gchar *tmpa_str = NULL, *tmpb_str;
  gchar *aline_str = NULL, *cline_str = NULL, *mline_str = NULL, *malines_str = NULL;
  FarsightStream *stream = self->fs_stream;
  FarsightMediaType mediatype = /* XXX: not implemented in FS! ->
				   farsight_stream_get_media_type(stream) */ FARSIGHT_MEDIA_TYPE_AUDIO;
    
  FarsightTransportInfo *transport = self->fs_local_transport;
  const GList *li, *codecs = NULL;
  int result = -1;

  /* Note: impl limits ...
   * - no multi-stream support
   * - no IPv6 support (missing from the Farsight API?)
   */

  if (transport && transport->proto == FARSIGHT_NETWORK_PROTOCOL_UDP) {
    cline_str = g_strdup_printf("c=IN IP4 %s%s", transport->ip, c_crlf);
    /* leave end of 'mline_str' for PT ids */
    mline_str = g_strdup_printf("m=%s %u RTP/AVP", 
				priv_fs_media_type_str(mediatype), transport->port);
    codecs = farsight_stream_get_local_codecs (self->fs_stream);
  }
  else {
    cline_str = g_strdup("c=IN IP4 0.0.0.0\r\n");
    /* no transport, so need to check codecs */
  }

  for (li = codecs; li; li = g_list_next (li)) {
    FarsightCodec *codec = (FarsightCodec*) li->data;
    /* step: add entry to media a-lines */
    if (codec) {
      tmpa_str = malines_str;
      tmpb_str = g_strdup_printf("a=rtpmap:%d %s/%u%s", 
				 codec->id,
				 codec->encoding_name,
				 codec->clock_rate, 
				 c_crlf);
      if (malines_str)
	malines_str = g_strconcat(malines_str, tmpb_str, NULL), g_free(tmpb_str);
      else
	malines_str = tmpb_str;

      g_free(tmpa_str);

      /* step: add PT id to mline */
      tmpa_str = mline_str;
      tmpb_str = g_strdup_printf(" %d", codec->id);
      mline_str = g_strconcat(mline_str, tmpb_str, NULL);
      g_free(tmpa_str), g_free(tmpb_str);
    }
  }

  tmpa_str = g_strconcat(c_sdp_version, mline_str, c_crlf, cline_str, malines_str, NULL);
  printf("Regenerated local SDP:{\n%s}\n", tmpa_str);
  g_object_set(G_OBJECT(self), 
	       "localsdp", tmpa_str, NULL);

  g_free(aline_str);
  g_free(cline_str);
  g_free(mline_str);
  g_free(malines_str);
  g_free(tmpa_str);
}

static GList *priv_remote_sdp_codecs(SscMediaFarsight *self)
{
  SscMedia *parent = SSC_MEDIA (self);
  sdp_session_t *r_sdp = sdp_session(parent->sm_sdp_remote);
  sdp_connection_t *r_c = (!r_sdp) ? NULL : sdp_media_connections(r_sdp->sdp_media);
  GList *res = NULL;
  FarsightCodec *codec = NULL;

  g_debug(__func__);

  if (r_c) {
    sdp_media_t *sdpmedia = r_sdp->sdp_media;

    /* XXX: should find the matching media, now just use the first one */

    while (sdpmedia) {
      if (sdpmedia->m_type ==  sdp_media_audio ||
	  sdpmedia->m_type == sdp_media_video) {
	sdp_rtpmap_t *rtpmap = sdpmedia->m_rtpmaps;
	while (rtpmap) {
	  codec = g_new0(FarsightCodec, 1);

	  /* RFC2327: see "m=" line definition 
	   *  - note, 'encoding_params' is assumed to be channel
	   *    count (i.e. channels in farsight) */ 

	  codec->encoding_name = g_strdup(rtpmap->rm_encoding);
	  codec->clock_rate = rtpmap->rm_rate;
	  if (rtpmap->rm_params) 
	    codec->channels = atoi(rtpmap->rm_params);
	  else 
	    codec->channels = 0;
	  codec->media_type = 
	    (sdpmedia->m_type ==  sdp_media_audio ? 
	     FARSIGHT_MEDIA_TYPE_AUDIO : FARSIGHT_MEDIA_TYPE_VIDEO);
	  /* XXX: copy extra SDP attributes */
	  codec->optional_params = NULL;

	  res = g_list_append(res, (gpointer)codec);

	  rtpmap = rtpmap->rm_next;
	}

	/* note: only describe one media */
	break;
      }

      sdpmedia = sdpmedia->m_next;
    }  
  }

  if (g_list_length(res) == 0) {
    g_list_free(res), res = NULL;
  }

  return res;
}

static void priv_free_codec_list(GList *list)
{
  GList *i;
  for (i = list; i; i = g_list_next(i)) {
    FarsightCodec *item = (FarsightCodec *)(i->data);
    if (item) {
      if (item->encoding_name) g_free((gchar*)item->encoding_name);
      g_free(item);
    }
  }
  g_list_free(list);
}

static GList *priv_remote_sdp_candidates(SscMediaFarsight *self)
{
  SscMedia *parent = SSC_MEDIA (self);
  sdp_session_t *r_sdp = sdp_session(parent->sm_sdp_remote);
  sdp_connection_t *r_c = (!r_sdp) ? NULL : sdp_media_connections(r_sdp->sdp_media);
  GList *res = NULL;

  g_debug(__func__);

  if (r_c) {
    FarsightTransportInfo *tport = g_new0(FarsightTransportInfo, 1);
    res = g_list_append(res, (gpointer)tport);

    tport->candidate_id = "sipsdp";
    tport->ip = g_strdup(r_c->c_address);
    tport->proto = FARSIGHT_NETWORK_PROTOCOL_UDP;
    tport->proto_subtype = "RTP";
    tport->proto_profile = "AVP";
    tport->preference = 0.1;
    tport->type = FARSIGHT_CANDIDATE_TYPE_LOCAL;
    tport->username = NULL;
    tport->password = NULL;
  }  

  return res;
}

static void priv_free_candidate_list(GList *list)
{
  GList *i;
  for (i = list; i; i = g_list_next(i)) {
    FarsightTransportInfo *item = (FarsightTransportInfo *)(i->data);
    if (item) {
      if (item->ip) g_free((gchar*)item->ip);
      g_free(item);
    }
  }
  g_list_free(list);
}

/**
 * Called when either local or remote SDP is modified
 * by the signaling events.
 */
static int priv_refresh_farsight(SscMedia *parent)
{
  SscMediaFarsight *self = SSC_MEDIA_FARSIGHT (parent);
  FarsightStreamState state = farsight_stream_get_state(self->fs_stream);
  sdp_session_t *r_sdp = sdp_session(parent->sm_sdp_remote);
  int res = 0;

  g_debug(__func__);

  if (r_sdp && self->fs_remote_transport == NULL) {
    GList *codecs = priv_remote_sdp_codecs(self);
    GList *rcands = priv_remote_sdp_candidates(self);

    if (rcands && codecs) {
      self->fs_remote_transport = (FarsightTransportInfo*)(rcands->data);
      
      farsight_stream_set_remote_codecs(self->fs_stream,
					codecs);
      
      
      farsight_stream_set_remote_candidate_list(self->fs_stream,
						rcands);
     
      farsight_stream_set_active_candidate_pair(
        self->fs_stream,
	self->fs_local_transport->candidate_id,
	self->fs_remote_transport->candidate_id);

      priv_free_codec_list(codecs);
      priv_free_candidate_list(rcands);
    }

    if (state == FARSIGHT_STREAM_STATE_STOPPED) {
      farsight_stream_start(self->fs_stream);
    }
  }


  
  return res;
}

static int priv_deactivate_farsight(SscMedia *parent)
{
  SscMediaFarsight *self = SSC_MEDIA_FARSIGHT (parent);

  g_assert(ssc_media_is_initialized(parent) == TRUE);

  g_debug(__func__);
  farsight_stream_stop(self->fs_stream);
  parent->sm_state = sm_disabled;

  self->fs_local_transport = NULL;
  self->fs_remote_transport = NULL;

  g_assert(ssc_media_is_initialized(parent) != TRUE);
}

static int priv_static_capabilities_farsight(SscMedia *parent, char **dest)
{
  SscMediaFarsight *self = SSC_MEDIA_FARSIGHT (parent);
  su_home_t *home = parent->sm_home;
  char *caps_sdp_str = NULL;

  /* XXX: not really 100% accurate, we most likely have support for
   *      more codecs */
  caps_sdp_str = su_strcat(home, caps_sdp_str, 
			   "v=0\r\n"
			   "m=audio 0 RTP/AVP 0\r\n"
			   "a=rtpmap:0 PCMU/8000\r\n");

  *dest = strdup(caps_sdp_str);
  su_free(home, caps_sdp_str);

  return 0;
}

#endif /* HAVE_FARSIGHT */
