/*
 * 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:build-tool
 * @title: LatexilaBuildTool
 * @short_description: Build tool
 *
 * A build tool. It contains some basic properties: a label, a description, an
 * icon, etc. It contains a list of file extensions for which the build tool can
 * run on. More interestingly, it contains the list of #LatexilaBuildJob's to
 * run. And a list of files to open when the build jobs are successfully run.
 */

#include "latexila-build-tool.h"
#include <string.h>
#include <glib/gi18n.h>
#include "latexila-build-job.h"
#include "latexila-build-view.h"
#include "latexila-utils.h"

struct _LatexilaBuildToolPrivate
{
	gchar *label;
	gchar *description;
	gchar *extensions;
	gchar *icon;
	gchar *files_to_open;
	gchar **files_to_open_split;
	gint id;

	/* A list of LatexilaBuildJob's. */
	GQueue *jobs;

	guint running_tasks_count;

	guint enabled : 1;
};

/* Used for running the build tool. */
typedef struct _TaskData TaskData;
struct _TaskData
{
	GFile *file;
	LatexilaBuildView *build_view;
	GtkTreeIter main_title;

	/* Position in priv->jobs. */
	GList *current_job;

	/* Position in priv->files_to_open_split. */
	gchar **current_file_to_open;

	GtkTreeIter open_file_job_title;

	GSList *job_results;
};

enum
{
	PROP_0,
	PROP_LABEL,
	PROP_DESCRIPTION,
	PROP_EXTENSIONS,
	PROP_ICON,
	PROP_FILES_TO_OPEN,
	PROP_ID,
	PROP_ENABLED
};

G_DEFINE_TYPE_WITH_PRIVATE (LatexilaBuildTool, latexila_build_tool, G_TYPE_OBJECT)

/* Prototypes */
static void run_job (GTask *task);
static void open_file (GTask *task);

static TaskData *
task_data_new (void)
{
	return g_slice_new0 (TaskData);
}

static void
task_data_free (TaskData *data)
{
	if (data != NULL)
	{
		g_clear_object (&data->file);
		g_clear_object (&data->build_view);
		g_slist_free_full (data->job_results, g_object_unref);

		g_slice_free (TaskData, data);
	}
}

static void
latexila_build_tool_get_property (GObject    *object,
				  guint       prop_id,
				  GValue     *value,
				  GParamSpec *pspec)
{
	LatexilaBuildTool *build_tool = LATEXILA_BUILD_TOOL (object);

	switch (prop_id)
	{
		case PROP_LABEL:
			g_value_set_string (value, build_tool->priv->label);
			break;

		case PROP_DESCRIPTION:
			g_value_set_string (value, build_tool->priv->description);
			break;

		case PROP_EXTENSIONS:
			g_value_set_string (value, build_tool->priv->extensions);
			break;

		case PROP_ICON:
			g_value_set_string (value, build_tool->priv->icon);
			break;

		case PROP_FILES_TO_OPEN:
			g_value_set_string (value, build_tool->priv->files_to_open);
			break;

		case PROP_ID:
			g_value_set_int (value, build_tool->priv->id);
			break;

		case PROP_ENABLED:
			g_value_set_boolean (value, build_tool->priv->enabled);
			break;

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

static void
latexila_build_tool_set_property (GObject      *object,
				  guint         prop_id,
				  const GValue *value,
				  GParamSpec   *pspec)
{
	LatexilaBuildTool *build_tool = LATEXILA_BUILD_TOOL (object);

	/* The build tool can not be modified when it is running. */
	g_return_if_fail (build_tool->priv->running_tasks_count == 0);

	switch (prop_id)
	{
		case PROP_LABEL:
			g_free (build_tool->priv->label);
			build_tool->priv->label = g_value_dup_string (value);
			break;

		case PROP_DESCRIPTION:
			g_free (build_tool->priv->description);
			build_tool->priv->description = g_value_dup_string (value);
			break;

		case PROP_EXTENSIONS:
			g_free (build_tool->priv->extensions);
			build_tool->priv->extensions = g_value_dup_string (value);
			break;

		case PROP_ICON:
			g_free (build_tool->priv->icon);
			build_tool->priv->icon = g_value_dup_string (value);
			break;

		case PROP_FILES_TO_OPEN:
			g_free (build_tool->priv->files_to_open);
			build_tool->priv->files_to_open = g_value_dup_string (value);

			g_strfreev (build_tool->priv->files_to_open_split);
			build_tool->priv->files_to_open_split = NULL;
			if (build_tool->priv->files_to_open != NULL)
			{
				build_tool->priv->files_to_open_split = g_strsplit (build_tool->priv->files_to_open, " ", -1);
			}
			break;

		case PROP_ID:
			build_tool->priv->id = g_value_get_int (value);
			break;

		case PROP_ENABLED:
			build_tool->priv->enabled = g_value_get_boolean (value);
			break;

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

static void
latexila_build_tool_dispose (GObject *object)
{
	LatexilaBuildTool *build_tool = LATEXILA_BUILD_TOOL (object);

	if (build_tool->priv->jobs != NULL)
	{
		g_queue_free_full (build_tool->priv->jobs, g_object_unref);
		build_tool->priv->jobs = NULL;
	}

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

static void
latexila_build_tool_finalize (GObject *object)
{
	LatexilaBuildTool *build_tool = LATEXILA_BUILD_TOOL (object);

	g_free (build_tool->priv->label);
	g_free (build_tool->priv->description);
	g_free (build_tool->priv->extensions);
	g_free (build_tool->priv->icon);
	g_free (build_tool->priv->files_to_open);
	g_strfreev (build_tool->priv->files_to_open_split);

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

static void
latexila_build_tool_class_init (LatexilaBuildToolClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->get_property = latexila_build_tool_get_property;
	object_class->set_property = latexila_build_tool_set_property;
	object_class->dispose = latexila_build_tool_dispose;
	object_class->finalize = latexila_build_tool_finalize;

	g_object_class_install_property (object_class,
					 PROP_LABEL,
					 g_param_spec_string ("label",
							      "Label",
							      "",
							      NULL,
							      G_PARAM_READWRITE |
							      G_PARAM_CONSTRUCT |
							      G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (object_class,
					 PROP_DESCRIPTION,
					 g_param_spec_string ("description",
							      "Description",
							      "",
							      NULL,
							      G_PARAM_READWRITE |
							      G_PARAM_CONSTRUCT |
							      G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (object_class,
					 PROP_EXTENSIONS,
					 g_param_spec_string ("extensions",
							      "Extensions",
							      "",
							      NULL,
							      G_PARAM_READWRITE |
							      G_PARAM_CONSTRUCT |
							      G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (object_class,
					 PROP_ICON,
					 g_param_spec_string ("icon",
							      "Icon",
							      "",
							      NULL,
							      G_PARAM_READWRITE |
							      G_PARAM_CONSTRUCT |
							      G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (object_class,
					 PROP_FILES_TO_OPEN,
					 g_param_spec_string ("files-to-open",
							      "Files to open",
							      "",
							      NULL,
							      G_PARAM_READWRITE |
							      G_PARAM_CONSTRUCT |
							      G_PARAM_STATIC_STRINGS));

	/**
	 * LatexilaBuildTool:id:
	 *
	 * The build tool ID. It is used only by the default build tools, for saving
	 * in #GSettings the lists of enabled/disabled build tools.
	 */
	g_object_class_install_property (object_class,
					 PROP_ID,
					 g_param_spec_int ("id",
							   "ID",
							   "",
							   0,
							   G_MAXINT,
							   0,
							   G_PARAM_READWRITE |
							   G_PARAM_CONSTRUCT |
							   G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (object_class,
					 PROP_ENABLED,
					 g_param_spec_boolean ("enabled",
							       "Enabled",
							       "",
							       FALSE,
							       G_PARAM_READWRITE |
							       G_PARAM_CONSTRUCT |
							       G_PARAM_STATIC_STRINGS));
}

static void
latexila_build_tool_init (LatexilaBuildTool *self)
{
	self->priv = latexila_build_tool_get_instance_private (self);

	self->priv->jobs = g_queue_new ();
}

/**
 * latexila_build_tool_new:
 *
 * Returns: a new #LatexilaBuildTool object.
 */
LatexilaBuildTool *
latexila_build_tool_new (void)
{
	return g_object_new (LATEXILA_TYPE_BUILD_TOOL, NULL);
}

/**
 * latexila_build_tool_clone:
 * @build_tool: the build tool to clone.
 *
 * Clones a build tool (deep copy).
 *
 * Returns: (transfer full): the cloned build tool.
 */
LatexilaBuildTool *
latexila_build_tool_clone (LatexilaBuildTool *build_tool)
{
	LatexilaBuildTool *new_build_tool;
	GList *l;

	g_return_val_if_fail (LATEXILA_IS_BUILD_TOOL (build_tool), NULL);

	new_build_tool = g_object_new (LATEXILA_TYPE_BUILD_TOOL,
				       "label", build_tool->priv->label,
				       "description", build_tool->priv->description,
				       "extensions", build_tool->priv->extensions,
				       "icon", build_tool->priv->icon,
				       "files-to-open", build_tool->priv->files_to_open,
				       "enabled", build_tool->priv->enabled,
				       "id", build_tool->priv->id,
				       NULL);

	for (l = build_tool->priv->jobs->head; l != NULL; l = l->next)
	{
		LatexilaBuildJob *build_job = l->data;
		LatexilaBuildJob *new_build_job = latexila_build_job_clone (build_job);

		latexila_build_tool_add_job (new_build_tool, new_build_job);
		g_object_unref (new_build_job);
	}

	return new_build_tool;
}

/**
 * latexila_build_tool_get_description:
 * @build_tool: a #LatexilaBuildTool.
 *
 * Gets the description. The label is returned if the description is empty.
 *
 * Returns: the description.
 */
const gchar *
latexila_build_tool_get_description (LatexilaBuildTool *build_tool)
{
	if (build_tool->priv->description == NULL ||
	    build_tool->priv->description[0] == '\0')
	{
		return build_tool->priv->label;
	}

	return build_tool->priv->description;
}

/**
 * latexila_build_tool_add_job:
 * @build_tool: a #LatexilaBuildTool.
 * @build_job: a #LatexilaBuildJob.
 *
 * Adds a build job at the end of the list (in O(1)).
 */
void
latexila_build_tool_add_job (LatexilaBuildTool *build_tool,
			     LatexilaBuildJob  *build_job)
{
	g_return_if_fail (LATEXILA_IS_BUILD_TOOL (build_tool));
	g_return_if_fail (LATEXILA_IS_BUILD_JOB (build_job));

	/* The build tool can not be modified when it is running. */
	g_return_if_fail (build_tool->priv->running_tasks_count == 0);

	g_queue_push_tail (build_tool->priv->jobs, build_job);
	g_object_ref (build_job);
}

/**
 * latexila_build_tool_get_jobs:
 * @build_tool: a #LatexilaBuildTool.
 *
 * Returns: (element-type LatexilaBuildJob) (transfer none): the list of
 * #LatexilaBuildJob's.
 */
GList *
latexila_build_tool_get_jobs (LatexilaBuildTool *build_tool)
{
	g_return_val_if_fail (LATEXILA_IS_BUILD_TOOL (build_tool), NULL);

	return build_tool->priv->jobs->head;
}

/**
 * latexila_build_tool_to_xml:
 * @tool: a #LatexilaBuildTool object.
 *
 * Returns: the XML contents of the build tool. Free with g_free().
 */
gchar *
latexila_build_tool_to_xml (LatexilaBuildTool *tool)
{
	GString *contents;
	gchar *escaped_text;
	GList *l;

	g_return_val_if_fail (LATEXILA_IS_BUILD_TOOL (tool), NULL);

	contents = g_string_new (NULL);

	g_string_append_printf (contents,
				"\n  <tool enabled=\"%s\" extensions=\"%s\" icon=\"%s\">\n",
				tool->priv->enabled ? "true" : "false",
				tool->priv->extensions != NULL ? tool->priv->extensions : "",
				tool->priv->icon != NULL ? tool->priv->icon : "");

	escaped_text = g_markup_printf_escaped ("    <label>%s</label>\n"
						"    <description>%s</description>\n",
						tool->priv->label != NULL ? tool->priv->label : "",
						tool->priv->description != NULL ? tool->priv->description : "");

	g_string_append (contents, escaped_text);
	g_free (escaped_text);

	for (l = tool->priv->jobs->head; l != NULL; l = l->next)
	{
		LatexilaBuildJob *job = l->data;

		escaped_text = latexila_build_job_to_xml (job);
		g_string_append (contents, escaped_text);
		g_free (escaped_text);
	}

	escaped_text = g_markup_printf_escaped ("    <open>%s</open>\n",
						tool->priv->files_to_open != NULL ? tool->priv->files_to_open : "");
	g_string_append (contents, escaped_text);
	g_free (escaped_text);

	g_string_append (contents, "  </tool>\n");

	return g_string_free (contents, FALSE);
}

static void
failed (GTask *task)
{
	TaskData *data = g_task_get_task_data (task);
	GCancellable *cancellable;
	LatexilaBuildState state;

	cancellable = g_task_get_cancellable (task);
	if (g_cancellable_is_cancelled (cancellable))
	{
		state = LATEXILA_BUILD_STATE_ABORTED;
	}
	else
	{
		state = LATEXILA_BUILD_STATE_FAILED;
	}

	latexila_build_view_set_title_state (data->build_view,
					     &data->main_title,
					     state);

	g_task_return_boolean (task, FALSE);
	g_object_unref (task);
}

static void
query_exists_cb (GFile        *file,
		 GAsyncResult *result,
		 GTask        *task)
{
	TaskData *data = g_task_get_task_data (task);
	gboolean file_exists;
	GCancellable *cancellable;
	gchar *uri = NULL;
	GError *error = NULL;

	file_exists = latexila_utils_file_query_exists_finish (file, result);

	cancellable = g_task_get_cancellable (task);
	if (g_cancellable_is_cancelled (cancellable))
	{
		latexila_build_view_set_title_state (data->build_view,
						     &data->open_file_job_title,
						     LATEXILA_BUILD_STATE_ABORTED);
		failed (task);
		goto out;
	}

	uri = g_file_get_uri (file);

	if (!file_exists)
	{
		LatexilaBuildMsg *msg;

		latexila_build_view_set_title_state (data->build_view,
						     &data->open_file_job_title,
						     LATEXILA_BUILD_STATE_FAILED);

		msg = latexila_build_msg_new ();
		msg->text = g_strdup_printf (_ ("The file '%s' doesn't exist."), uri);
		msg->type = LATEXILA_BUILD_MSG_TYPE_ERROR;

		latexila_build_view_append_single_message (data->build_view,
							   &data->open_file_job_title,
							   msg);

		latexila_build_msg_free (msg);
		failed (task);
		goto out;
	}

	/* Show URI */

	latexila_utils_show_uri (GTK_WIDGET (data->build_view),
				 uri,
				 GDK_CURRENT_TIME,
				 &error);

	if (error != NULL)
	{
		LatexilaBuildMsg *msg;

		latexila_build_view_set_title_state (data->build_view,
						     &data->open_file_job_title,
						     LATEXILA_BUILD_STATE_FAILED);

		msg = latexila_build_msg_new ();
		msg->text = g_strdup_printf (_ ("Failed to open '%s':"), uri);
		msg->type = LATEXILA_BUILD_MSG_TYPE_ERROR;

		latexila_build_view_append_single_message (data->build_view,
							   &data->open_file_job_title,
							   msg);

		g_free (msg->text);
		msg->text = g_strdup (error->message);
		msg->type = LATEXILA_BUILD_MSG_TYPE_INFO;

		latexila_build_view_append_single_message (data->build_view,
							   &data->open_file_job_title,
							   msg);

		latexila_build_msg_free (msg);
		g_error_free (error);

		failed (task);
		goto out;
	}

	latexila_build_view_set_title_state (data->build_view,
					     &data->open_file_job_title,
					     LATEXILA_BUILD_STATE_SUCCEEDED);

	data->current_file_to_open++;
	open_file (task);

out:
	g_object_unref (file);
	g_free (uri);
}

static void
open_file (GTask *task)
{
	TaskData *data = g_task_get_task_data (task);
	const gchar *file_to_open;
	gchar *filename;
	gchar *filename_for_display;
	gchar *shortname;
	gchar *shortname_for_display;
	gchar *uri;
	gchar *uri_for_display;
	gchar *basename;
	gchar *message;
	GFile *file;

	while (TRUE)
	{
		if (data->current_file_to_open == NULL ||
		    data->current_file_to_open[0] == NULL)
		{
			/* Finished */
			latexila_build_view_set_title_state (data->build_view,
							     &data->main_title,
							     LATEXILA_BUILD_STATE_SUCCEEDED);

			g_task_return_boolean (task, TRUE);
			g_object_unref (task);
			return;
		}

		/* Check if the file to open is an empty string. It happens if there are
		 * two contiguous spaces in priv->files_to_open for example.
		 */
		if (data->current_file_to_open[0][0] == '\0')
		{
			data->current_file_to_open++;
		}
		else
		{
			break;
		}
	}

	file_to_open = data->current_file_to_open[0];

	/* Replace placeholders */

	filename = g_file_get_uri (data->file);
	filename_for_display = g_file_get_parse_name (data->file);

	shortname = latexila_utils_get_shortname (filename);
	shortname_for_display = latexila_utils_get_shortname (filename_for_display);

	if (strstr (file_to_open, "$filename") != NULL)
	{
		uri = latexila_utils_str_replace (file_to_open, "$filename", filename);
		uri_for_display = latexila_utils_str_replace (file_to_open, "$filename", filename_for_display);
	}
	else if (strstr (file_to_open, "$shortname") != NULL)
	{
		uri = latexila_utils_str_replace (file_to_open, "$shortname", shortname);
		uri_for_display = latexila_utils_str_replace (file_to_open, "$shortname", shortname_for_display);
	}
	else
	{
		uri = g_strdup_printf ("file://%s", file_to_open);
		uri_for_display = g_strdup (file_to_open);
	}

	/* Add job title in the build view */

	basename = g_path_get_basename (uri_for_display);
	message = g_strdup_printf (_ ("Open %s"), basename);

	data->open_file_job_title = latexila_build_view_add_job_title (data->build_view,
								       message,
								       LATEXILA_BUILD_STATE_RUNNING);

	/* Check if the file exists */

	file = g_file_new_for_uri (uri);

	latexila_utils_file_query_exists_async (file,
						g_task_get_cancellable (task),
						(GAsyncReadyCallback) query_exists_cb,
						task);

	g_free (filename);
	g_free (filename_for_display);
	g_free (shortname);
	g_free (shortname_for_display);
	g_free (uri);
	g_free (uri_for_display);
	g_free (basename);
	g_free (message);
}

static void
open_files (GTask *task)
{
	TaskData *data = g_task_get_task_data (task);
	LatexilaBuildTool *build_tool = g_task_get_source_object (task);

	data->current_file_to_open = build_tool->priv->files_to_open_split;
	open_file (task);
}

static void
run_job_cb (LatexilaBuildJob *build_job,
	    GAsyncResult     *result,
	    GTask            *task)
{
	TaskData *data = g_task_get_task_data (task);
	gboolean success;

	data->job_results = g_slist_prepend (data->job_results,
					     g_object_ref (result));

	success = latexila_build_job_run_finish (build_job, result);

	if (success)
	{
		data->current_job = data->current_job->next;
		run_job (task);
	}
	else
	{
		failed (task);
	}
}

static void
run_job (GTask *task)
{
	TaskData *data = g_task_get_task_data (task);
	LatexilaBuildJob *build_job;

	if (g_task_return_error_if_cancelled (task))
	{
		g_object_unref (task);
		return;
	}

	if (data->current_job == NULL)
	{
		open_files (task);
		return;
	}

	build_job = data->current_job->data;

	latexila_build_job_run_async (build_job,
				      data->file,
				      data->build_view,
				      g_task_get_cancellable (task),
				      (GAsyncReadyCallback) run_job_cb,
				      task);
}

/**
 * latexila_build_tool_run_async:
 * @build_tool: a build tool.
 * @file: a file.
 * @build_view: a build view.
 * @cancellable: a #GCancellable object.
 * @callback: the callback to call when the operation is finished.
 * @user_data: the data to pass to the callback function.
 *
 * Run a build tool on a file with the messages displayed in a build view.
 */
void
latexila_build_tool_run_async (LatexilaBuildTool   *build_tool,
			       GFile               *file,
			       LatexilaBuildView   *build_view,
			       GCancellable        *cancellable,
			       GAsyncReadyCallback  callback,
			       gpointer             user_data)
{
	GTask *task;
	TaskData *data;

	g_return_if_fail (LATEXILA_IS_BUILD_TOOL (build_tool));
	g_return_if_fail (G_IS_FILE (file));
	g_return_if_fail (LATEXILA_IS_BUILD_VIEW (build_view));

	task = g_task_new (build_tool, cancellable, callback, user_data);
	build_tool->priv->running_tasks_count++;

	data = task_data_new ();
	g_task_set_task_data (task, data, (GDestroyNotify) task_data_free);

	data->file = g_object_ref (file);
	data->build_view = g_object_ref (build_view);

	latexila_build_view_clear (build_view);

	data->main_title = latexila_build_view_add_main_title (build_view,
							       build_tool->priv->label,
							       LATEXILA_BUILD_STATE_RUNNING);

	data->current_job = build_tool->priv->jobs->head;
	run_job (task);
}

/**
 * latexila_build_tool_run_finish:
 * @build_tool: a build tool.
 * @result: a #GAsyncResult.
 *
 * Finishes the operation started with latexila_build_tool_run_async().
 *
 * Before calling this function you should keep a reference to @result as long
 * as the build messages are displayed in the build view. @result is needed for
 * example to show/hide some messages when the "More details" button is toggled.
 */
void
latexila_build_tool_run_finish (LatexilaBuildTool *build_tool,
				GAsyncResult      *result)
{
	GTask *task;
	TaskData *data;
	GCancellable *cancellable;

	g_return_if_fail (g_task_is_valid (result, build_tool));

	task = G_TASK (result);
	data = g_task_get_task_data (task);

	cancellable = g_task_get_cancellable (task);
	if (g_cancellable_is_cancelled (cancellable))
	{
		latexila_build_view_set_title_state (data->build_view,
						     &data->main_title,
						     LATEXILA_BUILD_STATE_ABORTED);
	}

	g_task_propagate_boolean (task, NULL);
	build_tool->priv->running_tasks_count--;
}
