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

#include "gfls-config.h"
#include "gfls-encoding-convert.h"
#include <glib/gi18n-lib.h>
#include "gfls-bytes-region-builder.h"
#include "gfls-iconv.h"

/**
 * SECTION:gfls-encoding-convert
 * @Title: GflsEncodingConvert
 * @Short_description: Character encoding conversion
 *
 * Functions used for character encoding conversion.
 */

/* # Use-case
 *
 * The gfls_encoding_convert() function has been designed for loading a file
 * into a #GtkTextBuffer scenario. Regarding the performances, the insertions in
 * the #GtkTextBuffer are the slow part, so there must be as few insertions as
 * possible.
 *
 * In other words, if there are no invalid chars and the target encoding is
 * UTF-8, have as a result a single UTF-8 string that can be inserted at once.
 *
 * # Output format
 *
 * For the output format, a previous implementation created a list of "Chunk"
 * structs containing two fields: a #GBytes for the data, and a boolean for
 * whether it's valid/invalid. But it consumed more memory.
 *
 * A worst case scenario is when there are lots of interleavings of
 * valid/invalid chunks.
 *
 * The current output format with a #GBytes combined with a #GflsBytesRegion
 * is normally more memory efficient.
 *
 * # Implementation notes about memory allocations
 *
 * Since we don't know how big the output will be, memory reallocations are
 * necessary. So use g_malloc() and g_realloc() family of functions. At the end,
 * there is the need to realloc at the right size, to not consume more memory
 * than necessary.
 */

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

/* Invariants:
 * - 0 <= (@position - @data) <= @size
 * - @n_bytes_left <= @size
 * - @n_bytes_left == @size - (@position - @data)
 *
 * It would be possible to remove one field because there are some redundancies.
 * But the code is probably easier if the redundancy is kept.
 */
typedef struct
{
	/* Points to the beginning of the buffer data.
	 * Can be owned or unowned.
	 */
	gchar *data;

	/* Points to the current position in @data.
	 * Always unowned.
	 */
	gchar *position;

	/* The total size of the memory allocation of @data. */
	gsize size;

	/* The number of bytes not yet written in @data, starting at @position. */
	gsize n_bytes_left;
} Buffer;

typedef struct
{
	GflsIconv *conv;

	/* The @data field is unowned. */
	Buffer input_buffer;

	/* The @data field is owned. */
	Buffer output_buffer;
	GflsBytesRegionBuilder *output_bytes_valid_region_builder;

	guint allow_invalid_characters : 1;
} Converter;

static gboolean
check_buffer_invariants (Buffer *buffer)
{
	gintptr n_bytes_written;

	n_bytes_written = buffer->position - buffer->data;

	g_return_val_if_fail (n_bytes_written >= 0, FALSE);
	g_return_val_if_fail (n_bytes_written <= (gintptr)buffer->size, FALSE);
	g_return_val_if_fail (buffer->n_bytes_left <= buffer->size, FALSE);
	g_return_val_if_fail (buffer->n_bytes_left == buffer->size - n_bytes_written, FALSE);

	return TRUE;
}

static gboolean
check_class_invariants (Converter *conv)
{
	g_return_val_if_fail (check_buffer_invariants (&conv->input_buffer), FALSE);
	g_return_val_if_fail (check_buffer_invariants (&conv->output_buffer), FALSE);

	if (conv->output_bytes_valid_region_builder == NULL)
	{
		g_return_val_if_fail (conv->output_buffer.data == NULL, FALSE);
	}
	else
	{
		gsize n_bytes_written1;
		gsize n_bytes_written2;
		gsize n_bytes_written3;

		g_return_val_if_fail (conv->output_buffer.data != NULL, FALSE);

		/* There are multiple ways to know the number of bytes written. */
		n_bytes_written1 = gfls_bytes_region_builder_get_current_size (conv->output_bytes_valid_region_builder);
		n_bytes_written2 = conv->output_buffer.size - conv->output_buffer.n_bytes_left;
		n_bytes_written3 = conv->output_buffer.position - conv->output_buffer.data;

		g_return_val_if_fail (n_bytes_written1 == n_bytes_written2, FALSE);
		g_return_val_if_fail (n_bytes_written1 == n_bytes_written3, FALSE);
	}

	return TRUE;
}

static void
converter_init (Converter *conv,
		GBytes    *input_bytes,
		gboolean   allow_invalid_characters)
{
	conv->conv = gfls_iconv_new ();

	conv->input_buffer.data = (gchar *) g_bytes_get_data (input_bytes, NULL);
	conv->input_buffer.position = conv->input_buffer.data;
	conv->input_buffer.size = g_bytes_get_size (input_bytes);
	conv->input_buffer.n_bytes_left = conv->input_buffer.size;

	conv->allow_invalid_characters = allow_invalid_characters != FALSE;

	g_return_if_fail (check_class_invariants (conv));

	/* The output is lazily initialized, see init_output(). */
}

static void
init_output (Converter *conv)
{
	if (conv->output_bytes_valid_region_builder != NULL)
	{
		return;
	}

	conv->output_bytes_valid_region_builder = gfls_bytes_region_builder_new ();

	/* Allocate the same size as the input + some margin, to hopefully
	 * reduce the number of re-allocations.
	 */
	if (!g_size_checked_add (&conv->output_buffer.size,
				 conv->input_buffer.size,
				 OUTPUT_BUFFER_STEP_SIZE))
	{
		g_assert_not_reached ();
	}

	/* Use g_malloc0(), more secure than g_malloc(). */
	conv->output_buffer.data = g_malloc0 (conv->output_buffer.size);

	conv->output_buffer.position = conv->output_buffer.data;
	conv->output_buffer.n_bytes_left = conv->output_buffer.size;
}

static void
enlarge_output_buffer (Converter *conv,
		       gsize      n_additional_bytes_needed)
{
	gsize n_additional_bytes;
	gsize new_size;
	gsize n_bytes_written;

	g_return_if_fail (conv->output_buffer.data != NULL);

	n_additional_bytes = MAX (n_additional_bytes_needed, OUTPUT_BUFFER_STEP_SIZE);

	if (!g_size_checked_add (&new_size,
				 conv->output_buffer.size,
				 n_additional_bytes))
	{
		g_assert_not_reached ();
	}

	n_bytes_written = conv->output_buffer.position - conv->output_buffer.data;

	conv->output_buffer.data = g_realloc (conv->output_buffer.data, new_size);
	conv->output_buffer.position = conv->output_buffer.data + n_bytes_written;
	conv->output_buffer.size = new_size;
	conv->output_buffer.n_bytes_left += n_additional_bytes;

	/* Fill the new chunk with 0's. */
	memset (conv->output_buffer.data + new_size - n_additional_bytes,
		0,
		n_additional_bytes);
}

static void
free_output (Converter *conv)
{
	g_free (conv->output_buffer.data);
	conv->output_buffer.data = NULL;
	conv->output_buffer.position = NULL;
	conv->output_buffer.size = 0;
	conv->output_buffer.n_bytes_left = 0;

	gfls_bytes_region_builder_free (conv->output_bytes_valid_region_builder, TRUE);
	conv->output_bytes_valid_region_builder = NULL;
}

static void
converter_finalize (Converter *conv)
{
	gfls_iconv_free (conv->conv);
	free_output (conv);
}

static void
append_valid_sub_region (Converter *conv)
{
	gsize n_bytes_written_before;
	gsize n_bytes_written_after;
	gsize sub_region_length;

	n_bytes_written_before = gfls_bytes_region_builder_get_current_size (conv->output_bytes_valid_region_builder);
	n_bytes_written_after = conv->output_buffer.position - conv->output_buffer.data;

	g_return_if_fail (n_bytes_written_before <= n_bytes_written_after);

	sub_region_length = n_bytes_written_after - n_bytes_written_before;

	if (sub_region_length != 0)
	{
		gfls_bytes_region_builder_append (conv->output_bytes_valid_region_builder,
						  sub_region_length,
						  TRUE);
	}
}

static void
handle_iconv_result_ok (Converter *conv)
{
	g_warn_if_fail (conv->input_buffer.n_bytes_left == 0);
	append_valid_sub_region (conv);
}

static void
handle_iconv_result_error (Converter *conv)
{
	free_output (conv);
}

static void
handle_iconv_result_illegal_sequence (Converter *conv)
{
	g_return_if_fail (conv->input_buffer.n_bytes_left > 0);
	g_return_if_fail (conv->allow_invalid_characters);

	/* First chunk: what has been converted successfully. */

	append_valid_sub_region (conv);

	/* Second chunk: the invalid byte from the input buffer. */

	if (conv->output_buffer.n_bytes_left == 0)
	{
		enlarge_output_buffer (conv, 1);
	}

	*conv->output_buffer.position = *conv->input_buffer.position;

	conv->input_buffer.position++;
	conv->input_buffer.n_bytes_left--;

	conv->output_buffer.position++;
	conv->output_buffer.n_bytes_left--;

	gfls_bytes_region_builder_append (conv->output_bytes_valid_region_builder, 1, FALSE);
}

static void
handle_iconv_result_incomplete_input (Converter *conv)
{
	g_return_if_fail (conv->input_buffer.n_bytes_left > 0);
	g_return_if_fail (conv->allow_invalid_characters);

	/* First chunk: what has been converted successfully. */

	append_valid_sub_region (conv);

	/* Second chunk: the remained bytes from the input buffer. */

	if (conv->output_buffer.n_bytes_left < conv->input_buffer.n_bytes_left)
	{
		enlarge_output_buffer (conv, conv->input_buffer.n_bytes_left - conv->output_buffer.n_bytes_left);
	}

	memcpy (conv->output_buffer.position,
		conv->input_buffer.position,
		conv->input_buffer.n_bytes_left);

	gfls_bytes_region_builder_append (conv->output_bytes_valid_region_builder,
					  conv->input_buffer.n_bytes_left,
					  FALSE);

	conv->input_buffer.position += conv->input_buffer.n_bytes_left;
	conv->output_buffer.position += conv->input_buffer.n_bytes_left;
	conv->output_buffer.n_bytes_left -= conv->input_buffer.n_bytes_left;
	conv->input_buffer.n_bytes_left = 0;
}

static void
handle_iconv_result_output_buffer_full (Converter *conv)
{
	append_valid_sub_region (conv);
	enlarge_output_buffer (conv, OUTPUT_BUFFER_STEP_SIZE);
}

static void
handle_iconv_result_lossy_conversion (Converter  *conv,
				      GError    **error)
{
	g_set_error (error,
		     G_CONVERT_ERROR,
		     G_CONVERT_ERROR_FAILED,
		     _("A number of nonreversible conversions have been performed."));

	handle_iconv_result_error (conv);
}

static void
handle_invalid_character_encountered (Converter  *conv,
				      GError    **error)
{
	g_return_if_fail (!conv->allow_invalid_characters);

	g_set_error (error,
		     G_CONVERT_ERROR,
		     G_CONVERT_ERROR_FAILED,
		     _("An invalid character has been encountered."));

	handle_iconv_result_error (conv);
}

static gboolean
convert (Converter  *conv,
	 GError    **error)
{
	gboolean success = TRUE;

	init_output (conv);

	while (conv->input_buffer.n_bytes_left > 0)
	{
		GflsIconvResult conv_result;

		g_return_val_if_fail (check_class_invariants (conv), FALSE);

		conv_result = gfls_iconv_feed (conv->conv,
					       &conv->input_buffer.position, &conv->input_buffer.n_bytes_left,
					       &conv->output_buffer.position, &conv->output_buffer.n_bytes_left,
					       error);

		if (conv_result == GFLS_ICONV_RESULT_OK)
		{
			handle_iconv_result_ok (conv);
			break;
		}
		else if (conv_result == GFLS_ICONV_RESULT_ERROR)
		{
			handle_iconv_result_error (conv);
			success = FALSE;
			break;
		}
		else if (conv_result == GFLS_ICONV_RESULT_ILLEGAL_SEQUENCE)
		{
			if (conv->allow_invalid_characters)
			{
				handle_iconv_result_illegal_sequence (conv);
				continue;
			}
			else
			{
				handle_invalid_character_encountered (conv, error);
				success = FALSE;
				break;
			}
		}
		else if (conv_result == GFLS_ICONV_RESULT_INCOMPLETE_INPUT)
		{
			if (conv->allow_invalid_characters)
			{
				handle_iconv_result_incomplete_input (conv);
				break;
			}
			else
			{
				handle_invalid_character_encountered (conv, error);
				success = FALSE;
				break;
			}
		}
		else if (conv_result == GFLS_ICONV_RESULT_OUTPUT_BUFFER_FULL)
		{
			handle_iconv_result_output_buffer_full (conv);
			continue;
		}
		else if (conv_result == GFLS_ICONV_RESULT_LOSSY_CONVERSION)
		{
			handle_iconv_result_lossy_conversion (conv, error);
			success = FALSE;
			break;
		}
	}

	g_return_val_if_fail (check_class_invariants (conv), FALSE);
	return success;
}

static gboolean
finish_convert (Converter  *conv,
		GError    **error)
{
	GflsIconvResult conv_result;
	gboolean success = TRUE;

	conv->input_buffer.data = NULL;
	conv->input_buffer.position = NULL;
	conv->input_buffer.size = 0;
	conv->input_buffer.n_bytes_left = 0;

	if (conv->output_buffer.n_bytes_left < OUTPUT_BUFFER_STEP_SIZE)
	{
		enlarge_output_buffer (conv, OUTPUT_BUFFER_STEP_SIZE - conv->output_buffer.n_bytes_left);
	}

	g_return_val_if_fail (check_class_invariants (conv), FALSE);

	/* Do not call it in a loop because the output_buffer should be big
	 * enough.
	 */
	conv_result = gfls_iconv_feed (conv->conv,
				       NULL, NULL,
				       &conv->output_buffer.position, &conv->output_buffer.n_bytes_left,
				       error);

	if (conv_result == GFLS_ICONV_RESULT_OK)
	{
		handle_iconv_result_ok (conv);
	}
	else if (conv_result == GFLS_ICONV_RESULT_ERROR)
	{
		handle_iconv_result_error (conv);
		success = FALSE;
	}
	else if (conv_result == GFLS_ICONV_RESULT_ILLEGAL_SEQUENCE ||
		 conv_result == GFLS_ICONV_RESULT_INCOMPLETE_INPUT ||
		 conv_result == GFLS_ICONV_RESULT_OUTPUT_BUFFER_FULL)
	{
		/* Unexpected */
		g_warn_if_reached ();
		handle_iconv_result_error (conv);
		success = FALSE;
	}
	else if (conv_result == GFLS_ICONV_RESULT_LOSSY_CONVERSION)
	{
		handle_iconv_result_lossy_conversion (conv, error);
		success = FALSE;
	}

	g_return_val_if_fail (check_class_invariants (conv), FALSE);
	return success;
}

/**
 * gfls_encoding_convert:
 * @input_bytes: the input #GBytes to convert.
 * @to_codeset: destination codeset.
 * @from_codeset: source codeset.
 * @allow_invalid_characters: whether invalid characters are allowed.
 * @output_bytes: (out) (transfer full): the output #GBytes. It must initially
 *   point to a %NULL #GBytes value.
 * @output_bytes_valid_region: (out) (transfer full): the output
 *   #GflsBytesRegion that represents the valid characters. It must initially
 *   point to a %NULL #GflsBytesRegion value.
 * @error: location to a %NULL #GError, or %NULL.
 *
 * This function converts @input_bytes from a codeset to another.
 *
 * @from_codeset and @to_codeset must be compatible with iconv, see
 * gfls_iconv_open().
 *
 * Note that @from_codeset and @to_codeset can be equal. It is useful to
 * identify the valid characters from the invalid ones.
 *
 * The output is the combination of @output_bytes and
 * @output_bytes_valid_region. Output bytes that are part of
 * @output_bytes_valid_region are the valid characters (successfully converted).
 * The other output bytes are invalid characters (copied as is from
 * @input_bytes).
 *
 * If @allow_invalid_characters is %FALSE:
 * - As soon as an invalid character is encountered, %FALSE is returned by this
 *   function and @output_bytes and @output_bytes_valid_region will return
 *   %NULL.
 * - Otherwise, if everything goes well, %TRUE is returned alongside the
 *   @output_bytes and @output_bytes_valid_region (the latter contains in this
 *   case only one, valid sub-region).
 *
 * Returns: %TRUE on success, %FALSE otherwise.
 * Since: 0.4
 */
gboolean
gfls_encoding_convert (GBytes           *input_bytes,
		       const gchar      *to_codeset,
		       const gchar      *from_codeset,
		       gboolean          allow_invalid_characters,
		       GBytes          **output_bytes,
		       GflsBytesRegion **output_bytes_valid_region,
		       GError          **error)
{
	Converter *conv;
	gboolean success = FALSE;

	g_return_val_if_fail (input_bytes != NULL, FALSE);
	g_return_val_if_fail (to_codeset != NULL, FALSE);
	g_return_val_if_fail (from_codeset != NULL, FALSE);
	g_return_val_if_fail (output_bytes != NULL && *output_bytes == NULL, FALSE);
	g_return_val_if_fail (output_bytes_valid_region != NULL && *output_bytes_valid_region == NULL, FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	conv = g_alloca0 (sizeof (Converter));
	converter_init (conv, input_bytes, allow_invalid_characters);

	if (!gfls_iconv_open (conv->conv, to_codeset, from_codeset, error))
	{
		goto out;
	}

	if (!convert (conv, error))
	{
		goto out;
	}

	if (!finish_convert (conv, error))
	{
		goto out;
	}

	if (!gfls_iconv_close (conv->conv, error))
	{
		goto out;
	}

	if (!check_class_invariants (conv))
	{
		goto out;
	}

	/* Success */

	if (conv->output_bytes_valid_region_builder != NULL)
	{
		gsize final_size;
		gchar *output_data;

		final_size = gfls_bytes_region_builder_get_current_size (conv->output_bytes_valid_region_builder);

		output_data = g_realloc (conv->output_buffer.data, final_size);

		conv->output_buffer.data = NULL;
		conv->output_buffer.position = NULL;
		conv->output_buffer.size = 0;
		conv->output_buffer.n_bytes_left = 0;

		*output_bytes = g_bytes_new_take (output_data, final_size);
		output_data = NULL;

		*output_bytes_valid_region = gfls_bytes_region_builder_free (conv->output_bytes_valid_region_builder, FALSE);
		conv->output_bytes_valid_region_builder = NULL;

		success = TRUE;
	}

out:
	converter_finalize (conv);
	return success;
}
