/* SPDX-FileCopyrightText: 2026 - Sébastien Wilmet
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */

#include "gfls-bytes-region.h"
#include "gfls-bytes-region-private.h"

/**
 * SECTION:gfls-bytes-region
 * @Title: GflsBytesRegion
 * @Short_description: A group of sub-regions of a #GBytes
 *
 * #GflsBytesRegion permits to store a group of sub-regions of a #GBytes. Use a
 * #GflsBytesRegionBuilder to construct a #GflsBytesRegion.
 *
 * Use-case: the region delimits valid (or invalid) bytes in a specified
 * character encoding, possibly after a conversion (or at least a validation).
 *
 * To iterate through the sub-regions, you need to use a #GflsBytesRegionIter:
 * |[
 * GflsBytesRegion *region;
 * GflsBytesRegionIter *iter;
 * GBytes *bytes;
 *
 * if (!gfls_bytes_region_match_bytes (region, bytes))
 * {
 *         return;
 * }
 *
 * iter = gfls_bytes_region_get_start_iter (region);
 *
 * while (!gfls_bytes_region_iter_is_end (region, iter))
 * {
 *         gsize sub_region_size = 0;
 *         gsize offset = 0;
 *         gboolean is_part_of_region = FALSE;
 *         gconstpointer sub_region_data;
 *
 *         gfls_bytes_region_iter_get_sub_region (region,
 *                                                iter,
 *                                                &sub_region_size,
 *                                                &offset,
 *                                                &is_part_of_region);
 *
 *         sub_region_data = g_bytes_get_region (bytes,
 *                                               sub_region_size,
 *                                               offset,
 *                                               1);
 *
 *         // Do something useful with the sub-region.
 *
 *         gfls_bytes_region_iter_next (region, iter);
 * }
 *
 * gfls_bytes_region_iter_free (region, iter);
 * ]|
 *
 * This traverses the #GBytes from start to end. @is_part_of_region permits to
 * know if the sub-region is part of the #GflsBytesRegion or if it is a "hole".
 * (A hole doesn't mean that there are no bytes, the meaning depends on the
 * use-case).
 */

struct _GflsBytesRegion
{
	GArray *sub_regions; /* (element-type GflsBytesSubRegionBounds) */
	gsize total_size;
};

/* Example:
 * - GflsBytesRegion's @sub_regions is: [1, 3], [4, 5]
 * - GflsBytesRegion's @total_size is: 6
 *
 * Then the GflsBytesRegionIter will go through these values:
 * - { 0, TRUE }  : corresponds to the [0, 1] range.
 * - { 0, FALSE } : before=false which means *inside* the sub-region [1, 3].
 * - { 1, TRUE }  : between [1, 3] and [4, 5], so [3, 4].
 * - { 1, FALSE } : [4, 5].
 * - { 2, TRUE }  : outside the sub_regions GArray, which means *after* the last
 *                  sub-region, here [5, 6].
 * - { 2, FALSE } : the end iterator.
 *
 * A GflsBytesRegionIter needs to internally skip empty ranges.
 */
struct _GflsBytesRegionIter
{
	guint sub_region_index;
	guint before : 1;
};

GflsBytesRegion *
_gfls_bytes_region_new (GArray *sub_regions,
			gsize   total_size)
{
	GflsBytesRegion *region;

	g_return_val_if_fail (sub_regions != NULL, NULL);

	region = g_new0 (GflsBytesRegion, 1);
	region->sub_regions = g_array_ref (sub_regions);
	region->total_size = total_size;

	return region;
}

/**
 * gfls_bytes_region_free:
 * @region: (nullable): a #GflsBytesRegion.
 *
 * Frees @region.
 *
 * Since: 0.4
 */
void
gfls_bytes_region_free (GflsBytesRegion *region)
{
	if (region != NULL)
	{
		g_array_unref (region->sub_regions);
		g_free (region);
	}
}

/**
 * gfls_bytes_region_match_bytes:
 * @region: a #GflsBytesRegion.
 * @bytes: a #GBytes.
 *
 * Returns: whether @region can be applied to @bytes (it checks the total size).
 * Since: 0.4
 */
gboolean
gfls_bytes_region_match_bytes (GflsBytesRegion *region,
			       GBytes          *bytes)
{
	g_return_val_if_fail (region != NULL, FALSE);
	g_return_val_if_fail (bytes != NULL, FALSE);

	return region->total_size == g_bytes_get_size (bytes);
}

static gboolean
iter_validate (GflsBytesRegion     *region,
	       GflsBytesRegionIter *iter,
	       gboolean             treat_end_iter_as_valid)
{
	gboolean valid;

	g_return_val_if_fail (region != NULL, FALSE);
	g_return_val_if_fail (iter != NULL, FALSE);

	valid = iter->sub_region_index <= region->sub_regions->len;

	if (treat_end_iter_as_valid)
	{
		return valid;
	}

	return (valid &&
		!gfls_bytes_region_iter_is_end (region, iter));
}

static GflsBytesSubRegionBounds
iter_get_sub_region_bounds (GflsBytesRegion     *region,
			    GflsBytesRegionIter *iter)
{
	GflsBytesSubRegionBounds prev_sub_region_bounds = { 0 };
	GflsBytesSubRegionBounds next_sub_region_bounds = { 0 };
	GflsBytesSubRegionBounds ret = { 0 };

	g_return_val_if_fail (iter_validate (region, iter, FALSE), ret);

	/* _Inside_ a sub-region (the easy case) */

	if (!iter->before)
	{
		return g_array_index (region->sub_regions,
				      GflsBytesSubRegionBounds,
				      iter->sub_region_index);
	}

	/* _Outside_ a sub-region */

	// Get prev_sub_region_bounds

	if (iter->sub_region_index == 0)
	{
		prev_sub_region_bounds.start = 0;
		prev_sub_region_bounds.end = 0;
	}
	else
	{
		prev_sub_region_bounds = g_array_index (region->sub_regions,
							GflsBytesSubRegionBounds,
							iter->sub_region_index - 1);
	}

	// Get next_sub_region_bounds

	if (iter->sub_region_index < region->sub_regions->len)
	{
		next_sub_region_bounds = g_array_index (region->sub_regions,
							GflsBytesSubRegionBounds,
							iter->sub_region_index);
	}
	else
	{
		next_sub_region_bounds.start = region->total_size;
		next_sub_region_bounds.end = region->total_size;
	}

	// Return value

	ret.start = prev_sub_region_bounds.end;
	ret.end = next_sub_region_bounds.start;

	return ret;
}

static gboolean
iter_sub_region_is_empty (GflsBytesRegion     *region,
			  GflsBytesRegionIter *iter)
{
	gsize sub_region_size = 0;
	gsize offset = 0;
	gboolean is_part_of_region = FALSE;

	gfls_bytes_region_iter_get_sub_region (region,
					       iter,
					       &sub_region_size,
					       &offset,
					       &is_part_of_region);

	return sub_region_size == 0;
}

/* "Simple", i.e. without skipping empty sub-regions. */
static void
iter_next_simple (GflsBytesRegion     *region,
		  GflsBytesRegionIter *iter)
{
	if (gfls_bytes_region_iter_is_end (region, iter))
	{
		/* NOP */
		return;
	}

	if (iter->before)
	{
		iter->before = FALSE;
	}
	else
	{
		iter->before = TRUE;
		iter->sub_region_index++;
	}
}

static void
iter_skip_empty_sub_regions (GflsBytesRegion     *region,
			     GflsBytesRegionIter *iter)
{
	while (!gfls_bytes_region_iter_is_end (region, iter) &&
	       iter_sub_region_is_empty (region, iter))
	{
		iter_next_simple (region, iter);
	}
}

/**
 * gfls_bytes_region_get_start_iter: (skip)
 * @region: a #GflsBytesRegion.
 *
 * Returns: (transfer full): a new #GflsBytesRegionIter located at the
 *   beginning. Free with gfls_bytes_region_iter_free().
 * Since: 0.4
 */
GflsBytesRegionIter *
gfls_bytes_region_get_start_iter (GflsBytesRegion *region)
{
	GflsBytesRegionIter *iter;

	g_return_val_if_fail (region != NULL, NULL);

	iter = g_new0 (GflsBytesRegionIter, 1);
	iter->sub_region_index = 0;
	iter->before = TRUE;

	iter_skip_empty_sub_regions (region, iter);

	return iter;
}

/**
 * gfls_bytes_region_iter_free:
 * @region: a #GflsBytesRegion.
 * @iter: (nullable): a #GflsBytesRegionIter.
 *
 * Frees @iter.
 *
 * Since: 0.4
 */
void
gfls_bytes_region_iter_free (GflsBytesRegion     *region,
			     GflsBytesRegionIter *iter)
{
	g_free (iter);
}

/**
 * gfls_bytes_region_iter_is_end:
 * @region: a #GflsBytesRegion.
 * @iter: a #GflsBytesRegionIter.
 *
 * Returns: whether @iter is the end iterator.
 * Since: 0.4
 */
gboolean
gfls_bytes_region_iter_is_end (GflsBytesRegion     *region,
			       GflsBytesRegionIter *iter)
{
	g_return_val_if_fail (region != NULL, FALSE);
	g_return_val_if_fail (iter != NULL, FALSE);

	return (!iter->before &&
		iter->sub_region_index == region->sub_regions->len);
}

/**
 * gfls_bytes_region_iter_get_sub_region:
 * @region: a #GflsBytesRegion.
 * @iter: a #GflsBytesRegionIter. It must not be the end iterator.
 * @sub_region_size: (out) (not nullable): the sub-region size.
 * @offset: (out) (not nullable): the offset to the start of the sub-region.
 * @is_part_of_region: (out) (not nullable): whether the sub-region is part of the region.
 *
 * Gets the sub-region at this iterator.
 *
 * @sub_region_size and @offset can be used as arguments to
 * g_bytes_get_region().
 *
 * @is_part_of_region has the same meaning as for
 * gfls_bytes_region_builder_append(). #GflsBytesRegionIter iterates on both the
 * region and the holes, so that the corresponding #GBytes is traversed from
 * start to end.
 *
 * Since: 0.4
 */
void
gfls_bytes_region_iter_get_sub_region (GflsBytesRegion     *region,
				       GflsBytesRegionIter *iter,
				       gsize               *sub_region_size,
				       gsize               *offset,
				       gboolean            *is_part_of_region)
{
	GflsBytesSubRegionBounds sub_region_bounds;

	if (sub_region_size != NULL)
	{
		*sub_region_size = 0;
	}
	if (offset != NULL)
	{
		*offset = 0;
	}
	if (is_part_of_region != NULL)
	{
		*is_part_of_region = FALSE;
	}

	g_return_if_fail (region != NULL);
	g_return_if_fail (iter != NULL);
	g_return_if_fail (iter_validate (region, iter, FALSE));
	g_return_if_fail (sub_region_size != NULL);
	g_return_if_fail (offset != NULL);
	g_return_if_fail (is_part_of_region != NULL);

	sub_region_bounds = iter_get_sub_region_bounds (region, iter);

	g_return_if_fail (sub_region_bounds.end >= sub_region_bounds.start);
	*sub_region_size = sub_region_bounds.end - sub_region_bounds.start;

	*offset = sub_region_bounds.start;

	*is_part_of_region = !iter->before;
}

/**
 * gfls_bytes_region_iter_next:
 * @region: a #GflsBytesRegion.
 * @iter: a #GflsBytesRegionIter. The end iterator is accepted as an input
 *   value.
 *
 * Moves @iter to the next sub-region.
 *
 * If all sub-regions have been traversed, @iter is set to the end iterator.
 *
 * Since: 0.4
 */
void
gfls_bytes_region_iter_next (GflsBytesRegion     *region,
			     GflsBytesRegionIter *iter)
{
	g_return_if_fail (region != NULL);
	g_return_if_fail (iter != NULL);
	g_return_if_fail (iter_validate (region, iter, TRUE));

	iter_next_simple (region, iter);
	iter_skip_empty_sub_regions (region, iter);
}

/**
 * gfls_bytes_region_to_string:
 * @region: a #GflsBytesRegion.
 *
 * The format is:
 *
 * `[offset, sub_region_size, is_part_of_region]\n`
 *
 * One line per sub-region, in order.
 *
 * Returns: (transfer full): a string representation of @region.
 * Since: 0.4
 */
gchar *
gfls_bytes_region_to_string (GflsBytesRegion *region)
{
	GString *string;
	GflsBytesRegionIter *iter;

	g_return_val_if_fail (region != NULL, NULL);

	string = g_string_new (NULL);

	iter = gfls_bytes_region_get_start_iter (region);

	while (!gfls_bytes_region_iter_is_end (region, iter))
	{
		gsize sub_region_size = 0;
		gsize offset = 0;
		gboolean is_part_of_region = FALSE;

		gfls_bytes_region_iter_get_sub_region (region,
						       iter,
						       &sub_region_size,
						       &offset,
						       &is_part_of_region);

		g_string_append_printf (string,
					"[%" G_GSIZE_FORMAT ", %" G_GSIZE_FORMAT ", %d]\n",
					offset,
					sub_region_size,
					is_part_of_region);

		gfls_bytes_region_iter_next (region, iter);
	}

	gfls_bytes_region_iter_free (region, iter);

	return g_string_free (string, FALSE);
}
