/* vim: set sw=8: -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * gsf-output-bzip.c: wrapper to compress to bzipped output
 *
 * Copyright (C) 2003-2006 Dom Lachowicz (cinamod@hotmail.com)
 *               2002-2006 Jon K Hellan (hellan@acm.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-bzip.h>
#include <gsf/gsf.h>
#include <glib/gi18n-lib.h>

#ifdef HAVE_BZ2
#include <bzlib.h>
#define BZ_BUFSIZE (32 * 1024)
#endif

static GObjectClass *parent_class;

struct _GsfOutputBzip {
	GsfOutput  output;

#ifdef HAVE_BZ2
	GsfOutput *sink; /* compressed data */
	bz_stream  stream;
	guint8    *buf;
	size_t     buf_size;
#endif
};

typedef struct {
	GsfOutputClass output_class;
} GsfOutputBzipClass;

static void
gsf_output_bzip_finalize (GObject *obj)
{
#ifdef HAVE_BZ2
	GsfOutputBzip *bzip = (GsfOutputBzip *)obj;

	if (bzip->sink != NULL) {
		g_object_unref (bzip->sink);
		bzip->sink = NULL;
	}
	g_free (bzip->buf);
#endif
	parent_class->finalize (obj);
}

#ifdef HAVE_BZ2
static gboolean
init_bzip (GsfOutputBzip *bzip, GError **err)
{
	int ret;

	ret = BZ2_bzCompressInit (&bzip->stream, 6, 0, 0);

	if (ret != BZ_OK) {
		if (err != NULL)
			*err = g_error_new (gsf_output_error_id (), 0,
					    _("Unable to initialize BZ2 library"));
		return FALSE;
	}
	if (!bzip->buf) {
		bzip->buf_size = BZ_BUFSIZE;
		bzip->buf = g_new (guint8, bzip->buf_size);
	}
	bzip->stream.next_out  = bzip->buf;
	bzip->stream.avail_out = bzip->buf_size;

	return TRUE;
}

static gboolean
bzip_output_block (GsfOutputBzip *bzip)
{
	size_t num_bytes = bzip->buf_size - bzip->stream.avail_out;

	if (!gsf_output_write (bzip->sink, num_bytes, bzip->buf))
		return FALSE;

	bzip->stream.next_out  = bzip->buf;
	bzip->stream.avail_out = bzip->buf_size;

	return TRUE;
}

static gboolean
bzip_flush (GsfOutputBzip *bzip)
{
	int zret;

	do {
		zret = BZ2_bzCompress (&bzip->stream, BZ_FINISH);
		if (zret == BZ_FINISH_OK) {
			/*  In this case BZ_FINISH_OK means more buffer space
			    needed  */
			if (!bzip_output_block (bzip))
				return FALSE;
		}
	} while (zret == BZ_FINISH_OK);
	if (zret != BZ_STREAM_END) {
		g_warning ("Unexpected error code %d from bzlib during compression.",
			   zret);
		return FALSE;
	}
	if (!bzip_output_block (bzip))
		return FALSE;

	return TRUE;
}
#endif

static gboolean
gsf_output_bzip_write (GsfOutput *output,
		       size_t num_bytes, guint8 const *data)
{
	g_return_val_if_fail (GSF_IS_OUTPUT_BZIP (output), FALSE);

#ifdef HAVE_BZ2
	GsfOutputBzip *bzip = GSF_OUTPUT_BZIP (output);

	g_return_val_if_fail (num_bytes == 0 || data != NULL, FALSE);

	bzip->stream.next_in  = (unsigned char *) data;
	while (num_bytes > 0) {
		bzip->stream.avail_in = (num_bytes > G_MAXUINT32) ? G_MAXUINT32 : (unsigned int)num_bytes;
		num_bytes -= bzip->stream.avail_in;

		while (bzip->stream.avail_in > 0) {
			int zret;

			if (bzip->stream.avail_out == 0) {
				if (!bzip_output_block (bzip))
					return FALSE;
			}

			zret = BZ2_bzCompress (&bzip->stream, BZ_RUN);
			if (zret != BZ_RUN_OK) {
				g_warning ("Unexpected error code %d from bzlib during compression.",
					   zret);
				return FALSE;
			}
		}
	}

	if (bzip->stream.avail_out == 0) {
		if (!bzip_output_block (bzip))
			return FALSE;
	}

	return TRUE;
#else
	(void)num_bytes;
	(void)data;
	return FALSE;
#endif
}

static gboolean
gsf_output_bzip_seek (G_GNUC_UNUSED GsfOutput *output,
		      G_GNUC_UNUSED gsf_off_t offset,
		      G_GNUC_UNUSED GSeekType whence)
{
	g_return_val_if_fail (GSF_IS_OUTPUT_BZIP (output), FALSE);

	return FALSE;
}

static gboolean
gsf_output_bzip_close (GsfOutput *output)
{
	g_return_val_if_fail (GSF_IS_OUTPUT_BZIP (output), FALSE);

#ifdef HAVE_BZ2
	GsfOutputBzip *bzip = GSF_OUTPUT_BZIP (output);
	gboolean rt;

	rt = bzip_flush (bzip);
	BZ2_bzCompressEnd (&bzip->stream);

	return rt;
#else
	(void)output;
	return FALSE;
#endif
}

static void
gsf_output_bzip_init (GObject *obj)
{
#ifdef HAVE_BZ2
	GsfOutputBzip *bzip = GSF_OUTPUT_BZIP (obj);

	bzip->sink = NULL;
	bzip->stream.bzalloc	= NULL;
	bzip->stream.bzfree	= NULL;
	bzip->stream.opaque	= NULL;
	bzip->stream.next_in	= NULL;
	bzip->stream.next_out	= NULL;
	bzip->stream.avail_in	= bzip->stream.avail_out = 0;
	bzip->buf		= NULL;
	bzip->buf_size		= 0;
#else
	(void)obj;
#endif
}

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

	gobject_class->finalize = gsf_output_bzip_finalize;
	output_class->Write	= gsf_output_bzip_write;
	output_class->Seek	= gsf_output_bzip_seek;
	output_class->Close	= gsf_output_bzip_close;

	parent_class = g_type_class_peek_parent (gobject_class);
}

GSF_CLASS (GsfOutputBzip, gsf_output_bzip,
	   gsf_output_bzip_class_init, gsf_output_bzip_init,
	   GSF_OUTPUT_TYPE)

/**
 * gsf_output_bzip_new:
 * @sink: The underlying data source.
 * @err: (out) (optional) (nullable): place to store a #GError if anything goes wrong.
 *
 * Adds a reference to @sink.
 *
 * Returns: a new file or %NULL.
 **/
GsfOutput *
gsf_output_bzip_new (GsfOutput *sink, GError **err)
{
	if (err)
		*err = NULL;

#ifdef HAVE_BZ2
	GsfOutputBzip *bzip;

	g_return_val_if_fail (GSF_IS_OUTPUT (sink), NULL);

	bzip = g_object_new (GSF_OUTPUT_BZIP_TYPE, NULL);

	g_object_ref (sink);
	bzip->sink = sink;

	if (!init_bzip (bzip, err)) {
		g_object_unref (bzip);
		return NULL;
	}

	return GSF_OUTPUT (bzip);
#else
	(void)sink;
	if (err)
		*err = g_error_new (gsf_output_error_id (), 0,
				    _("BZ2 support not enabled"));
	return NULL;
#endif
}
