/* vim: set sw=8: -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * gsf-output.c: interface for storing data
 *
 * Copyright (C) 2002-2006 Jody Goldberg (jody@gnome.org)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2.1 of the GNU Lesser General Public
 * License as published by the Free Software Foundation.
 *
 * 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
 * USA
 */

#include <gsf-config.h>
#include <gsf/gsf-output.h>
#include <gsf/gsf.h>
#include <glib/gi18n-lib.h>

#include <string.h>

/*
 * FIXME!
 *
 * We cannot extend GsfOutput, so for now we hang this on the object as
 * an attribute.
 */
#define MODTIME_ATTR "GsfOutput::modtime"


static gsf_off_t gsf_output_real_vprintf (GsfOutput *output,
	char const* format, va_list args) G_GNUC_PRINTF (2, 0);

#define GET_CLASS(instance) G_TYPE_INSTANCE_GET_CLASS (instance, GSF_OUTPUT_TYPE, GsfOutputClass)

static GObjectClass *parent_class;

enum {
	PROP_0,
	PROP_NAME,
	PROP_SIZE,
	PROP_CLOSED,
	PROP_POS,
	PROP_MODTIME,
	PROP_CONTAINER
};

static void
gsf_output_set_property (GObject      *object,
			 guint         property_id,
			 GValue const *value,
			 GParamSpec   *pspec)
{
	GsfOutput *output = GSF_OUTPUT (object);

	switch (property_id) {
	case PROP_NAME:
		gsf_output_set_name (output, g_value_get_string (value));
		break;
	case PROP_MODTIME:
		gsf_output_set_modtime (output, g_value_get_boxed (value));
		break;
	case PROP_CONTAINER:
		gsf_output_set_container (output, g_value_get_object (value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
		break;
	}
}

static void
gsf_output_get_property (GObject     *object,
			 guint        property_id,
			 GValue      *value,
			 GParamSpec  *pspec)
{
	GsfOutput *output = GSF_OUTPUT (object);

	/* gsf_off_t is typedef'd to gint64 */

	switch (property_id) {
	case PROP_NAME:
		g_value_set_string (value, gsf_output_name (output));
		break;
	case PROP_SIZE:
		g_value_set_int64 (value, gsf_output_size (output));
		break;
	case PROP_CLOSED:
		g_value_set_boolean (value, gsf_output_is_closed (output));
		break;
	case PROP_POS:
		g_value_set_int64 (value, gsf_output_tell (output));
		break;
	case PROP_MODTIME:
		g_value_set_boxed (value, gsf_output_get_modtime (output));
		break;
	case PROP_CONTAINER:
		g_value_set_object (value, output->container);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
		break;
	}
}

static void
gsf_output_dispose (GObject *obj)
{
	GsfOutput *output = GSF_OUTPUT (obj);

	if (!output->is_closed) {
		/* g_warning ("Disposing of an unclosed stream"); */
		gsf_output_close (output);
	}

	gsf_output_set_container (output, NULL);
	gsf_output_set_name (output, NULL);
	gsf_output_set_modtime (output, NULL);

	g_free (output->printf_buf);
	output->printf_buf = NULL;

	g_clear_error (&output->err);

	parent_class->dispose (obj);
}

static void
gsf_output_init (GObject *obj)
{
	GsfOutput *output = GSF_OUTPUT (obj);

	output->cur_offset	= 0;
	output->cur_size	= 0;
	output->name		= NULL;
	output->wrapped_by	= NULL;
	output->container	= NULL;
	output->err		= NULL;
	output->is_closed	= FALSE;
	output->printf_buf	= NULL;
	output->printf_buf_size = 0;
}

static void
gsf_output_class_init (GObjectClass *gobject_class)
{
	GsfOutputClass  *output_class  = GSF_OUTPUT_CLASS (gobject_class);

	gobject_class->dispose      = gsf_output_dispose;
	gobject_class->set_property = gsf_output_set_property;
	gobject_class->get_property = gsf_output_get_property;
	output_class->Vprintf       = gsf_output_real_vprintf;

	parent_class = g_type_class_peek_parent (gobject_class);

	g_object_class_install_property
		(gobject_class,
		 PROP_NAME,
		 g_param_spec_string ("name",
				      _("Name"),
				      _("The output's name"),
				      NULL,
				      G_PARAM_STATIC_STRINGS |
				      G_PARAM_READWRITE));

	/**
	 * GsfOutput:size:
	 *
	 * The current file size.
	 */
	g_object_class_install_property
		(gobject_class,
		 PROP_SIZE,
		 g_param_spec_int64 ("size",
				     _("Size"),
				     _("The output's size"),
				     0, G_MAXINT64, 0,
				     G_PARAM_STATIC_STRINGS |
				     G_PARAM_READABLE));

	/**
	 * GsfOutput:is-closed:
	 *
	 * %TRUE if the output has been closed.
	 */
	g_object_class_install_property
		(gobject_class,
		 PROP_CLOSED,
		 g_param_spec_boolean ("is-closed",
				       _("Is Closed"),
				       _("Whether the output is closed"),
				       FALSE,
				       G_PARAM_STATIC_STRINGS |
				       G_PARAM_READABLE));

	/**
	 * GsfOutput:position:
	 *
	 * The current position in the output.
	 */
	g_object_class_install_property
		(gobject_class,
		 PROP_POS,
		 g_param_spec_int64 ("position",
				     _("Position"),
				     _("The output's current position"),
				     0, G_MAXINT64, 0,
				     G_PARAM_STATIC_STRINGS |
				     G_PARAM_READABLE));

	/**
	 * GsfOutput:modtime:
	 *
	 * The time the output was last updated.  This must be set on object
	 * construction and represents the timestamp to put on the resulting
	 * file or #GsfOutfile member.  Not all derived classes will actually
	 * do anything with this property.
	 */
	g_object_class_install_property
		(gobject_class,
		 PROP_MODTIME,
		 g_param_spec_boxed
		 ("modtime",
		  _("Modification time"),
		  _("An optional GDateTime representing the time the output was last changed"),
		  G_TYPE_DATE_TIME,
		  G_PARAM_STATIC_STRINGS |
		  G_PARAM_CONSTRUCT_ONLY |
		  G_PARAM_READWRITE));

	/**
	 * GsfOutput:container:
	 *
	 * The container, optionally %NULL, in which this output lives.
	 */
	g_object_class_install_property
		(gobject_class, PROP_CONTAINER,
		 g_param_spec_object ("container",
				      _("Container"),
				      _("The parent GsfOutfile"),
				      GSF_OUTFILE_TYPE,
				      G_PARAM_STATIC_STRINGS |
				      G_PARAM_READWRITE));
}

GSF_CLASS_ABSTRACT (GsfOutput, gsf_output,
		    gsf_output_class_init, gsf_output_init,
		    G_TYPE_OBJECT)

/**
 * gsf_output_name:
 * @output: #GsfOutput
 *
 * Give the name of @output.
 *
 * Returns: (transfer none) (nullable): @output's name in utf8 form.
 **/
char const *
gsf_output_name (GsfOutput const *output)
{
	g_return_val_if_fail (GSF_IS_OUTPUT (output), NULL);
	return output->name;
}

/**
 * gsf_output_container:
 * @output:
 *
 * Returns: (transfer none) (nullable): @output's container
 **/
GsfOutfile *
gsf_output_container (GsfOutput const *output)
{
	g_return_val_if_fail (GSF_IS_OUTPUT (output), NULL);
	return output->container;
}

/**
 * gsf_output_size:
 * @output: #GsfOutput
 *
 * Determine the size of the output stream @output.
 *
 * Returns: the size of the output, or -1 if it does not have a size.
 **/
gsf_off_t
gsf_output_size (GsfOutput *output)
{
	g_return_val_if_fail (GSF_IS_OUTPUT (output), -1);
	return output->cur_size;
}

/**
 * gsf_output_close: (virtual Close)
 * @output: #GsfOutput
 *
 * Close a stream.
 *
 * Returns: %FALSE on error
 **/
gboolean
gsf_output_close (GsfOutput *output)
{
	gboolean res;

	g_return_val_if_fail (GSF_IS_OUTPUT (output),
		gsf_output_set_error (output, 0, "<internal>"));
	g_return_val_if_fail (!output->is_closed,
		gsf_output_set_error (output, 0, "<internal>"));

	/* The implementation will log any errors, but we can never try to
	 * close multiple times even on failure.
	 */
	res = GET_CLASS (output)->Close (output);
	output->is_closed = TRUE;
	return res;
}

/**
 * gsf_output_is_closed:
 * @output: #GsfOutput
 *
 * Returns: %TRUE if @output has already been closed.
 **/
gboolean
gsf_output_is_closed (GsfOutput const *output)
{
	g_return_val_if_fail (GSF_IS_OUTPUT (output), TRUE);
	return output->is_closed;
}

/**
 * gsf_output_tell:
 * @output: #GsfOutput
 *
 * Tell the current position in @output, similar to
 * <citerefentry><refentrytitle>ftell</refentrytitle>
 * <manvolnum>3</manvolnum></citerefentry>.
 *
 * Returns: the current position in the file
 **/
gsf_off_t
gsf_output_tell	(GsfOutput *output)
{
	g_return_val_if_fail (output != NULL, 0);

	return output->cur_offset;
}

/**
 * gsf_output_seek: (virtual Seek)
 * @output: #GsfOutput
 * @offset: Relative amount to reposition
 * @whence: What the offset is relative to.
 *
 * Reposition in output stream @output. @whence specifies what the offset is
 * relative to: the beginning of the stream (%G_SEEK_SET), current position in
 * the stream (%G_SEEK_CUR) or the end of the stream (%G_SEEK_END).
 * This function is similar to
 * <citerefentry><refentrytitle>fseek</refentrytitle>
 * <manvolnum>3</manvolnum></citerefentry>.
 *
 * Returns: %FALSE on error.
 **/
gboolean
gsf_output_seek	(GsfOutput *output, gsf_off_t offset, GSeekType whence)
{
	gsf_off_t pos = offset;

	g_return_val_if_fail (output != NULL, FALSE);

	switch (whence) {
	case G_SEEK_SET: break;
	case G_SEEK_CUR: pos += output->cur_offset;	break;
	case G_SEEK_END: pos += output->cur_size;	break;
	default :
		g_warning ("Invalid seek type %d", (int)whence);
		return FALSE;
	}

	if (pos < 0) {
		g_warning ("Invalid seek position %" GSF_OFF_T_FORMAT
			   ", which is before the start of the file", pos);
		return FALSE;
	}

	/* If we go nowhere, just return.  This in particular handles null
	 * seeks for streams with no seek method.
	 */
	if (pos == output->cur_offset)
		return TRUE;

	if (GET_CLASS (output)->Seek (output, offset, whence)) {
		/* NOTE : it is possible for the current pos to be beyond the
		 * end of the file.  The intervening space is not filled with 0
		 * until something is written.
		 */
		output->cur_offset = pos;
		return TRUE;
	}

	/* the implementation should have assigned whatever errors are necessary */
	return FALSE;
}


static inline gboolean
gsf_output_inc_cur_offset (GsfOutput *output, gsf_off_t num_bytes)
{
	if (G_UNLIKELY (num_bytes < 0 ||
			(GSF_OFF_T_MAX - num_bytes) < output->cur_offset)) {
		return gsf_output_set_error (output, 0, "Output size overflow.");
	}

	output->cur_offset += num_bytes;

	if (output->cur_size < output->cur_offset) {
		output->cur_size = output->cur_offset;
	}

	return TRUE;
}


/**
 * gsf_output_write: (virtual Write)
 * @output: Output stream
 * @num_bytes: Number of bytes to write
 * @data: (in) (array length=num_bytes): Data to write.
 *
 * Write @num_bytes of @data to @output.
 *
 * Returns: %FALSE on error.
 **/
gboolean
gsf_output_write (GsfOutput *output,
		  size_t num_bytes, guint8 const *data)
{
	g_return_val_if_fail (output != NULL, FALSE);

	if (num_bytes == 0)
		return TRUE;
	if (GET_CLASS (output)->Write (output, num_bytes, data))
		return gsf_output_inc_cur_offset (output, num_bytes);

	/* the implementation should have assigned whatever errors are necessary */
	return FALSE;
}

/**
 * gsf_output_error:
 * @output:
 *
 * Returns: (transfer none) (nullable): the last error logged on the output
 **/
GError const *
gsf_output_error (GsfOutput const *output)
{
	g_return_val_if_fail (GSF_IS_OUTPUT (output), NULL);
	return output->err;
}

/**
 * gsf_output_set_name:
 * @output: #GsfOutput
 * @name: (nullable): the new name
 *
 * <note>This is a utility routine that should only be used by derived
 * outputs.</note>
 *
 * Returns: %TRUE if the assignment was ok.
 **/
gboolean
gsf_output_set_name (GsfOutput *output, char const *name)
{
	g_return_val_if_fail (GSF_IS_OUTPUT (output), FALSE);

	if (g_strcmp0 (name, output->name)) {
		g_free (output->name);
		output->name = g_strdup (name);
		g_object_notify (G_OBJECT (output), "name");
	}

	return TRUE;
}

/**
 * gsf_output_set_name_from_filename:
 * @output: the output stream
 * @filename: (nullable): the (fs-sys encoded) filename
 *
 * <note>This is a utility routine that should only be used by derived
 * outputs.</note>
 *
 * Returns: %TRUE if the assignment was ok.
 **/
gboolean
gsf_output_set_name_from_filename (GsfOutput *output, char const *filename)
{
	char *name;
	gboolean res;

	g_return_val_if_fail (GSF_IS_OUTPUT (output), FALSE);

	name = filename
		? g_filename_to_utf8 (filename, -1, NULL, NULL, NULL)
		: NULL;
	res = gsf_output_set_name (output, name);
	g_free (name);
	return res;
}

/**
 * gsf_output_set_container:
 * @output: #GsfOutput
 * @container: (nullable): #GsfOutfile
 *
 * <note>This is a utility routine that should only be used by derived
 * outputs.</note>
 *
 * Returns: %TRUE if the assignment was ok.
 **/
gboolean
gsf_output_set_container (GsfOutput *output, GsfOutfile *container)
{
	g_return_val_if_fail (GSF_IS_OUTPUT (output), FALSE);

	if (container != NULL)
		g_object_ref (container);
	if (output->container != NULL)
		g_object_unref (output->container);
	output->container = container;
	return TRUE;
}

/**
 * gsf_output_set_error:
 * @output: #GsfOutput
 * @code: The error id
 * @format: printf style format string
 * @...: arguments for @format
 *
 * <note>This is a utility routine that should only be used by derived
 * outputs.</note>
 *
 * Returns: Always returns %FALSE to facilitate its use.
 **/
gboolean
gsf_output_set_error (GsfOutput  *output,
		      gint        code,
		      char const *format,
		      ...)
{
	g_return_val_if_fail (GSF_IS_OUTPUT (output), FALSE);

	g_clear_error (&output->err);

	if (format != NULL) {
		char *message;
		va_list args;

		va_start (args, format);
		message = g_strdup_vprintf (format, args);
		va_end (args);

		output->err = g_error_new_literal (gsf_output_error_id (),
		                                   code,
		                                   message);
		g_free (message);
	}

	return FALSE;
}

static void
cb_output_unwrap (GsfOutput *wrapee, G_GNUC_UNUSED GObject *wrapper)
{
	wrapee->wrapped_by = NULL;
}

/**
 * gsf_output_wrap:
 * @wrapper:
 * @wrapee:
 *
 * Returns: %TRUE if the wrapping succeeded.
 **/
gboolean
gsf_output_wrap (GObject *wrapper, GsfOutput *wrapee)
{
	g_return_val_if_fail (wrapper != NULL, FALSE);
	g_return_val_if_fail (wrapee != NULL, FALSE);

	if (wrapee->wrapped_by != NULL) {
		g_warning ("Attempt to wrap an output that is already wrapped.");
		return FALSE;
	}

	g_object_weak_ref (wrapper,
			   (GWeakNotify) cb_output_unwrap, wrapee);
	wrapee->wrapped_by = wrapper;
	return TRUE;
}

/**
 * gsf_output_unwrap:
 * @wrapper:
 * @wrapee:
 *
 * Returns: %TRUE if the unwrapping succeeded.
 **/
gboolean
gsf_output_unwrap (GObject *wrapper, GsfOutput *wrapee)
{
	g_return_val_if_fail (wrapee != NULL, FALSE);
	g_return_val_if_fail (wrapee->wrapped_by == wrapper, FALSE);

	wrapee->wrapped_by = NULL;
	g_object_weak_unref (wrapper,
			     (GWeakNotify) cb_output_unwrap, wrapee);
	return TRUE;
}

/**
 * gsf_output_get_modtime:
 * @output: the output stream
 *
 * Returns: (transfer none) (nullable): A #GDateTime representing when
 * the output was last modified
 */
GDateTime *
gsf_output_get_modtime (GsfOutput *output)
{
	g_return_val_if_fail (GSF_IS_OUTPUT (output), NULL);

	return g_object_get_data (G_OBJECT (output), MODTIME_ATTR);
}

/**
 * gsf_output_set_modtime:
 * @output: the output stream
 * @modtime: (transfer none) (nullable): the new modification time.
 *
 * Returns: %TRUE if the assignment was ok.
 */
gboolean
gsf_output_set_modtime (GsfOutput *output, GDateTime *modtime)
{
	g_return_val_if_fail (GSF_IS_OUTPUT (output), FALSE);

	if (modtime)
		modtime = g_date_time_add (modtime, 0); /* Copy */

	/* This actually also works for null modtime.  */
	g_object_set_data_full (G_OBJECT (output),
				MODTIME_ATTR, modtime,
				(GDestroyNotify)g_date_time_unref);

	return TRUE;
}


GQuark
gsf_output_error_id (void)
{
	static GQuark quark;
	if (!quark)
		quark = g_quark_from_static_string ("gsf_output_error");
	return quark;
}

/**
 * gsf_output_printf:
 * @output: A #GsfOutput
 * @format: The printf-style format string
 * @...: the arguments for @format
 *
 * Output @Varargs to @output using the format string @format, similar to
 * <citerefentry><refentrytitle>printf</refentrytitle>
 * <manvolnum>3</manvolnum></citerefentry>.
 *
 * Returns: %TRUE if successful, %FALSE if not
 **/
gboolean
gsf_output_printf (GsfOutput *output, char const *format, ...)
{
	va_list args;
	gboolean res;

	va_start (args, format);
	res = (gsf_output_vprintf (output, format, args) >= 0);
	va_end (args);
	return res;
}

/**
 * gsf_output_vprintf: (virtual Vprintf)
 * @output: A #GsfOutput
 * @format: The printf-style format string
 * @args: the arguments for @format
 *
 * Output @args to @output using the format string @format, similar to
 * <citerefentry><refentrytitle>vprintf</refentrytitle>
 * <manvolnum>3</manvolnum></citerefentry>.
 *
 * Returns: number of bytes printed, a negative value if not successful
 **/
gsf_off_t
gsf_output_vprintf (GsfOutput *output, char const *format, va_list args)
{
	gsf_off_t num_bytes;

	g_return_val_if_fail (output != NULL, -1);
	g_return_val_if_fail (format != NULL, -1);
	/* g_return_val_if_fail (strlen (format) > 0, -1); -- Why? */

	num_bytes = GET_CLASS (output)->Vprintf (output, format, args);

	if (num_bytes >= 0)
		if (!gsf_output_inc_cur_offset (output, num_bytes))
			return -1;
	return num_bytes;
}

static gsf_off_t
gsf_output_real_vprintf (GsfOutput *output, char const *fmt, va_list args)
{
	gsf_off_t reslen;
	va_list args2;

	/*
	 * We need to make a copy as args will become unusable after
	 * the g_vsnprintf call.
	 */
	G_VA_COPY (args2, args);

	if (NULL == output->printf_buf) {
		output->printf_buf_size = 128;
		output->printf_buf = g_new (char, output->printf_buf_size);
	}
	reslen = g_vsnprintf (output->printf_buf, output->printf_buf_size, fmt, args);

	/* handle C99 or older -1 case of vsnprintf */
	if (reslen < 0 || reslen >= output->printf_buf_size) {
		g_free (output->printf_buf);
		output->printf_buf = g_strdup_vprintf (fmt, args2);
		reslen = output->printf_buf_size = strlen (output->printf_buf);
	}
	va_end (args2);

	if (reslen == 0 ||
	    GET_CLASS (output)->Write (output, reslen, output->printf_buf))
		return reslen;

	return -1;
}

/**
 * gsf_output_puts:
 * @output: A #GsfOutput
 * @line: Nul terminated string to write
 *
 * Like fputs, this assumes that the line already ends with a newline
 *
 * Returns: %TRUE if successful, %FALSE if not
 **/
gboolean
gsf_output_puts (GsfOutput *output, char const *line)
{
	size_t nbytes = 0;

	g_return_val_if_fail (line != NULL, FALSE);

	nbytes = strlen (line);
	return gsf_output_write (output, nbytes, line);
}
