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

#include "gfls-config.h"
#include "gfls-iconv.h"
#include <errno.h>
#include <glib/gi18n-lib.h>

/**
 * SECTION:gfls-iconv
 * @Title: GflsIconv
 * @Short_description: To use iconv more comfortably
 *
 * #GflsIconv is a small wrapper for the g_iconv() family of functions, to
 * use iconv more comfortably. #GflsIconv returns #GError's and enum values
 * for the different situations.
 *
 * Call the functions in this order:
 * - gfls_iconv_new()
 * - gfls_iconv_open()
 * - gfls_iconv_feed() in a loop.
 * - gfls_iconv_feed() with @inbuf and @inbytes_left set to %NULL (in a
 *   loop too if the output buffer is full).
 * - gfls_iconv_close()
 * - gfls_iconv_free()
 *
 * Note that GLib provides the g_convert() family of functions. As the
 * g_convert() documentation explains, it is necessary to use g_iconv() (or
 * #GflsIconv) for streaming conversions. One other use-case is to have
 * more control over invalid characters: in the case of the
 * libgedit-gtksourceview library, it is desirable to apply a #GtkTextTag to
 * them.
 *
 * # Avoid iconv if possible
 *
 * However, if you have the choice, don't use iconv! It's a crappy API and has
 * several design flaws (and #GflsIconv cannot fix them):
 * - When `iconv()` returns `(size_t)-1` (e.g., when the output buffer is full),
 *   we don't know if it has performed lossy conversions. In a normal situation,
 *   the number of lossy conversions performed is returned by `iconv()` as its
 *   return value.
 *
 *   To fix this flaw, an `iconv2()` function could return the number of lossy
 *   conversions performed as an output parameter. And also an
 *   `iconv_open2` `()` function could take a flags parameter to configure
 *   whether lossy conversions are allowed.
 *
 * - The `EILSEQ` error returned by `iconv()` can mean two different things:
 *   there is either an invalid character in the input buffer (so invalid wrt.
 *   the *origin* codeset), or there is a valid character in the input buffer
 *   that cannot be represented in the *target* codeset.
 *
 *   To fix this flaw, an `iconv2()` function could simply return two different
 *   error codes.
 *
 * - When the `EILSEQ` error is returned by `iconv()`, “`*inbuf` is left
 *   pointing to the beginning of the invalid multibyte sequence”, however we
 *   don't know the length of the multibyte sequence! So in practice we assume
 *   only one invalid byte, and call `iconv()` again with `inbuf` pointing to
 *   the next byte, which may fail again, and so on.
 *
 *   An `iconv2()` function would need a more elaborate API to return this kind
 *   of information. For example by returning/filling a struct as an optional
 *   output parameter.
 *
 * Additionally, `iconv()` has different implementations (so working code on
 * Linux can behave differently on a BSD or Solaris).
 */

#define CLOSED_CONV_DESCRIPTOR ((GIConv) -1)

struct _GflsIconv
{
	GIConv conv_descriptor;
};

static gboolean
is_opened (GflsIconv *conv)
{
	return conv->conv_descriptor != CLOSED_CONV_DESCRIPTOR;
}

/**
 * gfls_iconv_new: (skip)
 *
 * Returns: (transfer full): a new #GflsIconv.
 * Since: 0.4
 */
GflsIconv *
gfls_iconv_new (void)
{
	GflsIconv *conv;

	conv = g_new0 (GflsIconv, 1);
	conv->conv_descriptor = CLOSED_CONV_DESCRIPTOR;

	return conv;
}

/**
 * gfls_iconv_open: (skip)
 * @conv: a #GflsIconv.
 * @to_codeset: destination codeset.
 * @from_codeset: source codeset.
 * @error: location to a #GError, or %NULL to ignore errors.
 *
 * Similar to g_iconv_open(). The difference is that it returns a #GError
 * instead of `errno`.
 *
 * Returns: %TRUE on success, %FALSE on error.
 * Since: 0.4
 */
gboolean
gfls_iconv_open (GflsIconv    *conv,
		 const gchar  *to_codeset,
		 const gchar  *from_codeset,
		 GError      **error)
{
	g_return_val_if_fail (conv != 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 (error == NULL || *error == NULL, FALSE);
	g_return_val_if_fail (!is_opened (conv), FALSE);

	conv->conv_descriptor = g_iconv_open (to_codeset, from_codeset);

	if (conv->conv_descriptor == CLOSED_CONV_DESCRIPTOR)
	{
		gint saved_errno = errno;
		errno = 0;

		if (saved_errno == EINVAL)
		{
			g_set_error (error,
				     G_CONVERT_ERROR,
				     G_CONVERT_ERROR_NO_CONVERSION,
				     _("Conversion from character set “%s” to “%s” is not supported."),
				     from_codeset,
				     to_codeset);
		}
		else
		{
			g_set_error (error,
				     G_CONVERT_ERROR,
				     G_CONVERT_ERROR_FAILED,
				     _("Failed to open a character set converter from “%s” to “%s”: %s"),
				     from_codeset,
				     to_codeset,
				     g_strerror (saved_errno));
		}

		return FALSE;
	}

	return TRUE;
}

/**
 * gfls_iconv_feed: (skip)
 * @conv: a #GflsIconv.
 * @inbuf: (nullable): bytes to convert.
 * @inbytes_left: (nullable) (inout): bytes remaining to convert in @inbuf.
 * @outbuf: (not nullable): converted output bytes.
 * @outbytes_left: (not nullable) (inout): bytes available to fill in @outbuf.
 * @error: location to a #GError, or %NULL to ignore errors.
 *
 * Similar to g_iconv(). This function reads the `errno` value and converts it
 * either to a #GError, or to a #GflsIconvResult enumeration value.
 *
 * @error is set only when %GFLS_ICONV_RESULT_ERROR is returned.
 *
 * Returns: a #GflsIconvResult enumeration value.
 * Since: 0.4
 */
GflsIconvResult
gfls_iconv_feed (GflsIconv  *conv,
		 gchar     **inbuf,
		 gsize      *inbytes_left,
		 gchar     **outbuf,
		 gsize      *outbytes_left,
		 GError    **error)
{
	gsize iconv_ret;

	g_return_val_if_fail (conv != NULL, FALSE);
	g_return_val_if_fail (outbuf != NULL, FALSE);
	g_return_val_if_fail (outbytes_left != NULL, FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	iconv_ret = g_iconv (conv->conv_descriptor,
			     inbuf, inbytes_left,
			     outbuf, outbytes_left);

	if (iconv_ret == (gsize)-1)
	{
		gint saved_errno = errno;
		errno = 0;

		if (saved_errno == EILSEQ)
		{
			return GFLS_ICONV_RESULT_ILLEGAL_SEQUENCE;
		}
		else if (saved_errno == EINVAL)
		{
			return GFLS_ICONV_RESULT_INCOMPLETE_INPUT;
		}
		else if (saved_errno == E2BIG)
		{
			return GFLS_ICONV_RESULT_OUTPUT_BUFFER_FULL;
		}
		else
		{
			g_set_error (error,
				     G_CONVERT_ERROR,
				     G_CONVERT_ERROR_FAILED,
				     _("Error during character set conversion: %s"),
				     g_strerror (saved_errno));

			return GFLS_ICONV_RESULT_ERROR;
		}
	}
	else if (iconv_ret > 0)
	{
		/* Note: in the other cases above (e.g., buffer full), how to
		 * know if iconv() has performed lossy conversions before
		 * encountering the error condition? So the outbuf might contain
		 * lossy conversions without a way to know it!
		 */
		return GFLS_ICONV_RESULT_LOSSY_CONVERSION;
	}

	return GFLS_ICONV_RESULT_OK;
}

/**
 * gfls_iconv_feed_discard_output: (skip)
 * @conv: a #GflsIconv.
 * @inbuf: (nullable): bytes to convert.
 * @inbytes_left: (nullable) (inout): bytes remaining to convert in @inbuf.
 * @error: location to a #GError, or %NULL to ignore errors.
 *
 * Similar to gfls_iconv_feed() but without the output buffer parameters.
 *
 * Returns: a #GflsIconvResult enumeration value.
 *   %GFLS_ICONV_RESULT_OUTPUT_BUFFER_FULL is never returned by this
 *   function.
 * Since: 0.4
 */
GflsIconvResult
gfls_iconv_feed_discard_output (GflsIconv  *conv,
				gchar     **inbuf,
				gsize      *inbytes_left,
				GError    **error)
{
#define OUTPUT_BUFFER_SIZE (1024)
	gchar output_buffer[OUTPUT_BUFFER_SIZE];
	GflsIconvResult result;

	g_return_val_if_fail (conv != NULL, FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	do
	{
		gchar *output_buffer_pos = output_buffer;
		gsize output_buffer_n_bytes_left = OUTPUT_BUFFER_SIZE;

		result = gfls_iconv_feed (conv,
					  inbuf, inbytes_left,
					  &output_buffer_pos, &output_buffer_n_bytes_left,
					  error);
	}
	while (result == GFLS_ICONV_RESULT_OUTPUT_BUFFER_FULL);

	return result;
#undef OUTPUT_BUFFER_SIZE
}

/**
 * gfls_iconv_close: (skip)
 * @conv: a #GflsIconv.
 * @error: location to a #GError, or %NULL to ignore errors.
 *
 * Similar to g_iconv_close(). The difference is that it returns a #GError
 * instead of `errno`.
 *
 * Returns: %TRUE on success, %FALSE on error.
 * Since: 0.4
 */
gboolean
gfls_iconv_close (GflsIconv  *conv,
		  GError    **error)
{
	g_return_val_if_fail (conv != NULL, FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	if (is_opened (conv))
	{
		gint close_ret = g_iconv_close (conv->conv_descriptor);

		if (close_ret == -1)
		{
			gint saved_errno = errno;
			errno = 0;

			g_set_error (error,
				     G_CONVERT_ERROR,
				     G_CONVERT_ERROR_FAILED,
				     _("Failed to close the character set converter: %s"),
				     g_strerror (saved_errno));

			return FALSE;
		}

		conv->conv_descriptor = CLOSED_CONV_DESCRIPTOR;
	}

	return TRUE;
}

/**
 * gfls_iconv_free: (skip)
 * @conv: (nullable): a #GflsIconv, or %NULL.
 *
 * Closes and frees @conv.
 *
 * If you need to know if closing @conv returns an error, call
 * gfls_iconv_close() explicitly beforehand.
 *
 * Since: 0.4
 */
void
gfls_iconv_free (GflsIconv *conv)
{
	if (conv != NULL)
	{
		gfls_iconv_close (conv, NULL);
		g_free (conv);
	}
}
