/*
 * empathy-sound-manager.c - Various sound related utility functions.
 * Copyright (C) 2009-2010 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 "empathy-sound-manager.h"

#include <glib/gi18n-lib.h>

#include "empathy-gsettings.h"
#include "empathy-presence-manager.h"
#include "empathy-utils.h"

#define DEBUG_FLAG EMPATHY_DEBUG_OTHER
#include "empathy-debug.h"

typedef struct {
  EmpathySound sound_id;
  const char * event_ca_id;
  const char * event_ca_description;
  const char * key;
} EmpathySoundEntry;

typedef struct {
  GtkWidget *widget;
  gint sound_id;
  guint play_interval;
  guint replay_timeout_id;
  EmpathySoundManager *self;
} EmpathyRepeatableSound;

/* NOTE: these entries MUST be in the same order than EmpathySound enum */
static EmpathySoundEntry sound_entries[LAST_EMPATHY_SOUND] = {
  { EMPATHY_SOUND_MESSAGE_INCOMING, "message-new-instant",
    N_("Received an instant message"), EMPATHY_PREFS_SOUNDS_INCOMING_MESSAGE } ,
  { EMPATHY_SOUND_MESSAGE_OUTGOING, "message-sent-instant",
    N_("Sent an instant message"), EMPATHY_PREFS_SOUNDS_OUTGOING_MESSAGE } ,
  { EMPATHY_SOUND_CONVERSATION_NEW, "message-new-instant",
    N_("Incoming chat request"), EMPATHY_PREFS_SOUNDS_NEW_CONVERSATION },
  { EMPATHY_SOUND_CONTACT_CONNECTED, "service-login",
    N_("Contact connected"), EMPATHY_PREFS_SOUNDS_CONTACT_LOGIN },
  { EMPATHY_SOUND_CONTACT_DISCONNECTED, "service-logout",
    N_("Contact disconnected"), EMPATHY_PREFS_SOUNDS_CONTACT_LOGOUT },
  { EMPATHY_SOUND_ACCOUNT_CONNECTED, "service-login",
    N_("Connected to server"), EMPATHY_PREFS_SOUNDS_SERVICE_LOGIN },
  { EMPATHY_SOUND_ACCOUNT_DISCONNECTED, "service-logout",
    N_("Disconnected from server"), EMPATHY_PREFS_SOUNDS_SERVICE_LOGOUT },
  { EMPATHY_SOUND_PHONE_INCOMING, "phone-incoming-call",
    N_("Incoming voice call"), NULL },
  { EMPATHY_SOUND_PHONE_OUTGOING, "phone-outgoing-calling",
    N_("Outgoing voice call"), NULL },
  { EMPATHY_SOUND_PHONE_HANGUP, "phone-hangup",
    N_("Voice call ended"), NULL },
};

G_DEFINE_TYPE (EmpathySoundManager, empathy_sound_manager, G_TYPE_OBJECT)

struct _EmpathySoundManagerPrivate
{
  /* A hash table containing currently repeating sounds. The format is the
   * following:
   * Key: An EmpathySound
   * Value : The EmpathyRepeatableSound associated with that EmpathySound. */
  GHashTable *repeating_sounds;
  GSettings *gsettings_sound;
};

static void
empathy_sound_manager_dispose (GObject *object)
{
  EmpathySoundManager *self = (EmpathySoundManager *) object;

  tp_clear_pointer (&self->priv->repeating_sounds, g_hash_table_unref);
  tp_clear_object (&self->priv->gsettings_sound);

  G_OBJECT_CLASS (empathy_sound_manager_parent_class)->dispose (object);
}

static void
empathy_sound_manager_class_init (EmpathySoundManagerClass *cls)
{
  GObjectClass *object_class = G_OBJECT_CLASS (cls);

  object_class->dispose = empathy_sound_manager_dispose;

  g_type_class_add_private (cls, sizeof (EmpathySoundManagerPrivate));
}

static void
empathy_sound_widget_destroyed_cb (GtkWidget *widget,
    gpointer user_data)
{
  EmpathyRepeatableSound *repeatable_sound = user_data;

  /* The sound must be stopped... If it is waiting for replay, remove
   * it from hash table to cancel. Otherwise playing_finished_cb will be
   * called with an error. */
  if (repeatable_sound->replay_timeout_id != 0)
    {
      g_hash_table_remove (repeatable_sound->self->priv->repeating_sounds,
          GINT_TO_POINTER (repeatable_sound->sound_id));
    }
}

static void
repeating_sounds_item_delete (gpointer data)
{
  EmpathyRepeatableSound *repeatable_sound = data;

  if (repeatable_sound->replay_timeout_id != 0)
    g_source_remove (repeatable_sound->replay_timeout_id);

  if (repeatable_sound->widget != NULL)
    {
      g_signal_handlers_disconnect_by_func (repeatable_sound->widget,
          empathy_sound_widget_destroyed_cb, repeatable_sound);
    }

  g_object_unref (repeatable_sound->self);

  g_slice_free (EmpathyRepeatableSound, repeatable_sound);
}

static void
empathy_sound_manager_init (EmpathySoundManager *self)
{
  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
      EMPATHY_TYPE_SOUND_MANAGER, EmpathySoundManagerPrivate);

  self->priv->repeating_sounds = g_hash_table_new_full (NULL, NULL,
      NULL, repeating_sounds_item_delete);

  self->priv->gsettings_sound = g_settings_new (EMPATHY_PREFS_SOUNDS_SCHEMA);
}

EmpathySoundManager *
empathy_sound_manager_dup_singleton (void)
{
  static EmpathySoundManager *manager = NULL;

  if (G_LIKELY (manager != NULL))
      return g_object_ref (manager);

  manager = g_object_new (EMPATHY_TYPE_SOUND_MANAGER, NULL);

  g_object_add_weak_pointer (G_OBJECT (manager), (gpointer *) &manager);
  return manager;
}

static gboolean
empathy_check_available_state (void)
{
  TpConnectionPresenceType most_available_requested_presence;
  TpAccountManager *am;
  GList *accounts;

  /* We cannot use tp_account_manager_get_most_available_presence() or
   * empathy_presence_manager_get_state() because it is the requested presence
   * that matters, not the current presence.
   * See https://bugzilla.gnome.org/show_bug.cgi?id=704454 */
  most_available_requested_presence = TP_CONNECTION_PRESENCE_TYPE_UNSET;
  am = tp_account_manager_dup ();
  accounts = tp_account_manager_dup_valid_accounts (am);
  while (accounts != NULL)
    {
      TpAccount *account = accounts->data;
      TpConnectionPresenceType requested_presence;

      requested_presence = tp_account_get_requested_presence (account,
          NULL, NULL);

      if (tp_connection_presence_type_cmp_availability (requested_presence,
              most_available_requested_presence) > 0)
        most_available_requested_presence = requested_presence;

      g_object_unref (account);
      accounts = g_list_delete_link (accounts, accounts);
    }

  g_object_unref (am);

  if (most_available_requested_presence != TP_CONNECTION_PRESENCE_TYPE_AVAILABLE &&
    most_available_requested_presence != TP_CONNECTION_PRESENCE_TYPE_UNSET)
    return FALSE;

  return TRUE;
}

static gboolean
empathy_sound_pref_is_enabled (EmpathySoundManager *self,
    EmpathySound sound_id)
{
  EmpathySoundEntry *entry;

  entry = &(sound_entries[sound_id]);
  g_return_val_if_fail (entry->sound_id == sound_id, FALSE);

  if (entry->key == NULL)
    return TRUE;

  if (! g_settings_get_boolean (self->priv->gsettings_sound,
      EMPATHY_PREFS_SOUNDS_ENABLED))
    return FALSE;

  if (!empathy_check_available_state ())
    {
      if (g_settings_get_boolean (self->priv->gsettings_sound,
            EMPATHY_PREFS_SOUNDS_DISABLED_AWAY))
        return FALSE;
    }

  return g_settings_get_boolean (self->priv->gsettings_sound, entry->key);
}

/**
 * empathy_sound_manager_stop:
 * @self: a #EmpathySoundManager
 * @sound_id: The #EmpathySound to stop playing.
 *
 * Stop playing a sound. If it has been stated in loop with
 * empathy_sound_start_playing(), it will also stop replaying.
 */
void
empathy_sound_manager_stop (EmpathySoundManager *self,
    EmpathySound sound_id)
{
  EmpathySoundEntry *entry;
  EmpathyRepeatableSound *repeatable_sound;

  g_return_if_fail (sound_id < LAST_EMPATHY_SOUND);

  entry = &(sound_entries[sound_id]);
  g_return_if_fail (entry->sound_id == sound_id);

  repeatable_sound = g_hash_table_lookup (self->priv->repeating_sounds,
      GINT_TO_POINTER (sound_id));
  if (repeatable_sound != NULL)
    {
      /* The sound must be stopped... If it is waiting for replay, remove
       * it from hash table to cancel. Otherwise we'll cancel the sound
       * being played. */
      if (repeatable_sound->replay_timeout_id != 0)
        {
          g_hash_table_remove (self->priv->repeating_sounds,
              GINT_TO_POINTER (sound_id));
          return;
        }
    }

  ca_context_cancel (ca_gtk_context_get (), entry->sound_id);
}

static gboolean
empathy_sound_play_internal (GtkWidget *widget, EmpathySound sound_id,
  ca_finish_callback_t callback, gpointer user_data)
{
  EmpathySoundEntry *entry;
  ca_context *c;
  ca_proplist *p = NULL;

  entry = &(sound_entries[sound_id]);
  g_return_val_if_fail (entry->sound_id == sound_id, FALSE);

  c = ca_gtk_context_get ();
  ca_context_cancel (c, entry->sound_id);

  DEBUG ("Play sound \"%s\" (%s)",
         entry->event_ca_id,
         entry->event_ca_description);

  if (ca_proplist_create (&p) < 0)
    goto failed;

  if (ca_proplist_sets (p, CA_PROP_EVENT_ID, entry->event_ca_id) < 0)
    goto failed;

  if (ca_proplist_sets (p, CA_PROP_EVENT_DESCRIPTION,
          gettext (entry->event_ca_description)) < 0)
    goto failed;

  if (widget != NULL)
    {
      if (ca_gtk_proplist_set_for_widget (p, widget) < 0)
        goto failed;
    }

  ca_context_play_full (ca_gtk_context_get (), entry->sound_id, p, callback,
      user_data);

  ca_proplist_destroy (p);

  return TRUE;

failed:
  if (p != NULL)
    ca_proplist_destroy (p);

  return FALSE;
}

/**
 * empathy_sound_manager_play_full:
 * @self: a #EmpathySoundManager
 * @widget: The #GtkWidget from which the sound is originating.
 * @sound_id: The #EmpathySound to play.
 * @callback: The #ca_finish_callback_t function that will be called when the
 *            sound  has stopped playing.
 * @user_data: user data to pass to the function.
 *
 * Plays a sound.
 *
 * Returns %TRUE if the sound has successfully started playing, otherwise
 * returning %FALSE and @callback won't be called.
 *
 * This function returns %FALSE if the sound is already playing in loop using
 * %empathy_sound_start_playing.
 *
 * This function returns %FALSE if the sound is disabled in empathy preferences.
 *
 * Return value: %TRUE if the sound has successfully started playing, %FALSE
 *               otherwise.
 */
gboolean
empathy_sound_manager_play_full (EmpathySoundManager *self,
    GtkWidget *widget,
    EmpathySound sound_id,
    ca_finish_callback_t callback,
    gpointer user_data)
{
  g_return_val_if_fail (widget == NULL || GTK_IS_WIDGET (widget), FALSE);
  g_return_val_if_fail (sound_id < LAST_EMPATHY_SOUND, FALSE);

  if (!empathy_sound_pref_is_enabled (self, sound_id))
    return FALSE;

  /* The sound might already be playing repeatedly. If it's the case, we
   * immediadely return since there's no need to make it play again */
  if (g_hash_table_lookup (self->priv->repeating_sounds,
        GINT_TO_POINTER (sound_id)) != NULL)
    return FALSE;

  return empathy_sound_play_internal (widget, sound_id, callback, user_data);
}

/**
 * empathy_sound_manager_play:
 * @self: a #EmpathySoundManager
 * @widget: The #GtkWidget from which the sound is originating.
 * @sound_id: The #EmpathySound to play.
 *
 * Plays a sound. See %empathy_sound_manager_play_full for details.'
 *
 * Return value: %TRUE if the sound has successfully started playing, %FALSE
 *               otherwise.
 */
gboolean
empathy_sound_manager_play (EmpathySoundManager *self,
    GtkWidget *widget,
    EmpathySound sound_id)
{
  g_return_val_if_fail (widget == NULL || GTK_IS_WIDGET (widget), FALSE);
  g_return_val_if_fail (sound_id < LAST_EMPATHY_SOUND, FALSE);

  return empathy_sound_manager_play_full (self, widget, sound_id, NULL, NULL);
}

static void playing_finished_cb (ca_context *c, guint id, int error_code,
  gpointer user_data);

static gboolean
playing_timeout_cb (gpointer data)
{
  EmpathyRepeatableSound *repeatable_sound = data;
  gboolean playing;

  repeatable_sound->replay_timeout_id = 0;

  playing = empathy_sound_play_internal (repeatable_sound->widget,
      repeatable_sound->sound_id, playing_finished_cb, data);

  if (!playing)
    {
      DEBUG ("Failed to replay sound, stop repeating");
      g_hash_table_remove (repeatable_sound->self->priv->repeating_sounds,
          GINT_TO_POINTER (repeatable_sound->sound_id));
    }

  return FALSE;
}

static void
playing_finished_cb (ca_context *c, guint id, int error_code,
  gpointer user_data)
{
  EmpathyRepeatableSound *repeatable_sound = user_data;

  if (error_code != CA_SUCCESS)
    {
      DEBUG ("Error: %s", ca_strerror (error_code));
      g_hash_table_remove (repeatable_sound->self->priv->repeating_sounds,
          GINT_TO_POINTER (repeatable_sound->sound_id));
      return;
    }

  repeatable_sound->replay_timeout_id = g_timeout_add (
      repeatable_sound->play_interval, playing_timeout_cb, user_data);
}

/**
 * empathy_sound_manager_start_playing:
 * @self: a #EmpathySoundManager
 * @widget: The #GtkWidget from which the sound is originating.
 * @sound_id: The #EmpathySound to play.
 * @timeout_before_replay: The amount of time, in milliseconds, between two
 *                         consecutive play.
 *
 * Start playing a sound in loop. To stop the sound, call empathy_call_stop ()
 * by passing it the same @sound_id. Note that if you start playing a sound
 * multiple times, you'll have to call %empathy_sound_stop the same number of
 * times.
 *
 * Return value: %TRUE if the sound has successfully started playing.
 */
gboolean
empathy_sound_manager_start_playing (EmpathySoundManager *self,
    GtkWidget *widget,
    EmpathySound sound_id,
    guint timeout_before_replay)
{
  EmpathyRepeatableSound *repeatable_sound;
  gboolean playing = FALSE;

  g_return_val_if_fail (widget == NULL || GTK_IS_WIDGET (widget), FALSE);
  g_return_val_if_fail (sound_id < LAST_EMPATHY_SOUND, FALSE);

  if (!empathy_sound_pref_is_enabled (self, sound_id))
    return FALSE;

  if (g_hash_table_lookup (self->priv->repeating_sounds,
               GINT_TO_POINTER (sound_id)) != NULL)
    {
      /* The sound is already playing in loop. No need to continue. */
      return FALSE;
    }

  repeatable_sound = g_slice_new0 (EmpathyRepeatableSound);
  repeatable_sound->widget = widget;
  repeatable_sound->sound_id = sound_id;
  repeatable_sound->play_interval = timeout_before_replay;
  repeatable_sound->replay_timeout_id = 0;
  repeatable_sound->self = g_object_ref (self);

  g_hash_table_insert (self->priv->repeating_sounds, GINT_TO_POINTER (sound_id),
      repeatable_sound);

  if (widget != NULL)
    {
      g_signal_connect (G_OBJECT (widget), "destroy",
          G_CALLBACK (empathy_sound_widget_destroyed_cb),
          repeatable_sound);
    }

  playing = empathy_sound_play_internal (widget, sound_id, playing_finished_cb,
        repeatable_sound);

  if (!playing)
      g_hash_table_remove (self->priv->repeating_sounds,
          GINT_TO_POINTER (sound_id));

  return playing;
}
