/* Buffer size for inflate/deflate. */

#define CHUNK 0x4000

/* These are magic numbers for zlib, please refer to
   "/usr/include/zlib.h" for the details. */

#define windowBits 15
#define DEFLATE_ENABLE_ZLIB_GZIP 16
#define INFLATE_ENABLE_ZLIB_GZIP 32

#define CALL_ZLIB(x)						 \
	zlib_status = x;					 \
	if (zlib_status < 0) {					 \
	    deflateEnd (& gf->strm);				 \
	    croak ("zlib call %s returned error status %d",	 \
		   #x, zlib_status);				 \
	}							 \

typedef struct
{
    /* Input. */
    SV * in;
    const char * in_char;
    STRLEN in_length;
    /* Compression structure. */
    z_stream strm;
    /* Compression level. */
    int level;
    /* This holds the stuff. */
    unsigned char out_buffer[CHUNK];
    /* windowBits, adjusted for monkey business. */
    int wb;
    /* Optional file name for gzip format.  This can only take values
       for user-visible objects. */
    SV * file_name;
    /* User-defined modification time. */
    SV * mod_time;
    /* Gzip, not deflate or inflate. */
    unsigned int is_gzip : 1;
    /* "Raw" inflate or deflate without adler32 check. */
    unsigned int is_raw : 1;
    /* Copy Perl flags like UTF8 flag? */
    unsigned int copy_perl_flags : 1;
    /* User can see this object? */
    unsigned int user_object : 1;
}
gzip_faster_t;

/* See "http://www.gzip.org/zlib/rfc-gzip.html". */

#define GZIP_PERL_ID "GF\1\0"
#define GZIP_PERL_ID_LENGTH 4

/* Perl stuff. */

#define GZIP_PERL_LENGTH 1
#define EXTRA_LENGTH GZIP_PERL_ID_LENGTH + GZIP_PERL_LENGTH
#define GZIP_PERL_UTF8 (1<<0)
/* Add more Perl flags here like 

#define SOMETHING (1<<1)

etc. */

static void
gf_set_up (gzip_faster_t * gf)
{
    /* Extract the information from "gf->in". */
    gf->in_char = SvPV (gf->in, gf->in_length);
    gf->strm.next_in = (unsigned char *) gf->in_char;
    gf->strm.avail_in = gf->in_length;
    gf->strm.zalloc = Z_NULL;
    gf->strm.zfree = Z_NULL;
    gf->strm.opaque = Z_NULL;
    if (! gf->user_object) {
	gf->level = Z_DEFAULT_COMPRESSION;
    }
    gf->wb = windowBits;
}

/* Check that we are always dealing with a user-defined object, not a
   module-defined object, before altering the values. */

#define UO					\
    if (! gf->user_object) {			\
	croak ("THIS IS NOT A USER OBJECT");	\
    }


/* Delete the file name in the object. */

static void
gf_delete_file_name (gzip_faster_t * gf)
{
    UO;
    if (gf->file_name) {
	SvREFCNT_dec (gf->file_name);
	gf->file_name = 0;
    }
}

/* Delete the file name in the object. */

static void
gf_delete_mod_time (gzip_faster_t * gf)
{
    UO;
    if (gf->mod_time) {
	SvREFCNT_dec (gf->mod_time);
	gf->mod_time = 0;
    }
}

static void
gf_set_file_name (gzip_faster_t * gf, SV * file_name)
{
    UO;
    if (gf->file_name) {
	gf_delete_file_name (gf);
    }
    SvREFCNT_inc (file_name);
    gf->file_name = file_name;
}

static SV *
gf_get_file_name (gzip_faster_t * gf)
{
    UO;
    if (gf->file_name) {
	return gf->file_name;
    }
    return & PL_sv_undef;
}

static void
gf_set_mod_time (gzip_faster_t * gf, SV * mod_time)
{
    UO;
    if (gf->mod_time) {
	gf_delete_mod_time (gf);
    }
    SvREFCNT_inc (mod_time);
    gf->mod_time = mod_time;
}

static SV *
gf_get_mod_time (gzip_faster_t * gf)
{
    UO;
    if (gf->mod_time) {
	return gf->mod_time;
    }
    return & PL_sv_undef;
}

static void
check_avail_in (gzip_faster_t * gf)
{
    if (gf->strm.avail_in != 0) {
	croak ("Zlib did not finish processing the string: %d bytes left",
	       gf->strm.avail_in);
    }
}

static void
check_zlib_status (int zlib_status)
{
    if (zlib_status != Z_STREAM_END) {
	croak ("Zlib did not come to the end of the string: zlib_status = %d",
	       zlib_status);
    }
}

static SV *
gzip_faster (gzip_faster_t * gf)
{
    /* The output. */
    SV * zipped;
    /* The message from zlib. */
    int zlib_status;

    if (! SvOK (gf->in)) {
	warn ("Empty input");
	return & PL_sv_undef;
    }
    gf_set_up (gf);
    if (gf->in_length == 0) {
	warn ("Attempt to compress empty string");
	return & PL_sv_undef;
    }

    if (gf->is_gzip) {
	if (gf->is_raw) {
	    croak ("Raw deflate and gzip are incompatible");
	}
	gf->wb += DEFLATE_ENABLE_ZLIB_GZIP;
    }
    else {
	if (gf->is_raw) {
	    gf->wb = -gf->wb;
	}
    }
    CALL_ZLIB (deflateInit2 (& gf->strm, gf->level, Z_DEFLATED,
			     gf->wb, 8,
			     Z_DEFAULT_STRATEGY));

    if (gf->user_object) {
	if (gf->is_gzip) {
	    gz_header header = {0};
	    unsigned char extra[EXTRA_LENGTH];
	    /* Have at least one of the fields in the header been set? */
	    int set_header;
	    set_header = 0;
	    if (gf->copy_perl_flags) {
		memcpy (extra, GZIP_PERL_ID, GZIP_PERL_ID_LENGTH);
		extra[GZIP_PERL_ID_LENGTH] = 0;
		if (SvUTF8 (gf->in)) {
		    extra[GZIP_PERL_ID_LENGTH] |= GZIP_PERL_UTF8;
		}
		header.extra = extra;
		header.extra_len = EXTRA_LENGTH;
		set_header++;
	    }
	    if (gf->file_name) {
		char * fn;
		fn = SvPV_nolen (gf->file_name);
		header.name = (Bytef *) fn;
		set_header++;
	    }
	    if (gf->mod_time) {
		header.time = (uLong) SvUV (gf->mod_time);
		set_header++;
	    }
	    if (set_header) {
		CALL_ZLIB (deflateSetHeader (& gf->strm, & header));
	    }
	}
	else {
	    if (gf->copy_perl_flags) {
		warn ("wrong format: perl flags not copied: use gzip_format(1)");
	    }
	    if (gf->file_name || gf->mod_time) {
		warn ("wrong format: file name/modification time ignored: use gzip_format(1)");
	    }
	}
    }
    zipped = 0;

    do {
	unsigned int have;
	gf->strm.avail_out = CHUNK;
	gf->strm.next_out = gf->out_buffer;
	zlib_status = deflate (& gf->strm, Z_FINISH);
	switch (zlib_status) {
	case Z_OK:
	case Z_STREAM_END:
	case Z_BUF_ERROR:
	    /* Keep on chugging. */
	    break;

	case Z_STREAM_ERROR:
	    deflateEnd (& gf->strm);
	    /* This is supposed to never happen, but just in case it
	       does. */
	    croak ("Z_STREAM_ERROR from zlib during deflate");

	default:
	    deflateEnd (& gf->strm);
	    croak ("Unknown status %d from deflate", zlib_status);
	    break;
	}
	/* The number of bytes we "have". */
	have = CHUNK - gf->strm.avail_out;
	if (! zipped) {
	    zipped = newSVpv ((const char *) gf->out_buffer, have);
	}
	else {
	    sv_catpvn (zipped, (const char *) gf->out_buffer, have);
	}
    }
    while (gf->strm.avail_out == 0);
    check_avail_in (gf);
    check_zlib_status (zlib_status);
    deflateEnd (& gf->strm);
    if (gf->user_object) {
	if (gf->file_name) {
	    gf_delete_file_name (gf);
	}
    }
    return zipped;
}

#define GF_FILE_NAME_MAX 0x400

static SV *
gunzip_faster (gzip_faster_t * gf)
{
    /* The return value. */
    SV * plain;
    /* The message from zlib. */
    int zlib_status;
    /* The gzip library header. */
    gz_header header;
    /* The name of the file, if it exists. */
    unsigned char name[GF_FILE_NAME_MAX];
    /* Extra bytes in the header, if they exist. */
    unsigned char extra[EXTRA_LENGTH];

    if (! SvOK (gf->in)) {
	warn ("Empty input");
	return & PL_sv_undef;
    }
    gf_set_up (gf);
    if (gf->in_length == 0) {
	warn ("Attempt to uncompress empty string");
	return & PL_sv_undef;
    }

    if (gf->is_gzip) {
	if (gf->is_raw) {
	    croak ("Raw deflate and gzip are incompatible");
	}
	gf->wb += INFLATE_ENABLE_ZLIB_GZIP;
    }
    else {
	if (gf->is_raw) {
	    gf->wb = -gf->wb;
	}
    }
    CALL_ZLIB (inflateInit2 (& gf->strm, gf->wb));
    if (gf->user_object) {
	if (gf->is_gzip) {
	    if (gf->copy_perl_flags) {
		header.extra = extra;
		header.extra_max = EXTRA_LENGTH;
	    }
	    /* If there are stale file names or modification times in
	       our object, delete them. */
	    if (gf->file_name) {
		gf_delete_file_name (gf);
	    }
	    if (gf->mod_time) {
		gf_delete_mod_time (gf);
	    }
	    /* Point the header values to our buffer, "name", and give
	       the length of the buffer. */
	    header.name = name;
	    header.name_max = GF_FILE_NAME_MAX;
	    inflateGetHeader (& gf->strm, & header);
	}
    }

    /* Mark the return value as uninitialised. */
    plain = 0;

    do {
	unsigned int have;
	gf->strm.avail_out = CHUNK;
	gf->strm.next_out = gf->out_buffer;
	zlib_status = inflate (& gf->strm, Z_FINISH);
	switch (zlib_status) {
	case Z_OK:
	case Z_STREAM_END:
	case Z_BUF_ERROR:
	    break;

	case Z_DATA_ERROR:
	    inflateEnd (& gf->strm);
	    croak ("Data input to inflate is not in libz format");
	    break;

	case Z_MEM_ERROR:
	    inflateEnd (& gf->strm);
	    croak ("Out of memory during inflate");

	case Z_STREAM_ERROR:
	    inflateEnd (& gf->strm);
	    croak ("Internal error in zlib");

	default:
	    inflateEnd (& gf->strm);
	    croak ("Unknown status %d from inflate", zlib_status);
	    break;
	}
	have = CHUNK - gf->strm.avail_out;
	if (! plain) {
	    /* If the return value is uninitialised, set up a new
	       one. */
	    plain = newSVpv ((const char *) gf->out_buffer, have);
	}
	else {
	    /* If the return value was initialised, append the
	       contents of "gf->out_buffer" to it using
	       "sv_catpvn". */
	    sv_catpvn (plain, (const char *) gf->out_buffer, have);
	}
    }
    while (gf->strm.avail_out == 0);
    check_avail_in (gf);
    check_zlib_status (zlib_status);
    inflateEnd (& gf->strm);
    if (gf->user_object && gf->is_gzip && header.done == 1) {
	if (gf->copy_perl_flags) {
	    if (strncmp ((const char *) header.extra, GZIP_PERL_ID,
			 GZIP_PERL_ID_LENGTH) == 0) {
		unsigned is_utf8;
		is_utf8 = header.extra[GZIP_PERL_ID_LENGTH] & GZIP_PERL_UTF8;
		if (is_utf8) {
		    SvUTF8_on (plain);
		}
	    }
	}
	if (header.name && header.name_max > 0) {
	    gf->file_name = newSVpv ((const char *) header.name, 0);
	    SvREFCNT_inc (gf->file_name);
	}
	else {
	    gf_delete_file_name (gf);
	}
	/* If the header includes a modification time, copy it into
	   $gf->mod_time. If the header doesn't include a modification
	   time, make sure the modification time of $gf is completely
	   clear, so the user doesn't get old junk data. */
	if (header.time) {
	    /* I'm not 100% sure of the type conversion
	       here. header.time is uLong in the zlib documentation,
	       and UV is unsigned int, or something? */
	    gf->mod_time = newSVuv (header.time);
	    SvREFCNT_inc (gf->mod_time);
	}
	else {
	    gf_delete_mod_time (gf);
	}
    }
    return plain;
}

static void
new_user_object (gzip_faster_t * gf)
{
    gf->file_name = 0;
    gf->mod_time = 0;
    gf->is_gzip = 1;
    gf->is_raw = 0;
    gf->user_object = 1;
    gf->level = Z_DEFAULT_COMPRESSION;
}

static void
set_compression_level (gzip_faster_t * gf, int level)
{
    if (level < Z_NO_COMPRESSION) {
	warn ("Cannot set compression level to less than %d",
	      Z_NO_COMPRESSION);
	gf->level = Z_NO_COMPRESSION;
    }
    else if (level > Z_BEST_COMPRESSION) {
	warn ("Cannot set compression level to more than %d",
	      Z_BEST_COMPRESSION);
	gf->level = Z_BEST_COMPRESSION;
    }
    else {
	gf->level = level;
    }
}