/*
 * NuFX archive manipulation library
 * Copyright (C) 2000-2007 by Andy McFadden, All Rights Reserved.
 * This is free software; you can redistribute it and/or modify it under the
 * terms of the BSD License, see the file COPYING-LIB.
 *
 * Support for the "deflate" algorithm, via the "zlib" library.
 *
 * This compression format is totally unsupported on the Apple II.  This
 * is provided primarily for the benefit of Apple II emulators that want
 * a better storage format for disk images than SHK+LZW or a ZIP file.
 *
 * This code was developed and tested with ZLIB_VERSION "1.1.3".  It is
 * expected to work with any version >= 1.1.3 and < 2.x.  Please visit
 * http://www.zlib.org/ for more information.
 */
#include "NufxLibPriv.h"

#ifdef ENABLE_DEFLATE
#include "zlib.h"

#define kNuDeflateLevel 9       /* use maximum compression */


/*
 * Alloc and free functions provided to zlib.
 */
static voidpf Nu_zalloc(voidpf opaque, uInt items, uInt size)
{
    return Nu_Malloc(opaque, items * size);
}
static void Nu_zfree(voidpf opaque, voidpf address)
{
    Nu_Free(opaque, address);
}


/*
 * ===========================================================================
 *      Compression
 * ===========================================================================
 */

/*
 * Compress "srcLen" bytes from "pStraw" to "fp".
 */
NuError Nu_CompressDeflate(NuArchive* pArchive, NuStraw* pStraw, FILE* fp,
    uint32_t srcLen, uint32_t* pDstLen, uint16_t* pCrc)
{
    NuError err = kNuErrNone;
    z_stream zstream;
    int zerr;
    Bytef* outbuf = NULL;

    Assert(pArchive != NULL);
    Assert(pStraw != NULL);
    Assert(fp != NULL);
    Assert(srcLen > 0);
    Assert(pDstLen != NULL);
    Assert(pCrc != NULL);

    err = Nu_AllocCompressionBufferIFN(pArchive);
    if (err != kNuErrNone)
        return err;

    /* allocate a similarly-sized buffer for the output */
    outbuf = Nu_Malloc(pArchive, kNuGenCompBufSize);
    BailAlloc(outbuf);

    /*
     * Initialize the zlib stream.
     */
    zstream.zalloc = Nu_zalloc;
    zstream.zfree = Nu_zfree;
    zstream.opaque = pArchive;
    zstream.next_in = NULL;
    zstream.avail_in = 0;
    zstream.next_out = outbuf;
    zstream.avail_out = kNuGenCompBufSize;
    zstream.data_type = Z_UNKNOWN;

    zerr = deflateInit(&zstream, kNuDeflateLevel);
    if (zerr != Z_OK) {
        err = kNuErrInternal;
        if (zerr == Z_VERSION_ERROR) {
            Nu_ReportError(NU_BLOB, err,
                "installed zlib is not compatible with linked version (%s)",
                ZLIB_VERSION);
        } else {
            Nu_ReportError(NU_BLOB, err,
                "call to deflateInit failed (zerr=%d)", zerr);
        }
        goto bail;
    }

    /*
     * Loop while we have data.
     */
    do {
        uint32_t getSize;
        int flush;

        /* should be able to read a full buffer every time */
        if (zstream.avail_in == 0 && srcLen) {
            getSize = (srcLen > kNuGenCompBufSize) ? kNuGenCompBufSize : srcLen;
            DBUG(("+++ reading %ld bytes\n", getSize));

            err = Nu_StrawRead(pArchive, pStraw, pArchive->compBuf, getSize);
            if (err != kNuErrNone) {
                Nu_ReportError(NU_BLOB, err, "deflate read failed");
                goto z_bail;
            }

            srcLen -= getSize;

            *pCrc = Nu_CalcCRC16(*pCrc, pArchive->compBuf, getSize);

            zstream.next_in = pArchive->compBuf;
            zstream.avail_in = getSize;
        }

        if (srcLen == 0)
            flush = Z_FINISH;       /* tell zlib that we're done */
        else
            flush = Z_NO_FLUSH;     /* more to come! */

        zerr = deflate(&zstream, flush);
        if (zerr != Z_OK && zerr != Z_STREAM_END) {
            err = kNuErrInternal;
            Nu_ReportError(NU_BLOB, err, "zlib deflate call failed (zerr=%d)",
                zerr);
            goto z_bail;
        }

        /* write when we're full or when we're done */
        if (zstream.avail_out == 0 ||
            (zerr == Z_STREAM_END && zstream.avail_out != kNuGenCompBufSize))
        {
            DBUG(("+++ writing %d bytes\n", zstream.next_out - outbuf));
            err = Nu_FWrite(fp, outbuf, zstream.next_out - outbuf);
            if (err != kNuErrNone) {
                Nu_ReportError(NU_BLOB, err, "fwrite failed in deflate");
                goto z_bail;
            }

            zstream.next_out = outbuf;
            zstream.avail_out = kNuGenCompBufSize;
        }
    } while (zerr == Z_OK);

    Assert(zerr == Z_STREAM_END);       /* other errors should've been caught */

    *pDstLen = zstream.total_out;

z_bail:
    deflateEnd(&zstream);        /* free up any allocated structures */

bail:
    if (outbuf != NULL)
        free(outbuf);
    return err;
}


/*
 * ===========================================================================
 *      Expansion
 * ===========================================================================
 */

/*
 * Expand from "infp" to "pFunnel".
 */
NuError Nu_ExpandDeflate(NuArchive* pArchive, const NuRecord* pRecord,
    const NuThread* pThread, FILE* infp, NuFunnel* pFunnel, uint16_t* pCrc)
{
    NuError err = kNuErrNone;
    z_stream zstream;
    int zerr;
    uint32_t compRemaining;
    Bytef* outbuf;

    Assert(pArchive != NULL);
    Assert(pThread != NULL);
    Assert(infp != NULL);
    Assert(pFunnel != NULL);

    err = Nu_AllocCompressionBufferIFN(pArchive);
    if (err != kNuErrNone)
        return err;

    /* allocate a similarly-sized buffer for the output */
    outbuf = Nu_Malloc(pArchive, kNuGenCompBufSize);
    BailAlloc(outbuf);

    compRemaining = pThread->thCompThreadEOF;

    /*
     * Initialize the zlib stream.
     */
    zstream.zalloc = Nu_zalloc;
    zstream.zfree = Nu_zfree;
    zstream.opaque = pArchive;
    zstream.next_in = NULL;
    zstream.avail_in = 0;
    zstream.next_out = outbuf;
    zstream.avail_out = kNuGenCompBufSize;
    zstream.data_type = Z_UNKNOWN;

    zerr = inflateInit(&zstream);
    if (zerr != Z_OK) {
        err = kNuErrInternal;
        if (zerr == Z_VERSION_ERROR) {
            Nu_ReportError(NU_BLOB, err,
                "installed zlib is not compatible with linked version (%s)",
                ZLIB_VERSION);
        } else {
            Nu_ReportError(NU_BLOB, err,
                "call to inflateInit failed (zerr=%d)", zerr);
        }
        goto bail;
    }

    /*
     * Loop while we have data.
     */
    do {
        uint32_t getSize;

        /* read as much as we can */
        if (zstream.avail_in == 0) {
            getSize = (compRemaining > kNuGenCompBufSize) ?
                        kNuGenCompBufSize : compRemaining;
            DBUG(("+++ reading %ld bytes (%ld left)\n", getSize,
                compRemaining));

            err = Nu_FRead(infp, pArchive->compBuf, getSize);
            if (err != kNuErrNone) {
                Nu_ReportError(NU_BLOB, err, "inflate read failed");
                goto z_bail;
            }

            compRemaining -= getSize;

            zstream.next_in = pArchive->compBuf;
            zstream.avail_in = getSize;
        }

        /* uncompress the data */
        zerr = inflate(&zstream, Z_NO_FLUSH);
        if (zerr != Z_OK && zerr != Z_STREAM_END) {
            err = kNuErrInternal;
            Nu_ReportError(NU_BLOB, err, "zlib inflate call failed (zerr=%d)",
                zerr);
            goto z_bail;
        }

        /* write every time there's anything (buffer will usually be full) */
        if (zstream.avail_out != kNuGenCompBufSize) {
            DBUG(("+++ writing %d bytes\n", zstream.next_out - outbuf));
            err = Nu_FunnelWrite(pArchive, pFunnel, outbuf,
                    zstream.next_out - outbuf);
            if (err != kNuErrNone) {
                Nu_ReportError(NU_BLOB, err, "write failed in inflate");
                goto z_bail;
            }

            if (pCrc != NULL)
                *pCrc = Nu_CalcCRC16(*pCrc, outbuf, zstream.next_out - outbuf);

            zstream.next_out = outbuf;
            zstream.avail_out = kNuGenCompBufSize;
        }
    } while (zerr == Z_OK);

    Assert(zerr == Z_STREAM_END);       /* other errors should've been caught */

    if (zstream.total_out != pThread->actualThreadEOF) {
        err = kNuErrBadData;
        Nu_ReportError(NU_BLOB, err,
            "size mismatch on inflated file (%ld vs %u)",
            zstream.total_out, pThread->actualThreadEOF);
        goto z_bail;
    }

z_bail:
    inflateEnd(&zstream);        /* free up any allocated structures */

bail:
    if (outbuf != NULL)
        free(outbuf);
    return err;
}

#endif /*ENABLE_DEFLATE*/
