/* -*- Mode: C; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 3 -*- */

/*
 *  File-Roller
 *
 *  Copyright (C) 2001 The Free Software Foundation, 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 Street #330, Boston, MA 02111-1307, USA.
 *
 *  $Id$
 */

#include <fcntl.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <glib.h>
#include <gdk/gdk.h>
#include <gtk/gtkmain.h>
#include <gtk/gtksignal.h>
#include "fr-process.h"


#define REFRESH_RATE 100


enum {
   START,
   DONE,
   LAST_SIGNAL
};

static guint fr_process_signals[LAST_SIGNAL] = { 0 };


G_DEFINE_TYPE (FRProcess, fr_process, G_TYPE_OBJECT)


static void
fr_process_dispose (GObject *object)
{
   FRProcess *fr_proc;

   g_return_if_fail (object != NULL);
   g_return_if_fail (FR_IS_PROCESS (object));
  
   fr_proc = FR_PROCESS (object);

   fr_process_stop (fr_proc);
   fr_process_clear (fr_proc);
   g_ptr_array_free (fr_proc->comm, FALSE);
   g_ptr_array_free (fr_proc->dir, FALSE);

   if (fr_proc->row_output != NULL) {
      g_list_foreach (fr_proc->row_output, (GFunc) g_free, NULL);
      g_list_free (fr_proc->row_output);
      fr_proc->row_output = NULL;
   }

   /* Chain up */
   if (G_OBJECT_CLASS (fr_process_parent_class)->dispose)
      G_OBJECT_CLASS (fr_process_parent_class)->dispose (object);
}


static void
fr_process_class_init (FRProcessClass *class)
{
   GObjectClass *gobject_class;

   gobject_class = (GObjectClass *) class;

   fr_process_signals[START] =
      g_signal_new ("start",
                    G_TYPE_FROM_CLASS (gobject_class),
                    G_SIGNAL_RUN_LAST,
                    G_STRUCT_OFFSET (FRProcessClass, start),
                    NULL, NULL,
                    g_cclosure_marshal_VOID__VOID,
                    G_TYPE_NONE, 0);
   fr_process_signals[DONE] =
      g_signal_new ("done",
                    G_TYPE_FROM_CLASS (gobject_class),
                    G_SIGNAL_RUN_LAST,
                    G_STRUCT_OFFSET (FRProcessClass, done),
                    NULL, NULL,
                    g_cclosure_marshal_VOID__INT,
                    G_TYPE_NONE, 1,
                    G_TYPE_INT);

   gobject_class->dispose = fr_process_dispose;
   class->done = NULL;
}


static void
fr_process_init (FRProcess *fr_proc)
{
   fr_proc->comm = g_ptr_array_new ();
   fr_proc->dir = g_ptr_array_new ();
   fr_proc->n_comm = -1;

   fr_proc->command_pid = 0;
   fr_proc->output_fd = 0;

   fr_proc->log_timeout = -1;
   fr_proc->not_processed = 0;
   fr_proc->row_output = NULL;

   fr_proc->proc_line_func = NULL;
   fr_proc->proc_line_data = NULL;

   fr_proc->running = FALSE;

   fr_proc->use_standard_locale = TRUE;
}


FRProcess * 
fr_process_new (void)
{
   FRProcess *fr_proc;
   fr_proc = FR_PROCESS (g_object_new (FR_TYPE_PROCESS, NULL));
   return fr_proc;
}


void
fr_process_begin_command (FRProcess *fr_proc, 
                          char *arg)
{
   g_return_if_fail (fr_proc != NULL);

   fr_proc->n_comm++;
   g_ptr_array_add (fr_proc->comm, g_list_prepend (NULL, g_strdup (arg)));
   g_ptr_array_add (fr_proc->dir, NULL);
}


void
fr_process_set_working_dir (FRProcess *fr_proc, 
                            char *dir)
{
   g_return_if_fail (fr_proc != NULL);
   g_ptr_array_index (fr_proc->dir, fr_proc->n_comm) = g_strdup (dir);
}


void
fr_process_add_arg (FRProcess *fr_proc, 
                    char *arg)
{
   g_return_if_fail (fr_proc != NULL);
   g_ptr_array_index (fr_proc->comm, fr_proc->n_comm)
      = g_list_prepend (g_ptr_array_index (fr_proc->comm, fr_proc->n_comm),
                        g_strdup (arg));
}


void
fr_process_end_command (FRProcess *fr_proc)
{
   g_return_if_fail (fr_proc != NULL);
   g_ptr_array_index (fr_proc->comm, fr_proc->n_comm)
      = g_list_reverse (g_ptr_array_index (fr_proc->comm, fr_proc->n_comm));
}


void
fr_process_clear (FRProcess *fr_proc)
{
   gint i;

   g_return_if_fail (fr_proc != NULL);

   for (i = 0; i <= fr_proc->n_comm; i++) {
      /* free command. */
      g_list_foreach (g_ptr_array_index (fr_proc->comm, i), (GFunc) g_free, NULL);
      g_list_free (g_ptr_array_index (fr_proc->comm, i));
      g_ptr_array_index (fr_proc->comm, i) = NULL;

      /* free working directory. */
      if (g_ptr_array_index (fr_proc->dir, i) != NULL)
         g_free (g_ptr_array_index (fr_proc->dir, i));
      g_ptr_array_index (fr_proc->dir, i) = NULL;
   }

   for (i = 0; i <= fr_proc->n_comm; i++) {
      g_ptr_array_remove_index (fr_proc->comm, 0);
      g_ptr_array_remove_index (fr_proc->dir, 0);
   }

   fr_proc->n_comm = -1;
}


void
fr_process_set_proc_line_func (FRProcess *fr_proc, 
                               ProcLineFunc func,
                               gpointer data)
{
   g_return_if_fail (fr_proc != NULL);
   fr_proc->proc_line_func = func;
   fr_proc->proc_line_data = data;
}


static gboolean
process_output (FRProcess *fr_proc)
{
   int n, i;
   char *line, *eol;

   n = read (fr_proc->output_fd, 
             fr_proc->buffer + fr_proc->not_processed, 
             FR_PROCCESS_BUFFER_SIZE - fr_proc->not_processed);

   if (n <= 0)
      return FALSE;

   fr_proc->buffer[fr_proc->not_processed + n] = 0;

   line = fr_proc->buffer;
   while ((eol = strchr (line, '\n')) != NULL) {
      *eol = 0;

      fr_proc->row_output = g_list_prepend (fr_proc->row_output,
                                            g_strdup (line));

      if (fr_proc->proc_stdout && (fr_proc->proc_line_func != NULL))
         (*fr_proc->proc_line_func) (line, 
                                     fr_proc->proc_line_data);
      line = eol + 1;
   }
	
   /* shift unprocessed text to the beginning. */

   fr_proc->not_processed = strlen (line);
   for (i = 0; *line != 0; line++, i++)
      fr_proc->buffer[i] = *line;

   return TRUE;
}


static gint check_child (gpointer data);


static void
start_current_command (FRProcess *fr_proc)
{
   pid_t pid;
   int pipe_fd[2];

   if (pipe (pipe_fd) < 0) {
      fr_proc->error = FR_PROC_ERROR_PIPE;
      g_signal_emit (G_OBJECT (fr_proc), 
                     fr_process_signals[DONE], 0,
                     fr_proc->error);
      return;
   }

   pid = fork ();

   if (pid < 0) {
      close (pipe_fd[0]);
      close (pipe_fd[1]);

      fr_proc->error = FR_PROC_ERROR_FORK;
      g_signal_emit (G_OBJECT (fr_proc), 
                     fr_process_signals[DONE], 0,
                     fr_proc->error);

      return;
   }

   if (pid == 0) {
      GList *arg_list, *scan;
      char *command;
      char *dir;
      char ** argv;
      int i = 0;

      arg_list = g_ptr_array_index (fr_proc->comm, fr_proc->current_command);
      dir = g_ptr_array_index (fr_proc->dir, fr_proc->current_command);
      command = (char *) arg_list->data;

      if (dir != NULL) {
#ifdef DEBUG
         g_print ("cd %s\n", dir); 
#endif
         if (chdir (dir) != 0)
            _exit (FR_PROC_ERROR_CANNOT_CHDIR);
      }

#ifdef DEBUG
      g_print ("%s", command); 
#endif

      argv = g_new (char *, g_list_length (arg_list) + 1);
      for (scan = arg_list; scan; scan = scan->next) {
         argv[i++] = scan->data;

#ifdef DEBUG
         g_print (" %s", (char*) scan->data);
#endif
      }
      argv[i] = NULL;

#ifdef DEBUG
      g_print ("\n"); 
#endif

      close (pipe_fd[0]);
      dup2 (pipe_fd[1], STDOUT_FILENO);
      /* dup2 (pipe_fd[1], STDERR_FILENO); FIXME */	

      if (fr_proc->use_standard_locale)
         putenv ("LC_ALL=C");

      execvp (command, argv);

      _exit (255);
   }

   close (pipe_fd[1]);

   fr_proc->command_pid = pid;
   fr_proc->output_fd = pipe_fd[0];
   fcntl (fr_proc->output_fd, F_SETFL, O_NONBLOCK);

   fr_proc->not_processed = 0;
   fr_proc->log_timeout = gtk_timeout_add (REFRESH_RATE, 
                                           check_child,
                                           fr_proc);
}


static gint
check_child (gpointer data)
{
   FRProcess *fr_proc = data;
   pid_t pid;
   int status;
	
   process_output (fr_proc);

   pid = waitpid (fr_proc->command_pid, &status, WNOHANG);
   if (pid != fr_proc->command_pid) 
      return TRUE;

   if (WIFEXITED (status)) {
      if (WEXITSTATUS (status) == 0)
         fr_proc->error = FR_PROC_ERROR_NONE;
      else if (WEXITSTATUS (status) == 255) 
         fr_proc->error = FR_PROC_ERROR_COMMAND_NOT_FOUND;
      else if (WEXITSTATUS (status) == FR_PROC_ERROR_CANNOT_CHDIR)
         fr_proc->error = FR_PROC_ERROR_CANNOT_CHDIR;
      else
         fr_proc->error = FR_PROC_ERROR_GENERIC;
   } else
      fr_proc->error = FR_PROC_ERROR_EXITED_ABNORMALLY;

   /* Remove check. */

   gtk_timeout_remove (fr_proc->log_timeout);
   fr_proc->log_timeout = -1;
   fr_proc->command_pid = 0;

   /* Read all pending output. */

   if (fr_proc->error == FR_PROC_ERROR_NONE)
      while (process_output (fr_proc)) ;

   close (fr_proc->output_fd);
   fr_proc->output_fd = 0;

   /* Execute next command. */

   if ((fr_proc->error == FR_PROC_ERROR_NONE)
       && (fr_proc->current_command < fr_proc->n_comm)) {
      fr_proc->current_command++;
      start_current_command (fr_proc);
      return FALSE;
   }
	
   fr_proc->current_command = -1;

   if (fr_proc->row_output != NULL)
      fr_proc->row_output = g_list_reverse (fr_proc->row_output);

   fr_proc->running = FALSE;

   g_signal_emit (G_OBJECT (fr_proc), 
                  fr_process_signals[DONE], 0,
                  fr_proc->error);

   return FALSE;
}


void
fr_process_use_standard_locale (FRProcess *fr_proc,
                                gboolean use_stand_locale)
{
   g_return_if_fail (fr_proc != NULL);
   fr_proc->use_standard_locale = use_stand_locale;
}


void
fr_process_start (FRProcess *fr_proc,
                  gboolean proc_stdout)
{
   g_return_if_fail (fr_proc != NULL);
   g_return_if_fail (fr_proc->n_comm != -1);

   if (fr_proc->running)
      return;

   fr_proc->proc_stdout = proc_stdout;

   if (fr_proc->row_output != NULL) {
      g_list_foreach (fr_proc->row_output, (GFunc) g_free, NULL);
      g_list_free (fr_proc->row_output);
      fr_proc->row_output = NULL;
   }

   g_signal_emit (G_OBJECT (fr_proc), 
                  fr_process_signals[START], 0);

   fr_proc->current_command = 0;
   start_current_command (fr_proc);

   fr_proc->running = TRUE;
}


void
fr_process_stop (FRProcess *fr_proc)
{
   g_return_if_fail (fr_proc != NULL);

   if (! fr_proc->running)
      return;

   if (fr_proc->log_timeout != -1) {
      gtk_timeout_remove (fr_proc->log_timeout);
      fr_proc->log_timeout = -1;
   }

   kill (fr_proc->command_pid, SIGKILL);
   waitpid (fr_proc->command_pid, NULL, WUNTRACED);
   fr_proc->command_pid = 0;
   close (fr_proc->output_fd);
   fr_proc->output_fd = 0;

   fr_proc->running = FALSE;

   fr_proc->error = FR_PROC_ERROR_STOPPED;
   g_signal_emit (G_OBJECT (fr_proc), 
                  fr_process_signals[DONE], 0,
                  fr_proc->error);
}
