/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 *  Authors: Jeffrey Stedfast <fejj@ximian.com>
 *
 *  Copyright 2001-2004 Ximian, Inc. (www.ximian.com)
 *
 *  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) any later version.
 *
 *  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., 59 Temple Street #330, Boston, MA 02111-1307, USA.
 *
 */


#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <string.h>

#include <zlib.h>

#include "camel-mime-filter-gzip.h"


/* rfc1952 */

enum {
	GZIP_FLAG_FTEXT     = (1 << 0),
	GZIP_FLAG_FHCRC     = (1 << 1),
	GZIP_FLAG_FEXTRA    = (1 << 2),
	GZIP_FLAG_FNAME     = (1 << 3),
	GZIP_FLAG_FCOMMENT  = (1 << 4),
	GZIP_FLAG_RESERVED0 = (1 << 5),
	GZIP_FLAG_RESERVED1 = (1 << 6),
	GZIP_FLAG_RESERVED2 = (1 << 7),
};

#define GZIP_FLAG_RESERVED (GZIP_FLAG_RESERVED0 | GZIP_FLAG_RESERVED1 | GZIP_FLAG_RESERVED2)

typedef union {
	unsigned char buf[10];
	struct {
		guint8 id1;
		guint8 id2;
		guint8 cm;
		guint8 flg;
		guint32 mtime;
		guint8 xfl;
		guint8 os;
	} v;
} gzip_hdr_t;

typedef union {
	struct {
		guint16 xlen;
		guint16 xlen_nread;
		guint16 crc16;
		
		guint8 got_hdr:1;
		guint8 is_valid:1;
		guint8 got_xlen:1;
		guint8 got_fname:1;
		guint8 got_fcomment:1;
		guint8 got_crc16:1;
	} unzip;
	struct {
		guint32 wrote_hdr:1;
	} zip;
} gzip_state_t;

struct _CamelMimeFilterGZipPrivate {
	z_stream *stream;
	
	gzip_state_t state;
	gzip_hdr_t hdr;
	
	guint32 crc32;
	guint32 isize;
};

static void camel_mime_filter_gzip_class_init (CamelMimeFilterGZipClass *klass);
static void camel_mime_filter_gzip_init (CamelMimeFilterGZip *filter, CamelMimeFilterGZipClass *klass);
static void camel_mime_filter_gzip_finalize (CamelObject *object);

static void filter_filter (CamelMimeFilter *filter, char *in, size_t len, size_t prespace,
			   char **out, size_t *outlen, size_t *outprespace);
static void filter_complete (CamelMimeFilter *filter, char *in, size_t len, size_t prespace,
			     char **out, size_t *outlen, size_t *outprespace);
static void filter_reset (CamelMimeFilter *filter);


static CamelMimeFilterClass *parent_class = NULL;


CamelType
camel_mime_filter_gzip_get_type (void)
{
	static CamelType type = CAMEL_INVALID_TYPE;
	
	if (type == CAMEL_INVALID_TYPE) {
		type = camel_type_register (camel_mime_filter_get_type (),
					    "CamelMimeFilterGZip",
					    sizeof (CamelMimeFilterGZip),
					    sizeof (CamelMimeFilterGZipClass),
					    (CamelObjectClassInitFunc) camel_mime_filter_gzip_class_init,
					    NULL,
					    (CamelObjectInitFunc) camel_mime_filter_gzip_init,
					    (CamelObjectFinalizeFunc) camel_mime_filter_gzip_finalize);
	}
	
	return type;
}


static void
camel_mime_filter_gzip_class_init (CamelMimeFilterGZipClass *klass)
{
	CamelMimeFilterClass *filter_class = (CamelMimeFilterClass *) klass;
	
	parent_class = CAMEL_MIME_FILTER_CLASS (camel_type_get_global_classfuncs (camel_mime_filter_get_type ()));
	
	filter_class->reset = filter_reset;
	filter_class->filter = filter_filter;
	filter_class->complete = filter_complete;
}

static void
camel_mime_filter_gzip_init (CamelMimeFilterGZip *filter, CamelMimeFilterGZipClass *klass)
{
	filter->priv = g_new0 (struct _CamelMimeFilterGZipPrivate, 1);
	filter->priv->stream = g_new0 (z_stream, 1);
	filter->priv->crc32 = crc32 (0, Z_NULL, 0);
}

static void
camel_mime_filter_gzip_finalize (CamelObject *object)
{
	CamelMimeFilterGZip *gzip = (CamelMimeFilterGZip *) object;
	struct _CamelMimeFilterGZipPrivate *priv = gzip->priv;
	
	if (gzip->mode == CAMEL_MIME_FILTER_GZIP_MODE_ZIP)
		deflateEnd (priv->stream);
	else
		inflateEnd (priv->stream);
	
	g_free (priv->stream);
	g_free (priv);
}


static void
gzip_filter (CamelMimeFilter *filter, char *in, size_t len, size_t prespace,
	     char **out, size_t *outlen, size_t *outprespace, int flush)
{
	CamelMimeFilterGZip *gzip = (CamelMimeFilterGZip *) filter;
	struct _CamelMimeFilterGZipPrivate *priv = gzip->priv;
	int retval;
	
	if (!priv->state.zip.wrote_hdr) {
		priv->hdr.v.id1 = 31;
		priv->hdr.v.id2 = 139;
		priv->hdr.v.cm = Z_DEFLATED;
		priv->hdr.v.mtime = 0;
		priv->hdr.v.flg = 0;
		if (gzip->level == Z_BEST_COMPRESSION)
			priv->hdr.v.xfl = 2;
		else if (gzip->level == Z_BEST_SPEED)
			priv->hdr.v.xfl = 4;
		else
			priv->hdr.v.xfl = 0;
		priv->hdr.v.os = 255;
		
		camel_mime_filter_set_size (filter, (len * 2) + 22, FALSE);
		
		memcpy (filter->outbuf, priv->hdr.buf, 10);
		
		priv->stream->next_out = filter->outbuf + 10;
		priv->stream->avail_out = filter->outsize - 10;
		
		priv->state.zip.wrote_hdr = TRUE;
	} else {
		camel_mime_filter_set_size (filter, (len * 2) + 12, FALSE);
		
		priv->stream->next_out = filter->outbuf;
		priv->stream->avail_out = filter->outsize;
	}
	
	priv->stream->next_in = in;
	priv->stream->avail_in = len;
	
	do {
		/* FIXME: handle error cases? */
		if ((retval = deflate (priv->stream, flush)) != Z_OK)
			fprintf (stderr, "gzip: %d: %s\n", retval, priv->stream->msg);
		
		if (flush == Z_FULL_FLUSH) {
			size_t outlen;
			
			outlen = filter->outsize - priv->stream->avail_out;
			camel_mime_filter_set_size (filter, outlen + (priv->stream->avail_in * 2) + 12, TRUE);
			priv->stream->avail_out = filter->outsize - outlen;
			priv->stream->next_out = filter->outbuf + outlen;
			
			if (priv->stream->avail_in == 0) {
				guint32 val;
				
				val = GUINT32_TO_LE (priv->crc32);
				memcpy (priv->stream->next_out, &val, 4);
				priv->stream->avail_out -= 4;
				priv->stream->next_out += 4;
				
				val = GUINT32_TO_LE (priv->isize);
				memcpy (priv->stream->next_out, &val, 4);
				priv->stream->avail_out -= 4;
				priv->stream->next_out += 4;
				
				break;
			}
		} else {
			if (priv->stream->avail_in > 0)
				camel_mime_filter_backup (filter, priv->stream->next_in, priv->stream->avail_in);
			
			break;
		}
	} while (1);
	
	priv->crc32 = crc32 (priv->crc32, in, len - priv->stream->avail_in);
	priv->isize += len - priv->stream->avail_in;
	
	*out = filter->outbuf;
	*outlen = filter->outsize - priv->stream->avail_out;
	*outprespace = filter->outpre;
}

static void
gunzip_filter (CamelMimeFilter *filter, char *in, size_t len, size_t prespace,
	       char **out, size_t *outlen, size_t *outprespace, int flush)
{
	CamelMimeFilterGZip *gzip = (CamelMimeFilterGZip *) filter;
	struct _CamelMimeFilterGZipPrivate *priv = gzip->priv;
	guint16 need, val;
	int retval;
	
	if (!priv->state.unzip.got_hdr) {
		if (len < 10) {
			camel_mime_filter_backup (filter, in, len);
			return;
		}
		
		memcpy (priv->hdr.buf, in, 10);
		priv->state.unzip.got_hdr = TRUE;
		len -= 10;
		in += 10;
		
		priv->state.unzip.is_valid = (priv->hdr.v.id1 == 31 &&
					      priv->hdr.v.id2 == 139 &&
					      priv->hdr.v.cm == Z_DEFLATED);
	}
	
	if (!priv->state.unzip.is_valid)
		return;
	
	if (priv->hdr.v.flg & GZIP_FLAG_FEXTRA) {
		if (!priv->state.unzip.got_xlen) {
			if (len < 2) {
				camel_mime_filter_backup (filter, in, len);
				return;
			}
			
			memcpy (&val, in, 2);
			priv->state.unzip.xlen = GUINT16_FROM_LE (val);
			priv->state.unzip.got_xlen = TRUE;
			len -= 2;
			in += 2;
		}
		
		if (priv->state.unzip.xlen_nread < priv->state.unzip.xlen) {
			need = priv->state.unzip.xlen - priv->state.unzip.xlen_nread;
			
			if (need < len) {
				priv->state.unzip.xlen_nread += need;
				len -= need;
				in += need;
			} else {
				priv->state.unzip.xlen_nread += len;
				return;
			}
		}
	}
	
	if ((priv->hdr.v.flg & GZIP_FLAG_FNAME) && !priv->state.unzip.got_fname) {
		while (*in && len > 0) {
			len--;
			in++;
		}
		
		if (*in == '\0' && len > 0) {
			priv->state.unzip.got_fname = TRUE;
			len--;
			in++;
		} else {
			return;
		}
	}
	
	if ((priv->hdr.v.flg & GZIP_FLAG_FCOMMENT) && !priv->state.unzip.got_fcomment) {
		while (*in && len > 0) {
			len--;
			in++;
		}
		
		if (*in == '\0' && len > 0) {
			priv->state.unzip.got_fcomment = TRUE;
			len--;
			in++;
		} else {
			return;
		}
	}
	
	if ((priv->hdr.v.flg & GZIP_FLAG_FHCRC) && !priv->state.unzip.got_crc16) {
		if (len < 2) {
			camel_mime_filter_backup (filter, in, len);
			return;
		}
		
		memcpy (&val, in, 2);
		priv->state.unzip.crc16 = GUINT16_FROM_LE (val);
		len -= 2;
		in += 2;
	}
	
	if (len == 0)
		return;
	
	camel_mime_filter_set_size (filter, (len * 2) + 12, FALSE);
	
	priv->stream->next_in = in;
	priv->stream->avail_in = len - 8;
	
	priv->stream->next_out = filter->outbuf;
	priv->stream->avail_out = filter->outsize;
	
	do {
		/* FIXME: handle error cases? */
		if ((retval = inflate (priv->stream, flush)) != Z_OK)
			fprintf (stderr, "gunzip: %d: %s\n", retval, priv->stream->msg);
		
		if (flush == Z_FULL_FLUSH) {
			size_t outlen;
			
			if (priv->stream->avail_in == 0) {
				/* FIXME: extract & compare calculated crc32 and isize values? */
				break;
			}
			
			outlen = filter->outsize - priv->stream->avail_out;
			camel_mime_filter_set_size (filter, outlen + (priv->stream->avail_in * 2) + 12, TRUE);
			priv->stream->avail_out = filter->outsize - outlen;
			priv->stream->next_out = filter->outbuf + outlen;
		} else {
			priv->stream->avail_in += 8;
			
			if (priv->stream->avail_in > 0)
				camel_mime_filter_backup (filter, priv->stream->next_in, priv->stream->avail_in);
			
			break;
		}
	} while (1);
	
	/* FIXME: if we keep this, we could check that the gzip'd
	 * stream is sane, but how would we tell our consumer if it
	 * was/wasn't? */
	/*priv->crc32 = crc32 (priv->crc32, in, len - priv->stream->avail_in - 8);
	  priv->isize += len - priv->stream->avail_in - 8;*/
	
	*out = filter->outbuf;
	*outlen = filter->outsize - priv->stream->avail_out;
	*outprespace = filter->outpre;
}

static void
filter_filter (CamelMimeFilter *filter, char *in, size_t len, size_t prespace,
	       char **out, size_t *outlen, size_t *outprespace)
{
	CamelMimeFilterGZip *gzip = (CamelMimeFilterGZip *) filter;
	
	if (gzip->mode == CAMEL_MIME_FILTER_GZIP_MODE_ZIP)
		gzip_filter (filter, in, len, prespace, out, outlen, outprespace, Z_SYNC_FLUSH);
	else
		gunzip_filter (filter, in, len, prespace, out, outlen, outprespace, Z_SYNC_FLUSH);
}

static void
filter_complete (CamelMimeFilter *filter, char *in, size_t len, size_t prespace,
		 char **out, size_t *outlen, size_t *outprespace)
{
	CamelMimeFilterGZip *gzip = (CamelMimeFilterGZip *) filter;
	
	if (gzip->mode == CAMEL_MIME_FILTER_GZIP_MODE_ZIP)
		gzip_filter (filter, in, len, prespace, out, outlen, outprespace, Z_FULL_FLUSH);
	else
		gunzip_filter (filter, in, len, prespace, out, outlen, outprespace, Z_FULL_FLUSH);
}

/* should this 'flush' outstanding state/data bytes? */
static void
filter_reset (CamelMimeFilter *filter)
{
	CamelMimeFilterGZip *gzip = (CamelMimeFilterGZip *) filter;
	struct _CamelMimeFilterGZipPrivate *priv = gzip->priv;
	
	memset (&priv->state, 0, sizeof (priv->state));
	
	if (gzip->mode == CAMEL_MIME_FILTER_GZIP_MODE_ZIP)
		deflateReset (priv->stream);
	else
		inflateReset (priv->stream);
	
	priv->crc32 = crc32 (0, Z_NULL, 0);
	priv->isize = 0;
}


/**
 * camel_mime_filter_gzip_new:
 * @mode: zip or unzip
 * @level: compression level
 *
 * Creates a new gzip (or gunzip) filter.
 *
 * Returns a new gzip (or gunzip) filter.
 **/
CamelMimeFilter *
camel_mime_filter_gzip_new (CamelMimeFilterGZipMode mode, int level)
{
	CamelMimeFilterGZip *new;
	int retval;
	
	new = (CamelMimeFilterGZip *) camel_object_new (CAMEL_TYPE_MIME_FILTER_GZIP);
	new->mode = mode;
	new->level = level;
	
	if (mode == CAMEL_MIME_FILTER_GZIP_MODE_ZIP)
		retval = deflateInit2 (new->priv->stream, level, Z_DEFLATED, -MAX_WBITS, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY);
	else
		retval = inflateInit2 (new->priv->stream, -MAX_WBITS);
	
	if (retval != Z_OK) {
		camel_object_unref (new);
		return NULL;
	}
	
	return (CamelMimeFilter *) new;
}
