/*
Copyright 2011 Canonical Ltd.

Authors:
    Conor Curran <conor.curran@canonical.com>

This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE.  See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <libindicator/indicator-image-helper.h>
#include <libnotify/notify.h>

#include "config.h"

#include "sound-state-manager.h"
#include "dbus-shared-names.h"

typedef struct _SoundStateManagerPrivate SoundStateManagerPrivate;

struct _SoundStateManagerPrivate
{
  GDBusProxy* dbus_proxy;  
  GHashTable* volume_states;  
  GList* blocked_animation_list;
  SoundState current_state;
  GtkImage* speaker_image;
  NotifyNotification* notification;  
  GSettings *settings_manager;  
};

#define SOUND_STATE_MANAGER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUND_TYPE_STATE_MANAGER, SoundStateManagerPrivate))
G_DEFINE_TYPE (SoundStateManager, sound_state_manager, G_TYPE_OBJECT);

static GtkIconSize design_team_size;
static gint blocked_id;
static gint animation_id;
static GList* blocked_iter = NULL;
static gboolean can_animate = FALSE;

//Notifications
static void sound_state_manager_notification_init (SoundStateManager* self);

//Animation/State related
static void sound_state_manager_prepare_blocked_animation (SoundStateManager* self);
static gboolean sound_state_manager_start_animation (gpointer user_data);
static gboolean sound_state_manager_fade_back_to_mute_image (gpointer user_data);
static void sound_state_manager_reset_mute_blocking_animation (SoundStateManager* self);
static void sound_state_manager_free_the_animation_list (SoundStateManager* self);
static void sound_state_manager_prepare_state_image_names (SoundStateManager* self);
static void sound_state_signal_cb ( GDBusProxy* proxy,
                                    gchar* sender_name,
                                    gchar* signal_name,
                                    GVariant* parameters,
                                    gpointer user_data );
static gboolean sound_state_manager_can_proceed_with_blocking_animation (SoundStateManager* self);


static void
sound_state_manager_init (SoundStateManager* self)
{
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);

  priv->dbus_proxy = NULL;
  priv->volume_states = NULL;
  priv->speaker_image = NULL;
  priv->blocked_animation_list = NULL;
  priv->notification = NULL;  
  priv->settings_manager = NULL;

  priv->settings_manager = g_settings_new("com.canonical.indicators.sound");

  sound_state_manager_notification_init (self);
  
  sound_state_manager_prepare_state_image_names (self);
  sound_state_manager_prepare_blocked_animation (self);

  priv->current_state = UNAVAILABLE;
  priv->speaker_image = indicator_image_helper (g_hash_table_lookup (priv->volume_states,
                                                                     GINT_TO_POINTER(priv->current_state)));
}

static void
sound_state_manager_finalize (GObject *object)
{
	/* TODO: Add deinitalization code here */

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

static void
sound_state_manager_dispose (GObject *object)
{
  SoundStateManager* self = SOUND_STATE_MANAGER (object);
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);
  
  g_hash_table_destroy (priv->volume_states);

  sound_state_manager_free_the_animation_list (self);

  if (priv->notification) {
    notify_uninit();
  }

  g_object_unref(priv->settings_manager);
  
  G_OBJECT_CLASS (sound_state_manager_parent_class)->dispose (object);
}


static void
sound_state_manager_class_init (SoundStateManagerClass *klass)
{
	GObjectClass* object_class = G_OBJECT_CLASS (klass);

	object_class->finalize = sound_state_manager_finalize;
  object_class->dispose = sound_state_manager_dispose;

  g_type_class_add_private (klass, sizeof (SoundStateManagerPrivate));
  
  design_team_size = gtk_icon_size_register("design-team-size", 22, 22);  
}

static void
sound_state_manager_notification_init (SoundStateManager* self)
{
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);

  if (!notify_init(PACKAGE_NAME))
    return;

  GList* caps = notify_get_server_caps();
  gboolean has_notify_osd = FALSE;

  if (caps) {
    if (g_list_find_custom(caps, "x-canonical-private-synchronous",
                           (GCompareFunc) g_strcmp0)) {
      has_notify_osd = TRUE;
    }
    g_list_foreach(caps, (GFunc) g_free, NULL);
    g_list_free(caps);
  }

  if (has_notify_osd) {
    priv->notification = notify_notification_new(PACKAGE_NAME, NULL, NULL);
    notify_notification_set_hint_string(priv->notification,
                                        "x-canonical-private-synchronous", PACKAGE_NAME);
  }
}

void
sound_state_manager_show_notification (SoundStateManager *self,
                                       double value)
{
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);

  if (priv->notification == NULL || 
      g_settings_get_boolean (priv->settings_manager, "show-notify-osd-on-scroll") == FALSE){
    return;
  }

  char *icon;
  const int notify_value = CLAMP((int)value, -1, 101);
  SoundState state = sound_state_manager_get_current_state (self);
      
  if (state == ZERO_LEVEL) {
    // Not available for all the themes
    icon = "audio-volume-off";
  } else if (state == LOW_LEVEL) {
    icon = "audio-volume-low";
  } else if (state == MEDIUM_LEVEL) {
    icon = "audio-volume-medium";
  } else if (state == HIGH_LEVEL) {
    icon = "audio-volume-high";
  } else {
    icon = "audio-volume-muted";
  }

  notify_notification_update(priv->notification, PACKAGE_NAME, NULL, icon);
  notify_notification_set_hint_int32(priv->notification, "value", notify_value);
  notify_notification_show(priv->notification, NULL);
}


/*
Prepare states versus images names hash.
*/
static void
sound_state_manager_prepare_state_image_names (SoundStateManager* self)
{
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);
  priv->volume_states = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free);
  
  g_hash_table_insert (priv->volume_states, GINT_TO_POINTER(MUTED), g_strdup("audio-volume-muted-panel"));
  g_hash_table_insert (priv->volume_states, GINT_TO_POINTER(ZERO_LEVEL), g_strdup("audio-volume-low-zero-panel"));
  g_hash_table_insert (priv->volume_states, GINT_TO_POINTER(LOW_LEVEL), g_strdup("audio-volume-low-panel"));
  g_hash_table_insert (priv->volume_states, GINT_TO_POINTER(MEDIUM_LEVEL), g_strdup("audio-volume-medium-panel"));
  g_hash_table_insert (priv->volume_states, GINT_TO_POINTER(HIGH_LEVEL), g_strdup("audio-volume-high-panel"));
  g_hash_table_insert (priv->volume_states, GINT_TO_POINTER(BLOCKED), g_strdup("audio-volume-muted-blocking-panel"));
  g_hash_table_insert (priv->volume_states, GINT_TO_POINTER(UNAVAILABLE), g_strdup("audio-output-none-panel"));
}

/*
prepare_blocked_animation:
Prepares the array of images to be used in the blocked animation.
Only called at startup.
*/
static void
sound_state_manager_prepare_blocked_animation (SoundStateManager* self)
{
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);
  
  gchar* blocked_name = g_hash_table_lookup(priv->volume_states,
                                            GINT_TO_POINTER(BLOCKED));
  gchar* muted_name = g_hash_table_lookup(priv->volume_states,
                                          GINT_TO_POINTER(MUTED));

  GtkImage* temp_image = indicator_image_helper(muted_name);
  GdkPixbuf* mute_buf = gtk_image_get_pixbuf(temp_image);

  temp_image = indicator_image_helper(blocked_name);
  GdkPixbuf* blocked_buf = gtk_image_get_pixbuf(temp_image);

  if (mute_buf == NULL || blocked_buf == NULL) {
    //g_debug("Don bother with the animation, the theme aint got the goods !");
    return;
  }

  int i;

  // sample 51 snapshots - range : 0-256
  for (i = 0; i < 51; i++) {
    gdk_pixbuf_composite(mute_buf, blocked_buf, 0, 0,
                         gdk_pixbuf_get_width(mute_buf),
                         gdk_pixbuf_get_height(mute_buf),
                         0, 0, 1, 1, GDK_INTERP_BILINEAR, MIN(255, i * 5));
    priv->blocked_animation_list = g_list_append(priv->blocked_animation_list,
                                                 gdk_pixbuf_copy(blocked_buf));
  }
  can_animate = TRUE;
  g_object_ref_sink(mute_buf);
  g_object_unref(mute_buf);
  g_object_ref_sink(blocked_buf);
  g_object_unref(blocked_buf);
}


GtkImage*
sound_state_manager_get_current_icon (SoundStateManager* self)
{
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);
  return priv->speaker_image;  
}

SoundState
sound_state_manager_get_current_state (SoundStateManager* self)
{
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);
  return priv->current_state;
}

/**
 * sound_state_manager_connect_to_dbus:
 * @returns: void
 * When ready the indicator-sound calls this method to enable state communication 
 * between the indicator and the service. 
 **/
void
sound_state_manager_connect_to_dbus (SoundStateManager* self, GDBusProxy* proxy)
{
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);
  priv->dbus_proxy = proxy;
  g_signal_connect (priv->dbus_proxy, "g-signal",
                    G_CALLBACK (sound_state_signal_cb), self);

  g_dbus_proxy_call ( priv->dbus_proxy,
                      "GetSoundState",
                      NULL,
		                  G_DBUS_CALL_FLAGS_NONE,
                      -1,
                      NULL,
                      (GAsyncReadyCallback)sound_state_manager_get_state_cb,
                      self);  
}

void
sound_state_manager_get_state_cb (GObject *object,
                                  GAsyncResult *res,
                                  gpointer user_data)
{
  g_return_if_fail (SOUND_IS_STATE_MANAGER (user_data));
  SoundStateManager* self = SOUND_STATE_MANAGER (user_data);
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);
  
  GVariant *result, *value;
  GError *error = NULL;
  result = g_dbus_proxy_call_finish ( priv->dbus_proxy,
                                      res,
                                      &error );

  if (error != NULL) {
    g_warning("get_sound_state call failed: %s", error->message);
    g_error_free(error);
    return;
  }

  value = g_variant_get_child_value(result, 0);
  priv->current_state = (SoundState)g_variant_get_int32(value);

  gchar* image_name = g_hash_table_lookup (priv->volume_states, 
                                           GINT_TO_POINTER(priv->current_state) );
  indicator_image_helper_update (priv->speaker_image, image_name);
  
  g_variant_unref(value);
  g_variant_unref(result);
}

void 
sound_state_manager_deal_with_disconnect (SoundStateManager* self)
{
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);
  priv->current_state = UNAVAILABLE;

  gchar* image_name = g_hash_table_lookup (priv->volume_states, 
                                           GINT_TO_POINTER(priv->current_state) );
  indicator_image_helper_update (priv->speaker_image, image_name);
}

static void 
sound_state_signal_cb ( GDBusProxy* proxy,
                        gchar* sender_name,
                        gchar* signal_name,
                        GVariant* parameters,
                        gpointer user_data)
{
  //g_debug ( "!!! sound state manager signal_cb" );

  g_return_if_fail (SOUND_IS_STATE_MANAGER (user_data));
  SoundStateManager* self = SOUND_STATE_MANAGER (user_data);
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);

  g_variant_ref (parameters);
  GVariant *value = g_variant_get_child_value (parameters, 0);
  gint update = g_variant_get_int32 (value);

  //g_debug ( "!!! signal_cb with value %i", update);

  priv->current_state = (SoundState)update;

  g_variant_unref (parameters);

  if (g_strcmp0(signal_name, INDICATOR_SOUND_SIGNAL_STATE_UPDATE) == 0){

    gchar* image_name = g_hash_table_lookup (priv->volume_states, 
                                             GINT_TO_POINTER(priv->current_state) );
    if (priv->current_state == BLOCKED &&
        sound_state_manager_can_proceed_with_blocking_animation (self) == TRUE) {
      blocked_id = g_timeout_add_seconds (4,
                                          sound_state_manager_start_animation,
                                          self);
      indicator_image_helper_update (priv->speaker_image, image_name);
    }
    else{
      indicator_image_helper_update (priv->speaker_image, image_name); 
    }
  }
  else {
    g_warning ("sorry don't know what signal this is - %s", signal_name);
  }
}

void
sound_state_manager_style_changed_cb (GtkWidget *widget,
                                      GtkStyle  *previous_style,
                                      gpointer user_data)
{
  g_debug("Just caught a style change event");
  g_return_if_fail (SOUND_IS_STATE_MANAGER (user_data));
  SoundStateManager* self = SOUND_STATE_MANAGER (user_data);
  sound_state_manager_reset_mute_blocking_animation (self);
  sound_state_manager_free_the_animation_list (self);
  sound_state_manager_prepare_blocked_animation (self);
}

static void
sound_state_manager_reset_mute_blocking_animation (SoundStateManager* self)
{
  if (animation_id != 0) {
    //g_debug("about to remove the animation_id callback from the mainloop!!**");
    g_source_remove(animation_id);
    animation_id = 0;
  }
  if (blocked_id != 0) {
    //g_debug("about to remove the blocked_id callback from the mainloop!!**");
    g_source_remove(blocked_id);
    blocked_id = 0;
  }
}

static void
sound_state_manager_free_the_animation_list (SoundStateManager* self)
{
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);
  
  if (priv->blocked_animation_list != NULL) {
    g_list_foreach (priv->blocked_animation_list, (GFunc)g_object_unref, NULL);
    g_list_free (priv->blocked_animation_list);
    priv->blocked_animation_list = NULL;
  }
}


static gboolean
sound_state_manager_start_animation (gpointer userdata)
{
  g_return_val_if_fail (SOUND_IS_STATE_MANAGER (userdata), FALSE);
  SoundStateManager* self = SOUND_STATE_MANAGER (userdata);
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE(self);
  
  blocked_iter = priv->blocked_animation_list;
  blocked_id = 0;
  animation_id = g_timeout_add (50,
                                sound_state_manager_fade_back_to_mute_image,
                                self);
  return FALSE;
}

static gboolean
sound_state_manager_fade_back_to_mute_image (gpointer user_data)
{
  g_return_val_if_fail (SOUND_IS_STATE_MANAGER (user_data), FALSE);
  SoundStateManager* self = SOUND_STATE_MANAGER (user_data);
  SoundStateManagerPrivate* priv = SOUND_STATE_MANAGER_GET_PRIVATE (self);

  if (blocked_iter != NULL) {
    gtk_image_set_from_pixbuf (priv->speaker_image, blocked_iter->data);
    blocked_iter = blocked_iter->next;
    return TRUE;
  } else {
    animation_id = 0;
    //g_debug("exit from animation now\n");
    g_dbus_proxy_call ( priv->dbus_proxy,
                        "GetSoundState",
                        NULL,
		                    G_DBUS_CALL_FLAGS_NONE,
                        -1,
                        NULL,
                        (GAsyncReadyCallback)sound_state_manager_get_state_cb,
                        self);  
   
    return FALSE;
  }
}


// Simple static helper to determine if the coast is clear to animate
static
gboolean sound_state_manager_can_proceed_with_blocking_animation (SoundStateManager* self)
{
  return (can_animate && blocked_id == 0 && animation_id == 0 );
}