/*
 * e-data-capture.c
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) version 3.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with the program; if not, see <http://www.gnu.org/licenses/>
 *
 */

/**
 * SECTION: e-data-capture
 * @include: e-util/e-util.h
 * @short_description: Capture data from streams
 *
 * #EDataCapture is a #GConverter that captures data until the end of
 * the input data is seen, then emits a #EDataCapture:finished signal
 * with the captured data in a #GBytes instance.
 *
 * When used with #GConverterInputStream or #GConverterOutputStream,
 * an #EDataCapture can discreetly capture the stream content for the
 * purpose of caching.
 **/

#include "e-data-capture.h"

#include <string.h>

#define E_DATA_CAPTURE_GET_PRIVATE(obj) \
	(G_TYPE_INSTANCE_GET_PRIVATE \
	((obj), E_TYPE_DATA_CAPTURE, EDataCapturePrivate))

typedef struct _SignalClosure SignalClosure;

struct _EDataCapturePrivate {
	GMainContext *main_context;
	GByteArray *byte_array;
	GMutex byte_array_lock;
};

struct _SignalClosure {
	GWeakRef data_capture;
	GBytes *data;
};

enum {
	PROP_0,
	PROP_MAIN_CONTEXT
};

enum {
	FINISHED,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL];

/* Forward Declarations */
static void	e_data_capture_converter_init	(GConverterIface *interface);

G_DEFINE_TYPE_WITH_CODE (
	EDataCapture,
	e_data_capture,
	G_TYPE_OBJECT,
	G_IMPLEMENT_INTERFACE (
		G_TYPE_CONVERTER,
		e_data_capture_converter_init))

static void
signal_closure_free (SignalClosure *signal_closure)
{
	g_weak_ref_set (&signal_closure->data_capture, NULL);
	g_bytes_unref (signal_closure->data);

	g_slice_free (SignalClosure, signal_closure);
}

static gboolean
data_capture_emit_finished_idle_cb (gpointer user_data)
{
	SignalClosure *signal_closure = user_data;
	EDataCapture *data_capture;

	data_capture = g_weak_ref_get (&signal_closure->data_capture);

	if (data_capture != NULL) {
		g_signal_emit (
			data_capture,
			signals[FINISHED], 0,
			signal_closure->data);
		g_object_unref (data_capture);
	}

	return FALSE;
}

static void
data_capture_set_main_context (EDataCapture *data_capture,
                               GMainContext *main_context)
{
	g_return_if_fail (data_capture->priv->main_context == NULL);

	if (main_context != NULL)
		g_main_context_ref (main_context);
	else
		main_context = g_main_context_ref_thread_default ();

	data_capture->priv->main_context = main_context;
}

static void
data_capture_set_property (GObject *object,
                           guint property_id,
                           const GValue *value,
                           GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_MAIN_CONTEXT:
			data_capture_set_main_context (
				E_DATA_CAPTURE (object),
				g_value_get_boxed (value));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
data_capture_get_property (GObject *object,
                           guint property_id,
                           GValue *value,
                           GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_MAIN_CONTEXT:
			g_value_take_boxed (
				value,
				e_data_capture_ref_main_context (
				E_DATA_CAPTURE (object)));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
data_capture_finalize (GObject *object)
{
	EDataCapturePrivate *priv;

	priv = E_DATA_CAPTURE_GET_PRIVATE (object);

	g_main_context_unref (priv->main_context);

	g_byte_array_free (priv->byte_array, TRUE);
	g_mutex_clear (&priv->byte_array_lock);

	/* Chain up to parent's finalize() method. */
	G_OBJECT_CLASS (e_data_capture_parent_class)->finalize (object);
}

static GConverterResult
data_capture_convert (GConverter *converter,
                      gconstpointer inbuf,
                      gsize inbuf_size,
                      gpointer outbuf,
                      gsize outbuf_size,
                      GConverterFlags flags,
                      gsize *bytes_read,
                      gsize *bytes_written,
                      GError **error)
{
	EDataCapture *data_capture;
	GConverterResult result;

	data_capture = E_DATA_CAPTURE (converter);

	/* Output buffer needs to be at least as large as the input buffer.
	 * The error message should never make it to the user interface so
	 * no need to translate. */
	if (outbuf_size < inbuf_size) {
		g_set_error_literal (
			error, G_IO_ERROR,
			G_IO_ERROR_NO_SPACE,
			"EDataCapture needs more space");
		return G_CONVERTER_ERROR;
	}

	memcpy (outbuf, inbuf, inbuf_size);
	*bytes_read = *bytes_written = inbuf_size;

	g_mutex_lock (&data_capture->priv->byte_array_lock);

	g_byte_array_append (
		data_capture->priv->byte_array, inbuf, inbuf_size);

	if ((flags & G_CONVERTER_INPUT_AT_END) != 0) {
		GSource *idle_source;
		GMainContext *main_context;
		SignalClosure *signal_closure;

		signal_closure = g_slice_new0 (SignalClosure);
		g_weak_ref_set (&signal_closure->data_capture, data_capture);
		signal_closure->data = g_bytes_new (
			data_capture->priv->byte_array->data,
			data_capture->priv->byte_array->len);

		main_context = e_data_capture_ref_main_context (data_capture);

		idle_source = g_idle_source_new ();
		g_source_set_callback (
			idle_source,
			data_capture_emit_finished_idle_cb,
			signal_closure,
			(GDestroyNotify) signal_closure_free);
		g_source_set_priority (idle_source, G_PRIORITY_HIGH_IDLE);
		g_source_attach (idle_source, main_context);
		g_source_unref (idle_source);

		g_main_context_unref (main_context);
	}

	g_mutex_unlock (&data_capture->priv->byte_array_lock);

	if ((flags & G_CONVERTER_INPUT_AT_END) != 0)
		result = G_CONVERTER_FINISHED;
	else if ((flags & G_CONVERTER_FLUSH) != 0)
		result = G_CONVERTER_FLUSHED;
	else
		result = G_CONVERTER_CONVERTED;

	return result;
}

static void
data_capture_reset (GConverter *converter)
{
	EDataCapture *data_capture;

	data_capture = E_DATA_CAPTURE (converter);

	g_mutex_lock (&data_capture->priv->byte_array_lock);

	g_byte_array_set_size (data_capture->priv->byte_array, 0);

	g_mutex_unlock (&data_capture->priv->byte_array_lock);
}

static void
e_data_capture_class_init (EDataCaptureClass *class)
{
	GObjectClass *object_class;

	g_type_class_add_private (class, sizeof (EDataCapturePrivate));

	object_class = G_OBJECT_CLASS (class);
	object_class->set_property = data_capture_set_property;
	object_class->get_property = data_capture_get_property;
	object_class->finalize = data_capture_finalize;

	/**
	 * EDataCapture:main-context:
	 *
	 * The #GMainContext from which to emit the #EDataCapture::finished
	 * signal.
	 **/
	g_object_class_install_property (
		object_class,
		PROP_MAIN_CONTEXT,
		g_param_spec_boxed (
			"main-context",
			"Main Context",
			"The main loop context from "
			"which to emit the 'finished' signal",
			G_TYPE_MAIN_CONTEXT,
			G_PARAM_READWRITE |
			G_PARAM_CONSTRUCT_ONLY |
			G_PARAM_STATIC_STRINGS));

	/**
	 * EDataCapture::finished:
	 * @data_capture: the #EDataCapture that received the signal
	 * @data: the captured data
	 *
	 * The ::finished signal is emitted when there is no more input
	 * data to be captured.
	 **/
	signals[FINISHED] = g_signal_new (
		"finished",
		G_TYPE_FROM_CLASS (class),
		G_SIGNAL_RUN_FIRST,
		G_STRUCT_OFFSET (EDataCaptureClass, finished),
		NULL, NULL, NULL,
		G_TYPE_NONE, 1,
		G_TYPE_BYTES);
}

static void
e_data_capture_converter_init (GConverterIface *interface)
{
	interface->convert = data_capture_convert;
	interface->reset = data_capture_reset;
}

static void
e_data_capture_init (EDataCapture *data_capture)
{
	data_capture->priv = E_DATA_CAPTURE_GET_PRIVATE (data_capture);

	data_capture->priv->byte_array = g_byte_array_new ();
	g_mutex_init (&data_capture->priv->byte_array_lock);
}

/**
 * e_data_capture_new:
 * @main_context: a #GMainContext, or %NULL
 *
 * Creates a new #EDataCapture.  If @main_context is %NULL, then the
 * #EDataCapture:finished signal will be emitted from the thread-default
 * #GMainContext for this thread.
 *
 * Returns: an #EDataCapture
 **/
EDataCapture *
e_data_capture_new (GMainContext *main_context)
{
	return g_object_new (
		E_TYPE_DATA_CAPTURE,
		"main-context", main_context, NULL);
}

/**
 * e_data_capture_ref_main_context:
 * @data_capture: an #EDataCapture
 *
 * Returns the #GMainContext from which the #EDataCapture:finished signal
 * is emitted.
 *
 * The returned #GMainContext is referenced for thread-safety and must be
 * unreferenced with g_main_context_unref() when finished with it.
 *
 * Returns: a #GMainContext
 **/
GMainContext *
e_data_capture_ref_main_context (EDataCapture *data_capture)
{
	g_return_val_if_fail (E_IS_DATA_CAPTURE (data_capture), NULL);

	return g_main_context_ref (data_capture->priv->main_context);
}