/*
 * SPDX-License-Identifier: LGPL-2.0-or-later
 */

/**
 * CamelFilterOutputStream
 *
 * Filtered output stream.
 *
 * #CamelFilterOutputStream is similar to #GConverterOutputStream, except it
 * operates on a #CamelMimeFilter instead of a #GConverter.
 *
 * This class is meant to be a temporary solution until all of Camel's MIME
 * filters are ported to the #GConverter interface.
 **/

#include "camel-filter-output-stream.h"

#include <string.h>

#define READ_PAD (128)		/* bytes padded before buffer */
#define READ_SIZE (4096)

struct _CamelFilterOutputStreamPrivate {
	CamelMimeFilter *filter;
};

enum {
	PROP_0,
	PROP_FILTER,
	N_PROPS
};

static GParamSpec *properties[N_PROPS] = { NULL, };

G_DEFINE_TYPE_WITH_PRIVATE (
	CamelFilterOutputStream,
	camel_filter_output_stream,
	G_TYPE_FILTER_OUTPUT_STREAM)

static void
filter_output_stream_set_filter (CamelFilterOutputStream *filter_stream,
                                 CamelMimeFilter *filter)
{
	g_return_if_fail (CAMEL_IS_MIME_FILTER (filter));
	g_return_if_fail (filter_stream->priv->filter == NULL);

	filter_stream->priv->filter = g_object_ref (filter);
}

static void
filter_output_stream_set_property (GObject *object,
                                   guint property_id,
                                   const GValue *value,
                                   GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_FILTER:
			filter_output_stream_set_filter (
				CAMEL_FILTER_OUTPUT_STREAM (object),
				g_value_get_object (value));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
filter_output_stream_get_property (GObject *object,
                                   guint property_id,
                                   GValue *value,
                                   GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_FILTER:
			g_value_set_object (
				value,
				camel_filter_output_stream_get_filter (
				CAMEL_FILTER_OUTPUT_STREAM (object)));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
filter_output_stream_dispose (GObject *object)
{
	CamelFilterOutputStreamPrivate *priv;

	priv = CAMEL_FILTER_OUTPUT_STREAM (object)->priv;

	/* XXX GOutputStream calls flush() one last time during
	 *     dispose(), so chain up before clearing our filter. */
	G_OBJECT_CLASS (camel_filter_output_stream_parent_class)->
		dispose (object);

	g_clear_object (&priv->filter);
}

static gssize
filter_output_stream_write (GOutputStream *stream,
                            gconstpointer buffer,
                            gsize count,
                            GCancellable *cancellable,
                            GError **error)
{
	CamelMimeFilter *filter;
	GOutputStream *base_stream;
	gchar real_buffer[READ_SIZE + READ_PAD];
	const gchar *input_buffer = buffer;
	gsize bytes_left = count;

	filter = camel_filter_output_stream_get_filter (
		CAMEL_FILTER_OUTPUT_STREAM (stream));
	base_stream = g_filter_output_stream_get_base_stream (
		G_FILTER_OUTPUT_STREAM (stream));

	while (bytes_left > 0) {
		gsize length;
		gsize presize;
		gchar *bufptr;
		gboolean success;

		bufptr = real_buffer + READ_PAD;
		length = MIN (READ_SIZE, bytes_left);
		memcpy (bufptr, input_buffer, length);
		input_buffer += length;
		bytes_left -= length;

		presize = READ_PAD;

		camel_mime_filter_filter (
			filter, bufptr, length, presize,
			&bufptr, &length, &presize);

		/* XXX The bytes_written argument can be NULL,
		 *     even though the API docs don't say so. */
		success = g_output_stream_write_all (
			base_stream, bufptr, length,
			NULL, cancellable, error);
		if (!success)
			return -1;

		if (camel_mime_filter_get_request_stop (filter))
			return 0;
	}

	return count;
}

static gboolean
filter_output_stream_flush (GOutputStream *stream,
                            GCancellable *cancellable,
                            GError **error)
{
	CamelMimeFilter *filter;
	GOutputStream *base_stream;
	gchar *bufptr = (gchar *) "";
	gsize length = 0;
	gsize presize = 0;
	gboolean success = TRUE;

	filter = camel_filter_output_stream_get_filter (
		CAMEL_FILTER_OUTPUT_STREAM (stream));
	base_stream = g_filter_output_stream_get_base_stream (
		G_FILTER_OUTPUT_STREAM (stream));

	camel_mime_filter_complete (
		filter, bufptr, length, presize,
		&bufptr, &length, &presize);

	if (length > 0) {
		/* XXX The bytes_written argument can be NULL,
		 *     even though the API docs don't say so. */
		success = g_output_stream_write_all (
			base_stream, bufptr, length,
			NULL, cancellable, error);
	}

	if (success) {
		success = g_output_stream_flush (
			base_stream, cancellable, error);
	}

	return success;
}

static void
camel_filter_output_stream_class_init (CamelFilterOutputStreamClass *class)
{
	GObjectClass *object_class;
	GOutputStreamClass *stream_class;

	object_class = G_OBJECT_CLASS (class);
	object_class->set_property = filter_output_stream_set_property;
	object_class->get_property = filter_output_stream_get_property;
	object_class->dispose = filter_output_stream_dispose;

	stream_class = G_OUTPUT_STREAM_CLASS (class);
	stream_class->write_fn = filter_output_stream_write;
	stream_class->flush = filter_output_stream_flush;

	/**
	 * CamelFilterOutputStream:filter
	 *
	 * The MIME filter object
	 **/
	properties[PROP_FILTER] =
		g_param_spec_object (
			"filter", NULL, NULL,
			CAMEL_TYPE_MIME_FILTER,
			G_PARAM_READWRITE |
			G_PARAM_CONSTRUCT_ONLY |
			G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
camel_filter_output_stream_init (CamelFilterOutputStream *filter_stream)
{
	filter_stream->priv = camel_filter_output_stream_get_instance_private (filter_stream);
}

/**
 * camel_filter_output_stream_new:
 * @base_stream: a #GOutputStream
 * @filter: a #CamelMimeFilter
 *
 * Creates a new filtered output stream for the @base_stream.
 *
 * Returns: a new #GOutputStream
 *
 * Since: 3.12
 **/
GOutputStream *
camel_filter_output_stream_new (GOutputStream *base_stream,
                                CamelMimeFilter *filter)
{
	g_return_val_if_fail (G_IS_OUTPUT_STREAM (base_stream), NULL);
	g_return_val_if_fail (CAMEL_IS_MIME_FILTER (filter), NULL);

	return g_object_new (
		CAMEL_TYPE_FILTER_OUTPUT_STREAM,
		"base-stream", base_stream,
		"filter", filter, NULL);
}

/**
 * camel_filter_output_stream_get_filter:
 * @filter_stream: a #CamelFilterOutputStream
 *
 * Gets the #CamelMimeFilter that is used by @filter_stream.
 *
 * Returns: (transfer none): a #CamelMimeFilter
 *
 * Since: 3.12
 **/
CamelMimeFilter *
camel_filter_output_stream_get_filter (CamelFilterOutputStream *filter_stream)
{
	g_return_val_if_fail (
		CAMEL_IS_FILTER_OUTPUT_STREAM (filter_stream), NULL);

	return filter_stream->priv->filter;
}

