/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * shell_script.c
 * This file is part of gedit
 *
 * Copyright (C) 2002 Paolo Maggi 
 *
 * 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. 
 */
 
/*
 * Plugin written by Rafael Jannone <rafael@jannone.org>
 * Based on the shell_output.c source code by Paolo Maggi.
 * See the ChangeLog files for a list of changes. 
 */

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

#include <glib/gi18n.h>
#include <glade/glade-xml.h>
#include <libgnome/gnome-url.h>
#include <libgnome/gnome-util.h>
#include <libgnomevfs/gnome-vfs.h>
#include <libgnomevfs/gnome-vfs-mime-handlers.h>
#include <libgnomeui/gnome-entry.h>
#include <libgnomeui/gnome-file-entry.h>

#include <string.h>

#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>

#include <gedit/gedit-plugin.h>
#include <gedit/gedit-debug.h>
#include <gedit/gedit-utils.h>
#include <gedit/gedit-menus.h>
#include <gedit/gedit-file.h>
#include <gedit/gedit-mdi.h>
#include <gedit/gedit-output-window.h>
#include <gedit/gedit-metadata-manager.h>

#define MENU_ITEM_PATH		"/menu/Tools/ToolsOps_3/"
#define MENU_ITEM_NAME		"PluginShellScript"

#define USER_GEDIT_LOCATION		".gedit-2/"
#define USER_GEDIT_SCRIPTS_LOCATION 	USER_GEDIT_LOCATION "scripts/"

typedef enum {
	NOT_RUNNING,
	RUNNING,
	MAKE_IT_STOP,
	MAKE_IT_CLOSE
} RunStatus;


typedef struct _Task {
	GtkWidget *dialog;
	GtkWidget *cancel_button;

	GtkWindow *toplevel_window;
	GtkWidget *output_window;

	gchar *command_line;
	gint child_pid;
	GIOChannel *ioc_stdout;
	GIOChannel *ioc_stderr;

	gboolean	failed;
	
	RunStatus	running;
} Task;

typedef struct _Scripts {
	GSList	*items;
	GnomeVFSMonitorHandle *monitor;	
} Scripts;

static GeditPlugin*	plugin;

static Scripts*	scripts = NULL;
static gchar*	gedit_dir = NULL;
static gchar*	scripts_dir = NULL;
static gchar*	scripts_uri = NULL;

static gboolean	ensure_scripts_dir_exists (void);

static gboolean	handle_command_output (GIOChannel *ioc, 
				GIOCondition condition, gpointer data);

static void	stream_document (GeditDocument *doc, GIOChannel *channel);

static void	run_script_cb (BonoboUIComponent *uic, gpointer user_data, 
			       const gchar* verbname);

G_MODULE_EXPORT GeditPluginState update_ui (GeditPlugin *plugin, BonoboWindow *window);
G_MODULE_EXPORT GeditPluginState destroy (GeditPlugin *pd);
G_MODULE_EXPORT GeditPluginState activate (GeditPlugin *pd);
G_MODULE_EXPORT GeditPluginState deactivate (GeditPlugin *pd);
G_MODULE_EXPORT GeditPluginState init (GeditPlugin *pd);
G_MODULE_EXPORT GeditPluginState configure (GeditPlugin *p, GtkWidget *parent);


/* script headers */

static void	scripts_dir_cb (GnomeVFSMonitorHandle *handle,
					const gchar *monitor_uri,
					const gchar *info_uri,
					GnomeVFSMonitorEventType event_type,
					gpointer user_data);

static 
Scripts*	scripts_new ();

static void	scripts_destroy (Scripts* scripts);
static gint	scripts_item_cmp (gconstpointer a, gconstpointer b);
static void	scripts_add_item (Scripts* scripts, const gchar* item);
static void	scripts_remove_item (Scripts* scripts, const gchar* item);


/* task headers */

static Task*	task_new ();
static void	task_destroy (Task* task);
static void	task_cancel (Task* task);


/* scripts code */

static void scripts_dir_cb (GnomeVFSMonitorHandle *handle,
					const gchar *monitor_uri,
					const gchar *info_uri,
					GnomeVFSMonitorEventType event_type,
					gpointer user_data)
{
	gchar* filename = g_path_get_basename (info_uri);

	gedit_debug (DEBUG_PLUGINS, "event on %s, %s\n", filename, info_uri);

	switch (event_type) {
		case GNOME_VFS_MONITOR_EVENT_DELETED:
			scripts_remove_item (scripts, filename);
			break;
		case GNOME_VFS_MONITOR_EVENT_CHANGED:
			scripts_remove_item (scripts, filename);
			scripts_add_item (scripts, filename);
			break;
		case GNOME_VFS_MONITOR_EVENT_CREATED:
			scripts_add_item (scripts, filename);
			break;
		default:
			break;
	}
	g_free (filename);
}

static Scripts*
scripts_new ()
{
	GnomeVFSResult result;
	GnomeVFSDirectoryHandle *handle = NULL;

	Scripts* scripts;
	
	gedit_debug (DEBUG_PLUGINS, "");	
	
	scripts = (Scripts*) g_malloc (sizeof(Scripts));
	scripts->items = NULL;
	scripts->monitor = NULL;

	if (!ensure_scripts_dir_exists ())
	{
		/* fixme: we might want to show a message box here */
		return scripts;
	}

	if ((result = gnome_vfs_directory_open (&handle, scripts_dir, 
			GNOME_VFS_FILE_INFO_DEFAULT)) == GNOME_VFS_OK) 
	{
		GnomeVFSFileInfo *file = gnome_vfs_file_info_new ();

		while (GNOME_VFS_OK == gnome_vfs_directory_read_next(handle, file)) 
		{
			/* fixme: find a better selection method 
				can_be_executable doesn't seem to be of much help
			 */
			if (
				(GNOME_VFS_FILE_TYPE_SYMBOLIC_LINK == file->type ||
					GNOME_VFS_FILE_TYPE_REGULAR == file->type) &&
				(!file->mime_type || 
					gnome_vfs_mime_can_be_executable(file->mime_type)) && 
				file->name[0] != '.'
			)
			{
				scripts_add_item (scripts, file->name);
			}
		}
		gnome_vfs_file_info_unref (file);
		gnome_vfs_directory_close(handle);
	}

	if (GNOME_VFS_OK !=
		gnome_vfs_monitor_add(&scripts->monitor, scripts_dir, GNOME_VFS_MONITOR_DIRECTORY,
			scripts_dir_cb, NULL))
	{
		g_warning ("Can't add a monitor to the scripts directory '%s'", scripts_dir); 
	}

	return scripts;
}

static void
scripts_destroy (Scripts* scripts)
{
	GSList *node;

	gedit_debug (DEBUG_PLUGINS, "");

	/* stopping monitor */
	if (scripts->monitor) {
		gnome_vfs_monitor_cancel (scripts->monitor);
	}

	/* deleting menu entries */
	node = scripts->items;
	while (node) 
	{
		gchar *script_file = (gchar*) g_slist_nth_data (node, 0);
		gedit_menus_remove_menu_item_all (MENU_ITEM_PATH, script_file);
		node = g_slist_next(node);
	}

	/* destroying each item */
	node = scripts->items;
	while (node) 
	{
		gchar *script_file = (gchar*) g_slist_nth_data (node, 0);
		g_free(script_file);
		node = g_slist_next(node);
	}
	
	g_slist_free (scripts->items);

	g_free (scripts);
}

static gint
scripts_item_cmp (gconstpointer a, gconstpointer b) 
{
	return g_utf8_collate((const gchar*)a, (const gchar*)b);
}

static void
scripts_add_item (Scripts* scripts, const gchar* item)
{
	GList *top_windows;

	gedit_debug (DEBUG_PLUGINS, "");

        top_windows = gedit_get_top_windows ();

	item = g_strdup(item);

	scripts->items = g_slist_prepend (scripts->items, (gpointer) item);

        while (top_windows)
        {
        	/* fixme: here we need a way to add items sorted alphabetically 
        		it would be nice to retrieve the mime icon too
        	 */
		gedit_menus_add_menu_item (BONOBO_WINDOW (top_windows->data),
			MENU_ITEM_PATH, item,
			item, "", GTK_STOCK_EXECUTE, run_script_cb);

		plugin->update_ui (plugin, BONOBO_WINDOW (top_windows->data));

		top_windows = g_list_next (top_windows);
        }
}

static void
scripts_remove_item (Scripts* scripts, const gchar* item)
{
	GSList *node;
	
	gedit_debug (DEBUG_PLUGINS, "");	
	
	node = g_slist_find_custom (scripts->items, item, scripts_item_cmp);
	if (node) {
		gedit_menus_remove_menu_item_all (MENU_ITEM_PATH, item);
		scripts->items = g_slist_delete_link (scripts->items, node);
	}
}


/* task code */

static Task*
task_new (const gchar* script_file)
{
	Task* task;

	gboolean retval;
	gchar *curdir;
	gchar *argv[2];
	gint standard_input;
	gint standard_output;
	gint standard_error;
	GeditDocument *doc;

	gedit_debug (DEBUG_PLUGINS, "running %s", script_file);

	task = (Task*) g_malloc(sizeof(Task));

	task->dialog = NULL;
	task->failed = FALSE;

	doc = gedit_get_active_document ();

	argv[0] = g_strdup(script_file);
	argv[1] = NULL;

	curdir = g_get_current_dir();
	retval = g_spawn_async_with_pipes (curdir,
			argv,
			NULL,
			G_SPAWN_SEARCH_PATH,
			NULL,
			NULL,
			&task->child_pid,
			&standard_input,
			&standard_output,
			&standard_error,
			NULL);

	g_free (argv[0]);
	g_free (curdir);

	if (retval)
	{	
		GIOChannel *ioc_stdin;
		GIOChannel *ioc_stdout;
		GIOChannel *ioc_stderr;
		const gchar *encoding = NULL;

		gchar *markup;
		gchar *line;

		task->running = RUNNING;
		task->command_line = g_strdup (script_file);
		task->toplevel_window = GTK_WINDOW (gedit_get_active_window());

		task->output_window = 
			gedit_mdi_get_output_window_from_window (
					BONOBO_WINDOW (task->toplevel_window));
		gedit_output_window_clear (GEDIT_OUTPUT_WINDOW (task->output_window));
		
		/* fixme: this is a workaround because get_output_window makes it visible by accident */
		gtk_widget_hide (task->output_window);

		line = g_markup_escape_text (task->command_line, -1);
		markup = g_strdup_printf ("<i>%s</i>: <b>%s</b>", _("Executing command"), line);
		gedit_output_window_append_line (GEDIT_OUTPUT_WINDOW (task->output_window), 
						 markup,
						 TRUE);
	
		g_free (line);
		g_free (markup);

		ioc_stdin = g_io_channel_unix_new (standard_input);
		ioc_stdout = g_io_channel_unix_new (standard_output);
		ioc_stderr = g_io_channel_unix_new (standard_error);

		task->ioc_stdout = ioc_stdout;
		task->ioc_stderr = ioc_stderr;

		g_get_charset (&encoding);
		
		/* fixme: check errors */
		g_io_channel_set_encoding (ioc_stdin, encoding, NULL);
		g_io_channel_set_encoding (ioc_stdout, encoding, NULL);
		g_io_channel_set_encoding (ioc_stderr, encoding, NULL);

		g_io_channel_set_flags (ioc_stdin, G_IO_FLAG_NONBLOCK, NULL);
		g_io_channel_set_flags (ioc_stdout, G_IO_FLAG_NONBLOCK, NULL);
		g_io_channel_set_flags (ioc_stderr, G_IO_FLAG_NONBLOCK, NULL);

		gedit_document_begin_user_action (doc);

		g_io_add_watch (ioc_stdout,
				G_IO_IN | G_IO_HUP,
			    	handle_command_output,
		    		task);

		g_io_add_watch (ioc_stderr,
				G_IO_IN | G_IO_HUP,
			    	handle_command_output,
		    		task);

		stream_document(doc, ioc_stdin);
		g_io_channel_write_unichar (ioc_stdin, '\n', NULL);

		/* fixme: why does g_io_channel_close breaks the pipe? 
			can we close the channel more cleanly?
		 */
		g_io_channel_shutdown (ioc_stdin, TRUE, NULL);
		
		gtk_text_buffer_delete_selection (GTK_TEXT_BUFFER (doc), TRUE, TRUE);

		g_io_channel_unref (ioc_stdin);
		g_io_channel_unref (ioc_stdout);
		g_io_channel_unref (ioc_stderr);
	}

	return task;
}

/* fixme: we need a proper GUI to be able to call task_cancel
	a throbber with cancel button perhaps?
	the pop-up window approach might be too intrusive
 */
static void
task_cancel (Task* task)
{
	gedit_debug (DEBUG_PLUGINS, "");

	if (task->running == RUNNING)
	{	
		task->running = MAKE_IT_CLOSE;
	
		kill (task->child_pid, SIGKILL);	
		wait (NULL);
	}

	if (task->running == MAKE_IT_CLOSE)
	{
		gchar *line;

		if (GEDIT_IS_OUTPUT_WINDOW (task->output_window))
			gedit_output_window_append_line (
				GEDIT_OUTPUT_WINDOW (task->output_window), "", TRUE);

		line = g_strdup_printf ("<i>%s</i>.", _("Stopped"));
		
		if (GEDIT_IS_OUTPUT_WINDOW (task->output_window))
			gedit_output_window_append_line (
				GEDIT_OUTPUT_WINDOW (task->output_window), line, TRUE);

		g_free (line);
	}
}

static void
task_destroy (Task* task)
{
	if (task->dialog)
		gtk_widget_destroy (task->dialog);

	g_free (task->command_line);
	g_free (task);

	gedit_debug(DEBUG_PLUGINS, "%s\n", "task destroyed");
}


/* plugin code */

static gboolean
ensure_scripts_dir_exists (void)
{
	gboolean found = FALSE;

	GnomeVFSURI* uri;
	
	gedit_debug (DEBUG_PLUGINS, "");	
	
	uri = gnome_vfs_uri_new (scripts_uri);
	if (uri)
		found = gnome_vfs_uri_exists (uri);
	gnome_vfs_uri_unref (uri);

	/* fixme: is it a good idea to mkdir the scripts_dir after all?? 
		maybe we should ask for user confirmation
	 */

	if (!found) {
		gedit_debug(DEBUG_PLUGINS, "creating %s\n", USER_GEDIT_SCRIPTS_LOCATION);
		/* fixme: what would be the correct perm? 
			0700 is my "safer" guess
		 */
		gnome_vfs_make_directory (gedit_dir, 0700);	
		found = (GNOME_VFS_OK == gnome_vfs_make_directory (scripts_dir, 0700));
	}
	
	if (!found) {
		g_warning ("Can't create scripts directory '%s'", USER_GEDIT_SCRIPTS_LOCATION);
	}
	return found;
}

static void
run_script_cb (BonoboUIComponent *uic, gpointer user_data, const gchar* verbname)
{
	gchar* path;

	gedit_debug (DEBUG_PLUGINS, "");

	path = g_strconcat (scripts_dir, verbname, NULL);
	
	/* run task */
	task_new (path);

	g_free(path);
}

static gboolean
handle_command_output (GIOChannel *ioc, GIOCondition condition, gpointer data) 
{
	GeditDocument *doc;

	gboolean not_running = FALSE;
	
	Task *task = (Task *)data;

	gedit_debug (DEBUG_PLUGINS, "");

	doc = gedit_get_active_document ();

	if ((condition & G_IO_IN) != 0)
	{       
		GError	*error = NULL;
		gchar   *string = NULL;
		gsize	 len = 0;
		gsize	 pos = 0;
		gchar 	*line;
	
		gedit_debug (DEBUG_PLUGINS, "1");
	
		if (!ioc->is_readable)
			return TRUE;

		gedit_debug (DEBUG_PLUGINS, "1.1");

		do 
		{
			gint  	 status;
			
			if (task->running != RUNNING) 
			{
				not_running = TRUE;
				break;
			}
				
			gedit_debug (DEBUG_PLUGINS, "1.2");
		
			do 
			{
				status = g_io_channel_read_line (ioc, &string, &len, &pos, &error);
				
				if (len == 0)
 				{
 					/* G_IO_IN + EOF = G_IO_HUP
 					 * -> the pipe is broken
 					 */
					not_running = TRUE;
					break;
				}
				
				while (gtk_events_pending ()) 
				{
					if (task->running == MAKE_IT_CLOSE) 
						return FALSE;
						
					gtk_main_iteration (); 				
				}

				if (task->running == MAKE_IT_CLOSE) 
					return FALSE;

				
			} while (status == G_IO_STATUS_AGAIN);
			
			if (not_running)
				break;
			
			if (status != G_IO_STATUS_NORMAL) 
			{
				gedit_debug (DEBUG_PLUGINS, "1.2.1");

				if (error != NULL) 
				{
					g_warning ("Error reading stdout: %s", error->message);
					g_error_free (error);

					task->failed = TRUE;
				}
				
				continue;
			}		
			
			if (len <= 0)
				continue;
				
			gedit_debug (DEBUG_PLUGINS, "1.3");

			line = g_markup_escape_text (string, pos);

			if (ioc == task->ioc_stdout)	
			{
				gtk_text_buffer_insert_at_cursor (GTK_TEXT_BUFFER (doc), string, -1);
			}
			else
			{
				gchar *markup;
				
				markup = g_strdup_printf ("<span foreground=\"red\">%s</span>", line);

				gedit_output_window_append_line (
						GEDIT_OUTPUT_WINDOW (task->output_window), markup, TRUE);
				
				g_free (markup);

				gtk_widget_show (task->output_window);
			}
			
			g_free (line);				
			while (gtk_events_pending ()) 
			{
				if (task->running == MAKE_IT_CLOSE)
					return FALSE;
				
				gtk_main_iteration (); 				
			}

			if (task->running == MAKE_IT_CLOSE) 
				return FALSE;

			g_free (string);
						
		} while (g_io_channel_get_buffer_condition (ioc) == G_IO_IN);
	}

	if ((condition != G_IO_IN) || not_running) 
	{
		gchar *line;
		gboolean done = FALSE;
		
		gedit_debug (DEBUG_PLUGINS, "2");

		g_io_channel_shutdown (ioc, TRUE, NULL);

		if (ioc != task->ioc_stdout)
			return FALSE;

		if (task->running == MAKE_IT_CLOSE)
			return FALSE;

		if ((task->running == MAKE_IT_STOP))
			line = g_strdup_printf ("<i>%s</i>.", _("Stopped"));
		else
		{
			done = TRUE;
			
			if (!task->failed)
				line = g_strdup_printf ("<i>%s</i>.", _("Done"));
			else
				line = g_strdup_printf ("<i>%s</i>.", _("Failed"));
		}
	
		gedit_output_window_append_line (GEDIT_OUTPUT_WINDOW (task->output_window), "", TRUE);

		gedit_output_window_append_line (GEDIT_OUTPUT_WINDOW (task->output_window), line, TRUE);
		
		g_free (line);

		while (gtk_events_pending ()) 
		{
			if (task->running == MAKE_IT_CLOSE)
				return FALSE;
				
			gtk_main_iteration (); 				
		}

		if (task->running == MAKE_IT_CLOSE)
			return FALSE;

		if (done)
			task->running = NOT_RUNNING;
	
		task_destroy (task);
		task = NULL;
		
		gedit_document_end_user_action (doc);
		
		return FALSE;
	}

	return TRUE;
}

static void
stream_document (GeditDocument *doc, GIOChannel *channel)
{
	GtkTextIter start, end;

	gedit_debug (DEBUG_PLUGINS, "");
	
	g_return_if_fail (doc != NULL);

	/* stream entire document when selection is not available */
	if (!gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (doc),
						   &start, &end))
	{
		gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (doc), &start);
		gtk_text_buffer_get_end_iter (GTK_TEXT_BUFFER (doc), &end);
	}

	/* fixme: test performance, streaming char by char might be too slow */
	while (!gtk_text_iter_is_end (&start) &&
	       !gtk_text_iter_equal (&start, &end))
	{
		gunichar c;
		
		c = gtk_text_iter_get_char (&start);		
		g_io_channel_write_unichar (channel, c, NULL);
				
		gtk_text_iter_forward_char (&start);
	}
}


/* export code */

G_MODULE_EXPORT GeditPluginState
destroy (GeditPlugin *plugin)
{
	gedit_debug (DEBUG_PLUGINS, "");

	g_free (gedit_dir);
	g_free (scripts_dir);
	g_free (scripts_uri);

	return PLUGIN_OK;
}
	
G_MODULE_EXPORT GeditPluginState
update_ui (GeditPlugin *plugin, BonoboWindow *window)
{
	BonoboUIComponent *uic;
	
	gedit_debug (DEBUG_PLUGINS, "");
	
	g_return_val_if_fail (window != NULL, PLUGIN_ERROR);

	uic = gedit_get_ui_component_from_window (window);

	gedit_menus_set_verb_sensitive (uic, "/commands/" MENU_ITEM_NAME, TRUE);

	return PLUGIN_OK;
}

G_MODULE_EXPORT GeditPluginState
activate (GeditPlugin *pd)
{
        gedit_debug (DEBUG_PLUGINS, "");

	scripts = scripts_new();

        return PLUGIN_OK;
}

G_MODULE_EXPORT GeditPluginState
deactivate (GeditPlugin *pd)
{
	gedit_debug (DEBUG_PLUGINS, "");

	scripts_destroy(scripts);
	scripts = NULL;

	return PLUGIN_OK;
}

G_MODULE_EXPORT GeditPluginState
init (GeditPlugin *pd)
{
	/* initialize */
	gedit_debug (DEBUG_PLUGINS, "");

	plugin = pd;
	pd->private_data = NULL;

	gedit_dir = gnome_util_prepend_user_home (USER_GEDIT_LOCATION);
	scripts_dir = gnome_util_prepend_user_home (USER_GEDIT_SCRIPTS_LOCATION);
	scripts_uri = g_strconcat ("file://", scripts_dir, NULL);

	return PLUGIN_OK;
}

G_MODULE_EXPORT GeditPluginState
configure (GeditPlugin *p, GtkWidget *parent)
{
	GError *error = NULL;
	
	gedit_debug (DEBUG_PLUGINS, "");

	if (!ensure_scripts_dir_exists ()) {
		return PLUGIN_ERROR;
	}
	else
	if (!gnome_url_show (scripts_uri, &error)) {
		g_warning("Error opening the scripts directory '%s': %s\n", scripts_uri, error->message);
		g_error_free (error);
	}

	return PLUGIN_OK;
}
