/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * Copyright (C) 2003-2005 Imendio HB
 * Copyright (C) 2002-2003 Richard Hult <richard@imendio.com>
 * Copyright (C) 2002 CodeFactory AB
 *
 * 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.
 */

#include <config.h>
#include <time.h>
#include <string.h>
#include <math.h>
#include <glib/gi18n.h>
#include <gdk/gdk.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>
#include <gconf/gconf-client.h>

#include "drwright.h"
#include "drw-break-window.h"
#include "drw-monitor.h"
#include "drw-utils.h"

#define BLINK_TIMEOUT        200
#define BLINK_TIMEOUT_MIN    120
#define BLINK_TIMEOUT_FACTOR 100

#define POPUP_ITEM_ENABLED 1
#define POPUP_ITEM_BREAK   2

typedef enum {
	STATE_START,
	STATE_IDLE,
	STATE_TYPE,
	STATE_WARN_TYPE,
	STATE_WARN_IDLE,
	STATE_BREAK_SETUP,
	STATE_BREAK,
	STATE_BREAK_DONE_SETUP,
	STATE_BREAK_DONE
} DrwState;

struct _DrWright {
	/* Widgets. */
	GtkWidget      *break_window;
	GList          *secondary_break_windows;

	DrwMonitor     *monitor;

	GtkItemFactory *popup_factory;

	DrwState        state;
	GTimer         *timer;
	GTimer         *idle_timer;

	gint            last_elapsed_time;
	gint            save_last_time;

	gboolean        is_active;

	/* Time settings. */
	gint            type_time;
	gint            break_time;
	gint            warn_time;

	gboolean        enabled;

	guint           clock_timeout_id;
	guint           blink_timeout_id;

	gboolean        blink_on;

	GtkStatusIcon  *icon;

	GdkPixbuf      *neutral_bar;
	GdkPixbuf      *red_bar;
	GdkPixbuf      *green_bar;
	GdkPixbuf      *disabled_bar;
	GdkPixbuf      *composite_bar;

	GtkWidget      *warn_dialog;
};

static void     activity_detected_cb           (DrwMonitor     *monitor,
						DrWright       *drwright);
static gboolean maybe_change_state             (DrWright       *drwright);
static gboolean update_tooltip                 (DrWright       *drwright);
static void     break_window_done_cb           (GtkWidget      *window,
						DrWright       *dr);
static void     break_window_postpone_cb       (GtkWidget      *window,
						DrWright       *dr);
static void     break_window_destroy_cb        (GtkWidget      *window,
						DrWright       *dr);
static void     popup_break_cb                 (gpointer        callback_data,
						guint           action,
						GtkWidget      *widget);
static void     popup_preferences_cb           (gpointer        callback_data,
						guint           action,
						GtkWidget      *widget);
static void     popup_about_cb                 (gpointer        callback_data,
						guint           action,
						GtkWidget      *widget);
static gchar *  item_factory_trans_cb          (const gchar    *path,
						gpointer        data);
static void     init_tray_icon                 (DrWright       *dr);
static GList *  create_secondary_break_windows (void);



#define GIF_CB(x) ((GtkItemFactoryCallback)(x))

static GtkItemFactoryEntry popup_items[] = {
/*	{ N_("/_Enabled"),      NULL, GIF_CB (popup_enabled_cb),     POPUP_ITEM_ENABLED, "<ToggleItem>", NULL },*/
	/* translators: keep the initial "/" */
	{ N_("/_Preferences"),  NULL, GIF_CB (popup_preferences_cb), 0,                  "<StockItem>",  GTK_STOCK_PREFERENCES },
	{ N_("/_About"),        NULL, GIF_CB (popup_about_cb),       0,                  "<StockItem>",  GTK_STOCK_ABOUT },
	{ "/sep1",              NULL, NULL,                          0,                  "<Separator>",  NULL },
	{ N_("/_Take a Break"), NULL, GIF_CB (popup_break_cb),       POPUP_ITEM_BREAK,   "<Item>",       NULL }
};

extern gboolean debug;

static void
setup_debug_values (DrWright *dr)
{
	dr->type_time = 5;
	dr->warn_time = 4;
	dr->break_time = 10;
}

static void
update_icon (DrWright *dr)
{
	GdkPixbuf *pixbuf;
	GdkPixbuf *tmp_pixbuf;
	gint       width, height;
	gfloat     r;
	gint       offset;
	gboolean   set_pixbuf;

	if (!dr->enabled) {
		gtk_status_icon_set_from_pixbuf (dr->icon,
						 dr->disabled_bar);
		return;
	}

	tmp_pixbuf = gdk_pixbuf_copy (dr->neutral_bar);

	width = gdk_pixbuf_get_width (tmp_pixbuf);
	height = gdk_pixbuf_get_height (tmp_pixbuf);

	set_pixbuf = TRUE;

	switch (dr->state) {
	case STATE_BREAK:
	case STATE_BREAK_SETUP:
		r = 1;
		break;

	case STATE_BREAK_DONE:
	case STATE_BREAK_DONE_SETUP:
	case STATE_START:
		r = 0;
		break;

	default:
		r = (float) (g_timer_elapsed (dr->timer, NULL) + dr->save_last_time) /
		    (float) dr->type_time;
		break;
	}

	offset = CLAMP ((height - 0) * (1.0 - r), 1, height - 0);

	switch (dr->state) {
	case STATE_WARN_TYPE:
	case STATE_WARN_IDLE:
		pixbuf = dr->red_bar;
		set_pixbuf = FALSE;
		break;

	case STATE_BREAK_SETUP:
	case STATE_BREAK:
		pixbuf = dr->red_bar;
		break;

	default:
		pixbuf = dr->green_bar;
	}

	gdk_pixbuf_composite (pixbuf,
			      tmp_pixbuf,
			      0,
			      offset,
			      width,
			      height - offset,
			      0,
			      0,
			      1.0,
			      1.0,
			      GDK_INTERP_BILINEAR,
			      255);

	if (set_pixbuf) {
		gtk_status_icon_set_from_pixbuf (dr->icon,
						 tmp_pixbuf);
	}

	if (dr->composite_bar) {
		g_object_unref (dr->composite_bar);
	}

	dr->composite_bar = tmp_pixbuf;
}

static gboolean
blink_timeout_cb (DrWright *dr)
{
	gfloat r;
	gint   timeout;

	r = (dr->type_time - g_timer_elapsed (dr->timer, NULL) - dr->save_last_time) / dr->warn_time;
	timeout = BLINK_TIMEOUT + BLINK_TIMEOUT_FACTOR * r;

	if (timeout < BLINK_TIMEOUT_MIN) {
		timeout = BLINK_TIMEOUT_MIN;
	}

	if (dr->blink_on || timeout == 0) {
		gtk_status_icon_set_from_pixbuf (dr->icon,
						 dr->composite_bar);
	} else {
		gtk_status_icon_set_from_pixbuf (dr->icon,
						 dr->neutral_bar);
	}

	dr->blink_on = !dr->blink_on;

	if (timeout) {
		dr->blink_timeout_id = g_timeout_add (timeout,
						      (GSourceFunc) blink_timeout_cb,
						      dr);
	} else {
		dr->blink_timeout_id = 0;
	}

	return FALSE;
}

static void
start_blinking (DrWright *dr)
{
	if (!dr->blink_timeout_id) {
		dr->blink_on = TRUE;
		blink_timeout_cb (dr);
	}

	/*gtk_widget_show (GTK_WIDGET (dr->icon));*/
}

static void
stop_blinking (DrWright *dr)
{
	if (dr->blink_timeout_id) {
		g_source_remove (dr->blink_timeout_id);
		dr->blink_timeout_id = 0;
	}

	/*gtk_widget_hide (GTK_WIDGET (dr->icon));*/
}

static gboolean
grab_keyboard_on_window (GdkWindow *window,
			 guint32    activate_time)
{
	GdkGrabStatus status;

	status = gdk_keyboard_grab (window, TRUE, activate_time);
	if (status == GDK_GRAB_SUCCESS) {
		return TRUE;
	}

	return FALSE;
}

static gboolean
break_window_map_event_cb (GtkWidget *widget,
			   GdkEvent  *event,
			   DrWright  *dr)
{
	grab_keyboard_on_window (dr->break_window->window, gtk_get_current_event_time ());

        return FALSE;
}

static gboolean
maybe_change_state (DrWright *dr)
{
	gint elapsed_time;
	gint elapsed_idle_time;

	if (debug) {
		g_timer_reset (dr->idle_timer);
	}

	elapsed_time = g_timer_elapsed (dr->timer, NULL) + dr->save_last_time;
	elapsed_idle_time = g_timer_elapsed (dr->idle_timer, NULL);

	if (elapsed_time > dr->last_elapsed_time + dr->warn_time) {
		/* If the timeout is delayed by the amount of warning time, then
		 * we must have been suspended or stopped, so we just start
		 * over.
		 */
		dr->state = STATE_START;
	}

	switch (dr->state) {
	case STATE_START:
		if (dr->break_window) {
			gtk_widget_destroy (dr->break_window);
			dr->break_window = NULL;
		}

		gtk_status_icon_set_from_pixbuf (dr->icon,
						 dr->neutral_bar);

		dr->save_last_time = 0;

		g_timer_start (dr->timer);
		g_timer_start (dr->idle_timer);

		if (dr->enabled) {
			dr->state = STATE_IDLE;
		}

		update_tooltip (dr);
		stop_blinking (dr);
		break;

	case STATE_IDLE:
		if (elapsed_idle_time >= dr->break_time) {
			dr->state = STATE_BREAK_DONE_SETUP;
		} else if (dr->is_active) {
			dr->state = STATE_TYPE;
		}
		break;

	case STATE_TYPE:
		if (elapsed_time >= dr->type_time - dr->warn_time) {
			dr->state = STATE_WARN_TYPE;

			start_blinking (dr);
 		} else if (elapsed_time >= dr->type_time) {
			dr->state = STATE_BREAK_SETUP;
		}
		else if (!dr->is_active) {
			dr->state = STATE_IDLE;
			g_timer_start (dr->idle_timer);
		}
		break;

	case STATE_WARN_TYPE:
		if (elapsed_time >= dr->type_time) {
			dr->state = STATE_BREAK_SETUP;
		}
		else if (!dr->is_active) {
			dr->state = STATE_WARN_IDLE;
		}
		break;

	case STATE_WARN_IDLE:
		if (elapsed_idle_time >= dr->break_time) {
			dr->state = STATE_BREAK_DONE_SETUP;
		}
		else if (dr->is_active) {
			dr->state = STATE_WARN_TYPE;
		}

		break;

	case STATE_BREAK_SETUP:
		/* Don't allow more than one break window to coexist, can happen
		 * if a break is manually enforced.
		 */
		if (dr->break_window) {
			dr->state = STATE_BREAK;
			break;
		}

		stop_blinking (dr);
		gtk_status_icon_set_from_pixbuf (dr->icon,
						 dr->red_bar);

		g_timer_start (dr->timer);

		dr->break_window = drw_break_window_new ();

		g_signal_connect (dr->break_window, "map_event",
				  G_CALLBACK (break_window_map_event_cb),
				  dr);

		g_signal_connect (dr->break_window,
				  "done",
				  G_CALLBACK (break_window_done_cb),
				  dr);

		g_signal_connect (dr->break_window,
				  "postpone",
				  G_CALLBACK (break_window_postpone_cb),
				  dr);

		g_signal_connect (dr->break_window,
				  "destroy",
				  G_CALLBACK (break_window_destroy_cb),
				  dr);

		dr->secondary_break_windows = create_secondary_break_windows ();

		gtk_widget_show (dr->break_window);

		dr->save_last_time = elapsed_time;
		dr->state = STATE_BREAK;
		break;

	case STATE_BREAK:
		if (elapsed_time - dr->save_last_time >= dr->break_time) {
			dr->state = STATE_BREAK_DONE_SETUP;
		}
		break;

	case STATE_BREAK_DONE_SETUP:
		stop_blinking (dr);
		gtk_status_icon_set_from_pixbuf (dr->icon,
						 dr->green_bar);

		dr->state = STATE_BREAK_DONE;
		break;

	case STATE_BREAK_DONE:
		if (dr->is_active) {
			dr->state = STATE_START;
			if (dr->break_window) {
				gtk_widget_destroy (dr->break_window);
				dr->break_window = NULL;
			}
		}
		break;
	}

	dr->is_active = FALSE;
	dr->last_elapsed_time = elapsed_time;

	update_icon (dr);

	return TRUE;
}

static gboolean
update_tooltip (DrWright *dr)
{
	gint   elapsed_time, min;
	gchar *str;

	if (!dr->enabled) {
		gtk_status_icon_set_tooltip (dr->icon,
					     _("Disabled"));
		return TRUE;
	}

	elapsed_time = g_timer_elapsed (dr->timer, NULL);

	min = floor (0.5 + (dr->type_time - elapsed_time - dr->save_last_time) / 60.0);

	if (min >= 1) {
		str = g_strdup_printf (ngettext("%d minute until the next break",
						"%d minutes until the next break",
						min), min);
	} else {
		str = g_strdup_printf (_("Less than one minute until the next break"));
	}

	gtk_status_icon_set_tooltip (dr->icon,
				     str);

	g_free (str);

	return TRUE;
}

static void
activity_detected_cb (DrwMonitor *monitor,
		      DrWright   *dr)
{
	dr->is_active = TRUE;
	g_timer_start (dr->idle_timer);
}

static void
gconf_notify_cb (GConfClient *client,
		 guint        cnxn_id,
		 GConfEntry  *entry,
		 gpointer     user_data)
{
	DrWright  *dr = user_data;
	GtkWidget *item;

	if (!strcmp (entry->key, GCONF_PATH "/type_time")) {
		if (entry->value->type == GCONF_VALUE_INT) {
			dr->type_time = 60 * gconf_value_get_int (entry->value);
			dr->warn_time = MIN (dr->type_time / 10, 5*60);

			dr->state = STATE_START;
		}
	}
	else if (!strcmp (entry->key, GCONF_PATH "/break_time")) {
		if (entry->value->type == GCONF_VALUE_INT) {
			dr->break_time = 60 * gconf_value_get_int (entry->value);
			dr->state = STATE_START;
		}
	}
	else if (!strcmp (entry->key, GCONF_PATH "/enabled")) {
		if (entry->value->type == GCONF_VALUE_BOOL) {
			dr->enabled = gconf_value_get_bool (entry->value);
			dr->state = STATE_START;

			item = gtk_item_factory_get_widget_by_action (dr->popup_factory,
								      POPUP_ITEM_BREAK);
			gtk_widget_set_sensitive (item, dr->enabled);

			update_tooltip (dr);
		}
	}

	maybe_change_state (dr);
}

static void
popup_break_cb (gpointer   callback_data,
		guint      action,
		GtkWidget *widget)
{
	DrWright *dr = callback_data;

	if (dr->enabled) {
		dr->state = STATE_BREAK_SETUP;
		maybe_change_state (dr);
	}
}

static void
popup_preferences_cb (gpointer   callback_data,
		      guint      action,
		      GtkWidget *widget)
{
	GdkScreen *screen;
	GError    *error = NULL;

	screen = gtk_widget_get_screen (widget);

	if (!gdk_spawn_command_line_on_screen (screen, "gnome-keyboard-properties --typing-break", &error)) {
		GtkWidget *error_dialog;

		error_dialog = gtk_message_dialog_new (NULL, 0,
						       GTK_MESSAGE_ERROR,
						       GTK_BUTTONS_CLOSE,
						       _("Unable to bring up the typing break properties dialog with the following error: %s"),
						       error->message);
		g_signal_connect (error_dialog,
				  "response",
				  G_CALLBACK (gtk_widget_destroy), NULL);
		gtk_window_set_resizable (GTK_WINDOW (error_dialog), FALSE);
		gtk_widget_show (error_dialog);

		g_error_free (error);
	}
}

static void
popup_about_cb (gpointer   callback_data,
		guint      action,
		GtkWidget *widget)
{
	gint   i;
	gchar *authors[] = {
		N_("Written by Richard Hult <richard@imendio.com>"),
		N_("Eye candy added by Anders Carlsson"),
		NULL
	};

	for (i = 0; authors [i]; i++)
		authors [i] = _(authors [i]);

	gtk_show_about_dialog (NULL,
			       "authors", authors,
			       "comments",  _("A computer break reminder."),
			       "logo-icon-name", "typing-monitor",
			       "translator-credits", _("translator-credits"),
			       "version", VERSION,
			       NULL);
}

static void
popup_menu_cb (GtkWidget *widget,
	       guint      button,
	       guint      activate_time,
	       DrWright  *dr)
{
	GtkWidget *menu;

	menu = gtk_item_factory_get_widget (dr->popup_factory, "");

	gtk_menu_popup (GTK_MENU (menu),
			NULL,
			NULL,
			gtk_status_icon_position_menu,
			dr->icon,
			0,
			gtk_get_current_event_time());
}

static void
break_window_done_cb (GtkWidget *window,
		      DrWright  *dr)
{
	gtk_widget_destroy (dr->break_window);

	dr->state = STATE_BREAK_DONE_SETUP;
	dr->break_window = NULL;

	update_tooltip (dr);
	maybe_change_state (dr);
}

static void
break_window_postpone_cb (GtkWidget *window,
			  DrWright  *dr)
{
	gint elapsed_time;

	gtk_widget_destroy (dr->break_window);

	dr->state = STATE_TYPE;
	dr->break_window = NULL;

	elapsed_time = g_timer_elapsed (dr->timer, NULL);

	if (elapsed_time + dr->save_last_time >= dr->type_time) {
		/* Typing time has expired, but break was postponed.
		 * We'll warn again in (elapsed * sqrt (typing_time))^2 */
		gfloat postpone_time = (((float) elapsed_time) / dr->break_time)
					* sqrt (dr->type_time);
		postpone_time *= postpone_time;
		dr->save_last_time = dr->type_time - MAX (dr->warn_time, (gint) postpone_time);
	}

	g_timer_start (dr->timer);
	maybe_change_state (dr);
	update_icon (dr);
	update_tooltip (dr);
}

static void
break_window_destroy_cb (GtkWidget *window,
			 DrWright  *dr)
{
	GList *l;

	for (l = dr->secondary_break_windows; l; l = l->next) {
		gtk_widget_destroy (l->data);
	}

	g_list_free (dr->secondary_break_windows);
	dr->secondary_break_windows = NULL;
}

static char *
item_factory_trans_cb (const gchar *path,
		       gpointer     data)
{
	return _((gchar*) path);
}

static void
init_tray_icon (DrWright *dr)
{
	dr->icon = gtk_status_icon_new_from_pixbuf (dr->neutral_bar);

	update_tooltip (dr);
	update_icon (dr);

	g_signal_connect (dr->icon,
			  "popup_menu",
			  G_CALLBACK (popup_menu_cb),
			  dr);
}

static GList *
create_secondary_break_windows (void)
{
	GdkDisplay *display;
	GdkScreen  *screen;
	GtkWidget  *window;
	gint        i;
	GList      *windows = NULL;

	display = gdk_display_get_default ();

	for (i = 0; i < gdk_display_get_n_screens (display); i++) {
		screen = gdk_display_get_screen (display, i);

		if (screen == gdk_screen_get_default ()) {
			/* Handled by DrwBreakWindow. */
			continue;
		}

		window = gtk_window_new (GTK_WINDOW_POPUP);

		windows = g_list_prepend (windows, window);

		gtk_window_set_screen (GTK_WINDOW (window), screen);

		gtk_window_set_default_size (GTK_WINDOW (window),
					     gdk_screen_get_width (screen),
					     gdk_screen_get_height (screen));

		gtk_widget_set_app_paintable (GTK_WIDGET (window), TRUE);
		drw_setup_background (GTK_WIDGET (window));
		gtk_window_stick (GTK_WINDOW (window));
		gtk_widget_show (window);
	}

	return windows;
}

DrWright *
drwright_new (void)
{
	DrWright  *dr;
	GtkWidget *item;
	GConfClient *client;

        dr = g_new0 (DrWright, 1);

	client = gconf_client_get_default ();

	gconf_client_add_dir (client,
			      GCONF_PATH,
			      GCONF_CLIENT_PRELOAD_NONE,
			      NULL);

	gconf_client_notify_add (client, GCONF_PATH,
				 gconf_notify_cb,
				 dr,
				 NULL,
				 NULL);

	dr->type_time = 60 * gconf_client_get_int (
		client, GCONF_PATH "/type_time", NULL);

	dr->warn_time = MIN (dr->type_time / 12, 60*3);

	dr->break_time = 60 * gconf_client_get_int (
		client, GCONF_PATH "/break_time", NULL);

	dr->enabled = gconf_client_get_bool (
		client,
		GCONF_PATH "/enabled",
		NULL);

	g_object_unref (client);

	if (debug) {
		setup_debug_values (dr);
	}

	dr->popup_factory = gtk_item_factory_new (GTK_TYPE_MENU,
						      "<main>",
						      NULL);
	gtk_item_factory_set_translate_func (dr->popup_factory,
					     item_factory_trans_cb,
					     NULL,
					     NULL);

	gtk_item_factory_create_items (dr->popup_factory,
				       G_N_ELEMENTS (popup_items),
				       popup_items,
				       dr);

	/*item = gtk_item_factory_get_widget_by_action (dr->popup_factory, POPUP_ITEM_ENABLED);
	gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), dr->enabled);*/

	item = gtk_item_factory_get_widget_by_action (dr->popup_factory, POPUP_ITEM_BREAK);
	gtk_widget_set_sensitive (item, dr->enabled);

	dr->timer = g_timer_new ();
	dr->idle_timer = g_timer_new ();

	dr->state = STATE_START;

	dr->monitor = drw_monitor_new ();

	g_signal_connect (dr->monitor,
			  "activity",
			  G_CALLBACK (activity_detected_cb),
			  dr);

	dr->neutral_bar = gdk_pixbuf_new_from_file (IMAGEDIR "/bar.png", NULL);
	dr->red_bar = gdk_pixbuf_new_from_file (IMAGEDIR "/bar-red.png", NULL);
	dr->green_bar = gdk_pixbuf_new_from_file (IMAGEDIR "/bar-green.png", NULL);
	dr->disabled_bar = gdk_pixbuf_new_from_file (IMAGEDIR "/bar-disabled.png", NULL);

	init_tray_icon (dr);

	g_timeout_add_seconds (12,
		       (GSourceFunc) update_tooltip,
		       dr);
	g_timeout_add_seconds (1,
		       (GSourceFunc) maybe_change_state,
		       dr);

	return dr;
}
