/* SPDX-FileCopyrightText: 2016-2025 - Sébastien Wilmet <swilmet@gnome.org>
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */

#include "tepl-buffer.h"
#include "tepl-abstract-factory.h"
#include "tepl-metadata-manager.h"
#include "tepl-settings.h"
#include "tepl-utils.h"

/**
 * SECTION:buffer
 * @Short_description: Subclass of #GtkSourceBuffer
 * @Title: TeplBuffer
 *
 * #TeplBuffer is a subclass of #GtkSourceBuffer, to add more features useful
 * for a text editor.
 *
 * It also adds an association to a #TeplFile that can be retrieved with
 * tepl_buffer_get_file(). The association cannot change. The same for
 * #TeplMetadata with tepl_buffer_get_metadata().
 *
 * The properties and signals have the tepl namespace, to avoid potential
 * conflicts in the future if the property or signal is moved to
 * #GtkSourceBuffer.
 */

typedef struct _TeplBufferPrivate TeplBufferPrivate;

struct _TeplBufferPrivate
{
	TeplFile *file;
	TeplMetadata *metadata;

	GtkTextTag *invalid_char_tag;

	guint n_nested_user_actions;
	guint idle_cursor_moved_id;
};

enum
{
	PROP_0,
	PROP_TEPL_SHORT_TITLE,
	PROP_TEPL_FULL_TITLE,
	N_PROPERTIES
};

enum
{
	SIGNAL_TEPL_CURSOR_MOVED,
	N_SIGNALS
};

static GParamSpec *properties[N_PROPERTIES];
static guint signals[N_SIGNALS];

G_DEFINE_TYPE_WITH_PRIVATE (TeplBuffer, tepl_buffer, GTK_SOURCE_TYPE_BUFFER)

static void
update_invalid_char_tag_style (TeplBuffer *buffer)
{
	TeplBufferPrivate *priv;
	GtkSourceStyleScheme *style_scheme;
	GtkSourceStyle *style = NULL;

	priv = tepl_buffer_get_instance_private (buffer);

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

	style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer));

	if (style_scheme != NULL)
	{
		style = gtk_source_style_scheme_get_style (style_scheme, "def:error");
	}

	gtk_source_style_apply (style, priv->invalid_char_tag);
}

static void
tepl_buffer_get_property (GObject    *object,
			  guint       prop_id,
			  GValue     *value,
			  GParamSpec *pspec)
{
	TeplBuffer *buffer = TEPL_BUFFER (object);

	switch (prop_id)
	{
		case PROP_TEPL_SHORT_TITLE:
			g_value_take_string (value, tepl_buffer_get_short_title (buffer));
			break;

		case PROP_TEPL_FULL_TITLE:
			g_value_take_string (value, tepl_buffer_get_full_title (buffer));
			break;

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

static void
tepl_buffer_dispose (GObject *object)
{
	TeplBufferPrivate *priv = tepl_buffer_get_instance_private (TEPL_BUFFER (object));

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

	if (priv->idle_cursor_moved_id != 0)
	{
		g_source_remove (priv->idle_cursor_moved_id);
		priv->idle_cursor_moved_id = 0;
	}

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

static gboolean
idle_cursor_moved_cb (gpointer user_data)
{
	TeplBuffer *buffer = TEPL_BUFFER (user_data);
	TeplBufferPrivate *priv = tepl_buffer_get_instance_private (buffer);

	g_signal_emit (buffer, signals[SIGNAL_TEPL_CURSOR_MOVED], 0);

	priv->idle_cursor_moved_id = 0;
	return G_SOURCE_REMOVE;
}

static void
install_idle_cursor_moved (TeplBuffer *buffer)
{
	TeplBufferPrivate *priv = tepl_buffer_get_instance_private (buffer);

	if (priv->idle_cursor_moved_id == 0)
	{
		/* High idle priority, because after loading a big file, the
		 * GtkTextView works in the background to compute the whole size
		 * etc. HIGH_IDLE permits to send the signal as soon as the
		 * content is fully loaded in the GtkTextBuffer, even if
		 * GtkTextView has not finished.
		 */
		priv->idle_cursor_moved_id = g_idle_add_full (G_PRIORITY_HIGH_IDLE,
							      idle_cursor_moved_cb,
							      buffer,
							      NULL);
	}
}

static void
tepl_buffer_begin_user_action (GtkTextBuffer *buffer)
{
	TeplBufferPrivate *priv = tepl_buffer_get_instance_private (TEPL_BUFFER (buffer));

	priv->n_nested_user_actions++;

	if (GTK_TEXT_BUFFER_CLASS (tepl_buffer_parent_class)->begin_user_action != NULL)
	{
		GTK_TEXT_BUFFER_CLASS (tepl_buffer_parent_class)->begin_user_action (buffer);
	}
}

static void
tepl_buffer_end_user_action (GtkTextBuffer *buffer)
{
	TeplBufferPrivate *priv = tepl_buffer_get_instance_private (TEPL_BUFFER (buffer));

	if (GTK_TEXT_BUFFER_CLASS (tepl_buffer_parent_class)->end_user_action != NULL)
	{
		GTK_TEXT_BUFFER_CLASS (tepl_buffer_parent_class)->end_user_action (buffer);
	}

	g_return_if_fail (priv->n_nested_user_actions > 0);
	priv->n_nested_user_actions--;

	if (priv->n_nested_user_actions == 0)
	{
		install_idle_cursor_moved (TEPL_BUFFER (buffer));
	}
}

static void
tepl_buffer_mark_set (GtkTextBuffer     *buffer,
		      const GtkTextIter *location,
		      GtkTextMark       *mark)
{
	TeplBufferPrivate *priv = tepl_buffer_get_instance_private (TEPL_BUFFER (buffer));

	if (GTK_TEXT_BUFFER_CLASS (tepl_buffer_parent_class)->mark_set != NULL)
	{
		GTK_TEXT_BUFFER_CLASS (tepl_buffer_parent_class)->mark_set (buffer, location, mark);
	}

	if (priv->n_nested_user_actions == 0 &&
	    mark == gtk_text_buffer_get_insert (buffer))
	{
		install_idle_cursor_moved (TEPL_BUFFER (buffer));
	}
}

static void
tepl_buffer_changed (GtkTextBuffer *buffer)
{
	TeplBufferPrivate *priv = tepl_buffer_get_instance_private (TEPL_BUFFER (buffer));

	if (GTK_TEXT_BUFFER_CLASS (tepl_buffer_parent_class)->changed != NULL)
	{
		GTK_TEXT_BUFFER_CLASS (tepl_buffer_parent_class)->changed (buffer);
	}

	if (priv->n_nested_user_actions == 0)
	{
		install_idle_cursor_moved (TEPL_BUFFER (buffer));
	}
}

static void
tepl_buffer_modified_changed (GtkTextBuffer *buffer)
{
	if (GTK_TEXT_BUFFER_CLASS (tepl_buffer_parent_class)->modified_changed != NULL)
	{
		GTK_TEXT_BUFFER_CLASS (tepl_buffer_parent_class)->modified_changed (buffer);
	}

	g_object_notify_by_pspec (G_OBJECT (buffer), properties[PROP_TEPL_SHORT_TITLE]);
	g_object_notify_by_pspec (G_OBJECT (buffer), properties[PROP_TEPL_FULL_TITLE]);
}

static void
tepl_buffer_class_init (TeplBufferClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	GtkTextBufferClass *text_buffer_class = GTK_TEXT_BUFFER_CLASS (klass);

	object_class->get_property = tepl_buffer_get_property;
	object_class->dispose = tepl_buffer_dispose;

	text_buffer_class->begin_user_action = tepl_buffer_begin_user_action;
	text_buffer_class->end_user_action = tepl_buffer_end_user_action;
	text_buffer_class->mark_set = tepl_buffer_mark_set;
	text_buffer_class->changed = tepl_buffer_changed;
	text_buffer_class->modified_changed = tepl_buffer_modified_changed;

	/**
	 * TeplBuffer:tepl-short-title:
	 *
	 * The short title. See tepl_buffer_get_short_title().
	 *
	 * Since: 3.0
	 */
	properties[PROP_TEPL_SHORT_TITLE] =
		g_param_spec_string ("tepl-short-title",
				     "tepl-short-title",
				     "",
				     NULL,
				     G_PARAM_READABLE |
				     G_PARAM_STATIC_STRINGS);

	/**
	 * TeplBuffer:tepl-full-title:
	 *
	 * The full title. See tepl_buffer_get_full_title().
	 *
	 * Since: 3.0
	 */
	properties[PROP_TEPL_FULL_TITLE] =
		g_param_spec_string ("tepl-full-title",
				     "tepl-full-title",
				     "",
				     NULL,
				     G_PARAM_READABLE |
				     G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties (object_class, N_PROPERTIES, properties);

	/**
	 * TeplBuffer::tepl-cursor-moved:
	 * @buffer: the #TeplBuffer emitting the signal.
	 *
	 * The ::tepl-cursor-moved signal is emitted when the insert mark is
	 * moved explicitely or when the buffer changes (insert/delete).
	 *
	 * A typical use-case for this signal is to update the cursor position
	 * in a statusbar.
	 *
	 * Since: 2.0
	 */
	signals[SIGNAL_TEPL_CURSOR_MOVED] =
		g_signal_new ("tepl-cursor-moved",
			      G_TYPE_FROM_CLASS (klass),
			      G_SIGNAL_RUN_FIRST,
			      G_STRUCT_OFFSET (TeplBufferClass, tepl_cursor_moved),
			      NULL, NULL, NULL,
			      G_TYPE_NONE, 0);
}

static void
file_short_name_notify_cb (TeplFile   *file,
			   GParamSpec *pspec,
			   TeplBuffer *buffer)
{
	g_object_notify_by_pspec (G_OBJECT (buffer), properties[PROP_TEPL_SHORT_TITLE]);
	g_object_notify_by_pspec (G_OBJECT (buffer), properties[PROP_TEPL_FULL_TITLE]);
}

static void
style_scheme_notify_cb (GtkSourceBuffer *buffer,
			GParamSpec      *pspec,
			gpointer         user_data)
{
	update_invalid_char_tag_style (TEPL_BUFFER (buffer));
}

static void
tepl_buffer_init (TeplBuffer *buffer)
{
	TeplBufferPrivate *priv;
	TeplAbstractFactory *factory;

	priv = tepl_buffer_get_instance_private (buffer);

	factory = tepl_abstract_factory_get_singleton ();
	priv->file = tepl_abstract_factory_create_file (factory);

	priv->metadata = tepl_metadata_new ();

	g_signal_connect_object (priv->file,
				 "notify::short-name",
				 G_CALLBACK (file_short_name_notify_cb),
				 buffer,
				 0);

	g_signal_connect (buffer,
			  "notify::style-scheme",
			  G_CALLBACK (style_scheme_notify_cb),
			  NULL);
}

/**
 * tepl_buffer_new:
 *
 * Returns: a new #TeplBuffer.
 * Since: 1.0
 */
TeplBuffer *
tepl_buffer_new (void)
{
	return g_object_new (TEPL_TYPE_BUFFER, NULL);
}

/**
 * tepl_buffer_get_file:
 * @buffer: a #TeplBuffer.
 *
 * Returns the #TeplFile of @buffer. The returned object is guaranteed to be the
 * same for the lifetime of @buffer.
 *
 * #TeplBuffer creates the #TeplFile with tepl_abstract_factory_create_file().
 *
 * Returns: (transfer none): the associated #TeplFile.
 * Since: 1.0
 */
TeplFile *
tepl_buffer_get_file (TeplBuffer *buffer)
{
	TeplBufferPrivate *priv;

	g_return_val_if_fail (TEPL_IS_BUFFER (buffer), NULL);

	priv = tepl_buffer_get_instance_private (buffer);
	return priv->file;
}

/**
 * tepl_buffer_get_metadata:
 * @buffer: a #TeplBuffer.
 *
 * Returns the #TeplMetadata of @buffer. The returned object is guaranteed to be
 * the same for the lifetime of @buffer.
 *
 * Returns: (transfer none): the associated #TeplMetadata.
 * Since: 5.0
 */
TeplMetadata *
tepl_buffer_get_metadata (TeplBuffer *buffer)
{
	TeplBufferPrivate *priv;

	g_return_val_if_fail (TEPL_IS_BUFFER (buffer), NULL);

	priv = tepl_buffer_get_instance_private (buffer);
	return priv->metadata;
}

/**
 * tepl_buffer_load_metadata_from_metadata_manager:
 * @buffer: a #TeplBuffer.
 *
 * Calls tepl_metadata_manager_copy_from() for #TeplFile:location (if not %NULL)
 * to the associated #TeplMetadata of @buffer.
 *
 * Since: 5.0
 */
void
tepl_buffer_load_metadata_from_metadata_manager (TeplBuffer *buffer)
{
	TeplBufferPrivate *priv;
	GFile *location;

	g_return_if_fail (TEPL_IS_BUFFER (buffer));

	priv = tepl_buffer_get_instance_private (buffer);

	location = tepl_file_get_location (priv->file);

	if (location != NULL)
	{
		TeplMetadataManager *manager;

		manager = tepl_metadata_manager_get_singleton ();
		tepl_metadata_manager_copy_from (manager, location, priv->metadata);
	}
}

/**
 * tepl_buffer_save_metadata_into_metadata_manager:
 * @buffer: a #TeplBuffer.
 *
 * Calls tepl_metadata_manager_merge_into() for #TeplFile:location (if not
 * %NULL) from the associated #TeplMetadata of @buffer.
 *
 * Since: 5.0
 */
void
tepl_buffer_save_metadata_into_metadata_manager (TeplBuffer *buffer)
{
	TeplBufferPrivate *priv;
	GFile *location;

	g_return_if_fail (TEPL_IS_BUFFER (buffer));

	priv = tepl_buffer_get_instance_private (buffer);

	location = tepl_file_get_location (priv->file);

	if (location != NULL)
	{
		TeplMetadataManager *manager;

		manager = tepl_metadata_manager_get_singleton ();
		tepl_metadata_manager_merge_into (manager, location, priv->metadata);
	}
}

/**
 * tepl_buffer_is_untouched:
 * @buffer: a #TeplBuffer.
 *
 * Returns whether @buffer is untouched.
 *
 * This function is for example useful to know if we can re-use this buffer to
 * load a file, instead of opening a new tab or window.
 *
 * For this function to return %TRUE, the @buffer must be empty, non-modified,
 * the undo/redo #GtkSourceBuffer history must be empty, and the
 * #TeplFile:location must be %NULL.
 *
 * Returns: %TRUE if @buffer has not been touched, %FALSE otherwise.
 * Since: 1.0
 */
gboolean
tepl_buffer_is_untouched (TeplBuffer *buffer)
{
	TeplBufferPrivate *priv;

	g_return_val_if_fail (TEPL_IS_BUFFER (buffer), FALSE);

	priv = tepl_buffer_get_instance_private (buffer);

	return (gtk_text_buffer_get_char_count (GTK_TEXT_BUFFER (buffer)) == 0 &&
		!gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (buffer)) &&
		!gtk_source_buffer_can_undo (GTK_SOURCE_BUFFER (buffer)) &&
		!gtk_source_buffer_can_redo (GTK_SOURCE_BUFFER (buffer)) &&
		tepl_file_get_location (priv->file) == NULL);
}

/**
 * tepl_buffer_get_short_title:
 * @buffer: a #TeplBuffer.
 *
 * Returns a title suitable for a tab label. It contains (in that order):
 * - '*' if the buffer is modified;
 * - the #TeplFile:short-name.
 *
 * Returns: the @buffer short title. Free the return value with g_free() when no
 * longer needed.
 * Since: 3.0
 */
gchar *
tepl_buffer_get_short_title (TeplBuffer *buffer)
{
	TeplBufferPrivate *priv;
	gchar *short_name;
	gchar *short_title;

	g_return_val_if_fail (TEPL_IS_BUFFER (buffer), NULL);

	priv = tepl_buffer_get_instance_private (buffer);

	short_name = tepl_file_get_short_name (priv->file);

	if (gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (buffer)))
	{
		short_title = g_strconcat ("*", short_name, NULL);
		g_free (short_name);
	}
	else
	{
		short_title = short_name;
	}

	return short_title;
}

/**
 * tepl_buffer_get_full_title:
 * @buffer: a #TeplBuffer.
 *
 * Returns a title suitable for a #GtkWindow title. It contains (in that order):
 * - the #TeplBuffer:tepl-short-title;
 * - the directory path in parenthesis if the #TeplFile:location isn't
 *   %NULL.
 *
 * Returns: the @buffer full title. Free the return value with g_free() when no
 * longer needed.
 * Since: 3.0
 */
gchar *
tepl_buffer_get_full_title (TeplBuffer *buffer)
{
	TeplBufferPrivate *priv;
	GFile *location;
	gchar *short_title;
	gchar *full_title;

	g_return_val_if_fail (TEPL_IS_BUFFER (buffer), NULL);

	priv = tepl_buffer_get_instance_private (buffer);

	location = tepl_file_get_location (priv->file);
	short_title = tepl_buffer_get_short_title (buffer);

	if (location != NULL &&
	    g_file_has_parent (location, NULL))
	{
		GFile *parent;
		gchar *directory;
		gchar *directory_tilde;

		parent = g_file_get_parent (location);
		directory = g_file_get_parse_name (parent);
		directory_tilde = tepl_utils_replace_home_dir_with_tilde (directory);

		full_title = g_strdup_printf ("%s (%s)", short_title, directory_tilde);
		g_free (short_title);

		g_object_unref (parent);
		g_free (directory);
		g_free (directory_tilde);
	}
	else
	{
		full_title = short_title;
	}

	return full_title;
}

static void
set_style_scheme_id (TeplBuffer  *buffer,
		     const gchar *style_scheme_id)
{
	GtkSourceStyleSchemeManager *manager;
	GtkSourceStyleScheme *style_scheme;

	g_assert (style_scheme_id != NULL);

	manager = gtk_source_style_scheme_manager_get_default ();
	style_scheme = gtk_source_style_scheme_manager_get_scheme (manager, style_scheme_id);

	if (style_scheme == NULL)
	{
		TeplSettings *settings = tepl_settings_get_singleton ();
		gchar *default_style_scheme_id;

		default_style_scheme_id = _tepl_settings_get_default_style_scheme_id (settings);

		g_warning_once ("Style scheme '%s' cannot be found, falling back to '%s' default style scheme.",
				style_scheme_id,
				default_style_scheme_id);

		style_scheme = gtk_source_style_scheme_manager_get_scheme (manager, default_style_scheme_id);
		if (style_scheme == NULL)
		{
			g_warning_once ("Default style scheme '%s' cannot be found, check your installation.",
					default_style_scheme_id);
		}

		g_free (default_style_scheme_id);
	}

	gtk_source_buffer_set_style_scheme (GTK_SOURCE_BUFFER (buffer), style_scheme);
}

static void
update_style_scheme (TeplBuffer *buffer)
{
	TeplSettings *settings;
	gchar *style_scheme_id;

	settings = tepl_settings_get_singleton ();
	style_scheme_id = tepl_settings_get_style_scheme_id (settings);

	if (style_scheme_id != NULL)
	{
		set_style_scheme_id (buffer, style_scheme_id);
		g_free (style_scheme_id);
	}
}

static void
style_scheme_id_notify_cb (TeplSettings *settings,
			   GParamSpec   *pspec,
			   TeplBuffer   *buffer)
{
	update_style_scheme (buffer);
}

/**
 * tepl_buffer_connect_style_scheme_settings:
 * @buffer: a #TeplBuffer.
 *
 * Connects #TeplSettings:style-scheme-id to #GtkSourceBuffer:style-scheme.
 *
 * Since: 6.11
 */
void
tepl_buffer_connect_style_scheme_settings (TeplBuffer *buffer)
{
	TeplSettings *settings = tepl_settings_get_singleton ();

	g_return_if_fail (TEPL_IS_BUFFER (buffer));

	g_signal_handlers_disconnect_by_func (settings, style_scheme_id_notify_cb, buffer);

	g_signal_connect_object (settings,
				 "notify::style-scheme-id",
				 G_CALLBACK (style_scheme_id_notify_cb),
				 buffer,
				 G_CONNECT_DEFAULT);

	update_style_scheme (buffer);
}

/**
 * tepl_buffer_get_selection_type:
 * @buffer: a #TeplBuffer.
 *
 * Returns: the current #TeplSelectionType.
 * Since: 1.0
 */
TeplSelectionType
tepl_buffer_get_selection_type (TeplBuffer *buffer)
{
	GtkTextIter start;
	GtkTextIter end;
	gint start_line;
	gint end_line;

	g_return_val_if_fail (TEPL_IS_BUFFER (buffer), TEPL_SELECTION_TYPE_NO_SELECTION);

	if (!gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (buffer), &start, &end))
	{
		return TEPL_SELECTION_TYPE_NO_SELECTION;
	}

	start_line = gtk_text_iter_get_line (&start);
	end_line = gtk_text_iter_get_line (&end);

	if (start_line == end_line)
	{
		return TEPL_SELECTION_TYPE_ON_SAME_LINE;
	}

	return TEPL_SELECTION_TYPE_MULTIPLE_LINES;
}

static void
text_tag_set_highest_priority (GtkTextTag    *tag,
			       GtkTextBuffer *buffer)
{
	GtkTextTagTable *table;
	gint n;

	table = gtk_text_buffer_get_tag_table (buffer);
	n = gtk_text_tag_table_get_size (table);
	gtk_text_tag_set_priority (tag, n - 1);
}

void
_tepl_buffer_set_as_invalid_character (TeplBuffer        *buffer,
				       const GtkTextIter *start,
				       const GtkTextIter *end)
{
	TeplBufferPrivate *priv;

	g_return_if_fail (TEPL_IS_BUFFER (buffer));
	g_return_if_fail (start != NULL);
	g_return_if_fail (end != NULL);

	priv = tepl_buffer_get_instance_private (buffer);

	if (priv->invalid_char_tag == NULL)
	{
		priv->invalid_char_tag = gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (buffer),
								     NULL,
								     NULL);

		update_invalid_char_tag_style (buffer);
	}

	/* Make sure the 'error' tag has the priority over
	 * syntax highlighting tags.
	 */
	text_tag_set_highest_priority (priv->invalid_char_tag,
	                               GTK_TEXT_BUFFER (buffer));

	gtk_text_buffer_apply_tag (GTK_TEXT_BUFFER (buffer),
	                           priv->invalid_char_tag,
	                           start,
	                           end);
}

gboolean
_tepl_buffer_has_invalid_chars (TeplBuffer *buffer)
{
	TeplBufferPrivate *priv;
	GtkTextIter start;

	g_return_val_if_fail (TEPL_IS_BUFFER (buffer), FALSE);

	priv = tepl_buffer_get_instance_private (buffer);

	if (priv->invalid_char_tag == NULL)
	{
		return FALSE;
	}

	gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (buffer), &start);

	if (gtk_text_iter_starts_tag (&start, priv->invalid_char_tag) ||
	    gtk_text_iter_forward_to_tag_toggle (&start, priv->invalid_char_tag))
	{
		return TRUE;
	}

	return FALSE;
}
