/*
 * This file is part of GNOME LaTeX.
 *
 * Copyright (C) 2014 - Sébastien Wilmet <swilmet@gnome.org>
 *
 * GNOME LaTeX 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 3 of the License, or
 * (at your option) any later version.
 *
 * GNOME LaTeX 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 GNOME LaTeX.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * SECTION:post-processor
 * @title: LatexilaPostProcessor
 * @short_description: post-processor base class
 *
 * When running a build tool, a post-processor is used to filter the output to
 * display only the relevant messages. The output can come from the
 * stdout/stderr of a build job command, or the contents of a log file, etc. In
 * GNOME LaTeX only the former is currently used, but it would be more robust to
 * read the LaTeX log file.
 *
 * For the no-output post-processor type, you should not need to create a
 * #LatexilaPostProcessor object, since the result is empty.
 *
 * The implementations sometimes assume that a post-processor can be used at
 * most one time.
 */

#include "latexila-post-processor.h"
#include "latexila-build-view.h"

#define BUFFER_SIZE 4096

struct _LatexilaPostProcessorPrivate
{
	GTask *task;
	GInputStream *stream;

	/* "+1" so we can nul-terminate the buffer. */
	gchar buffer[BUFFER_SIZE + 1];

	/* The @buffer is split by lines. But since the stream is read with a fixed
	 * size (BUFFER_SIZE), the last string returned by g_strsplit() is stored in
	 * @line_buffer. When the next block is read, the first line returned is
	 * appended to @line_buffer to have the whole line.
	 */
	GString *line_buffer;

	guint has_details : 1;
};

enum
{
	PROP_0,
	PROP_HAS_DETAILS
};

G_DEFINE_TYPE_WITH_PRIVATE (LatexilaPostProcessor, latexila_post_processor, G_TYPE_OBJECT)

/* Prototypes */
static void read_stream (LatexilaPostProcessor *pp);

/**
 * latexila_post_processor_get_type_from_name:
 * @name: the name of the post-processor.
 * @type: (out): the output post-processor type.
 *
 * Returns: %TRUE on success, %FALSE otherwise.
 */
gboolean
latexila_post_processor_get_type_from_name (const gchar               *name,
					    LatexilaPostProcessorType *type)
{
	g_assert (type != NULL);

	if (g_str_equal (name, "latexmk"))
	{
		*type = LATEXILA_POST_PROCESSOR_TYPE_LATEXMK;
		return TRUE;
	}

	if (g_str_equal (name, "latex"))
	{
		*type = LATEXILA_POST_PROCESSOR_TYPE_LATEX;
		return TRUE;
	}

	if (g_str_equal (name, "all-output"))
	{
		*type = LATEXILA_POST_PROCESSOR_TYPE_ALL_OUTPUT;
		return TRUE;
	}

	if (g_str_equal (name, "no-output"))
	{
		*type = LATEXILA_POST_PROCESSOR_TYPE_NO_OUTPUT;
		return TRUE;
	}

	return FALSE;
}

/**
 * latexila_post_processor_get_name_from_type:
 * @type: the post-processor type.
 *
 * Returns: the post-processor name.
 */
const gchar *
latexila_post_processor_get_name_from_type (LatexilaPostProcessorType type)
{
	switch (type)
	{
		case LATEXILA_POST_PROCESSOR_TYPE_LATEXMK:
			return "latexmk";

		case LATEXILA_POST_PROCESSOR_TYPE_LATEX:
			return "latex";

		case LATEXILA_POST_PROCESSOR_TYPE_ALL_OUTPUT:
			return "all-output";

		case LATEXILA_POST_PROCESSOR_TYPE_NO_OUTPUT:
			return "no-output";

		case LATEXILA_POST_PROCESSOR_TYPE_NB_TYPES:
		default:
			g_return_val_if_reached (NULL);
	}
}

static void
latexila_post_processor_get_property (GObject    *object,
				      guint       prop_id,
				      GValue     *value,
				      GParamSpec *pspec)
{
	LatexilaPostProcessor *pp = LATEXILA_POST_PROCESSOR (object);

	switch (prop_id)
	{
		case PROP_HAS_DETAILS:
			g_value_set_boolean (value, pp->priv->has_details);
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
			break;
	}
}

static void
latexila_post_processor_set_property (GObject      *object,
				      guint         prop_id,
				      const GValue *value,
				      GParamSpec   *pspec)
{
	LatexilaPostProcessor *pp = LATEXILA_POST_PROCESSOR (object);

	switch (prop_id)
	{
		case PROP_HAS_DETAILS:
			pp->priv->has_details = g_value_get_boolean (value);
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
			break;
	}
}

static void
latexila_post_processor_dispose (GObject *object)
{
	LatexilaPostProcessor *pp = LATEXILA_POST_PROCESSOR (object);

	g_clear_object (&pp->priv->task);
	g_clear_object (&pp->priv->stream);

	G_OBJECT_CLASS (latexila_post_processor_parent_class)->dispose (object);
}

static void
latexila_post_processor_finalize (GObject *object)
{
	LatexilaPostProcessor *pp = LATEXILA_POST_PROCESSOR (object);

	if (pp->priv->line_buffer != NULL)
	{
		g_string_free (pp->priv->line_buffer, TRUE);
		pp->priv->line_buffer = NULL;
	}

	G_OBJECT_CLASS (latexila_post_processor_parent_class)->finalize (object);
}

static void
latexila_post_processor_start_default (LatexilaPostProcessor *pp,
				       GFile                 *file)
{
	/* Do nothing. */
}

static void
latexila_post_processor_process_line_default (LatexilaPostProcessor *pp,
					      gchar                 *line)
{
	g_free (line);
}

static void
latexila_post_processor_end_default (LatexilaPostProcessor *pp,
				     gboolean               succeeded)
{
	/* Do nothing. */
}

static const GList *
latexila_post_processor_get_messages_default (LatexilaPostProcessor *pp,
					      gboolean               show_details)
{
	return NULL;
}

static GQueue *
latexila_post_processor_take_messages_default (LatexilaPostProcessor *pp)
{
	return NULL;
}

static void
latexila_post_processor_class_init (LatexilaPostProcessorClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->get_property = latexila_post_processor_get_property;
	object_class->set_property = latexila_post_processor_set_property;
	object_class->dispose = latexila_post_processor_dispose;
	object_class->finalize = latexila_post_processor_finalize;

	klass->start = latexila_post_processor_start_default;
	klass->process_line = latexila_post_processor_process_line_default;
	klass->end = latexila_post_processor_end_default;
	klass->get_messages = latexila_post_processor_get_messages_default;
	klass->take_messages = latexila_post_processor_take_messages_default;

	g_object_class_install_property (object_class,
					 PROP_HAS_DETAILS,
					 g_param_spec_boolean ("has-details",
							       "Has details",
							       "",
							       FALSE,
							       G_PARAM_READWRITE |
							       G_PARAM_CONSTRUCT |
							       G_PARAM_STATIC_STRINGS));
}

static void
latexila_post_processor_init (LatexilaPostProcessor *pp)
{
	pp->priv = latexila_post_processor_get_instance_private (pp);
}

/* Use this function to process a line for the first time, it will convert the
 * string to UTF-8.
 */
static void
process_line (LatexilaPostProcessor *pp,
	      gchar                 *line)
{
	gchar *utf8_line = NULL;

	/* locale is not UTF-8 */
	if (!g_get_charset (NULL))
	{
		utf8_line = g_locale_to_utf8 (line, -1, NULL, NULL, NULL);
	}
	else if (g_utf8_validate (line, -1, NULL))
	{
		utf8_line = line;
		line = NULL;
	}

	/* The LaTeX output can be in ISO-8859-1, with accents in a filename for
	 * instance.
	 */
	if (utf8_line == NULL)
	{
		utf8_line = g_convert (line, -1, "UTF-8", "ISO-8859-1", NULL, NULL, NULL);
	}

	if (utf8_line != NULL)
	{
		latexila_post_processor_process_line (pp, utf8_line);
	}
	else
	{
		g_warning ("Failed to convert subprocess output to UTF-8: %s", line);
	}

	g_free (line);
}

static void
read_stream_cb (GInputStream          *stream,
		GAsyncResult          *result,
		LatexilaPostProcessor *pp)
{
	gssize bytes_read;
	GCancellable *cancellable;
	gchar **lines;
	GError *error = NULL;

	bytes_read = g_input_stream_read_finish (stream, result, &error);

	cancellable = g_task_get_cancellable (pp->priv->task);
	if (g_cancellable_is_cancelled (cancellable))
	{
		if (error != NULL)
		{
			g_error_free (error);
		}

		g_task_return_boolean (pp->priv->task, FALSE);
		return;
	}

	if (error != NULL)
	{
		g_warning ("Error while reading the post-processor stream: %s", error->message);
		g_error_free (error);
		g_task_return_boolean (pp->priv->task, FALSE);
		return;
	}

	/* End of stream reached, process line_buffer. */
	if (bytes_read == 0)
	{
		/* Generally a single \n is present at the end of the stream, so an empty
		 * line is present in line_buffer. But we don't want to display it in the
		 * build view.
		 */
		if (pp->priv->line_buffer != NULL &&
		    pp->priv->line_buffer->str != NULL &&
		    pp->priv->line_buffer->str[0] != '\0')
		{
			gchar *line;

			line = g_string_free (pp->priv->line_buffer, FALSE);
			pp->priv->line_buffer = NULL;

			process_line (pp, line);
		}

		/* finished! */
		g_task_return_boolean (pp->priv->task, TRUE);
		return;
	}

	pp->priv->buffer[bytes_read] = '\0';

	lines = g_strsplit (pp->priv->buffer, "\n", 0);
	g_assert (lines != NULL);
	g_assert (lines[0] != NULL);

	if (pp->priv->line_buffer != NULL)
	{
		/* Merge line_buffer and the first line */
		g_string_append (pp->priv->line_buffer, lines[0]);
	}

	/* If a second line exists, we can call process_line().
	 * The first line must be replaced by the contents of line_buffer.
	 * And the last line must go to line_buffer.
	 */
	if (lines[1] != NULL)
	{
		gint last_line;
		gint i;

		if (pp->priv->line_buffer != NULL)
		{
			g_free (lines[0]);
			lines[0] = g_string_free (pp->priv->line_buffer, FALSE);
			pp->priv->line_buffer = NULL;
		}

		for (last_line = 1; lines[last_line+1] != NULL; last_line++)
		{
			;
		}

		pp->priv->line_buffer = g_string_new (lines[last_line]);
		g_free (lines[last_line]);
		lines[last_line] = NULL;

		for (i = 0; lines[i] != NULL; i++)
		{
			process_line (pp, lines[i]);
		}

		g_free (lines);
	}
	else
	{
		/* If not already done above, put the first line to line_buffer. */
		if (pp->priv->line_buffer == NULL)
		{
			pp->priv->line_buffer = g_string_new (lines[0]);
		}

		g_strfreev (lines);
	}

	/* Unfortunately for the computer, it is not finished. */
	read_stream (pp);
}

static void
read_stream (LatexilaPostProcessor *pp)
{
	g_input_stream_read_async (pp->priv->stream,
				   &pp->priv->buffer,
				   BUFFER_SIZE,
				   G_PRIORITY_DEFAULT,
				   g_task_get_cancellable (pp->priv->task),
				   (GAsyncReadyCallback) read_stream_cb,
				   pp);
}

/**
 * latexila_post_processor_process_async:
 * @pp: a post-processor.
 * @file: the #GFile on which the build tool is run.
 * @stream: the input stream to process.
 * @cancellable: a #GCancellable.
 * @callback: the callback to call when the operation is finished.
 * @user_data: the data to pass to the callback.
 *
 * Asynchronously process an input stream. The input stream can for example come
 * from the output of a command launched with #GSubprocess, or it can be the
 * input stream of a file (e.g. the LaTeX log file), etc.
 *
 * @callback will be called when the operation is finished. You can then call
 * latexila_post_processor_process_finish().
 */
void
latexila_post_processor_process_async (LatexilaPostProcessor *pp,
				       GFile                 *file,
				       GInputStream          *stream,
				       GCancellable          *cancellable,
				       GAsyncReadyCallback    callback,
				       gpointer               user_data)
{
	g_return_if_fail (LATEXILA_IS_POST_PROCESSOR (pp));
	g_return_if_fail (G_IS_FILE (file));
	g_return_if_fail (G_IS_INPUT_STREAM (stream));
	g_return_if_fail (G_IS_CANCELLABLE (cancellable));
	g_return_if_fail (pp->priv->task == NULL);

	pp->priv->task = g_task_new (pp, cancellable, callback, user_data);
	pp->priv->stream = g_object_ref (stream);

	latexila_post_processor_start (pp, file);

	if (pp->priv->line_buffer != NULL)
	{
		g_string_free (pp->priv->line_buffer, TRUE);
		pp->priv->line_buffer = NULL;
	}

	read_stream (pp);
}

/**
 * latexila_post_processor_process_finish:
 * @pp: a post-processor.
 * @result: a #GAsyncResult.
 * @succeeded: whether the subprocess has succeeded.
 *
 * Finishes an operation started with latexila_post_processor_process_async().
 * After calling this function, you can get the filtered messages with
 * latexila_post_processor_get_messages().
 */
void
latexila_post_processor_process_finish (LatexilaPostProcessor *pp,
					GAsyncResult          *result,
					gboolean               succeeded)
{
	g_return_if_fail (g_task_is_valid (result, pp));

	g_task_propagate_boolean (G_TASK (result), NULL);

	latexila_post_processor_end (pp, succeeded);

	g_clear_object (&pp->priv->task);
	g_clear_object (&pp->priv->stream);

	if (pp->priv->line_buffer != NULL)
	{
		g_string_free (pp->priv->line_buffer, TRUE);
		pp->priv->line_buffer = NULL;
	}
}

/**
 * latexila_post_processor_start:
 * @pp: a #LatexilaPostProcessor.
 * @file: the #GFile on which the build tool is run.
 *
 * Manually starts the post-processor.
 *
 * Not needed if you use latexila_post_processor_process_async().
 */
void
latexila_post_processor_start (LatexilaPostProcessor *pp,
			       GFile                 *file)
{
	g_return_if_fail (LATEXILA_IS_POST_PROCESSOR (pp));

	return LATEXILA_POST_PROCESSOR_GET_CLASS (pp)->start (pp, file);
}

/**
 * latexila_post_processor_process_line:
 * @pp: a #LatexilaPostProcessor.
 * @line: (transfer full): a line, without the newline character(s).
 *
 * Manually processes a line. This function takes ownership of @line. Free with
 * g_free() if you don't reuse the content.
 *
 * Not needed if you use latexila_post_processor_process_async().
 */
void
latexila_post_processor_process_line (LatexilaPostProcessor *pp,
				      gchar                 *line)
{
	g_return_if_fail (LATEXILA_IS_POST_PROCESSOR (pp));

	return LATEXILA_POST_PROCESSOR_GET_CLASS (pp)->process_line (pp, line);
}

/**
 * latexila_post_processor_end:
 * @pp: a #LatexilaPostProcessor.
 * @succeeded: whether the subprocess has succeeded.
 *
 * Manually ends the processing.
 *
 * Not needed if you use latexila_post_processor_process_async().
 */
void
latexila_post_processor_end (LatexilaPostProcessor *pp,
			     gboolean               succeeded)
{
	g_return_if_fail (LATEXILA_IS_POST_PROCESSOR (pp));

	succeeded = succeeded != FALSE;

	return LATEXILA_POST_PROCESSOR_GET_CLASS (pp)->end (pp, succeeded);
}

/**
 * latexila_post_processor_get_messages:
 * @pp: a post-processor.
 * @show_details: whether to show the details. Has no effect if
 * #LatexilaPostProcessor:has-details is %FALSE.
 *
 * Gets the filtered messages. Call this function only after calling
 * latexila_post_processor_process_finish() or latexila_post_processor_end().
 *
 * Another solution would have been to pass the #LatexilaBuildView to the
 * post-processor, so the filtered messages can directly be outputed to the
 * build view as they come. But some post-processors don't know what to output
 * directly. The latexmk post-processor can have a simplified output with only
 * the LaTeX messages, in which case the detailed messages are also available,
 * but this can be known only when all the stream has been processed.
 *
 * Obviously if the build view is passed to the post-processor, the latexmk
 * post-processor can output its messages only at the end. But another reason to
 * not pass the build view is for the unit tests. It is easier for the unit
 * tests to check the returned #GList than analyzing a #GtkTreeView. Of course
 * it would be possible to keep also the messages in a #GList and have this
 * function only for the unit tests, but it takes more memory (unless a custom
 * #GtkTreeModel is implemented), or another function is needed to configure
 * whether a #GList is kept in memory or not... It becomes a little too
 * complicated, and doesn't really worth the effort as most users use latexmk.
 *
 * The current solution is "good enough". And "good enough" is... "good enough".
 *
 * Returns: (transfer none) (element-type LatexilaBuildMsg): the tree of
 * filtered messages.
 */
const GList *
latexila_post_processor_get_messages (LatexilaPostProcessor *pp,
				      gboolean               show_details)
{
	g_return_val_if_fail (LATEXILA_IS_POST_PROCESSOR (pp), NULL);

	show_details = show_details != FALSE;

	return LATEXILA_POST_PROCESSOR_GET_CLASS (pp)->get_messages (pp, show_details);
}

/**
 * latexila_post_processor_take_messages: (skip)
 * @pp: a #LatexilaPostProcessor.
 *
 * Takes ownership of all the messages. Since #GQueue is not reference counted,
 * @pp is emptied and is thus useless after calling this function.
 *
 * Returns: (transfer full) (nullable): the tree of filtered messages, or %NULL.
 * Element types: #LatexilaBuildMsg.
 */
GQueue *
latexila_post_processor_take_messages (LatexilaPostProcessor *pp)
{
	g_return_val_if_fail (LATEXILA_IS_POST_PROCESSOR (pp), NULL);

	return LATEXILA_POST_PROCESSOR_GET_CLASS (pp)->take_messages (pp);
}
