//
// tardy - a tar post-processor
// Copyright (C) 2003, 2008-2011 Peter Miller
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 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
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//

#include <libtardy/ac/stdio.h>
#include <libtardy/ac/stdlib.h>
#include <libtardy/ac/string.h>
#include <libtardy/ac/unistd.h>
#include <libexplain/fflush.h>
#include <libexplain/fread.h>
#include <libexplain/fwrite.h>
#include <libexplain/mkstemp.h>

#include <libtardy/tar/output/filter/gzip.h>

#ifndef Z_BUFSIZE
#ifdef MAXSEG_64K
#define Z_BUFSIZE 4096 // minimize memory usage for 16-bit DOS
#else
#define Z_BUFSIZE 16384
#endif
#endif



tar_output_filter_gzip::~tar_output_filter_gzip()
{
    delete outbuf;
}


tar_output_filter_gzip::tar_output_filter_gzip(
    const tar_output::pointer &a_deeper
) :
    tar_output_filter(a_deeper),
    pass_through(true),
    outbuf(0),
    crc(0)
{
}


tar_output::pointer
tar_output_filter_gzip::create(const tar_output::pointer &a_deeper)
{
    return pointer(new tar_output_filter_gzip(a_deeper));
}


//
// Outputs a long in LSB order to the given file
//      (little endian)
//

static void
put_long(FILE *fp, unsigned long x)
{
    unsigned char buffer[4];
    buffer[0] = x;
    buffer[1] = x >> 8;
    buffer[2] = x >> 16;
    buffer[3] = x >> 24;
    fwrite(buffer, 1, sizeof(buffer), fp);
}


void
tar_output_filter_gzip::write_header(const tar_header &arg)
{
    if (arg.type != tar_header::type_normal)
    {
        pass_through = true;
        tar_output_filter::write_header(arg);
        return;
    }
    if (arg.size <= 1024)
    {
        // Don't bother with files which are too small to compress usefully.
        pass_through = true;
        tar_output_filter::write_header(arg);
        return;
    }
    pass_through = false;

    //
    // Remember the header for later use.
    //
    hdr = arg;

    //
    // Open a temporary file to hold the compressed file.
    //
    static char filename_template[] = "/tmp/tardy-XXXXXX";
    char tfn[sizeof(filename_template)];
    memcpy(tfn, filename_template, sizeof(tfn));
    int fd = explain_mkstemp_or_die(tfn);
    temp_filename = tfn;
    FILE *fp = fdopen(fd, "w+b");
    temp_fp = fp;

    //
    // Initialize the compression engine.
    //
    crc = crc32(0L, Z_NULL, 0);
    stream.avail_in = 0;
    stream.avail_out = 0;
    stream.next_in = NULL;
    stream.next_out = NULL;
    stream.opaque = (voidpf)0;
    stream.zalloc = (alloc_func)0;
    stream.zfree = (free_func)0;

    //
    // Set the parameters for the compression.
    // Note: windowBits is passed < 0 to suppress zlib header.
    //
    int err =
        deflateInit2
        (
            &stream,
            Z_BEST_COMPRESSION, // level
            Z_DEFLATED,         // method
            -MAX_WBITS,         // windowBits
            DEF_MEM_LEVEL,      // memLevel
            Z_DEFAULT_STRATEGY  // strategy
        );
    if (err != Z_OK)
        drop_dead(err);

    if (!outbuf)
        outbuf = new Byte[Z_BUFSIZE];
    stream.next_out = outbuf;
    stream.avail_out = Z_BUFSIZE;

    //
    // Write a very simple .gz header:
    //
    static unsigned char ghdr[] =
    {
        0x1F, 0x8B,  // gzip magic number
        Z_DEFLATED,  // method
        Z_FLAG_EXTRA_FIELD, // flags
        0, 0, 0, 0,  // time
        0,           // xflags
        3,           // always use unix OS_CODE
        4, 0,        // extra length
    };
    fwrite(ghdr, 1, sizeof(ghdr), fp);
    put_long(fp, hdr.size);
}


void
tar_output_filter_gzip::write_header_padding()
{
    if (pass_through)
        tar_output_filter::write_header_padding();
}


void
tar_output_filter_gzip::write_data(const void *data, int len)
{
    if (pass_through)
    {
        tar_output_filter::write_data(data, len);
        return;
    }

    FILE *fp = (FILE *)temp_fp;
    stream.next_in = (Bytef *)data;
    stream.avail_in = len;
    while (stream.avail_in != 0)
    {
        if (stream.avail_out == 0)
        {
            explain_fwrite_or_die(outbuf, Z_BUFSIZE, 1, fp);
            stream.next_out = outbuf;
            stream.avail_out = Z_BUFSIZE;
        }
        int err = deflate(&stream, Z_NO_FLUSH);
        if (err != Z_OK)
            drop_dead(err);
    }
    crc = crc32(crc, (Bytef *)data, len);
}


void
tar_output_filter_gzip::write_data_padding()
{
    if (!pass_through)
    {
        FILE *fp = (FILE *)temp_fp;

        //
        // finish sending the compressed stream
        //
        stream.avail_in = 0; // should be zero already anyway
        if (stream.avail_out == 0)
        {
            explain_fwrite_or_die(outbuf, 1, Z_BUFSIZE, fp);
            stream.next_out = outbuf;
            stream.avail_out = Z_BUFSIZE;
        }
        for (;;)
        {
            int err = deflate(&stream, Z_FINISH);
            if (err < 0)
                drop_dead(err);
            size_t len = Z_BUFSIZE - stream.avail_out;
            if (!len)
                break;
            explain_fwrite_or_die(outbuf, len, 1, fp);
            stream.next_out = outbuf;
            stream.avail_out = Z_BUFSIZE;
        }

        //
        // and the trailer
        //
        put_long(fp, crc);
        put_long(fp, stream.total_in);
        explain_fflush_or_die(fp);

        //
        // Clean up any resources we were using.
        //
        if (stream.state != NULL)
            deflateEnd(&stream);

        //
        // Now we have the temporary file, we know the size of the
        // compressed data, so we are able to write the tar file header
        // for the compressed form into the archive.
        //
        tar_header nhdr = hdr;
        nhdr.type = tar_header::type_normal_gzipped;
        nhdr.size = ftell(fp);
        tar_output_filter::write_header(nhdr);
        tar_output_filter::write_header_padding();

        //
        // Transfer the data from the temporary file into the archive.
        //
        rewind(fp);
        for (long opos = 0; opos < nhdr.size;)
        {
            unsigned long ret;
            unsigned long nbytes = nhdr.size - opos;
            if (nbytes > Z_BUFSIZE)
                nbytes = Z_BUFSIZE;
            ret = explain_fread_or_die(outbuf, 1, nbytes, fp);
            if (ret != nbytes)
            {
                fatal
                (
                    "%s: short read (requested %lu, got %lu)",
                    filename().c_str(),
                    (unsigned long)nbytes,
                    (unsigned long)ret
                );
            }
            tar_output_filter::write_data(outbuf, nbytes);
            opos += nbytes;
        }
        fclose(fp);
        temp_fp = 0;
        unlink(temp_filename.c_str());

        //
        // Go back to pass through mode (the default).
        //
        pass_through = true;
    }

    //
    // Write the data padding to the deeper file.
    //
    tar_output_filter::write_data_padding();
}


void
tar_output_filter_gzip::drop_dead(int err)
{
    fatal("gzip: %s (%s)", z_error(err), stream.msg);
}
