/* Bluefish HTML Editor
 * filter.c - external filter backend implementation
 *
 * Copyright (c) 2000 Antti-Juhani Kaijanaho
 *
 * 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 "default_include.h"

#include <gdk/gdk.h>
#include <glib.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

#include "filter.h"
#include "gtk_easy.h"


/* Putting UNUSED after a variable's name in a declaration tells GCC
   that the variable is meant to be unused, and this makes GCC not
   warn about this variable being unused.  For other compilers, UNUSED
   is ignored.  */
#ifdef __GNUC__
#define UNUSED __attribute__ ((unused))
#else
#define UNUSED					/* nothing */
#endif

#define MAX3(a,b,c) (MAX(MAX((a), (b)), (c)))

/* How long should we sleep after reading and writing a chunk?  Keep
   this small but not zero; this should be used to avoid the busy loop
   in filter_buffer() taking all the processor's time. */
#define SLEEPSEC 0				/* seconds, and */
#define SLEEPUSEC 50			/* microseconds */

/* Buffer capacity is divisible by CAPACITY_DIVISOR if capacity is
   ever modified by the functions here. */
#define CAPACITY_DIVISOR 64

/* All routines here return 0 if problems */



/********************* additions from Olivier ************************/
/* static void kill_subprocess_lcb(GtkWidget * widget, gpointer data)
{
	gtk_widget_destroy(GTK_WIDGET(gtk_widget_get_toplevel(widget)));
	g_free(data);
}

static void close_error_dialog_lcb(GtkWidget * widget, gpointer data)
{
	gtk_widget_destroy(GTK_WIDGET(gtk_widget_get_toplevel(widget)));
	g_free(data);
} */

/* static void subprocess_error_dialog(char const *message, int enable_killing, gpointer data)
{
	GtkWidget *win, *hbox, *vbox, *button;

	win = window_with_title(_("Filter error"), GTK_WIN_POS_MOUSE, GTK_WINDOW_DIALOG, 5);
	vbox = gtk_vbox_new(FALSE, 0);
	gtk_container_add(GTK_CONTAINER(win), vbox);
	gtk_box_pack_start(GTK_BOX(vbox), gtk_label_new(message), FALSE, FALSE, 0);
	hbox = gtk_hbox_new(FALSE, 0);
	gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
	button = bf_stock_ok_button(close_error_dialog_lcb, data);
	gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
	if (enable_killing) {
		button = bf_stock_button(_("Kill filter process"), kill_subprocess_lcb, data);
		gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
	}
	gtk_widget_show_all(win);
} */

/********************* ********************** ************************/





static
void enomem_box(void)
{
	g_warning(_("Out of memory.\n"));
}

#ifdef __GNUC__
#  define errno_box() errno_box_hlp(__PRETTY_FUNCTION__)
#else
#  define errno_box() errno_box_hlp(_("unknown"))
#endif

static
void errno_box_hlp(char const *fun)
{
	g_warning(_("filter.c (%s): System call failed: %s.\n"), fun, strerror(errno));
}

static
void stderr_box(char *s)
{
	g_warning(_("Subprocess standard error: %s"), s);
}

static
void error_box(char *s)
{
	g_warning("%s", s);
}

/* Assumes that bufp contains random data. */
static
int init_buffer(buffer_t * bufp, size_t init_capacity)
{
	g_assert(bufp != NULL);

	bufp->capacity = (1 + init_capacity / CAPACITY_DIVISOR)
		* CAPACITY_DIVISOR;
	bufp->buf = malloc(bufp->capacity * sizeof *bufp->buf);
	if (bufp->buf == NULL) {
		enomem_box();
		return 0;
	}
	bufp->length = 0;

	g_assert(bufp->buf != NULL);
	g_assert(bufp->capacity != 0);
	return 1;
}

/* Return nonzero iff the buffer can hold at least
   new_minimum_capacity elements after the function returns.  */
static
int buffer_ensure_capacity(buffer_t * bufp, size_t new_minimum_capacity)
{
	size_t new_cap;
	buf_elem_t *new_buf;

	g_assert(bufp != NULL);
	g_assert(bufp->length <= bufp->capacity);

	if (bufp->capacity >= new_minimum_capacity)
		return 1;

	new_cap = (1 + new_minimum_capacity / CAPACITY_DIVISOR)
		* CAPACITY_DIVISOR;

	new_buf = realloc(bufp->buf, new_cap * sizeof *bufp->buf);
	if (new_buf == NULL) {
		enomem_box();
		return 0;
	}

	bufp->buf = new_buf;
	bufp->capacity = new_cap;

	g_assert(bufp->buf != NULL);
	g_assert(bufp->capacity != 0);
	g_assert(bufp->length <= bufp->capacity);

	return 1;
}

/* Make bufp->capacity == bufp->length + 1 true, if possible.  Return
   zero iff capacity isn't at least bufp->length + 1 at return time. */
static
int buffer_trim(buffer_t * bufp)
{
	size_t new_cap;
	buf_elem_t *new_buf;

	g_assert(bufp != NULL);
	g_assert(bufp->buf != NULL);
	g_assert(bufp->capacity != 0);
	g_assert(bufp->length <= bufp->capacity);

	if (bufp->capacity == bufp->length + 1)
		return 1;

	new_cap = bufp->length + 1;
	new_buf = realloc(bufp->buf, new_cap * sizeof *bufp->buf);
	if (new_buf == NULL)
		return bufp->length + 1 <= bufp->capacity;

	bufp->buf = new_buf;
	bufp->capacity = new_cap;

	g_assert(bufp->buf != NULL);
	g_assert(bufp->capacity != 0);
	g_assert(bufp->length <= bufp->capacity);
	g_assert(bufp->capacity == bufp->length + 1);
	return 1;
}

/* This is the subprocess.  We dump all error messages to
   subprocess_stdio[2], which the parent will show as error boxes.  We
   use no GUI stuff here. */
static
void subprocess(char const *commandline, int subprocess_stdio[])
{
	int result;

	/* Our first job is to set up the subprocess' standard streams: ... */

	/* ... stdin ... */

	result = dup2(subprocess_stdio[0], STDIN_FILENO);
	if (result == -1) {
		g_warning(_("subprocess unable to dup2 stdin: %s\n"), strerror(errno));
		_exit(EXIT_FAILURE);
	}
	result = close(subprocess_stdio[0]);
	g_assert(result == 0);

	/* ... stdout ... */
	result = dup2(subprocess_stdio[1], STDOUT_FILENO);
	if (result == -1) {
		g_warning(_("subprocess unable to dup2 stdout: %s\n"), strerror(errno));
		_exit(EXIT_FAILURE);
	}
	result = close(subprocess_stdio[1]);
	g_assert(result == 0);

	/* ... and stderr. */
	result = dup2(subprocess_stdio[2], STDERR_FILENO);
	if (result == -1) {
		g_warning(_("subprocess unable to dup2 stderr: %s\n"), strerror(errno));
		_exit(EXIT_FAILURE);
	}
	result = close(subprocess_stdio[2]);
	g_assert(result == 0);

	/* Now we exec /bin/sh -c "commandline".  That last NULL is
	   important! */
	result = execl("/bin/sh", "sh", "-c", commandline, NULL);

	/* If all goes well, we never reach this line.  But of course,
	   the world is not perfect, and we occasionally fail to exec
	   the process.  We deal here with the problem.  */
	g_warning(_("subprocess unable to exec filter: %s\n"), strerror(errno));

	_exit(EXIT_FAILURE);
}

static
int spawn_subprocess(char const *commandline, int *stdin_fd,	/* subprocess stdin, nonblocking */
					 int *stdout_fd,	/* subprocess stdout,nonblocking */
					 int *stderr_fd,	/* subprocess stderr, nonblocking */
					 pid_t * sub_pid)
{
#ifndef WIN32 /*currently gcc doesn't support pipe() under WIN32 :-( */
	int pipefds[2];
	int subprocess_stdio[3];
	int result;

	/* Create subprocess standard input. */
	result = pipe(pipefds);
	if (result == -1) {
		errno_box();
		return 0;
	}
	subprocess_stdio[0] = pipefds[0];
	*stdin_fd = pipefds[1];

	/* Create subprocess standard output. */
	result = pipe(pipefds);
	if (result == -1) {
		errno_box();
		return 0;
	}
	subprocess_stdio[1] = pipefds[1];
	*stdout_fd = pipefds[0];

	/* Create subprocess standard error. */
	result = pipe(pipefds);
	if (result == -1) {
		errno_box();
		return 0;
	}
	subprocess_stdio[2] = pipefds[1];
	*stderr_fd = pipefds[0];

	/* Make the parent process side of the pipes nonblocking. */
	result = fcntl(*stdin_fd, F_SETFL, O_NONBLOCK);
	if (result == -1) {
		errno_box();
		return 0;
	}

	result = fcntl(*stdout_fd, F_SETFL, O_NONBLOCK);
	if (result == -1) {
		errno_box();
		return 0;
	}

	result = fcntl(*stderr_fd, F_SETFL, O_NONBLOCK);
	if (result == -1) {
		errno_box();
		return 0;
	}

	/* Create the subprocess. */
	DEBUG_MSG("filter.c (spawn_subprocess): forking...\n");
	*sub_pid = fork();
	if (*sub_pid == -1) {
		errno_box();
		return 0;
	} else if (*sub_pid == 0) {
		DEBUG_MSG("filter.c (spawn_subprocess) / child: fork successful.\n");
		result = close(*stdin_fd);
		g_assert(result == 0);
		result = close(*stdout_fd);
		g_assert(result == 0);
		result = close(*stderr_fd);
		g_assert(result == 0);
		subprocess(commandline, subprocess_stdio);
		g_assert_not_reached();
	}

	/* Parent process */
	DEBUG_MSG("filter.c (spawn_subprocess) / parent: fork successful.\n");
	result = close(subprocess_stdio[0]);
	g_assert(result == 0);
	result = close(subprocess_stdio[1]);
	g_assert(result == 0);
	result = close(subprocess_stdio[2]);
	g_assert(result == 0);
#endif
	return 1;
}

typedef struct {
	int eof_p;
	buffer_t *buf;
	size_t first_unwritten;
	int tag;
} input_data_t;

static
void write_lcb(gpointer data_gp, int fd, GdkInputCondition condition UNUSED)
{
	input_data_t *data;
	size_t count;
	ssize_t written;

	data = data_gp;
	count = data->buf->length - data->first_unwritten;

	if (data->eof_p)
		return;

	/* Retry on EAGAIN until successful.  Since EAGAIN probably
	   means that we're trying to feed too much to fit in the
	   kernel buffer, we try again with a smaller chunk.  On
	   EINTR, we try again in the next iteration.  */
  retry:
	written = write(fd, data->buf->buf + data->first_unwritten, count);

	if (written == -1 && errno == EAGAIN) {
		DEBUG_MSG("filter.c (write_lcb): EAGAIN with %d\n", written);
		count = count / 2 + 1;
		goto retry;
	}

	if (written == -1 && errno == EINTR) {
		DEBUG_MSG("filter.c (write_lcb): EINTR\n");
		return;
	}

	if (written == -1) {
		errno_box();
		return;
	}

	data->first_unwritten += written;
	DEBUG_MSG("filter.c (write_lcb): wrote %d bytes\n", written);

	if (data->first_unwritten == data->buf->length) {
		/* End of data. */
		register int close_result;
		gdk_input_remove(data->tag);
		close_result = close(fd);
		g_assert(close_result == 0);
		data->eof_p = 1;
		DEBUG_MSG("filter.c (write_lcb): End of data.\n");
		return;
	}
}

static
void read_lcb(gpointer data_gp, int fd, GdkInputCondition condition UNUSED)
{
	input_data_t *data;
	const size_t blk = 64;
	ssize_t readc;
	int ok;

	data = data_gp;
	DEBUG_MSG("filter.c (read_lcb): data = %X\n, fd=%d", (int) data, fd);

	if (data->eof_p)
		return;

	ok = buffer_ensure_capacity(data->buf, data->buf->length + blk);
	if (!ok) {
		enomem_box();
		return;
	}

	readc = read(fd, data->buf->buf + data->buf->length, blk);

	if (readc == -1 && errno == EINTR) {
		DEBUG_MSG("filter.c (read_lcb): EINTR\n");
		return;
	}

	if (readc == -1 && errno == EAGAIN) {
		DEBUG_MSG("filter.c (read_lcb): EAGAIN\n");
		return;
	}

	if (readc == -1) {
		errno_box();
		return;
	}

	if (readc == 0) {
		register int close_rv;
		/* End of file. */
		data->eof_p = 1;
		DEBUG_MSG("filter.c (read_lcb): data->eof_p == %d\n", data->eof_p);
		gdk_input_remove(data->tag);
		close_rv = close(fd);
		DEBUG_MSG("filter.c (read_lcb): close(%d) -> %d\n", fd, close_rv);
		g_assert(close_rv == 0);
		DEBUG_MSG("filter.c (read_lcb): end of file on fd %d, data->eof_p == %d\n", fd, data->eof_p);
		return;
	}

	data->buf->length += readc;
	DEBUG_MSG("filter.c (read_lcb): read %d bytes\n", readc);
}

int filter_buffer(char const *commandline, buffer_t in_buf, buffer_t * out_buf)
{
	int child_stdin, child_stdout, child_stderr;
	pid_t child_pid;
	int ok;
	int status;
	buffer_t err_buf = { 0, 0, 0 };
	input_data_t in_data = { 0, &in_buf, 0, 0 };
	input_data_t out_data = { 0, out_buf, 0, 0 };
	input_data_t err_data = { 0, &err_buf, 0, 0 };
	char *exitreason = NULL;

	init_buffer(out_buf, in_buf.length);
	init_buffer(&err_buf, 0);

	DEBUG_MSG("&out_data = %X, &err_data = %X\n", (int) &out_data, (int) &err_data);

	ok = spawn_subprocess(commandline, &child_stdin, &child_stdout, &child_stderr, &child_pid);
	if (!ok)
		return 0;

	DEBUG_MSG("child_stdin = %d, child_stdout = %d, child_stderr = %d\n", child_stdin, child_stdout, child_stderr);

	in_data.tag = gdk_input_add(child_stdin, GDK_INPUT_WRITE, write_lcb, &in_data);
	out_data.tag = gdk_input_add(child_stdout, GDK_INPUT_READ, read_lcb, &out_data);
	err_data.tag = gdk_input_add(child_stderr, GDK_INPUT_READ, read_lcb, &err_data);

	/* During this loop the writing and reading happens.  We wait
	   until all streams are closed. */
	while (!in_data.eof_p || !out_data.eof_p || !err_data.eof_p) {
#ifndef WIN32
		static struct timespec const req = { SLEEPSEC, SLEEPUSEC };
		static struct timespec rem;
		register int nanosleep_rv;
#endif

		while (gtk_events_pending())
			gtk_main_iteration();

		while (gtk_events_pending())
			gtk_main_iteration();

		/* Avoid a busy loop by stopping for a short period at
		   every iteration, thus giving more time slices for
		   other processes.  */
#ifndef WIN32 /* no nanosleep here */
		nanosleep_rv = nanosleep(&req, &rem);
		g_assert(nanosleep_rv == 0);
#endif    
	}

	DEBUG_MSG("filter.c (filter_buffer): invoking waitpid.\n");

#ifndef WIN32
	waitpid(child_pid, &status, 0);
#endif

	DEBUG_MSG("filter.c (filter_buffer): waitpid returned.\n");

	/* Incorporate the exit status in error_buf */
	if (WIFEXITED(status)) {
		if (WEXITSTATUS(status) != 0) {
			exitreason = g_strdup_printf(_("subprocess terminated abnormally with exit code %d\n"), WEXITSTATUS(status));
		}
	} else if (WIFSIGNALED(status)) {
		exitreason = g_strdup_printf(_("bluefish: subprocess was killed by signal %d\n"), WTERMSIG(status));
	}
	if (exitreason != NULL) {
		error_box(exitreason);
		g_free(exitreason);
	}

	if (err_buf.length > 0) {
		ok = buffer_trim(&err_buf);
		if (!ok) {
			enomem_box();
			return 0;
		}
		err_buf.buf[err_buf.length] = '\0';

		stderr_box(err_buf.buf);
	}

	/* Add the terminating null character. */
	ok = buffer_trim(out_buf);
	if (!ok) {
		enomem_box();
		return 0;
	}
	out_buf->buf[out_buf->length] = '\0';
	return 1;
}
