/*
 * 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-latexmk
 * @title: LatexilaPostProcessorLatexmk
 * @short_description: latexmk post-processor
 *
 * A post-processor used for the latexmk command. Internally, this
 * post-processor uses #LatexilaPostProcessorLatex.
 */

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

typedef enum
{
	/* Try to find something like:
	 * ------------
	 * Run number 1 of rule 'pdflatex'
	 * ------------
	 */
	STATE_SUB_TITLE_START,
	STATE_SUB_TITLE_END,

	/* Try to find something like:
	 * ------------
	 * Running 'pdflatex  -synctex=1 -recorder  "test.tex"'
	 * ------------
	 */
	STATE_SUB_COMMAND_START,
	STATE_SUB_COMMAND_END,

	/* Fetch the sub-command output. */
	STATE_SUB_COMMAND_OUTPUT_START,
	STATE_SUB_COMMAND_OUTPUT_IN,

	/* Fetch the Latexmk messages after a command output. */
	STATE_LATEXMK_MESSAGES,

	/* If no sub-titles have been found, display almost all output. */
	STATE_FALLBACK_START,
	STATE_FALLBACK_IN
} State;

struct _LatexilaPostProcessorLatexmkPrivate
{
	/* The tree of all messages. */
	GQueue *messages;

	State state;

	/* Number of separators encountered for the current state. */
	gint separator_count;

	/* We run the 'latex' post-processor only on the last latex or pdflatex rule.
	 * The first latex rules most probably have warnings that are fixed in the
	 * last latex rule.
	 * @last_latex_sub_command and @last_latex_messages are simple pointers inside
	 * @messages, so don't free them.
	 */
	LatexilaBuildMsg *last_latex_sub_command;
	GQueue *last_latex_lines;
	GList *last_latex_messages;

	GFile *file;

	GQueue *all_lines;

	guint last_rule_is_latex_rule : 1;
	guint store_all_lines : 1;
};

G_DEFINE_TYPE_WITH_PRIVATE (LatexilaPostProcessorLatexmk,
			    latexila_post_processor_latexmk,
			    LATEXILA_TYPE_POST_PROCESSOR)

static void
latexila_post_processor_latexmk_start (LatexilaPostProcessor *post_processor,
				       GFile                 *file)
{
	LatexilaPostProcessorLatexmk *pp = LATEXILA_POST_PROCESSOR_LATEXMK (post_processor);

	g_clear_object (&pp->priv->file);
	pp->priv->file = g_object_ref (file);
}

static void
add_top_level_message (LatexilaPostProcessorLatexmk *pp,
		       LatexilaBuildMsg             *msg)
{
	g_assert (msg != NULL);

	msg->type = LATEXILA_BUILD_MSG_TYPE_JOB_SUB_COMMAND;

	/* Do not expand the row, so the user has first a global view of what have
	 * been executed.
	 */
	msg->expand = FALSE;

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

	pp->priv->store_all_lines = FALSE;
}

static void
add_sub_message (LatexilaPostProcessorLatexmk *pp,
		 LatexilaBuildMsg             *sub_msg)
{
	LatexilaBuildMsg *last_toplevel_msg;

	g_assert (sub_msg != NULL);

	last_toplevel_msg = g_queue_peek_tail (pp->priv->messages);

	if (last_toplevel_msg == NULL)
	{
		g_warning ("PostProcessorLatexmk: try to add a sub-message without a top-level message.");
		latexila_build_msg_free (sub_msg);
		return;
	}

	sub_msg->type = LATEXILA_BUILD_MSG_TYPE_INFO;

	if (last_toplevel_msg->children == NULL)
	{
		last_toplevel_msg->children = g_queue_new ();
	}

	g_queue_push_tail (last_toplevel_msg->children, sub_msg);
}

static void
set_last_latex_sub_command (LatexilaPostProcessorLatexmk *pp,
			    LatexilaBuildMsg             *msg)
{
	if (pp->priv->last_latex_lines != NULL)
	{
		g_queue_free_full (pp->priv->last_latex_lines, g_free);
	}

	pp->priv->last_latex_lines = g_queue_new ();
	pp->priv->last_latex_sub_command = msg;
}

static gboolean
is_separator (const gchar *line)
{
	return g_str_has_prefix (line, "------------");
}

static void
fetch_sub_title (LatexilaPostProcessorLatexmk *pp,
		 gchar                        *line)
{
	static GRegex *regex_sub_title = NULL;
	GMatchInfo *match_info;

	g_assert (pp->priv->state == STATE_SUB_TITLE_START ||
		  pp->priv->state == STATE_SUB_TITLE_END);

	if (G_UNLIKELY (regex_sub_title == NULL))
	{
		GError *error = NULL;

		regex_sub_title = g_regex_new ("Run number \\d+ of rule '(?P<rule>.*)'",
					       G_REGEX_OPTIMIZE,
					       0,
					       &error);

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

	if (pp->priv->state == STATE_SUB_TITLE_END)
	{
		g_assert (pp->priv->separator_count == 1);

		if (is_separator (line))
		{
			pp->priv->separator_count = 0;
			pp->priv->state = STATE_SUB_COMMAND_START;
		}

		goto end;
	}

	if (is_separator (line))
	{
		pp->priv->separator_count++;

		if (pp->priv->separator_count == 2)
		{
			pp->priv->separator_count = 0;
			g_warning ("PostProcessorLatexmk: fetch sub-title failed, try again.");
		}

		goto end;
	}

	if (pp->priv->separator_count != 1)
	{
		goto end;
	}

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

	if (g_match_info_matches (match_info))
	{
		LatexilaBuildMsg *msg;
		gchar *rule;

		msg = latexila_build_msg_new ();
		msg->text = line;
		line = NULL;

		add_top_level_message (pp, msg);
		pp->priv->state = STATE_SUB_TITLE_END;

		rule = g_match_info_fetch_named (match_info, "rule");

		pp->priv->last_rule_is_latex_rule = (g_strcmp0 (rule, "latex") == 0 ||
						     g_strcmp0 (rule, "pdflatex") == 0);

		if (pp->priv->last_rule_is_latex_rule)
		{
			set_last_latex_sub_command (pp, msg);
		}

		g_free (rule);
	}

	g_match_info_free (match_info);

end:

	if (pp->priv->store_all_lines)
	{
		if (pp->priv->all_lines == NULL)
		{
			pp->priv->all_lines = g_queue_new ();
		}

		g_queue_push_tail (pp->priv->all_lines, line);
	}
	else
	{
		g_free (line);
	}
}

static void
fetch_sub_command (LatexilaPostProcessorLatexmk *pp,
		   gchar                        *line)
{
	static GRegex *regex_sub_command = NULL;
	GMatchInfo *match_info;

	g_assert (pp->priv->state == STATE_SUB_COMMAND_START ||
		  pp->priv->state == STATE_SUB_COMMAND_END);

	if (G_UNLIKELY (regex_sub_command == NULL))
	{
		GError *error = NULL;

		regex_sub_command = g_regex_new ("Running '(?P<command>.*)'",
						 G_REGEX_OPTIMIZE,
						 0,
						 &error);

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

	if (pp->priv->state == STATE_SUB_COMMAND_END)
	{
		g_assert (pp->priv->separator_count == 1);

		if (is_separator (line))
		{
			pp->priv->separator_count = 0;
			pp->priv->state = STATE_SUB_COMMAND_OUTPUT_START;
		}

		goto end;
	}

	if (is_separator (line))
	{
		pp->priv->separator_count++;

		if (pp->priv->separator_count == 2)
		{
			pp->priv->separator_count = 0;
			g_warning ("PostProcessorLatexmk: fetch sub-command failed, try again.");
		}

		goto end;
	}

	if (pp->priv->separator_count != 1)
	{
		goto end;
	}

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

	if (g_match_info_matches (match_info))
	{
		LatexilaBuildMsg *msg;
		gchar *command;

		msg = latexila_build_msg_new ();

		command = g_match_info_fetch_named (match_info, "command");
		msg->text = g_strdup_printf ("$ %s", command);
		g_free (command);

		add_sub_message (pp, msg);
		pp->priv->state = STATE_SUB_COMMAND_END;
	}

	g_match_info_free (match_info);

end:
	g_free (line);
}

static void
fetch_latexmk_messages (LatexilaPostProcessorLatexmk *pp,
			gchar                        *line)
{
	LatexilaBuildMsg *msg;

	g_assert (pp->priv->state == STATE_LATEXMK_MESSAGES);

	if (is_separator (line))
	{
		pp->priv->state = STATE_SUB_TITLE_START;
		fetch_sub_title (pp, line);
		return;
	}

	msg = latexila_build_msg_new ();
	msg->text = line;
	add_sub_message (pp, msg);
}

static gboolean
sub_command_output_matches_start_latexmk_message (const gchar *line)
{
	static GRegex *regex_for_rule = NULL;

	if (G_UNLIKELY (regex_for_rule == NULL))
	{
		GError *error = NULL;

		regex_for_rule = g_regex_new ("^For rule '.*', running",
					      G_REGEX_OPTIMIZE,
					      0,
					      &error);

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

	return (g_str_has_prefix (line, "Latexmk: applying rule") ||
		g_regex_match (regex_for_rule, line, 0, NULL));
}

static void
fetch_sub_command_output (LatexilaPostProcessorLatexmk *pp,
			  gchar                        *line)
{
	static GRegex *regex_rule = NULL;

	g_assert (pp->priv->state == STATE_SUB_COMMAND_OUTPUT_START ||
		  pp->priv->state == STATE_SUB_COMMAND_OUTPUT_IN);

	if (G_UNLIKELY (regex_rule == NULL))
	{
		GError *error = NULL;

		regex_rule = g_regex_new ("^Rule '.*':",
					  G_REGEX_OPTIMIZE,
					  0,
					  &error);

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

	if (pp->priv->state == STATE_SUB_COMMAND_OUTPUT_START)
	{
		if (sub_command_output_matches_start_latexmk_message (line))
		{
			goto end;
		}

		pp->priv->state = STATE_SUB_COMMAND_OUTPUT_IN;
	}

	if (!sub_command_output_matches_start_latexmk_message (line) &&
	    (g_str_has_prefix (line, "Latexmk:") ||
	     g_regex_match (regex_rule, line, 0, NULL)))
	{
		LatexilaBuildMsg *msg;

		pp->priv->state = STATE_LATEXMK_MESSAGES;

		msg = latexila_build_msg_new ();
		/* The latex command output is in English, so for consistency no need to
		 * translate this.
		 */
		msg->text = g_strdup ("Latexmk messages");
		add_top_level_message (pp, msg);

		fetch_latexmk_messages (pp, line);
		return;
	}

	/* If the rule is latex or pdflatex, we store the output. */
	if (pp->priv->last_rule_is_latex_rule)
	{
		g_queue_push_tail (pp->priv->last_latex_lines, line);
		line = NULL;
	}
	/* If it's another rule (bibtex, makeindex, etc), we show all output. */
	else
	{
		LatexilaBuildMsg *msg;

		msg = latexila_build_msg_new ();
		msg->text = line;
		line = NULL;

		add_sub_message (pp, msg);
	}

end:
	g_free (line);
}

static void
process_line_fallback (LatexilaPostProcessorLatexmk *pp,
		       gchar                        *line)
{
	g_assert (pp->priv->state == STATE_FALLBACK_START ||
		  pp->priv->state == STATE_FALLBACK_IN);

	if (pp->priv->state == STATE_FALLBACK_START &&
	    !g_str_has_prefix (line, "Latexmk: This is Latexmk") &&
	    !g_str_has_prefix (line, "**** Report bugs"))
	{
		pp->priv->state = STATE_FALLBACK_IN;
	}

	if (pp->priv->state == STATE_FALLBACK_IN)
	{
		LatexilaBuildMsg *msg;

		msg = latexila_build_msg_new ();
		msg->type = LATEXILA_BUILD_MSG_TYPE_INFO;
		msg->text = line;
		line = NULL;

		g_queue_push_tail (pp->priv->messages, msg);
	}

	g_free (line);
}

static void
latexila_post_processor_latexmk_process_line (LatexilaPostProcessor *post_processor,
					      gchar                 *line)
{
	LatexilaPostProcessorLatexmk *pp = LATEXILA_POST_PROCESSOR_LATEXMK (post_processor);

	switch (pp->priv->state)
	{
		case STATE_SUB_TITLE_START:
		case STATE_SUB_TITLE_END:
			fetch_sub_title (pp, line);
			break;

		case STATE_SUB_COMMAND_START:
		case STATE_SUB_COMMAND_END:
			fetch_sub_command (pp, line);
			break;

		case STATE_SUB_COMMAND_OUTPUT_START:
		case STATE_SUB_COMMAND_OUTPUT_IN:
			fetch_sub_command_output (pp, line);
			break;

		case STATE_LATEXMK_MESSAGES:
			fetch_latexmk_messages (pp, line);
			break;

		case STATE_FALLBACK_START:
		case STATE_FALLBACK_IN:
			process_line_fallback (pp, line);
			break;

		default:
			g_return_if_reached ();
	}
}

static void
run_latex_post_processor (LatexilaPostProcessorLatexmk *pp,
			  gboolean                      succeeded)
{
	LatexilaPostProcessor *pp_latex;
	LatexilaBuildMsg *msg;
	GQueue *initial_children;
	GList *l;
	gboolean has_details;
	gint latex_errors_count;

	g_assert (pp->priv->last_latex_sub_command != NULL);
	g_assert (pp->priv->last_latex_lines != NULL);

	pp_latex = latexila_post_processor_latex_new ();

	latexila_post_processor_start (pp_latex, pp->priv->file);

	for (l = pp->priv->last_latex_lines->head; l != NULL; l = l->next)
	{
		latexila_post_processor_process_line (pp_latex, l->data);
	}

	g_queue_free (pp->priv->last_latex_lines);
	pp->priv->last_latex_lines = NULL;

	latexila_post_processor_end (pp_latex, succeeded);

	msg = pp->priv->last_latex_sub_command;

	/* The initial children contain the command, for example. */
	initial_children = msg->children;

	msg->children = latexila_post_processor_take_messages (pp_latex);

	pp->priv->last_latex_messages = msg->children->head;

	if (initial_children != NULL)
	{
		for (l = initial_children->tail; l != NULL; l = l->prev)
		{
			g_queue_push_head (msg->children, l->data);
		}

		g_queue_free (initial_children);
	}

	/* Expand only the last latex command. */
	msg->expand = TRUE;

	/* Set has-details.
	 * Almost all the time, the user wants to see only the latex output.
	 */

	/* Another solution is to know if the last rule was a latex rule, but it
	 * doesn't work well. For example if a BibTeX error occurs, Latexmk run the
	 * latex command a second time, so the last command is latex, but the latex
	 * output has no errors, the error is only in BibTeX.
	 * So it's probably better to check the number of latex errors, as is done
	 * below.
	 */

	latex_errors_count = latexila_post_processor_latex_get_errors_count (LATEXILA_POST_PROCESSOR_LATEX (pp_latex));

	has_details = succeeded || latex_errors_count > 0;
	g_object_set (pp, "has-details", has_details, NULL);

	g_object_unref (pp_latex);
}

static void
process_all_output (LatexilaPostProcessorLatexmk *pp)
{
	LatexilaPostProcessor *post_processor = LATEXILA_POST_PROCESSOR (pp);
	GList *l;

	g_assert (pp->priv->messages->length == 0);
	g_assert (pp->priv->store_all_lines);

	if (pp->priv->all_lines == NULL)
	{
		return;
	}

	pp->priv->state = STATE_FALLBACK_START;
	pp->priv->store_all_lines = FALSE;

	for (l = pp->priv->all_lines->head; l != NULL; l = l->next)
	{
		latexila_post_processor_latexmk_process_line (post_processor, l->data);
	}

	g_queue_free (pp->priv->all_lines);
	pp->priv->all_lines = NULL;
}

static void
latexila_post_processor_latexmk_end (LatexilaPostProcessor *post_processor,
				     gboolean               succeeded)
{
	LatexilaPostProcessorLatexmk *pp = LATEXILA_POST_PROCESSOR_LATEXMK (post_processor);

	if (pp->priv->last_latex_sub_command != NULL)
	{
		run_latex_post_processor (pp, succeeded);
	}

	if (pp->priv->messages->length == 0)
	{
		process_all_output (pp);
	}
}

static const GList *
latexila_post_processor_latexmk_get_messages (LatexilaPostProcessor *post_processor,
					      gboolean               show_details)
{
	LatexilaPostProcessorLatexmk *pp = LATEXILA_POST_PROCESSOR_LATEXMK (post_processor);
	gboolean has_details;

	g_object_get (pp, "has-details", &has_details, NULL);

	if (has_details && !show_details)
	{
		return pp->priv->last_latex_messages;
	}

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

static GQueue *
latexila_post_processor_latexmk_take_messages (LatexilaPostProcessor *post_processor)
{
	LatexilaPostProcessorLatexmk *pp = LATEXILA_POST_PROCESSOR_LATEXMK (post_processor);
	GQueue *ret;

	ret = pp->priv->messages;

	pp->priv->messages = NULL;
	pp->priv->last_latex_messages = NULL;
	pp->priv->last_latex_sub_command = NULL;

	return ret;
}

static void
latexila_post_processor_latexmk_dispose (GObject *object)
{
	LatexilaPostProcessorLatexmk *pp = LATEXILA_POST_PROCESSOR_LATEXMK (object);

	g_clear_object (&pp->priv->file);

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

static void
latexila_post_processor_latexmk_finalize (GObject *object)
{
	LatexilaPostProcessorLatexmk *pp = LATEXILA_POST_PROCESSOR_LATEXMK (object);

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

	if (pp->priv->last_latex_lines != NULL)
	{
		g_queue_free_full (pp->priv->last_latex_lines, g_free);
	}

	if (pp->priv->all_lines != NULL)
	{
		g_queue_free_full (pp->priv->all_lines, g_free);
	}

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

static void
latexila_post_processor_latexmk_class_init (LatexilaPostProcessorLatexmkClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	LatexilaPostProcessorClass *pp_class = LATEXILA_POST_PROCESSOR_CLASS (klass);

	object_class->dispose = latexila_post_processor_latexmk_dispose;
	object_class->finalize = latexila_post_processor_latexmk_finalize;

	pp_class->start = latexila_post_processor_latexmk_start;
	pp_class->process_line = latexila_post_processor_latexmk_process_line;
	pp_class->end = latexila_post_processor_latexmk_end;
	pp_class->get_messages = latexila_post_processor_latexmk_get_messages;
	pp_class->take_messages = latexila_post_processor_latexmk_take_messages;
}

static void
latexila_post_processor_latexmk_init (LatexilaPostProcessorLatexmk *pp)
{
	pp->priv = latexila_post_processor_latexmk_get_instance_private (pp);

	pp->priv->messages = g_queue_new ();
	pp->priv->state = STATE_SUB_TITLE_START;
	pp->priv->store_all_lines = TRUE;
}

/**
 * latexila_post_processor_latexmk_new:
 *
 * Returns: a new #LatexilaPostProcessorLatexmk object.
 */
LatexilaPostProcessor *
latexila_post_processor_latexmk_new (void)
{
	return g_object_new (LATEXILA_TYPE_POST_PROCESSOR_LATEXMK, NULL);
}
