/*
 * 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-latex
 * @title: LatexilaPostProcessorLatex
 * @short_description: LaTeX post-processor
 *
 * A post-processor to filter the output of the latex or pdflatex commands.
 * There can be three types of errors: critical errors, warnings, or bad boxes.
 * There are also some other useful information: the size of the document, the
 * number of pages, and the number of errors.
 */

#include "latexila-post-processor-latex.h"
#include <stdlib.h>
#include <string.h>
#include "latexila-build-view.h"

#define NO_LINE (-1)

/* If a message is split into several lines, we enter in a different state to
 * fetch the end of the message.
 */
typedef enum
{
	STATE_START,
	STATE_BADBOX,
	STATE_WARNING,
	STATE_ERROR,
	STATE_ERROR_SEARCH_LINE,
	STATE_FILENAME,
	STATE_FILENAME_HEURISTIC
} State;

/* File opened by TeX. It is used to know on which file an error or warning
 * occurred. File's are pushed and popped in a stack.
 */
typedef struct _File File;
struct _File
{
	gchar *filename;

	/* There are basically two ways to detect the current file TeX is processing:
	 * 1) Use \Input (srctex or srcltx package) and \include exclusively. This will
	 *    cause (La)TeX to print the line ":<+ filename" in the log file when opening
	 *    a file, ":<-" when closing a file. Filenames pushed on the stack in this mode
	 *    are marked as reliable.
	 *
	 * 2) Since people will probably also use the \input command, we also have to
	 *    detect the old-fashioned way. TeX prints '(filename' when opening a file
	 *    and a ')' when closing one. It is impossible to detect this with 100%
	 *    certainty (TeX prints many messages and even text (a context) from the
	 *    TeX source file, there could be unbalanced parentheses), so we use an
	 *    heuristic algorithm. In heuristic mode a ')' will only be considered as
	 *    a signal that TeX is closing a file if the top of the stack is not
	 *    marked as "reliable".
	 *
	 * The method used here is almost the same as in Kile.
	 */
	guint reliable : 1;

	/* Non-existent files are also pushed on the stack, because the corresponding
	 * ')' will pop them. If we don't push them, wrong files are popped.
	 * When a new message is added, the last _existing_ file is taken.
	 */
	guint exists : 1;
};

struct _LatexilaPostProcessorLatexPrivate
{
	GQueue *messages;

	/* Current message. */
	LatexilaBuildMsg *cur_msg;

	State state;

	/* If a message is split into several lines, the lines are concatenated in
	 * line_buffer.
	 */
	GString *line_buffer;

	/* Number of lines in @line_buffer. */
	gint lines_count;

	/* If a filename is split into several lines. */
	GString *filename_buffer;

	/* The stack containing the files that TeX is processing. The top of the stack
	 * is the beginning of the list.
	 * Elements type: pointer to File
	 */
	GSList *stack_files;

	/* The directory where the document is compiled. */
	/* FIXME why not storing the GFile? */
	gchar *directory_path;

	/* For statistics. */
	gint badboxes_count;
	gint warnings_count;
	gint errors_count;
};

G_DEFINE_TYPE_WITH_PRIVATE (LatexilaPostProcessorLatex,
			    latexila_post_processor_latex,
			    LATEXILA_TYPE_POST_PROCESSOR)

static File *
file_new (void)
{
	return g_slice_new0 (File);
}

static void
file_free (File *file)
{
	if (file != NULL)
	{
		g_free (file->filename);
		g_slice_free (File, file);
	}
}

static void
set_line_buffer (LatexilaPostProcessorLatex *pp,
		 const gchar                *line)
{
	if (pp->priv->line_buffer != NULL)
	{
		g_string_free (pp->priv->line_buffer, TRUE);
	}

	pp->priv->line_buffer = g_string_new (line);
	pp->priv->lines_count = 1;
}

static void
append_to_line_buffer (LatexilaPostProcessorLatex *pp,
		       const gchar                *line)
{
	if (pp->priv->line_buffer == NULL)
	{
		set_line_buffer (pp, line);
	}
	else
	{
		g_string_append (pp->priv->line_buffer, line);
		pp->priv->lines_count++;
	}
}

static void
set_filename_buffer (LatexilaPostProcessorLatex *pp,
		     const gchar                *content)
{
	if (pp->priv->filename_buffer != NULL)
	{
		g_string_free (pp->priv->filename_buffer, TRUE);
	}

	pp->priv->filename_buffer = g_string_new (content);
}

static void
append_to_filename_buffer (LatexilaPostProcessorLatex *pp,
			   const gchar                *content)
{
	if (pp->priv->filename_buffer == NULL)
	{
		set_filename_buffer (pp, content);
	}
	else
	{
		g_string_append (pp->priv->filename_buffer, content);
	}
}

static gchar *
get_current_filename (LatexilaPostProcessorLatex *pp)
{
	GSList *l;

	for (l = pp->priv->stack_files; l != NULL; l = l->next)
	{
		File *file = l->data;

		/* TODO check lazily if the file exists */
		if (file->exists)
		{
			return g_strdup (file->filename);
		}
	}

	return NULL;
}

static void
add_message (LatexilaPostProcessorLatex *pp,
	     gboolean                    set_filename)
{
	static GRegex *regex_spaces = NULL;
	LatexilaBuildMsg *cur_msg;
	GError *error = NULL;

	cur_msg = pp->priv->cur_msg;
	g_return_if_fail (cur_msg != NULL);

	/* Exclude some useless messages. */
	if (cur_msg->type == LATEXILA_BUILD_MSG_TYPE_WARNING &&
	    g_strcmp0 (cur_msg->text, "There were undefined references.") == 0)
	{
		latexila_build_msg_reinit (cur_msg);
		goto end;
	}

	if (set_filename)
	{
		g_free (cur_msg->filename);
		cur_msg->filename = get_current_filename (pp);
	}

	if (G_UNLIKELY (regex_spaces == NULL))
	{
		regex_spaces = g_regex_new ("\\s{2,}", G_REGEX_OPTIMIZE, 0, &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			error = NULL;
		}
	}

	if (G_LIKELY (regex_spaces != NULL))
	{
		gchar *new_text;

		/* A message on several lines is sometimes indented, so when the lines are
		 * concatenated there are a lot of spaces. So multiple spaces are replaced
		 * by one space.
		 */
		new_text = g_regex_replace (regex_spaces,
					    cur_msg->text,
					    -1, 0, " ", 0,
					    &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			error = NULL;
		}

		if (new_text != NULL)
		{
			g_free (cur_msg->text);
			cur_msg->text = new_text;
		}
	}

	switch (cur_msg->type)
	{
		case LATEXILA_BUILD_MSG_TYPE_BADBOX:
			pp->priv->badboxes_count++;
			break;

		case LATEXILA_BUILD_MSG_TYPE_WARNING:
			pp->priv->warnings_count++;
			break;

		case LATEXILA_BUILD_MSG_TYPE_ERROR:
			pp->priv->errors_count++;
			break;

		case LATEXILA_BUILD_MSG_TYPE_MAIN_TITLE:
		case LATEXILA_BUILD_MSG_TYPE_JOB_TITLE:
		case LATEXILA_BUILD_MSG_TYPE_JOB_SUB_COMMAND:
		case LATEXILA_BUILD_MSG_TYPE_INFO:
		default:
			break;
	}

	g_queue_push_tail (pp->priv->messages, cur_msg);

	pp->priv->cur_msg = latexila_build_msg_new ();

end:
	pp->priv->state = STATE_START;

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

	pp->priv->lines_count = 0;
}

static void
latexila_post_processor_latex_start (LatexilaPostProcessor *post_processor,
				     GFile                 *file)
{
	LatexilaPostProcessorLatex *pp = LATEXILA_POST_PROCESSOR_LATEX (post_processor);
	GFile *parent;

	parent = g_file_get_parent (file);

	g_free (pp->priv->directory_path);
	pp->priv->directory_path = g_file_get_parse_name (parent);

	g_object_unref (parent);
}

static void
latexila_post_processor_latex_end (LatexilaPostProcessor *post_processor,
				   gboolean               succeeded)
{
	LatexilaPostProcessorLatex *pp = LATEXILA_POST_PROCESSOR_LATEX (post_processor);
	LatexilaBuildMsg *cur_msg = pp->priv->cur_msg;

	latexila_build_msg_reinit (cur_msg);

	/* Statistics. Since all the messages printed by TeX are in English, the
	 * string is not translated.
	 */
	cur_msg->type = LATEXILA_BUILD_MSG_TYPE_INFO;
	cur_msg->text = g_strdup_printf ("%d %s, %d %s, %d %s",
					 pp->priv->errors_count,
					 pp->priv->errors_count == 1 ? "error" : "errors",
					 pp->priv->warnings_count,
					 pp->priv->warnings_count == 1 ? "warning" : "warnings",
					 pp->priv->badboxes_count,
					 pp->priv->badboxes_count == 1 ? "badbox" : "badboxes");

	add_message (pp, FALSE);
}

static void
detect_badbox_line (LatexilaPostProcessorLatex *pp,
		    const gchar                *badbox_line,
		    gboolean                    current_line_is_empty)
{
	static GRegex *regex_badbox_lines = NULL;
	static GRegex *regex_badbox_line = NULL;
	static GRegex *regex_badbox_output = NULL;
	LatexilaBuildMsg *cur_msg = pp->priv->cur_msg;
	GError *error = NULL;

	if (G_UNLIKELY (regex_badbox_lines == NULL))
	{
		regex_badbox_lines = g_regex_new ("(.*) at lines (\\d+)--(\\d+)",
						  G_REGEX_OPTIMIZE,
						  0,
						  &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return;
		}
	}

	if (G_UNLIKELY (regex_badbox_line == NULL))
	{
		regex_badbox_line = g_regex_new ("(.*) at line (\\d+)",
						 G_REGEX_OPTIMIZE,
						 0,
						 &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return;
		}
	}

	if (G_UNLIKELY (regex_badbox_output == NULL))
	{
		regex_badbox_output = g_regex_new ("(.*)has occurred while \\\\output is active",
						   G_REGEX_OPTIMIZE,
						   0,
						   &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return;
		}
	}

	if (g_regex_match (regex_badbox_lines, badbox_line, 0, NULL))
	{
		gchar **strings;
		gint n1;
		gint n2;

		/* TODO use g_match_info_fetch_named() */
		strings = g_regex_split (regex_badbox_lines, badbox_line, 0);

		g_free (cur_msg->text);
		cur_msg->text = g_strdup (strings[1]);

		n1 = atoi (strings[2]);
		n2 = atoi (strings[3]);

		if (n1 <= n2)
		{
			cur_msg->start_line = n1;
			cur_msg->end_line = n2;
		}
		else
		{
			cur_msg->start_line = n2;
			cur_msg->end_line = n1;
		}

		add_message (pp, TRUE);

		g_strfreev (strings);
		return;
	}

	if (g_regex_match (regex_badbox_line, badbox_line, 0, NULL))
	{
		gchar **strings;

		strings = g_regex_split (regex_badbox_line, badbox_line, 0);

		g_free (cur_msg->text);
		cur_msg->text = g_strdup (strings[1]);

		cur_msg->start_line = atoi (strings[2]);

		add_message (pp, TRUE);

		g_strfreev (strings);
		return;
	}

	if (g_regex_match (regex_badbox_output, badbox_line, 0, NULL))
	{
		gchar **strings;

		strings = g_regex_split (regex_badbox_output, badbox_line, 0);

		g_free (cur_msg->text);
		cur_msg->text = g_strdup (strings[1]);
		cur_msg->start_line = NO_LINE;

		add_message (pp, TRUE);

		g_strfreev (strings);
		return;
	}

	if (pp->priv->lines_count > 4 || current_line_is_empty)
	{
		g_free (cur_msg->text);
		cur_msg->text = g_strdup (badbox_line);

		cur_msg->start_line = NO_LINE;

		add_message (pp, TRUE);
		return;
	}

	if (pp->priv->state == STATE_START)
	{
		pp->priv->state = STATE_BADBOX;
		set_line_buffer (pp, badbox_line);
	}
}

static gboolean
detect_badbox (LatexilaPostProcessorLatex *pp,
	       const gchar                *line)
{
	static GRegex *regex_badbox = NULL;
	GError *error = NULL;

	if (G_UNLIKELY (regex_badbox == NULL))
	{
		regex_badbox = g_regex_new ("^(Over|Under)full \\\\[hv]box",
					    G_REGEX_OPTIMIZE,
					    0,
					    &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return FALSE;
		}
	}

	switch (pp->priv->state)
	{
		case STATE_START:
			if (!g_regex_match (regex_badbox, line, 0, NULL))
			{
				return FALSE;
			}

			pp->priv->cur_msg->type = LATEXILA_BUILD_MSG_TYPE_BADBOX;
			detect_badbox_line (pp, line, FALSE);
			return TRUE;

		case STATE_BADBOX:
			detect_badbox_line (pp,
					    pp->priv->line_buffer->str,
					    line[0] == '\0');

			/* The return value is not important here. */
			return TRUE;

		case STATE_WARNING:
		case STATE_ERROR:
		case STATE_ERROR_SEARCH_LINE:
		case STATE_FILENAME:
		case STATE_FILENAME_HEURISTIC:
		default:
			g_return_val_if_reached (FALSE);
	}
}

static void
detect_warning_line (LatexilaPostProcessorLatex *pp,
		     const gchar                *warning,
		     gboolean                    current_line_is_empty)
{
	static GRegex *regex_warning_line = NULL;
	static GRegex *regex_warning_international_line = NULL;
	LatexilaBuildMsg *cur_msg = pp->priv->cur_msg;
	gint len;
	GError *error = NULL;

	if (G_UNLIKELY (regex_warning_line == NULL))
	{
		regex_warning_line = g_regex_new ("(.*) on input line (\\d+)\\.$",
						  G_REGEX_OPTIMIZE,
						  0,
						  &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return;
		}
	}

	if (G_UNLIKELY (regex_warning_international_line == NULL))
	{
		regex_warning_international_line = g_regex_new ("(.*)(\\d+)\\.$",
								G_REGEX_OPTIMIZE,
								0,
								&error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return;
		}
	}

	if (g_regex_match (regex_warning_line, warning, 0, NULL))
	{
		gchar **strings;

		strings = g_regex_split (regex_warning_line, warning, 0);

		g_free (cur_msg->text);
		cur_msg->text = g_strdup (strings[1]);

		cur_msg->start_line = atoi (strings[2]);

		add_message (pp, TRUE);

		g_strfreev (strings);
		return;
	}

	if (g_regex_match (regex_warning_international_line, warning, 0, NULL))
	{
		gchar **strings;

		strings = g_regex_split (regex_warning_international_line, warning, 0);

		g_free (cur_msg->text);
		cur_msg->text = g_strdup (strings[1]);

		cur_msg->start_line = atoi (strings[2]);

		add_message (pp, TRUE);

		g_strfreev (strings);
		return;
	}

	len = strlen (warning);
	if (warning[len-1] == '.' || pp->priv->lines_count > 5 || current_line_is_empty)
	{
		g_free (cur_msg->text);
		cur_msg->text = g_strdup (warning);

		cur_msg->start_line = NO_LINE;

		add_message (pp, TRUE);
		return;
	}

	if (pp->priv->state == STATE_START)
	{
		pp->priv->state = STATE_WARNING;
		set_line_buffer (pp, warning);
	}
}

static gboolean
detect_warning (LatexilaPostProcessorLatex *pp,
		const gchar                *line)
{
	static GRegex *regex_warning = NULL;
	static GRegex *regex_warning_no_file = NULL;
	LatexilaBuildMsg *cur_msg = pp->priv->cur_msg;
	GMatchInfo *match_info;
	GError *error = NULL;

	if (G_UNLIKELY (regex_warning == NULL))
	{
		regex_warning = g_regex_new ("^(((! )?(La|pdf)TeX)|Package|Class)"
					     "(?P<name>.*) Warning[^:]*:\\s*(?P<contents>.*)",
					     G_REGEX_OPTIMIZE | G_REGEX_CASELESS,
					     0,
					     &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return FALSE;
		}
	}

	if (G_UNLIKELY (regex_warning_no_file == NULL))
	{
		regex_warning_no_file = g_regex_new ("(No file .*)",
						     G_REGEX_OPTIMIZE,
						     0,
						     &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return FALSE;
		}
	}

	switch (pp->priv->state)
	{
		case STATE_START:
			g_regex_match (regex_warning, line, 0, &match_info);
			if (g_match_info_matches (match_info))
			{
				gchar *contents;
				gchar *name;

				cur_msg->type = LATEXILA_BUILD_MSG_TYPE_WARNING;

				contents = g_match_info_fetch_named (match_info, "contents");
				name = g_match_info_fetch_named (match_info, "name");
				name = g_strstrip (name);

				if (name[0] != '\0')
				{
					gchar *new_contents = g_strdup_printf ("%s: %s", name, contents);
					g_free (contents);
					contents = new_contents;
				}

				detect_warning_line (pp, contents, FALSE);

				g_free (contents);
				g_free (name);
				g_match_info_free (match_info);
				return TRUE;
			}

			g_match_info_free (match_info);
			match_info = NULL;

			if (g_regex_match (regex_warning_no_file, line, 0, NULL))
			{
				gchar **strings;

				cur_msg->type = LATEXILA_BUILD_MSG_TYPE_WARNING;

				strings = g_regex_split (regex_warning_no_file, line, 0);

				g_free (cur_msg->text);
				cur_msg->text = g_strdup (strings[1]);

				cur_msg->start_line = NO_LINE;

				add_message (pp, TRUE);

				g_strfreev (strings);
				return TRUE;
			}

			return FALSE;

		case STATE_WARNING:
			detect_warning_line (pp,
					     pp->priv->line_buffer->str,
					     line[0] == '\0');

			/* The return value is not important here. */
			return TRUE;

		case STATE_BADBOX:
		case STATE_ERROR:
		case STATE_ERROR_SEARCH_LINE:
		case STATE_FILENAME:
		case STATE_FILENAME_HEURISTIC:
		default:
			g_return_val_if_reached (FALSE);
	}
}

static gboolean
detect_error (LatexilaPostProcessorLatex *pp,
	      const gchar                *line)
{
	static GRegex *regex_latex_error = NULL;
	static GRegex *regex_pdflatex_error = NULL;
	static GRegex *regex_tex_error = NULL;
	static GRegex *regex_error_line = NULL;
	gboolean found;
	gchar *msg;
	gint len;
	LatexilaBuildMsg *cur_msg = pp->priv->cur_msg;
	GError *error = NULL;

	if (G_UNLIKELY (regex_latex_error == NULL))
	{
		regex_latex_error = g_regex_new ("^! LaTeX Error: (.*)$",
						 G_REGEX_OPTIMIZE,
						 0,
						 &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return FALSE;
		}
	}

	if (G_UNLIKELY (regex_pdflatex_error == NULL))
	{
		regex_pdflatex_error = g_regex_new ("^Error: pdflatex (.*)$",
						    G_REGEX_OPTIMIZE,
						    0,
						    &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return FALSE;
		}
	}

	if (G_UNLIKELY (regex_tex_error == NULL))
	{
		regex_tex_error = g_regex_new ("^! (.*)\\.$",
					       G_REGEX_OPTIMIZE,
					       0,
					       &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return FALSE;
		}
	}

	if (G_UNLIKELY (regex_error_line == NULL))
	{
		regex_error_line = g_regex_new ("^l\\.(\\d+)(.*)",
						G_REGEX_OPTIMIZE,
						0,
						&error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return FALSE;
		}
	}

	switch (pp->priv->state)
	{
		case STATE_START:
			found = FALSE;
			msg = NULL;

			if (g_regex_match (regex_latex_error, line, 0, NULL))
			{
				gchar **strings = g_regex_split (regex_latex_error, line, 0);
				msg = g_strdup (strings[1]);
				found = TRUE;
				g_strfreev (strings);
			}
			else if (g_regex_match (regex_pdflatex_error, line, 0, NULL))
			{
				gchar **strings = g_regex_split (regex_pdflatex_error, line, 0);
				msg = g_strdup (strings[1]);
				found = TRUE;
				g_strfreev (strings);
			}
			else if (g_regex_match (regex_tex_error, line, 0, NULL))
			{
				gchar **strings = g_regex_split (regex_tex_error, line, 0);
				msg = g_strdup (strings[1]);
				found = TRUE;
				g_strfreev (strings);
			}

			if (found)
			{
				pp->priv->lines_count++;
				cur_msg->type = LATEXILA_BUILD_MSG_TYPE_ERROR;

				len = strlen (line);

				/* The message is complete. */
				if (line[len-1] == '.')
				{
					g_free (cur_msg->text);
					cur_msg->text = msg;

					pp->priv->state = STATE_ERROR_SEARCH_LINE;
				}
				/* The message is split into several lines. */
				else
				{
					set_line_buffer (pp, msg);
					pp->priv->state = STATE_ERROR;
					g_free (msg);
				}

				return TRUE;
			}

			return FALSE;

		case STATE_ERROR:
			len = strlen (line);

			if (line[len-1] == '.')
			{
				g_free (cur_msg->text);
				cur_msg->text = g_string_free (pp->priv->line_buffer, FALSE);
				pp->priv->line_buffer = NULL;

				/* FIXME set lines_count to 0? */

				pp->priv->state = STATE_ERROR_SEARCH_LINE;
			}
			else if (pp->priv->lines_count > 4)
			{
				g_free (cur_msg->text);
				cur_msg->text = g_string_free (pp->priv->line_buffer, FALSE);
				pp->priv->line_buffer = NULL;

				cur_msg->start_line = NO_LINE;

				add_message (pp, TRUE);
			}

			/* The return value is not important here. */
			return TRUE;

		case STATE_ERROR_SEARCH_LINE:
			if (g_regex_match (regex_error_line, line, 0, NULL))
			{
				gchar **strings = g_regex_split (regex_error_line, line, 0);
				cur_msg->start_line = atoi (strings[1]);

				add_message (pp, TRUE);

				g_strfreev (strings);
				return TRUE;
			}
			else if (pp->priv->lines_count > 11)
			{
				cur_msg->start_line = NO_LINE;
				add_message (pp, TRUE);
				return TRUE;
			}

			return FALSE;

		case STATE_BADBOX:
		case STATE_WARNING:
		case STATE_FILENAME:
		case STATE_FILENAME_HEURISTIC:
		default:
			g_return_val_if_reached (FALSE);
	}
}

static gboolean
detect_other (LatexilaPostProcessorLatex *pp,
	      const gchar                *line)
{
	static GRegex *regex_other_bytes = NULL;
	LatexilaBuildMsg *cur_msg = pp->priv->cur_msg;
	GMatchInfo *match_info;
	GError *error = NULL;

	if (G_UNLIKELY (regex_other_bytes == NULL))
	{
		regex_other_bytes = g_regex_new ("(?P<nb>\\d+) bytes",
						 G_REGEX_OPTIMIZE,
						 0,
						 &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return FALSE;
		}
	}

	if (strstr (line, "Output written on") == NULL)
	{
		return FALSE;
	}

	cur_msg->start_line = NO_LINE;
	cur_msg->type = LATEXILA_BUILD_MSG_TYPE_INFO;

	g_regex_match (regex_other_bytes, line, 0, &match_info);

	if (g_match_info_matches (match_info))
	{
		gchar *nb_bytes_str;
		glong nb_bytes;
		gchar *human_size;
		gchar *new_line;

		nb_bytes_str = g_match_info_fetch_named (match_info, "nb");
		g_return_val_if_fail (nb_bytes_str != NULL, FALSE);

		nb_bytes = atol (nb_bytes_str);
		human_size = g_format_size (nb_bytes);

		new_line = g_regex_replace_literal (regex_other_bytes, line, -1, 0, human_size, 0, &error);

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

			g_free (cur_msg->text);
			cur_msg->text = g_strdup (line);
		}
		else
		{
			g_free (cur_msg->text);
			cur_msg->text = new_line;
		}

		g_free (nb_bytes_str);
		g_free (human_size);
	}
	else
	{
		g_free (cur_msg->text);
		cur_msg->text = g_strdup (line);
	}

	add_message (pp, FALSE);

	g_match_info_free (match_info);
	return TRUE;
}

/* Returns NULL if the filename does not exist.
 * Returns the path of the filename if it exists.
 */
static gchar *
get_path_if_file_exists (LatexilaPostProcessorLatex *pp,
			 const gchar                *filename)
{
	gchar *full_path;
	const gchar *extensions[] = {".tex", ".ltx", ".latex", ".dtx", ".ins", NULL};
	gint i;

	if (g_path_is_absolute (filename))
	{
		if (g_file_test (filename, G_FILE_TEST_IS_REGULAR))
		{
			return g_strdup (filename);
		}
		else
		{
			return NULL;
		}
	}

	if (g_str_has_prefix (filename, "./"))
	{
		full_path = g_build_filename (pp->priv->directory_path,
					      filename + 2,
					      NULL);
	}
	else
	{
		full_path = g_build_filename (pp->priv->directory_path,
					      filename,
					      NULL);
	}

	if (g_file_test (full_path, G_FILE_TEST_IS_REGULAR))
	{
		return full_path;
	}

	for (i = 0; extensions[i] != NULL; i++)
	{
		gchar *path_with_ext = g_strdup_printf ("%s%s", full_path, extensions[i]);

		if (g_file_test (path_with_ext, G_FILE_TEST_IS_REGULAR))
		{
			g_free (full_path);
			return path_with_ext;
		}

		g_free (path_with_ext);
	}

	g_free (full_path);
	return NULL;
}

static void
push_file_on_stack (LatexilaPostProcessorLatex *pp,
		    gboolean                    reliable)
{
	File *file;
	const gchar *bad_suffix = "pdfTeX";
	gchar *path;

	file = file_new ();
	file->reliable = reliable;
	file->filename = g_string_free (pp->priv->filename_buffer, FALSE);
	pp->priv->filename_buffer = NULL;

	/* Handle special case when a warning message is collapsed. */
	if (g_str_has_suffix (file->filename, bad_suffix))
	{
		gint pos = strlen (file->filename) - strlen (bad_suffix);
		file->filename[pos] = '\0';
	}

	path = get_path_if_file_exists (pp, file->filename);

	if (path != NULL)
	{
		g_free (file->filename);
		file->filename = path;
		file->exists = TRUE;
	}
	else
	{
		file->exists = FALSE;
	}

	pp->priv->stack_files = g_slist_prepend (pp->priv->stack_files, file);
}

static void
pop_file_from_stack (LatexilaPostProcessorLatex *pp)
{
	if (pp->priv->stack_files != NULL)
	{
		File *file = pp->priv->stack_files->data;
		file_free (file);
	}

	pp->priv->stack_files = g_slist_remove_link (pp->priv->stack_files,
						     pp->priv->stack_files);
}

static gboolean
file_exists (LatexilaPostProcessorLatex *pp,
	     const gchar                *filename)
{
	gchar *path;
	gboolean ret;

	path = get_path_if_file_exists (pp, filename);
	ret = path != NULL;

	g_free (path);
	return ret;
}

static void
update_stack_file_heuristic (LatexilaPostProcessorLatex *pp,
			     const gchar                *line)
{
	gboolean expect_filename;
	const gchar *line_pos;
	const gchar *line_pos_next;

	/* This is where the filename is supposed to start. */
	const gchar *start_filename = line;

	expect_filename = pp->priv->state == STATE_FILENAME_HEURISTIC;

	/* Handle special case. */
	if (expect_filename && line[0] == ')')
	{
		push_file_on_stack (pp, FALSE);
		expect_filename = FALSE;
		pp->priv->state = STATE_START;
	}

	for (line_pos = line; line_pos[0] != '\0'; line_pos = line_pos_next)
	{
		gboolean is_last_char = FALSE;
		gboolean next_is_terminator = FALSE;
		gunichar cur_char = g_utf8_get_char (line_pos);

		line_pos_next = g_utf8_next_char (line_pos);

		/* We're expecting a filename. If a filename really ends at this position,
		 * one of the following must be true:
		 *  1) Next character is a space, indicating the end of a filename
		 *    (yes, the path can't have spaces, this is a TeX limitation).
		 *  2) We're at the end of the line, the filename is probably
		 *     continued on the next line.
		 *  3) The file was closed already, signalled by the ')'.
		 */

		if (expect_filename)
		{
			is_last_char = line_pos_next[0] == '\0';

			if (is_last_char)
			{
				next_is_terminator = FALSE;
			}
			else
			{
				gunichar next_char = g_utf8_get_char (line_pos_next);
				next_is_terminator = (g_unichar_isspace (next_char) || next_char == ')');
			}
		}

		if (is_last_char || next_is_terminator)
		{
			gint pos;

			if (pp->priv->filename_buffer == NULL)
			{
				set_filename_buffer (pp, "");
			}

			g_string_append_len (pp->priv->filename_buffer,
					     start_filename,
					     line_pos_next - start_filename);

			if (pp->priv->filename_buffer->len == 0)
			{
				continue;
			}

			pos = line_pos - line;

			/* By default, an output line is 79 characters max. */
			if ((is_last_char && pos < 78) ||
			    next_is_terminator ||
			    file_exists (pp, pp->priv->filename_buffer->str))
			{
				push_file_on_stack (pp, FALSE);
				expect_filename = FALSE;
				pp->priv->state = STATE_START;
			}
			/* Guess the filename is continued on the next line, only if the
			 * current filename does not exist.
			 */
			else
			{
				g_assert (is_last_char);

				if (file_exists (pp, pp->priv->filename_buffer->str))
				{
					push_file_on_stack (pp, FALSE);
					expect_filename = FALSE;
					pp->priv->state = STATE_START;
				}
				else
				{
					pp->priv->state = STATE_FILENAME_HEURISTIC;
				}
			}
		}
		/* TeX is opening a file. */
		else if (cur_char == '(')
		{
			pp->priv->state = STATE_START;
			set_filename_buffer (pp, "");

			/* We need to extract the filename. */
			expect_filename = TRUE;
			start_filename = line_pos_next;
		}
		/* TeX is closing a file.
		 * If this filename was pushed on the stack by the reliable ":<+-"
		 * method, don't pop, a ":<-" will follow. This helps in preventing
		 * unbalanced ')' from popping filenames from the stack too soon.
		 */
		else if (cur_char == ')' && pp->priv->stack_files != NULL)
		{
			File *file = pp->priv->stack_files->data;

			if (!file->reliable)
			{
				pop_file_from_stack (pp);
			}
		}
	}
}

static void
update_stack_file (LatexilaPostProcessorLatex *pp,
		   const gchar                *line)
{
	static GRegex *regex_file_pop = NULL;
	GError *error = NULL;

	if (G_UNLIKELY (regex_file_pop == NULL))
	{
		regex_file_pop = g_regex_new ("(\\) )?:<-$",
					      G_REGEX_OPTIMIZE,
					      0,
					      &error);

		if (error != NULL)
		{
			g_warning ("PostProcessorLatex: %s", error->message);
			g_error_free (error);
			return;
		}
	}

	switch (pp->priv->state)
	{
		case STATE_START:
		case STATE_FILENAME_HEURISTIC:
			/* TeX is opening a file. */
			if (g_str_has_prefix (line, ":<+ "))
			{
				gchar *filename;

				filename = g_strdup (line + 4);
				g_strstrip (filename);

				set_filename_buffer (pp, filename);
				g_free (filename);

				pp->priv->state = STATE_FILENAME;
			}
			/* TeX closed a file. */
			else if (g_regex_match (regex_file_pop, line, 0, NULL) ||
				 g_str_has_prefix (line, ":<-"))
			{
				pop_file_from_stack (pp);
			}
			/* Fallback to the heuristic detection of filenames. */
			else
			{
				update_stack_file_heuristic (pp, line);
			}
			break;

		case STATE_FILENAME:
			/* The partial filename was followed by '(', this means that TeX is
			 * signalling it is opening the file. We are sure the filename is
			 * complete now. Don't call update_stack_file_heuristic()
			 * since we don't want the filename on the stack twice.
			 */
			if (line[0] == '(' ||
			    g_str_has_prefix (line, "\\openout"))
			{
				push_file_on_stack (pp, TRUE);
				pp->priv->state = STATE_START;
			}
			/* The partial filename was followed by a TeX error, meaning the
			 * file doesn't exist. Don't push it on the stack, instead try to
			 * detect the error.
			 */
			else if (line[0] == '!')
			{
				pp->priv->state = STATE_START;
				detect_error (pp, line);
			}
			else if (g_str_has_prefix (line, "No file"))
			{
				pp->priv->state = STATE_START;
				detect_warning (pp, line);
			}
			/* The filename is not complete. */
			else
			{
				gchar *line_stripped = g_strdup (line);
				g_strstrip (line_stripped);
				append_to_filename_buffer (pp, line_stripped);
				g_free (line_stripped);
			}
			break;

		case STATE_BADBOX:
		case STATE_WARNING:
		case STATE_ERROR:
		case STATE_ERROR_SEARCH_LINE:
		default:
			g_return_if_reached ();
	}
}

static void
latexila_post_processor_latex_process_line (LatexilaPostProcessor *post_processor,
					    gchar                 *line)
{
	LatexilaPostProcessorLatex *pp = LATEXILA_POST_PROCESSOR_LATEX (post_processor);

	if (line == NULL)
	{
		return;
	}

	if (pp->priv->state != STATE_START)
	{
		append_to_line_buffer (pp, line);
	}

	switch (pp->priv->state)
	{
		case STATE_START:
			if (line[0] == '\0')
			{
				break;
			}

			if (!(detect_badbox (pp, line) ||
			      detect_warning (pp, line) ||
			      detect_error (pp, line) ||
			      detect_other (pp, line)))
			{
				update_stack_file (pp, line);
			}
			break;

		case STATE_BADBOX:
			detect_badbox (pp, line);
			break;

		case STATE_WARNING:
			detect_warning (pp, line);
			break;

		case STATE_ERROR:
		case STATE_ERROR_SEARCH_LINE:
			detect_error (pp, line);
			break;

		case STATE_FILENAME:
		case STATE_FILENAME_HEURISTIC:
			update_stack_file (pp, line);
			break;

		default:
			g_return_if_reached ();
	}

	g_free (line);
}

static const GList *
latexila_post_processor_latex_get_messages (LatexilaPostProcessor *post_processor,
					    gboolean               show_details)
{
	LatexilaPostProcessorLatex *pp = LATEXILA_POST_PROCESSOR_LATEX (post_processor);

	return pp->priv->messages != NULL ? pp->priv->messages->head : NULL;
}

static GQueue *
latexila_post_processor_latex_take_messages (LatexilaPostProcessor *post_processor)
{
	LatexilaPostProcessorLatex *pp = LATEXILA_POST_PROCESSOR_LATEX (post_processor);
	GQueue *ret;

	ret = pp->priv->messages;
	pp->priv->messages = NULL;

	return ret;
}

static void
latexila_post_processor_latex_finalize (GObject *object)
{
	LatexilaPostProcessorLatex *pp = LATEXILA_POST_PROCESSOR_LATEX (object);

	if (pp->priv->messages != NULL)
	{
		g_queue_free_full (pp->priv->messages, (GDestroyNotify) latexila_build_msg_free);
	}

	if (pp->priv->cur_msg != NULL)
	{
		latexila_build_msg_free (pp->priv->cur_msg);
	}

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

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

	g_slist_free_full (pp->priv->stack_files, (GDestroyNotify) file_free);

	g_free (pp->priv->directory_path);

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

static void
latexila_post_processor_latex_class_init (LatexilaPostProcessorLatexClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	LatexilaPostProcessorClass *pp_class = LATEXILA_POST_PROCESSOR_CLASS (klass);

	object_class->finalize = latexila_post_processor_latex_finalize;

	pp_class->start = latexila_post_processor_latex_start;
	pp_class->end = latexila_post_processor_latex_end;
	pp_class->process_line = latexila_post_processor_latex_process_line;
	pp_class->get_messages = latexila_post_processor_latex_get_messages;
	pp_class->take_messages = latexila_post_processor_latex_take_messages;
}

static void
latexila_post_processor_latex_init (LatexilaPostProcessorLatex *pp)
{
	pp->priv = latexila_post_processor_latex_get_instance_private (pp);

	pp->priv->messages = g_queue_new ();
	pp->priv->cur_msg = latexila_build_msg_new ();
	pp->priv->state = STATE_START;
}

/**
 * latexila_post_processor_latex_new:
 *
 * Returns: a new #LatexilaPostProcessorLatex object.
 */
LatexilaPostProcessor *
latexila_post_processor_latex_new (void)
{
	return g_object_new (LATEXILA_TYPE_POST_PROCESSOR_LATEX, NULL);
}

/**
 * latexila_post_processor_latex_get_errors_count:
 * @pp: a #LatexilaPostProcessorLatex.
 *
 * Returns: the number of errors.
 */
gint
latexila_post_processor_latex_get_errors_count (LatexilaPostProcessorLatex *pp)
{
	g_return_val_if_fail (LATEXILA_IS_POST_PROCESSOR_LATEX (pp), 0);

	return pp->priv->errors_count;
}
