/* SPDX-FileCopyrightText: 2005-2007 - Paolo Maggi
 * SPDX-FileCopyrightText: 2007 - Steve Frécinaux
 * SPDX-FileCopyrightText: 2008 - Jesse van den Kieboom
 * SPDX-FileCopyrightText: 2014-2026 - Sébastien Wilmet
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"
#include "gtksourcefileloader.h"
#include <glib/gi18n-lib.h>
#include <gfls/gfls.h>
#include "gtksourcebufferoutputstream.h"

/**
 * SECTION:fileloader
 * @Title: GtkSourceFileLoader
 * @Short_description: Load a file into a GtkSourceBuffer
 * @See_also: #GtkSourceFile, #GtkSourceFileSaver
 *
 * A #GtkSourceFileLoader object permits to load the contents of a #GFile or a
 * #GInputStream into a #GtkSourceBuffer.
 *
 * A file loader should be used only for one load operation, including errors
 * handling. If an error occurs, you can reconfigure the loader and relaunch the
 * operation with gtk_source_file_loader_load_async().
 *
 * Running a #GtkSourceFileLoader is an undoable action for the
 * #GtkSourceBuffer. That is, gtk_source_buffer_begin_not_undoable_action() and
 * gtk_source_buffer_end_not_undoable_action() are called, which delete the
 * undo/redo history.
 *
 * After a file loading, the buffer is reset to the contents provided by the
 * #GFile or #GInputStream, so the buffer is set as “unmodified”, that is,
 * gtk_text_buffer_set_modified() is called with %FALSE. If the contents isn't
 * saved somewhere (for example if you load from stdin), then you should
 * probably call gtk_text_buffer_set_modified() with %TRUE after calling
 * gtk_source_file_loader_load_finish().
 */

#define DEBUG (FALSE)

/* 8 KiB */
#define DEFAULT_EXPECTED_SIZE (8192)

/* 200 MB */
#define DEFAULT_MAX_SIZE (200 * 1000 * 1000)

#define LOADER_QUERY_ATTRIBUTES G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," \
				G_FILE_ATTRIBUTE_STANDARD_TYPE "," \
				G_FILE_ATTRIBUTE_TIME_MODIFIED "," \
				G_FILE_ATTRIBUTE_STANDARD_SIZE "," \
				G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE

struct _GtkSourceFileLoaderPrivate
{
	/* Weak ref to the GtkSourceBuffer. A strong ref could create a
	 * reference cycle in an application. For example a subclass of
	 * GtkSourceBuffer can have a strong ref to the FileLoader.
	 */
	GtkSourceBuffer *source_buffer;

	/* Weak ref to the GtkSourceFile. A strong ref could create a reference
	 * cycle in an application. For example a subclass of GtkSourceFile can
	 * have a strong ref to the FileLoader.
	 */
	GtkSourceFile *file;

	GFile *location;

	/* The value of the :input-stream property. Do not confuse with the
	 * input_stream field in TaskData.
	 */
	GInputStream *input_stream_property;

	GSList *candidate_encodings;

	guint64 max_size;

	GtkSourceEncoding *auto_detected_encoding;
	GtkSourceNewlineType auto_detected_newline_type;
	GtkSourceCompressionType auto_detected_compression_type;

	guint task_is_running : 1;
};

typedef struct
{
	/* The two streams cannot be spliced directly, because:
	 * (1) We need to call the progress callback.
	 * (2) Sync methods must be used for the output stream, and async
	 *     methods for the input stream.
	 */
	GInputStream *input_stream;
	GtkSourceBufferOutputStream *output_stream;

	GFileInfo *info;

	GFileProgressCallback progress_cb;
	gpointer progress_cb_data;
	GDestroyNotify progress_cb_notify;

	GBytes *content;

	guint tried_mount : 1;
} TaskData;

enum
{
	PROP_0,
	PROP_BUFFER,
	PROP_FILE,
	PROP_LOCATION,
	PROP_INPUT_STREAM,
	PROP_MAX_SIZE,
	N_PROPERTIES
};

static GParamSpec *properties[N_PROPERTIES];

G_DEFINE_TYPE_WITH_PRIVATE (GtkSourceFileLoader, gtk_source_file_loader, G_TYPE_OBJECT)

static void open_file (GTask *task);

static TaskData *
task_data_new (void)
{
	return g_new0 (TaskData, 1);
}

static void
task_data_free (gpointer data)
{
	TaskData *task_data = data;

	if (task_data == NULL)
	{
		return;
	}

	g_clear_object (&task_data->input_stream);
	g_clear_object (&task_data->output_stream);
	g_clear_object (&task_data->info);
	g_bytes_unref (task_data->content);

	if (task_data->progress_cb_notify != NULL)
	{
		task_data->progress_cb_notify (task_data->progress_cb_data);
	}

	g_free (task_data);
}

static GtkSourceCompressionType
get_compression_type_from_content_type (const gchar *content_type)
{
	if (content_type == NULL)
	{
		return GTK_SOURCE_COMPRESSION_TYPE_NONE;
	}

	if (g_content_type_is_a (content_type, "application/x-gzip"))
	{
		return GTK_SOURCE_COMPRESSION_TYPE_GZIP;
	}

	return GTK_SOURCE_COMPRESSION_TYPE_NONE;
}

static void
gtk_source_file_loader_get_property (GObject    *object,
				     guint       prop_id,
				     GValue     *value,
				     GParamSpec *pspec)
{
	GtkSourceFileLoader *loader = GTK_SOURCE_FILE_LOADER (object);

	switch (prop_id)
	{
		case PROP_BUFFER:
			g_value_set_object (value, gtk_source_file_loader_get_buffer (loader));
			break;

		case PROP_FILE:
			g_value_set_object (value, gtk_source_file_loader_get_file (loader));
			break;

		case PROP_LOCATION:
			g_value_set_object (value, gtk_source_file_loader_get_location (loader));
			break;

		case PROP_INPUT_STREAM:
			g_value_set_object (value, gtk_source_file_loader_get_input_stream (loader));
			break;

		case PROP_MAX_SIZE:
			g_value_set_uint64 (value, gtk_source_file_loader_get_max_size (loader));
			break;

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

static void
gtk_source_file_loader_set_property (GObject      *object,
				     guint         prop_id,
				     const GValue *value,
				     GParamSpec   *pspec)
{
	GtkSourceFileLoader *loader = GTK_SOURCE_FILE_LOADER (object);

	switch (prop_id)
	{
		case PROP_BUFFER:
			g_assert (loader->priv->source_buffer == NULL);
			g_set_weak_pointer (&loader->priv->source_buffer,
					    g_value_get_object (value));
			break;

		case PROP_FILE:
			g_assert (loader->priv->file == NULL);
			g_set_weak_pointer (&loader->priv->file,
					    g_value_get_object (value));
			break;

		case PROP_LOCATION:
			g_assert (loader->priv->location == NULL);
			loader->priv->location = g_value_dup_object (value);
			break;

		case PROP_INPUT_STREAM:
			g_assert (loader->priv->input_stream_property == NULL);
			loader->priv->input_stream_property = g_value_dup_object (value);
			break;

		case PROP_MAX_SIZE:
			gtk_source_file_loader_set_max_size (loader, g_value_get_uint64 (value));
			break;

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

static void
set_default_candidate_encodings (GtkSourceFileLoader *loader)
{
	GSList *list;
	const GtkSourceEncoding *file_encoding;

	/* Get first the default candidates from GtkSourceEncoding. If the
	 * GtkSourceFile's encoding has been set by a FileLoader or FileSaver,
	 * put it at the beginning of the list.
	 */
	list = gtk_source_encoding_get_default_candidates ();

	if (loader->priv->file == NULL)
	{
		goto end;
	}

	file_encoding = gtk_source_file_get_encoding (loader->priv->file);

	if (file_encoding == NULL)
	{
		goto end;
	}

	list = g_slist_prepend (list, gtk_source_encoding_copy (file_encoding));
	list = gtk_source_encoding_remove_duplicates (list, GTK_SOURCE_ENCODING_DUPLICATES_KEEP_FIRST);

end:
	g_slist_free_full (loader->priv->candidate_encodings, (GDestroyNotify) gtk_source_encoding_free);
	loader->priv->candidate_encodings = list;
}

static void
gtk_source_file_loader_constructed (GObject *object)
{
	GtkSourceFileLoader *loader = GTK_SOURCE_FILE_LOADER (object);

	if (G_OBJECT_CLASS (gtk_source_file_loader_parent_class)->constructed != NULL)
	{
		G_OBJECT_CLASS (gtk_source_file_loader_parent_class)->constructed (object);
	}

	set_default_candidate_encodings (loader);

	if (loader->priv->file != NULL &&
	    loader->priv->location == NULL &&
	    loader->priv->input_stream_property == NULL)
	{
		loader->priv->location = gtk_source_file_get_location (loader->priv->file);

		if (loader->priv->location != NULL)
		{
			g_object_ref (loader->priv->location);
		}
		else
		{
			g_warning ("GtkSourceFileLoader: the GtkSourceFile's location is NULL. "
				   "Call gtk_source_file_set_location() or read from a GInputStream.");
		}
	}
}

static void
gtk_source_file_loader_dispose (GObject *object)
{
	GtkSourceFileLoader *loader = GTK_SOURCE_FILE_LOADER (object);

	g_clear_weak_pointer (&loader->priv->source_buffer);
	g_clear_weak_pointer (&loader->priv->file);
	g_clear_object (&loader->priv->location);
	g_clear_object (&loader->priv->input_stream_property);

	g_slist_free_full (loader->priv->candidate_encodings, (GDestroyNotify) gtk_source_encoding_free);
	loader->priv->candidate_encodings = NULL;

	gtk_source_encoding_free (loader->priv->auto_detected_encoding);
	loader->priv->auto_detected_encoding = NULL;

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

static void
gtk_source_file_loader_class_init (GtkSourceFileLoaderClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->get_property = gtk_source_file_loader_get_property;
	object_class->set_property = gtk_source_file_loader_set_property;
	object_class->constructed = gtk_source_file_loader_constructed;
	object_class->dispose = gtk_source_file_loader_dispose;

	/**
	 * GtkSourceFileLoader:buffer:
	 *
	 * The #GtkSourceBuffer to load the contents into. The
	 * #GtkSourceFileLoader object has a weak reference to the buffer.
	 *
	 * Since: 3.14
	 */
	properties[PROP_BUFFER] =
		g_param_spec_object ("buffer",
				     "buffer",
				     "",
				     GTK_SOURCE_TYPE_BUFFER,
				     G_PARAM_READWRITE |
				     G_PARAM_CONSTRUCT_ONLY |
				     G_PARAM_STATIC_STRINGS);

	/**
	 * GtkSourceFileLoader:file:
	 *
	 * The #GtkSourceFile. The #GtkSourceFileLoader object has a weak
	 * reference to the file.
	 *
	 * Since: 3.14
	 */
	properties[PROP_FILE] =
		g_param_spec_object ("file",
				     "file",
				     "",
				     GTK_SOURCE_TYPE_FILE,
				     G_PARAM_READWRITE |
				     G_PARAM_CONSTRUCT_ONLY |
				     G_PARAM_STATIC_STRINGS);

	/**
	 * GtkSourceFileLoader:location:
	 *
	 * The #GFile to load, or %NULL to load the contents from the
	 * #GtkSourceFileLoader:input-stream.
	 *
	 * If the #GtkSourceFileLoader:input-stream is %NULL, by default the
	 * location is taken from the #GtkSourceFile at construction time.
	 *
	 * Since: 3.14
	 */
	properties[PROP_LOCATION] =
		g_param_spec_object ("location",
				     "location",
				     "",
				     G_TYPE_FILE,
				     G_PARAM_READWRITE |
				     G_PARAM_CONSTRUCT_ONLY |
				     G_PARAM_STATIC_STRINGS);

	/**
	 * GtkSourceFileLoader:input-stream:
	 *
	 * The #GInputStream to load. Useful for reading stdin. If this property
	 * is set, the #GtkSourceFileLoader:location property is ignored.
	 *
	 * Since: 3.14
	 */
	properties[PROP_INPUT_STREAM] =
		g_param_spec_object ("input-stream",
				     "input-stream",
				     "",
				     G_TYPE_INPUT_STREAM,
				     G_PARAM_READWRITE |
				     G_PARAM_CONSTRUCT_ONLY |
				     G_PARAM_STATIC_STRINGS);

	/**
	 * GtkSourceFileLoader:max-size:
	 *
	 * The maximum number of bytes to read.
	 *
	 * If the content to load exceeds the limit,
	 * %GTK_SOURCE_FILE_LOADER_ERROR_TOO_BIG is returned.
	 *
	 * Since: 299.6
	 */
	properties[PROP_MAX_SIZE] =
		g_param_spec_uint64 ("max-size",
				     "max-size",
				     "",
				     0,
				     G_MAXUINT64,
				     DEFAULT_MAX_SIZE,
				     G_PARAM_READWRITE |
				     G_PARAM_CONSTRUCT |
				     G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties (object_class, N_PROPERTIES, properties);
}

static void
gtk_source_file_loader_init (GtkSourceFileLoader *loader)
{
	loader->priv = gtk_source_file_loader_get_instance_private (loader);

	loader->priv->auto_detected_newline_type = GTK_SOURCE_NEWLINE_TYPE_LF;
	loader->priv->auto_detected_compression_type = GTK_SOURCE_COMPRESSION_TYPE_NONE;
}

static void
finish (GTask *task)
{
	GtkSourceFileLoader *loader = GTK_SOURCE_FILE_LOADER (g_task_get_source_object (task));
	TaskData *task_data = g_task_get_task_data (task);
	GError *error = NULL;

#if DEBUG
	g_message ("%s", G_STRFUNC);
#endif

	/* Flush the stream to ensure proper line ending detection. */
	g_output_stream_flush (G_OUTPUT_STREAM (task_data->output_stream), NULL, NULL);

	gtk_source_encoding_free (loader->priv->auto_detected_encoding);
	loader->priv->auto_detected_encoding =
		gtk_source_buffer_output_stream_get_guessed (task_data->output_stream);

	loader->priv->auto_detected_newline_type =
		gtk_source_buffer_output_stream_detect_newline_type (task_data->output_stream);

	g_output_stream_close (G_OUTPUT_STREAM (task_data->output_stream),
			       g_task_get_cancellable (task),
			       &error);

	if (error != NULL)
	{
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	if (gtk_source_buffer_output_stream_get_num_fallbacks (task_data->output_stream) > 0)
	{
		g_task_return_new_error (task,
					 GTK_SOURCE_FILE_LOADER_ERROR,
					 GTK_SOURCE_FILE_LOADER_ERROR_CONVERSION_FALLBACK,
					 _("There was a character encoding conversion error "
					   "and it was needed to use a fallback character."));
		g_object_unref (task);
		return;
	}

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

static void
write_content (GTask *task)
{
	TaskData *task_data = g_task_get_task_data (task);
	GError *error = NULL;

#if DEBUG
	g_message ("%s", G_STRFUNC);
#endif

	/* We use sync methods on the buffer stream since it is in memory. Using
	 * async would be racy and we can end up with invalidated iters.
	 */
	g_output_stream_write_all (G_OUTPUT_STREAM (task_data->output_stream),
				   g_bytes_get_data (task_data->content, NULL),
				   g_bytes_get_size (task_data->content),
				   NULL,
				   g_task_get_cancellable (task),
				   &error);

	g_bytes_unref (task_data->content);
	task_data->content = NULL;

	if (error != NULL)
	{
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	finish (task);
}

static void
read_content_cb (GObject      *source_object,
		 GAsyncResult *result,
		 gpointer      user_data)
{
	GInputStream *input_stream = G_INPUT_STREAM (source_object);
	GTask *task = G_TASK (user_data);
	TaskData *task_data = g_task_get_task_data (task);
	gboolean is_truncated = FALSE;
	GError *error = NULL;

#if DEBUG
	g_message ("%s", G_STRFUNC);
#endif

	g_bytes_unref (task_data->content);
	task_data->content = gfls_input_stream_read_finish (input_stream,
							    result,
							    &is_truncated,
							    &error);

	if (error != NULL)
	{
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	if (is_truncated)
	{
		/* TODO: load the truncated content into the GtkSourceBuffer and
		 * return that info to the caller, in order to show for instance
		 * a warning infobar.
		 * Remember that it has been truncated, to warn the user when
		 * trying to save the GtkSourceBuffer.
		 */

		/* Free ASAP since it's quite big. */
		g_bytes_unref (task_data->content);
		task_data->content = NULL;

		/* TODO: idem as for the other
		 * GTK_SOURCE_FILE_LOADER_ERROR_TOO_BIG error thrown: provide
		 * the size limit information.
		 */
		g_task_return_new_error (task,
					 GTK_SOURCE_FILE_LOADER_ERROR,
					 GTK_SOURCE_FILE_LOADER_ERROR_TOO_BIG,
					 _("Limit on the number of bytes to read reached."));
		g_object_unref (task);
		return;
	}

	write_content (task);
}

/* TODO: we would like to obtain progress infos when loading a stream too, not
 * just a file. A stream lacks the total number of bytes, but it can be useful
 * to know how many bytes have been loaded so far, especially for remote content
 * accessed with a slow network connection.
 *
 * TODO: is the progress callback called a first time with number==0? User code
 * could rely on it to show progress. For remote content with a very long
 * round-trip time, the first progress callback with number > 0 could arrive
 * with a long delay.
 */
static void
read_content_progress_cb (gsize    number,
			  gpointer user_data)
{
	GTask *task = G_TASK (user_data);
	TaskData *task_data = g_task_get_task_data (task);

	g_return_if_fail (task_data->progress_cb != NULL);
	g_return_if_fail (g_file_info_has_attribute (task_data->info, G_FILE_ATTRIBUTE_STANDARD_SIZE));

	task_data->progress_cb (number,
				g_file_info_get_size (task_data->info),
				task_data->progress_cb_data);
}

static void
read_content (GTask *task)
{
	GtkSourceFileLoader *loader = GTK_SOURCE_FILE_LOADER (g_task_get_source_object (task));
	TaskData *task_data = g_task_get_task_data (task);
	gsize expected_size = DEFAULT_EXPECTED_SIZE;
	GflsSimpleProgressCallback my_progress_callback = NULL;
	gpointer my_progress_callback_data = NULL;

#if DEBUG
	g_message ("%s", G_STRFUNC);
#endif

	if (g_file_info_has_attribute (task_data->info, G_FILE_ATTRIBUTE_STANDARD_SIZE))
	{
		expected_size = g_file_info_get_size (task_data->info);

		if (task_data->progress_cb != NULL)
		{
			my_progress_callback = read_content_progress_cb;
			my_progress_callback_data = task;
		}
	}

	gfls_input_stream_read_async (task_data->input_stream,
				      expected_size,
				      MIN (loader->priv->max_size, G_MAXSIZE),
				      g_task_get_priority (task),
				      g_task_get_cancellable (task),
				      my_progress_callback,
				      my_progress_callback_data,
				      read_content_cb,
				      task);
}

static void
add_gzip_decompressor_stream (GTask *task)
{
	TaskData *task_data = g_task_get_task_data (task);
	GZlibDecompressor *decompressor;
	GInputStream *new_input_stream;

#if DEBUG
	g_message ("%s", G_STRFUNC);
#endif

	decompressor = g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP);

	new_input_stream = g_converter_input_stream_new (task_data->input_stream,
							 G_CONVERTER (decompressor));

	g_object_unref (task_data->input_stream);
	g_object_unref (decompressor);

	task_data->input_stream = new_input_stream;
}

static void
setup_input_stream (GTask *task)
{
	GtkSourceFileLoader *loader = GTK_SOURCE_FILE_LOADER (g_task_get_source_object (task));
	TaskData *task_data = g_task_get_task_data (task);

#if DEBUG
	g_message ("%s", G_STRFUNC);
#endif

	/* TODO: support gzip compression from an input stream.
	 * By first loading all the content into memory, then guess if it is
	 * compressed.
	 * Do the decompression on the whole content at once, not with a stream.
	 */
	loader->priv->auto_detected_compression_type = GTK_SOURCE_COMPRESSION_TYPE_NONE;

	if (loader->priv->input_stream_property != NULL)
	{
		task_data->input_stream = g_object_ref (loader->priv->input_stream_property);
	}
	else if (g_file_info_has_attribute (task_data->info, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE))
	{
		const gchar *content_type = g_file_info_get_content_type (task_data->info);

		switch (get_compression_type_from_content_type (content_type))
		{
			case GTK_SOURCE_COMPRESSION_TYPE_GZIP:
				add_gzip_decompressor_stream (task);
				loader->priv->auto_detected_compression_type = GTK_SOURCE_COMPRESSION_TYPE_GZIP;
				break;

			case GTK_SOURCE_COMPRESSION_TYPE_NONE:
				/* NOOP */
				break;

			default:
				g_assert_not_reached ();
		}
	}

	g_return_if_fail (task_data->input_stream != NULL);

	read_content (task);
}

static void
query_info_cb (GObject      *source_object,
	       GAsyncResult *result,
	       gpointer      user_data)
{
	GFile *location = G_FILE (source_object);
	GTask *task = G_TASK (user_data);
	GtkSourceFileLoader *loader = GTK_SOURCE_FILE_LOADER (g_task_get_source_object (task));
	TaskData *task_data = g_task_get_task_data (task);
	GError *error = NULL;

#if DEBUG
	g_message ("%s", G_STRFUNC);
#endif

	g_clear_object (&task_data->info);
	task_data->info = g_file_query_info_finish (location, result, &error);

	if (error != NULL)
	{
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	if (g_file_info_has_attribute (task_data->info, G_FILE_ATTRIBUTE_STANDARD_TYPE) &&
	    g_file_info_get_file_type (task_data->info) != G_FILE_TYPE_REGULAR)
	{
		g_task_return_new_error (task,
					 G_IO_ERROR,
					 G_IO_ERROR_NOT_REGULAR_FILE,
					 _("Not a regular file."));
		g_object_unref (task);
		return;
	}

	if (g_file_info_has_attribute (task_data->info, G_FILE_ATTRIBUTE_STANDARD_SIZE))
	{
		guint64 file_size = g_file_info_get_attribute_uint64 (task_data->info,
								      G_FILE_ATTRIBUTE_STANDARD_SIZE);

		if (file_size > loader->priv->max_size)
		{
			/* TODO: provide in the error message the infos about
			 * the file_size and the max_size limit, in a
			 * human-readable way, using for example MB or MiB
			 * according to what is most suitable.
			 */
			g_task_return_new_error (task,
						 GTK_SOURCE_FILE_LOADER_ERROR,
						 GTK_SOURCE_FILE_LOADER_ERROR_TOO_BIG,
						 _("The size of the file is too big."));
			g_object_unref (task);
			return;
		}
	}

	setup_input_stream (task);
}

static void
mount_cb (GObject      *source_object,
	  GAsyncResult *result,
	  gpointer      user_data)
{
	GFile *location = G_FILE (source_object);
	GTask *task = G_TASK (user_data);
	GError *error = NULL;

#if DEBUG
	g_message ("%s", G_STRFUNC);
#endif

	g_file_mount_enclosing_volume_finish (location, result, &error);

	if (error != NULL)
	{
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	/* Try again to open the file for reading. */
	open_file (task);
}

static void
recover_not_mounted (GTask *task)
{
	GtkSourceFileLoader *loader = GTK_SOURCE_FILE_LOADER (g_task_get_source_object (task));
	TaskData *task_data = g_task_get_task_data (task);
	GMountOperation *mount_operation;

#if DEBUG
	g_message ("%s", G_STRFUNC);
#endif

	mount_operation = _gtk_source_file_create_mount_operation (loader->priv->file);

	task_data->tried_mount = TRUE;

	g_file_mount_enclosing_volume (loader->priv->location,
				       G_MOUNT_MOUNT_NONE,
				       mount_operation,
				       g_task_get_cancellable (task),
				       mount_cb,
				       task);

	g_object_unref (mount_operation);
}

static void
open_file_cb (GObject      *source_object,
	      GAsyncResult *result,
	      gpointer      user_data)
{
	GFile *location = G_FILE (source_object);
	GTask *task = G_TASK (user_data);
	TaskData *task_data = g_task_get_task_data (task);
	GError *error = NULL;

#if DEBUG
	g_message ("%s", G_STRFUNC);
#endif

	g_clear_object (&task_data->input_stream);
	task_data->input_stream = G_INPUT_STREAM (g_file_read_finish (location, result, &error));

	if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_MOUNTED) &&
	    !task_data->tried_mount)
	{
		recover_not_mounted (task);
		g_clear_error (&error);
		return;
	}

	if (error != NULL)
	{
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	/* Get the file info: note that we cannot use
	 * g_file_input_stream_query_info_async() since it is not able to get
	 * the content type etc, beside it is not supported by gvfs. Using the
	 * file instead of the stream is slightly racy, but for loading this is
	 * not too bad...
	 * FIXME: find a better solution, to make it entirely race-free. For
	 * example by retrieving the entity tag (etag) before and after the
	 * operation, and checking that it hasn't changed.
	 */
	g_file_query_info_async (location,
				 LOADER_QUERY_ATTRIBUTES,
				 G_FILE_QUERY_INFO_NONE,
				 g_task_get_priority (task),
				 g_task_get_cancellable (task),
				 query_info_cb,
				 task);
}

static void
open_file (GTask *task)
{
	GtkSourceFileLoader *loader = GTK_SOURCE_FILE_LOADER (g_task_get_source_object (task));

#if DEBUG
	g_message ("%s", G_STRFUNC);
#endif

	g_file_read_async (loader->priv->location,
	                   g_task_get_priority (task),
			   g_task_get_cancellable (task),
	                   open_file_cb,
	                   task);
}

GQuark
gtk_source_file_loader_error_quark (void)
{
	static GQuark quark = 0;

	if (G_UNLIKELY (quark == 0))
	{
		quark = g_quark_from_static_string ("gtk-source-file-loader-error");
	}

	return quark;
}

/**
 * gtk_source_file_loader_new:
 * @buffer: the #GtkSourceBuffer to load the contents into.
 * @file: the #GtkSourceFile.
 *
 * Creates a new #GtkSourceFileLoader object. The contents is read from the
 * #GtkSourceFile's location. If not already done, call
 * gtk_source_file_set_location() before calling this constructor. The previous
 * location is anyway not needed, because as soon as the file loading begins,
 * the @buffer is emptied.
 *
 * Returns: a new #GtkSourceFileLoader object.
 * Since: 3.14
 */
GtkSourceFileLoader *
gtk_source_file_loader_new (GtkSourceBuffer *buffer,
			    GtkSourceFile   *file)
{
	g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), NULL);
	g_return_val_if_fail (GTK_SOURCE_IS_FILE (file), NULL);

	return g_object_new (GTK_SOURCE_TYPE_FILE_LOADER,
			     "buffer", buffer,
			     "file", file,
			     NULL);
}

/**
 * gtk_source_file_loader_new_from_stream:
 * @buffer: the #GtkSourceBuffer to load the contents into.
 * @file: the #GtkSourceFile.
 * @stream: the #GInputStream to load, e.g. stdin.
 *
 * Creates a new #GtkSourceFileLoader object. The contents is read from @stream.
 *
 * Returns: a new #GtkSourceFileLoader object.
 * Since: 3.14
 */
GtkSourceFileLoader *
gtk_source_file_loader_new_from_stream (GtkSourceBuffer *buffer,
					GtkSourceFile   *file,
					GInputStream    *stream)
{
	g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), NULL);
	g_return_val_if_fail (GTK_SOURCE_IS_FILE (file), NULL);
	g_return_val_if_fail (G_IS_INPUT_STREAM (stream), NULL);

	return g_object_new (GTK_SOURCE_TYPE_FILE_LOADER,
			     "buffer", buffer,
			     "file", file,
			     "input-stream", stream,
			     NULL);
}

/**
 * gtk_source_file_loader_set_candidate_encodings:
 * @loader: a #GtkSourceFileLoader.
 * @candidate_encodings: (element-type GtkSourceEncoding): a list of
 *   #GtkSourceEncoding's.
 *
 * Sets the candidate encodings for the file loading. The encodings are tried in
 * the same order as the list.
 *
 * For convenience, @candidate_encodings can contain duplicates. Only the first
 * occurrence of a duplicated encoding is kept in the list.
 *
 * If you don't call this function, the default list will be (in that order):
 * 1. If set, the #GtkSourceFile's encoding as returned by
 *    gtk_source_file_get_encoding().
 * 2. Plus the default candidates as returned by
 *    gtk_source_encoding_get_default_candidates().
 *
 * Since: 3.14
 */
void
gtk_source_file_loader_set_candidate_encodings (GtkSourceFileLoader *loader,
						GSList              *candidate_encodings)
{
	GSList *list;

	g_return_if_fail (GTK_SOURCE_IS_FILE_LOADER (loader));
	g_return_if_fail (!loader->priv->task_is_running);

	list = g_slist_copy_deep (candidate_encodings, gtk_source_encoding_copy_func, NULL);
	list = gtk_source_encoding_remove_duplicates (list, GTK_SOURCE_ENCODING_DUPLICATES_KEEP_FIRST);

	g_slist_free_full (loader->priv->candidate_encodings, (GDestroyNotify) gtk_source_encoding_free);
	loader->priv->candidate_encodings = list;
}

/**
 * gtk_source_file_loader_get_max_size:
 * @loader: a #GtkSourceFileLoader.
 *
 * Returns: the value of the #GtkSourceFileLoader:max-size property.
 * Since: 299.6
 */
guint64
gtk_source_file_loader_get_max_size (GtkSourceFileLoader *loader)
{
	g_return_val_if_fail (GTK_SOURCE_IS_FILE_LOADER (loader), DEFAULT_MAX_SIZE);
	return loader->priv->max_size;
}

/**
 * gtk_source_file_loader_set_max_size:
 * @loader: a #GtkSourceFileLoader.
 * @max_size: the new value.
 *
 * Sets the #GtkSourceFileLoader:max-size property.
 *
 * Since: 299.6
 */
void
gtk_source_file_loader_set_max_size (GtkSourceFileLoader *loader,
				     guint64              max_size)
{
	g_return_if_fail (GTK_SOURCE_IS_FILE_LOADER (loader));
	g_return_if_fail (!loader->priv->task_is_running);

	if (loader->priv->max_size != max_size)
	{
		loader->priv->max_size = max_size;
		g_object_notify_by_pspec (G_OBJECT (loader), properties[PROP_MAX_SIZE]);
	}
}

/**
 * gtk_source_file_loader_get_buffer:
 * @loader: a #GtkSourceFileLoader.
 *
 * Returns: (transfer none) (nullable): the value of the
 *   #GtkSourceFileLoader:buffer property.
 * Since: 3.14
 */
GtkSourceBuffer *
gtk_source_file_loader_get_buffer (GtkSourceFileLoader *loader)
{
	g_return_val_if_fail (GTK_SOURCE_IS_FILE_LOADER (loader), NULL);

	return loader->priv->source_buffer;
}

/**
 * gtk_source_file_loader_get_file:
 * @loader: a #GtkSourceFileLoader.
 *
 * Returns: (transfer none) (nullable): the value of the
 *   #GtkSourceFileLoader:file property.
 * Since: 3.14
 */
GtkSourceFile *
gtk_source_file_loader_get_file (GtkSourceFileLoader *loader)
{
	g_return_val_if_fail (GTK_SOURCE_IS_FILE_LOADER (loader), NULL);

	return loader->priv->file;
}

/**
 * gtk_source_file_loader_get_location:
 * @loader: a #GtkSourceFileLoader.
 *
 * Returns: (transfer none) (nullable): the value of the
 *   #GtkSourceFileLoader:location property.
 * Since: 3.14
 */
GFile *
gtk_source_file_loader_get_location (GtkSourceFileLoader *loader)
{
	g_return_val_if_fail (GTK_SOURCE_IS_FILE_LOADER (loader), NULL);

	return loader->priv->location;
}

/**
 * gtk_source_file_loader_get_input_stream:
 * @loader: a #GtkSourceFileLoader.
 *
 * Returns: (transfer none) (nullable): the value of the
 *   #GtkSourceFileLoader:input-stream property.
 * Since: 3.14
 */
GInputStream *
gtk_source_file_loader_get_input_stream (GtkSourceFileLoader *loader)
{
	g_return_val_if_fail (GTK_SOURCE_IS_FILE_LOADER (loader), NULL);

	return loader->priv->input_stream_property;
}

/**
 * gtk_source_file_loader_load_async:
 * @loader: a #GtkSourceFileLoader.
 * @io_priority: the I/O priority of the request. E.g. %G_PRIORITY_LOW,
 *   %G_PRIORITY_DEFAULT or %G_PRIORITY_HIGH.
 * @cancellable: (nullable): optional #GCancellable object, %NULL to ignore.
 * @progress_callback: (scope notified) (nullable): function to call back with
 *   progress information, or %NULL if progress information is not needed.
 * @progress_callback_data: user data to pass to @progress_callback.
 * @progress_callback_notify: (nullable): function to call on
 *   @progress_callback_data when the @progress_callback is no longer needed, or
 *   %NULL.
 * @callback: (scope async): a #GAsyncReadyCallback to call when the request is
 *   satisfied.
 * @user_data: user data to pass to @callback.
 *
 * Loads asynchronously the file or input stream contents into the
 * #GtkSourceBuffer. See the #GAsyncResult documentation to know how to use this
 * function.
 *
 * Since: 3.14
 */

/* The GDestroyNotify is needed, currently the following bug is not fixed:
 * https://gitlab.gnome.org/GNOME/gobject-introspection/-/issues/25
 */
void
gtk_source_file_loader_load_async (GtkSourceFileLoader   *loader,
				   gint                   io_priority,
				   GCancellable          *cancellable,
				   GFileProgressCallback  progress_callback,
				   gpointer               progress_callback_data,
				   GDestroyNotify         progress_callback_notify,
				   GAsyncReadyCallback    callback,
				   gpointer               user_data)
{
	GTask *task;
	TaskData *task_data;
	gboolean implicit_trailing_newline;

	g_return_if_fail (GTK_SOURCE_IS_FILE_LOADER (loader));
	g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
	g_return_if_fail (!loader->priv->task_is_running);

#if DEBUG
	g_message ("%s: start loading", G_STRFUNC);
#endif

	loader->priv->task_is_running = TRUE;

	/* TODO: Calling g_task_return_error_if_cancelled() a bit everywhere is
	 * maybe needed. Test the GCancellable, at least with print-testing.
	 */

	task = g_task_new (loader, cancellable, callback, user_data);
	g_task_set_priority (task, io_priority);

	task_data = task_data_new ();
	g_task_set_task_data (task, task_data, task_data_free);

	task_data->progress_cb = progress_callback;
	task_data->progress_cb_data = progress_callback_data;
	task_data->progress_cb_notify = progress_callback_notify;

	if (loader->priv->source_buffer == NULL ||
	    loader->priv->file == NULL ||
	    (loader->priv->location == NULL && loader->priv->input_stream_property == NULL))
	{
		g_task_return_boolean (task, FALSE);
		g_object_unref (task);
		return;
	}

	/* Update GtkSourceFile location directly. The other GtkSourceFile
	 * properties are updated when the operation is finished. But since the
	 * file is loaded, the previous contents is lost, so the previous
	 * location is anyway not needed. And for display purposes, the new
	 * location is directly needed (for example to display the filename in a
	 * tab or an info bar with the progress information).
	 */
	if (loader->priv->input_stream_property != NULL)
	{
		gtk_source_file_set_location (loader->priv->file, NULL);
	}
	else
	{
		gtk_source_file_set_location (loader->priv->file,
					      loader->priv->location);
	}

	implicit_trailing_newline = gtk_source_buffer_get_implicit_trailing_newline (loader->priv->source_buffer);

	/* The BufferOutputStream has a strong reference to the buffer.
         * We create the BufferOutputStream here so we are sure that the
         * buffer will not be destroyed during the file loading.
         */
	task_data->output_stream = gtk_source_buffer_output_stream_new (loader->priv->source_buffer,
									loader->priv->candidate_encodings,
									implicit_trailing_newline);

	if (loader->priv->input_stream_property != NULL)
	{
		task_data->info = g_file_info_new ();
		setup_input_stream (task);
	}
	else
	{
		open_file (task);
	}
}

/**
 * gtk_source_file_loader_load_finish:
 * @loader: a #GtkSourceFileLoader.
 * @result: a #GAsyncResult.
 * @error: a #GError, or %NULL.
 *
 * Finishes a file loading started with gtk_source_file_loader_load_async().
 *
 * If the contents has been loaded, the following #GtkSourceFile properties will
 * be updated: the location, the encoding, the newline type and the compression
 * type.
 *
 * Returns: whether the contents has been loaded successfully.
 * Since: 3.14
 */
gboolean
gtk_source_file_loader_load_finish (GtkSourceFileLoader  *loader,
				    GAsyncResult         *result,
				    GError              **error)
{
	gboolean ok;
	gboolean update_file_properties;
	GError *my_error = NULL;

	g_return_val_if_fail (GTK_SOURCE_IS_FILE_LOADER (loader), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
	g_return_val_if_fail (g_task_is_valid (result, loader), FALSE);
	g_return_val_if_fail (loader->priv->task_is_running, FALSE);

#if DEBUG
	g_message ("%s: finish loading", G_STRFUNC);
#endif

	ok = g_task_propagate_boolean (G_TASK (result), &my_error);

	if (error != NULL && my_error != NULL)
	{
		*error = g_error_copy (my_error);
	}

	/* Update the file properties if the contents has been loaded. The
	 * contents can be loaded successfully, or there can be encoding
	 * conversion errors with fallback characters. In the latter case, the
	 * encoding may be wrong, but since the contents has anyway been loaded,
	 * the file properties must be updated.
	 * With the other errors, normally the contents hasn't been loaded into
	 * the buffer, i.e. the buffer is still empty.
	 */
	update_file_properties = (ok || g_error_matches (my_error,
							 GTK_SOURCE_FILE_LOADER_ERROR,
							 GTK_SOURCE_FILE_LOADER_ERROR_CONVERSION_FALLBACK));

	if (update_file_properties && loader->priv->file != NULL)
	{
		TaskData *task_data;

		task_data = g_task_get_task_data (G_TASK (result));

		/* The location is already updated at the beginning of the
		 * operation.
		 */

		_gtk_source_file_set_encoding (loader->priv->file,
					       loader->priv->auto_detected_encoding);

		_gtk_source_file_set_newline_type (loader->priv->file,
						   loader->priv->auto_detected_newline_type);

		_gtk_source_file_set_compression_type (loader->priv->file,
						       loader->priv->auto_detected_compression_type);

		_gtk_source_file_set_externally_modified (loader->priv->file, FALSE);
		_gtk_source_file_set_deleted (loader->priv->file, FALSE);

		if (g_file_info_has_attribute (task_data->info, G_FILE_ATTRIBUTE_TIME_MODIFIED))
		{
			GTimeVal modification_time;

			g_file_info_get_modification_time (task_data->info, &modification_time);
			_gtk_source_file_set_modification_time (loader->priv->file, modification_time);
		}

		if (g_file_info_has_attribute (task_data->info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE))
		{
			gboolean readonly;

			readonly = !g_file_info_get_attribute_boolean (task_data->info,
								       G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE);

			_gtk_source_file_set_readonly (loader->priv->file, readonly);
		}
		else
		{
			_gtk_source_file_set_readonly (loader->priv->file, FALSE);
		}
	}

	g_clear_error (&my_error);

	loader->priv->task_is_running = FALSE;
	return ok;
}

/**
 * gtk_source_file_loader_get_encoding:
 * @loader: a #GtkSourceFileLoader.
 *
 * Returns: (transfer none): the detected encoding.
 * Since: 3.14
 */
const GtkSourceEncoding *
gtk_source_file_loader_get_encoding (GtkSourceFileLoader *loader)
{
	g_return_val_if_fail (GTK_SOURCE_IS_FILE_LOADER (loader), NULL);

	return loader->priv->auto_detected_encoding;
}

/**
 * gtk_source_file_loader_get_newline_type:
 * @loader: a #GtkSourceFileLoader.
 *
 * Returns: the detected newline type.
 * Since: 3.14
 */
GtkSourceNewlineType
gtk_source_file_loader_get_newline_type (GtkSourceFileLoader *loader)
{
	g_return_val_if_fail (GTK_SOURCE_IS_FILE_LOADER (loader),
			      GTK_SOURCE_NEWLINE_TYPE_LF);

	return loader->priv->auto_detected_newline_type;
}

/**
 * gtk_source_file_loader_get_compression_type:
 * @loader: a #GtkSourceFileLoader.
 *
 * Returns: the detected compression type.
 * Since: 3.14
 */
GtkSourceCompressionType
gtk_source_file_loader_get_compression_type (GtkSourceFileLoader *loader)
{
	g_return_val_if_fail (GTK_SOURCE_IS_FILE_LOADER (loader),
			      GTK_SOURCE_COMPRESSION_TYPE_NONE);

	return loader->priv->auto_detected_compression_type;
}
