/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*-

   nautilus-progress-info.h: file operation progress info.
 
   Copyright (C) 2007 Red Hat, Inc.
  
   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License as
   published by the Free Software Foundation; either version 2 of the
   License, or (at your option) any later version.
  
   This program 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
   General Public License for more details.
  
   You should have received a copy of the GNU General Public
   License along with this program; if not, write to the
   Free Software Foundation, Inc., 59 Temple Place - Suite 330,
   Boston, MA 02111-1307, USA.
  
   Author: Alexander Larsson <alexl@redhat.com>
*/

#include <config.h>
#include <math.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include <eel/eel-string.h>
#include <eel/eel-glib-extensions.h>
#include "nautilus-progress-info.h"

enum {
  CHANGED,
  PROGRESS_CHANGED,
  STARTED,
  FINISHED,
  LAST_SIGNAL
};

/* TODO:
 * Want an icon for the operation.
 * Add and implement cancel button
 */

#define SIGNAL_DELAY_MSEC 100

static guint signals[LAST_SIGNAL] = { 0 };

struct _NautilusProgressInfo
{
	GObject parent_instance;
	
	GCancellable *cancellable;
	
	char *status;
	char *details;
	double progress;
	gboolean activity_mode;
	gboolean started;
	gboolean finished;
	gboolean paused;
	
	GSource *idle_source;
	gboolean source_is_now;
	
	gboolean start_at_idle;
	gboolean finish_at_idle;
	gboolean changed_at_idle;
	gboolean progress_at_idle;
};

struct _NautilusProgressInfoClass
{
	GObjectClass parent_class;
};

static GList *active_progress_infos = NULL;

static GtkStatusIcon *status_icon = NULL;
static int n_progress_ops = 0;


G_LOCK_DEFINE_STATIC(progress_info);

G_DEFINE_TYPE (NautilusProgressInfo, nautilus_progress_info, G_TYPE_OBJECT)

GList *
nautilus_get_all_progress_info (void)
{
	GList *l;
	
	G_LOCK (progress_info);

	l = eel_g_object_list_copy (active_progress_infos);
	
	G_UNLOCK (progress_info);

	return l;
}

static void
nautilus_progress_info_finalize (GObject *object)
{
	NautilusProgressInfo *info;
	
	info = NAUTILUS_PROGRESS_INFO (object);

	g_free (info->status);
	g_free (info->details);
	g_object_unref (info->cancellable);
	
	if (G_OBJECT_CLASS (nautilus_progress_info_parent_class)->finalize) {
		(*G_OBJECT_CLASS (nautilus_progress_info_parent_class)->finalize) (object);
	}
}

static void
nautilus_progress_info_dispose (GObject *object)
{
	NautilusProgressInfo *info;
	
	info = NAUTILUS_PROGRESS_INFO (object);

	G_LOCK (progress_info);

	/* Remove from active list in dispose, since a get_all_progress_info()
	   call later could revive the object */
	active_progress_infos = g_list_remove (active_progress_infos, object);
	
	/* Destroy source in dispose, because the callback
	   could come here before the destroy, which should
	   ressurect the object for a while */
	if (info->idle_source) {
		g_source_destroy (info->idle_source);
		g_source_unref (info->idle_source);
		info->idle_source = NULL;
	}
	G_UNLOCK (progress_info);
}

static void
nautilus_progress_info_class_init (NautilusProgressInfoClass *klass)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
	
	gobject_class->finalize = nautilus_progress_info_finalize;
	gobject_class->dispose = nautilus_progress_info_dispose;
	
	signals[CHANGED] =
		g_signal_new ("changed",
			      NAUTILUS_TYPE_PROGRESS_INFO,
			      G_SIGNAL_RUN_LAST,
			      0,
			      NULL, NULL,
			      g_cclosure_marshal_VOID__VOID,
			      G_TYPE_NONE, 0);
	
	signals[PROGRESS_CHANGED] =
		g_signal_new ("progress-changed",
			      NAUTILUS_TYPE_PROGRESS_INFO,
			      G_SIGNAL_RUN_LAST,
			      0,
			      NULL, NULL,
			      g_cclosure_marshal_VOID__VOID,
			      G_TYPE_NONE, 0);
	
	signals[STARTED] =
		g_signal_new ("started",
			      NAUTILUS_TYPE_PROGRESS_INFO,
			      G_SIGNAL_RUN_LAST,
			      0,
			      NULL, NULL,
			      g_cclosure_marshal_VOID__VOID,
			      G_TYPE_NONE, 0);
	
	signals[FINISHED] =
		g_signal_new ("finished",
			      NAUTILUS_TYPE_PROGRESS_INFO,
			      G_SIGNAL_RUN_LAST,
			      0,
			      NULL, NULL,
			      g_cclosure_marshal_VOID__VOID,
			      G_TYPE_NONE, 0);
	
}

static gboolean
delete_event (GtkWidget *widget,
	      GdkEventAny *event)
{
	gtk_widget_hide (widget);
	return TRUE;
}

static void
status_icon_activate_cb (GtkStatusIcon *icon,
			 GtkWidget *progress_window)
{
	if (gtk_widget_get_visible (progress_window)) {
		gtk_widget_hide (progress_window);
	} else {
		gtk_window_present (GTK_WINDOW (progress_window));
	}
}

static GtkWidget *
get_progress_window (void)
{
	static GtkWidget *progress_window = NULL;
	GtkWidget *vbox;
	
	if (progress_window != NULL) {
		return progress_window;
	}
	
	progress_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
	gtk_window_set_resizable (GTK_WINDOW (progress_window),
				  FALSE);
	gtk_container_set_border_width (GTK_CONTAINER (progress_window), 10);
 
	gtk_window_set_title (GTK_WINDOW (progress_window),
			      _("File Operations"));
	gtk_window_set_wmclass (GTK_WINDOW (progress_window),
				"file_progress", "Nautilus");
	gtk_window_set_position (GTK_WINDOW (progress_window),
				 GTK_WIN_POS_CENTER);
	gtk_window_set_icon_name (GTK_WINDOW (progress_window),
				"system-file-manager");
	
	vbox = gtk_vbox_new (FALSE, 0);
	gtk_box_set_spacing (GTK_BOX (vbox), 5);
		
	gtk_container_add (GTK_CONTAINER (progress_window),
			   vbox);

	gtk_widget_show_all (progress_window);

	g_signal_connect (progress_window,
			  "delete_event",
			  (GCallback)delete_event, NULL);

	status_icon = gtk_status_icon_new_from_icon_name ("system-file-manager");
	g_signal_connect (status_icon, "activate",
			  (GCallback)status_icon_activate_cb,
			  progress_window);

	gtk_status_icon_set_visible (status_icon, FALSE);
	
	return progress_window;
}


typedef struct {
	GtkWidget *widget;
	NautilusProgressInfo *info;
	GtkLabel *status;
	GtkLabel *details;
	GtkProgressBar *progress_bar;
} ProgressWidgetData;

static void
progress_widget_data_free (ProgressWidgetData *data)
{
	g_object_unref (data->info);
	g_free (data);
}

static void
update_data (ProgressWidgetData *data)
{
	char *status, *details;
	char *markup;

	status = nautilus_progress_info_get_status (data->info);
	gtk_label_set_text (data->status, status);
	g_free (status);

	details = nautilus_progress_info_get_details (data->info);
	markup = g_markup_printf_escaped ("<span size='small'>%s</span>", details);
	gtk_label_set_markup (data->details, markup);
	g_free (details);
	g_free (markup);
}

static void
update_progress (ProgressWidgetData *data)
{
	double progress;

	progress = nautilus_progress_info_get_progress (data->info);
	if (progress < 0) {
		gtk_progress_bar_pulse (data->progress_bar);
	} else {
		gtk_progress_bar_set_fraction (data->progress_bar, progress);
	}
}

static void
update_status_icon_and_window (void)
{
	char *tooltip;

	tooltip = g_strdup_printf (ngettext ("%'d file operation active",
					     "%'d file operations active",
					     n_progress_ops),
				   n_progress_ops);
	gtk_status_icon_set_tooltip_text (status_icon, tooltip);
	g_free (tooltip);
	
	if (n_progress_ops == 0) {
		gtk_status_icon_set_visible (status_icon, FALSE);
		gtk_widget_hide (get_progress_window ());
	} else {
		gtk_status_icon_set_visible (status_icon, TRUE);
	}
}

static void
op_finished (ProgressWidgetData *data)
{
	gtk_widget_destroy (data->widget);
	
	n_progress_ops--;
	update_status_icon_and_window ();
}

static void
cancel_clicked (GtkWidget *button,
		ProgressWidgetData *data)
{
	nautilus_progress_info_cancel (data->info);
	gtk_widget_set_sensitive (button, FALSE);
}


static GtkWidget *
progress_widget_new (NautilusProgressInfo *info)
{
	ProgressWidgetData *data;
	GtkWidget *label, *progress_bar, *hbox, *vbox, *box, *button, *image;

	data = g_new0 (ProgressWidgetData, 1);
	data->info = g_object_ref (info);
	
	vbox = gtk_vbox_new (FALSE, 0);
	gtk_box_set_spacing (GTK_BOX (vbox), 5);

	     
	data->widget = vbox;
	g_object_set_data_full (G_OBJECT (data->widget),
				"data", data, 
				(GDestroyNotify)progress_widget_data_free);
	
	label = gtk_label_new ("status");
	gtk_widget_set_size_request (label, 500, -1);
	gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
	gtk_label_set_line_wrap_mode (GTK_LABEL (label), PANGO_WRAP_WORD_CHAR);
	gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);
	gtk_box_pack_start (GTK_BOX (vbox),
			    label,
			    TRUE, FALSE,
			    0);
	data->status = GTK_LABEL (label);			   
	
	hbox = gtk_hbox_new (FALSE,10);

	progress_bar = gtk_progress_bar_new ();
	data->progress_bar = GTK_PROGRESS_BAR (progress_bar);
	gtk_progress_bar_set_pulse_step (data->progress_bar, 0.05);
	box = gtk_vbox_new (FALSE,0);
	gtk_box_pack_start(GTK_BOX (box),
			   progress_bar,
			   TRUE,FALSE,
			   0);
	gtk_box_pack_start(GTK_BOX (hbox),
			   box,
			   TRUE,TRUE,
			   0);
	
	image = gtk_image_new_from_stock (GTK_STOCK_CANCEL,
					  GTK_ICON_SIZE_BUTTON);
	button = gtk_button_new ();
	gtk_container_add (GTK_CONTAINER (button), image);
	gtk_box_pack_start (GTK_BOX (hbox),
			    button,
			    FALSE,FALSE,
			    0);
	g_signal_connect (button, "clicked", (GCallback)cancel_clicked, data);
	
	gtk_box_pack_start (GTK_BOX (vbox),
			    hbox,
			    FALSE,FALSE,
			    0);

	label = gtk_label_new ("details");
	gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);
	gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
	gtk_box_pack_start (GTK_BOX (vbox),
			    label,
			    TRUE, FALSE,
			    0);
	data->details = GTK_LABEL (label);
	
	gtk_widget_show_all (data->widget);
	
	update_data (data);
	update_progress (data);

	g_signal_connect_swapped (data->info,
				  "changed",
				  (GCallback)update_data, data);
	g_signal_connect_swapped (data->info,
				  "progress_changed",
				  (GCallback)update_progress, data);
	g_signal_connect_swapped (data->info,
				  "finished",
				  (GCallback)op_finished, data);
	
	return data->widget;
}

static void
handle_new_progress_info (NautilusProgressInfo *info)
{
	GtkWidget *window, *progress;

	window = get_progress_window ();
	
	progress = progress_widget_new (info);
	gtk_box_pack_start (GTK_BOX (gtk_bin_get_child (GTK_BIN (window))),
			    progress,
			    FALSE, FALSE, 6);

	gtk_window_present (GTK_WINDOW (window));

	n_progress_ops++;
	update_status_icon_and_window ();	
}

static gboolean
new_op_started_timeout (NautilusProgressInfo *info)
{
	if (nautilus_progress_info_get_is_paused (info)) {
		return TRUE;
	}
	if (!nautilus_progress_info_get_is_finished (info)) {
		handle_new_progress_info (info);
	}
	g_object_unref (info);
	return FALSE;
}

static void
new_op_started (NautilusProgressInfo *info)
{
	g_signal_handlers_disconnect_by_func (info, (GCallback)new_op_started, NULL);
	g_timeout_add_seconds (2,
			       (GSourceFunc)new_op_started_timeout,
			       g_object_ref (info));
}

static void
nautilus_progress_info_init (NautilusProgressInfo *info)
{
	info->cancellable = g_cancellable_new ();

	G_LOCK (progress_info);
	active_progress_infos = g_list_append (active_progress_infos, info);
	G_UNLOCK (progress_info);

	g_signal_connect (info, "started", (GCallback)new_op_started, NULL);
}

NautilusProgressInfo *
nautilus_progress_info_new (void)
{
	NautilusProgressInfo *info;
	
	info = g_object_new (NAUTILUS_TYPE_PROGRESS_INFO, NULL);
	
	return info;
}

char *
nautilus_progress_info_get_status (NautilusProgressInfo *info)
{
	char *res;
	
	G_LOCK (progress_info);
	
	if (info->status) {
		res = g_strdup (info->status);
	} else {
		res = g_strdup (_("Preparing"));
	}
	
	G_UNLOCK (progress_info);
	
	return res;
}

char *
nautilus_progress_info_get_details (NautilusProgressInfo *info)
{
	char *res;
	
	G_LOCK (progress_info);
	
	if (info->details) {
		res = g_strdup (info->details);
	} else {
		res = g_strdup (_("Preparing"));
	}
	
	G_UNLOCK (progress_info);

	return res;
}

double
nautilus_progress_info_get_progress (NautilusProgressInfo *info)
{
	double res;
	
	G_LOCK (progress_info);

	if (info->activity_mode) {
		res = -1.0;
	} else {
		res = info->progress;
	}
	
	G_UNLOCK (progress_info);
	
	return res;
}

void
nautilus_progress_info_cancel (NautilusProgressInfo *info)
{
	G_LOCK (progress_info);
	
	g_cancellable_cancel (info->cancellable);
	
	G_UNLOCK (progress_info);
}

GCancellable *
nautilus_progress_info_get_cancellable (NautilusProgressInfo *info)
{
	GCancellable *c;
	
	G_LOCK (progress_info);
	
	c = g_object_ref (info->cancellable);
	
	G_UNLOCK (progress_info);
	
	return c;
}

gboolean
nautilus_progress_info_get_is_started (NautilusProgressInfo *info)
{
	gboolean res;
	
	G_LOCK (progress_info);
	
	res = info->started;
	
	G_UNLOCK (progress_info);
	
	return res;
}

gboolean
nautilus_progress_info_get_is_finished (NautilusProgressInfo *info)
{
	gboolean res;
	
	G_LOCK (progress_info);
	
	res = info->finished;
	
	G_UNLOCK (progress_info);
	
	return res;
}

gboolean
nautilus_progress_info_get_is_paused (NautilusProgressInfo *info)
{
	gboolean res;
	
	G_LOCK (progress_info);
	
	res = info->paused;
	
	G_UNLOCK (progress_info);
	
	return res;
}

static gboolean
idle_callback (gpointer data)
{
	NautilusProgressInfo *info = data;
	gboolean start_at_idle;
	gboolean finish_at_idle;
	gboolean changed_at_idle;
	gboolean progress_at_idle;
	GSource *source;

	source = g_main_current_source ();
	
	G_LOCK (progress_info);

	/* Protect agains races where the source has
	   been destroyed on another thread while it
	   was being dispatched.
	   Similar to what gdk_threads_add_idle does.
	*/
	if (g_source_is_destroyed (source)) {
		G_UNLOCK (progress_info);
		return FALSE;
	}

	/* We hadn't destroyed the source, so take a ref.
	 * This might ressurect the object from dispose, but
	 * that should be ok.
	 */
	g_object_ref (info);

	g_assert (source == info->idle_source);
	
	g_source_unref (source);
	info->idle_source = NULL;
	
	start_at_idle = info->start_at_idle;
	finish_at_idle = info->finish_at_idle;
	changed_at_idle = info->changed_at_idle;
	progress_at_idle = info->progress_at_idle;
	
	info->start_at_idle = FALSE;
	info->finish_at_idle = FALSE;
	info->changed_at_idle = FALSE;
	info->progress_at_idle = FALSE;
	
	G_UNLOCK (progress_info);
	
	if (start_at_idle) {
		g_signal_emit (info,
			       signals[STARTED],
			       0);
	}
	
	if (changed_at_idle) {
		g_signal_emit (info,
			       signals[CHANGED],
			       0);
	}
	
	if (progress_at_idle) {
		g_signal_emit (info,
			       signals[PROGRESS_CHANGED],
			       0);
	}
	
	if (finish_at_idle) {
		g_signal_emit (info,
			       signals[FINISHED],
			       0);
	}
	
	g_object_unref (info);
	
	return FALSE;
}

/* Called with lock held */
static void
queue_idle (NautilusProgressInfo *info, gboolean now)
{
	if (info->idle_source == NULL ||
	    (now && !info->source_is_now)) {
		if (info->idle_source) {
			g_source_destroy (info->idle_source);
			g_source_unref (info->idle_source);
			info->idle_source = NULL;
		}
		
		info->source_is_now = now;
		if (now) {
			info->idle_source = g_idle_source_new ();
		} else {
			info->idle_source = g_timeout_source_new (SIGNAL_DELAY_MSEC);
		}
		g_source_set_callback (info->idle_source, idle_callback, info, NULL);
		g_source_attach (info->idle_source, NULL);
	}
}

void
nautilus_progress_info_pause (NautilusProgressInfo *info)
{
	G_LOCK (progress_info);

	if (!info->paused) {
		info->paused = TRUE;
	}

	G_UNLOCK (progress_info);
}

void
nautilus_progress_info_resume (NautilusProgressInfo *info)
{
	G_LOCK (progress_info);

	if (info->paused) {
		info->paused = FALSE;
	}

	G_UNLOCK (progress_info);
}

void
nautilus_progress_info_start (NautilusProgressInfo *info)
{
	G_LOCK (progress_info);
	
	if (!info->started) {
		info->started = TRUE;
		
		info->start_at_idle = TRUE;
		queue_idle (info, TRUE);
	}
	
	G_UNLOCK (progress_info);
}

void
nautilus_progress_info_finish (NautilusProgressInfo *info)
{
	G_LOCK (progress_info);
	
	if (!info->finished) {
		info->finished = TRUE;
		
		info->finish_at_idle = TRUE;
		queue_idle (info, TRUE);
	}
	
	G_UNLOCK (progress_info);
}

void
nautilus_progress_info_take_status (NautilusProgressInfo *info,
				    char *status)
{
	G_LOCK (progress_info);
	
	if (eel_strcmp (info->status, status) != 0) {
		g_free (info->status);
		info->status = status;
		
		info->changed_at_idle = TRUE;
		queue_idle (info, FALSE);
	} else {
		g_free (status);
	}
	
	G_UNLOCK (progress_info);
}

void
nautilus_progress_info_set_status (NautilusProgressInfo *info,
				   const char *status)
{
	G_LOCK (progress_info);
	
	if (eel_strcmp (info->status, status) != 0) {
		g_free (info->status);
		info->status = g_strdup (status);
		
		info->changed_at_idle = TRUE;
		queue_idle (info, FALSE);
	}
	
	G_UNLOCK (progress_info);
}


void
nautilus_progress_info_take_details (NautilusProgressInfo *info,
				     char           *details)
{
	G_LOCK (progress_info);
	
	if (eel_strcmp (info->details, details) != 0) {
		g_free (info->details);
		info->details = details;
		
		info->changed_at_idle = TRUE;
		queue_idle (info, FALSE);
	} else {
		g_free (details);
	}
  
	G_UNLOCK (progress_info);
}

void
nautilus_progress_info_set_details (NautilusProgressInfo *info,
				    const char           *details)
{
	G_LOCK (progress_info);
	
	if (eel_strcmp (info->details, details) != 0) {
		g_free (info->details);
		info->details = g_strdup (details);
		
		info->changed_at_idle = TRUE;
		queue_idle (info, FALSE);
	}
  
	G_UNLOCK (progress_info);
}

void
nautilus_progress_info_pulse_progress (NautilusProgressInfo *info)
{
	G_LOCK (progress_info);

	info->activity_mode = TRUE;
	info->progress = 0.0;
	info->progress_at_idle = TRUE;
	queue_idle (info, FALSE);
	
	G_UNLOCK (progress_info);
}

void
nautilus_progress_info_set_progress (NautilusProgressInfo *info,
				     double                current,
				     double                total)
{
	double current_percent;
	
	if (total <= 0) {
		current_percent = 1.0;
	} else {
		current_percent = current / total;

		if (current_percent < 0) {
			current_percent	= 0;
		}
		
		if (current_percent > 1.0) {
			current_percent	= 1.0;
		}
	}
	
	G_LOCK (progress_info);
	
	if (info->activity_mode || /* emit on switch from activity mode */
	    fabs (current_percent - info->progress) > 0.005 /* Emit on change of 0.5 percent */
	    ) {
		info->activity_mode = FALSE;
		info->progress = current_percent;
		info->progress_at_idle = TRUE;
		queue_idle (info, FALSE);
	}
	
	G_UNLOCK (progress_info);
}
