/*
 * 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.
 *
 * Implementation of NuFunnel, NuStraw and ProgressUpdater.
 */
#include "NufxLibPriv.h"


/*
 * ===========================================================================
 *      Progress updater
 * ===========================================================================
 */

/*
 * Initialize the fields in a ProgressData structure, prior to compressing
 * data into a record.
 *
 * The same structure will be used when expanding all threads in a given
 * record.
 */
NuError Nu_ProgressDataInit_Compress(NuArchive* pArchive,
    NuProgressData* pProgressData, const NuRecord* pRecord,
    const UNICHAR* origPathnameUNI, const UNICHAR* pathnameUNI)
{
    const char* cp;

    Assert(pProgressData != NULL);
    Assert(pArchive != NULL);
    Assert(pRecord != NULL);
    Assert(origPathnameUNI != NULL);
    Assert(pathnameUNI != NULL);

    pProgressData->pRecord = pRecord;

    pProgressData->origPathnameUNI = origPathnameUNI;
    pProgressData->pathnameUNI = pathnameUNI;
    cp = strrchr(pathnameUNI, NuGetSepFromSysInfo(pRecord->recFileSysInfo));
    if (cp == NULL || *(cp+1) == '\0')
        pProgressData->filenameUNI = pProgressData->pathnameUNI;
    else
        pProgressData->filenameUNI = cp+1;

    pProgressData->operation = kNuOpAdd;
    pProgressData->state = kNuProgressPreparing;
    pProgressData->uncompressedLength = 0;
    pProgressData->uncompressedProgress = 0;

    pProgressData->compress.threadFormat = (NuThreadFormat)-1;

    /* ya know... if this is NULL, none of the above matters much */
    pProgressData->progressFunc = pArchive->progressUpdaterFunc;

    return kNuErrNone;
}


/*
 * Initialize the fields in a ProgressData structure, prior to expanding
 * data from a record.
 *
 * The same structure will be used when expanding all threads in a given
 * record.
 */
NuError Nu_ProgressDataInit_Expand(NuArchive* pArchive,
    NuProgressData* pProgressData, const NuRecord* pRecord,
    const UNICHAR* newPathnameUNI, UNICHAR newFssep,
    const UNICHAR* origPathnameUNI, NuValue convertEOL)
{
    const NuThread* pThreadIter;
    const char* cp;
    int i;

    Assert(pProgressData != NULL);
    Assert(pArchive != NULL);
    Assert(pRecord != NULL);
    Assert(newPathnameUNI != NULL);
    Assert(origPathnameUNI != NULL);
    Assert(newFssep != 0);

    pProgressData->pRecord = pRecord;
    pProgressData->expand.pThread = NULL;

    pProgressData->origPathnameUNI = origPathnameUNI;
    pProgressData->pathnameUNI = newPathnameUNI;
    cp = strrchr(newPathnameUNI, newFssep);
    if (cp == NULL || *(cp+1) == '\0')
        pProgressData->filenameUNI = newPathnameUNI;
    else
        pProgressData->filenameUNI = cp+1;

    pProgressData->expand.convertEOL = convertEOL;

    /* total up the data threads */
    pProgressData->expand.totalCompressedLength = 0;
    pProgressData->expand.totalUncompressedLength = 0;

    for (i = 0; i < (int)pRecord->recTotalThreads; i++) {
        pThreadIter = Nu_GetThread(pRecord, i);
        if (pThreadIter->thThreadClass != kNuThreadClassData)
            continue;
        pProgressData->expand.totalCompressedLength += pThreadIter->thCompThreadEOF;
        pProgressData->expand.totalUncompressedLength += pThreadIter->actualThreadEOF;
    }

    pProgressData->operation = kNuOpExtract;
    if (pArchive->testMode)
        pProgressData->operation = kNuOpTest;
    pProgressData->state = kNuProgressPreparing;
    pProgressData->uncompressedLength = 0;
    pProgressData->uncompressedProgress = 0;

    /* ya know... if this is NULL, none of the above matters much */
    pProgressData->progressFunc = pArchive->progressUpdaterFunc;

    return kNuErrNone;
}


/*
 * Do the setup on a ProgressData prior to compressing a thread.
 */
NuError Nu_ProgressDataCompressPrep(NuArchive* pArchive, NuStraw* pStraw,
    NuThreadFormat threadFormat, uint32_t sourceLen)
{
    NuProgressData* pProgressData;

    Assert(pArchive != NULL);
    Assert(pStraw != NULL);
    Assert(sourceLen < 32767*65536);

    pProgressData = pStraw->pProgress;
    if (pProgressData == NULL)
        return kNuErrNone;

    pProgressData->uncompressedLength = sourceLen;
    pProgressData->compress.threadFormat = threadFormat;

    return kNuErrNone;
}

/*
 * Do the setup on a ProgressData prior to expanding a thread.
 *
 * "pThread" is the thread being expanded.
 */
NuError Nu_ProgressDataExpandPrep(NuArchive* pArchive, NuFunnel* pFunnel,
    const NuThread* pThread)
{
    NuProgressData* pProgressData;

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

    pProgressData = pFunnel->pProgress;
    if (pProgressData == NULL)
        return kNuErrNone;

    /*pProgressData->compressedLength = pThread->thCompThreadEOF;*/
    pProgressData->uncompressedLength = pThread->actualThreadEOF;
    pProgressData->expand.pThread = pThread;

    return kNuErrNone;
}

/*
 * Compute a completion percentage.
 */
static int Nu_ComputePercent(uint32_t total, uint32_t progress)
{
    uint32_t perc;

    if (!total)
        return 0;

    if (total < 21474836) {
        perc = (progress * 100 + 50) / total;
        if (perc > 100)
            perc = 100;
    } else {
        perc = progress / (total / 100);
        if (perc > 100)
            perc = 100;
    }

    return (int) perc;
}

/*
 * Send the initial progress message, before the output file is opened
 * (when extracting) or the input file is opened (when adding).
 */
NuError Nu_SendInitialProgress(NuArchive* pArchive, NuProgressData* pProgress)
{
    NuResult result;

    Assert(pArchive != NULL);
    Assert(pProgress != NULL);

    if (pProgress->progressFunc == NULL)
        return kNuErrNone;

    pProgress->percentComplete = Nu_ComputePercent(
        pProgress->uncompressedLength, pProgress->uncompressedProgress);

    result = (*pProgress->progressFunc)(pArchive, (NuProgressData*) pProgress);

    if (result == kNuSkip)
        return kNuErrSkipped;   /* [dunno how well this works] */
    if (result == kNuAbort)
        return kNuErrAborted;

    return kNuErrNone;
}


/*
 * ===========================================================================
 *      NuFunnel object
 * ===========================================================================
 */

/*
 * Allocate and initialize a Funnel.
 */
NuError Nu_FunnelNew(NuArchive* pArchive, NuDataSink* pDataSink,
    NuValue convertEOL, NuValue convertEOLTo, NuProgressData* pProgress,
    NuFunnel** ppFunnel)
{
    NuError err = kNuErrNone;
    NuFunnel* pFunnel = NULL;

    Assert(ppFunnel != NULL);
    Assert(pDataSink != NULL);
    Assert(convertEOL == kNuConvertOff ||
           convertEOL == kNuConvertOn ||
           convertEOL == kNuConvertAuto);

    pFunnel = Nu_Calloc(pArchive, sizeof(*pFunnel));
    BailAlloc(pFunnel);
    pFunnel->buffer = Nu_Malloc(pArchive, kNuFunnelBufSize);
    BailAlloc(pFunnel->buffer);

    pFunnel->pDataSink = pDataSink;
    pFunnel->convertEOL = convertEOL;
    pFunnel->convertEOLTo = convertEOLTo;
    pFunnel->convertEOLFrom = kNuEOLUnknown;
    pFunnel->pProgress = pProgress;

    pFunnel->checkStripHighASCII = (pArchive->valStripHighASCII != 0);
    pFunnel->doStripHighASCII = false;  /* determined on first write */

    pFunnel->isFirstWrite = true;

bail:
    if (err != kNuErrNone)
        Nu_FunnelFree(pArchive, pFunnel);
    else
        *ppFunnel = pFunnel;
    return err;
}


/*
 * Free a Funnel.
 *
 * The data should already have been written; it's not the duty of a
 * "free" function to flush data out.
 */
NuError Nu_FunnelFree(NuArchive* pArchive, NuFunnel* pFunnel)
{
    if (pFunnel == NULL)
        return kNuErrNone;

#ifdef DEBUG_MSGS
    if (pFunnel->bufCount)
        Nu_ReportError(NU_BLOB_DEBUG, kNuErrNone,
            "freeing non-empty funnel");
#endif

    Nu_Free(pArchive, pFunnel->buffer);
    Nu_Free(pArchive, pFunnel);

    return kNuErrNone;
}


#if 0
/*
 * Set the maximum amount of output we're willing to push through the
 * funnel.  Attempts to write more than this many bytes will fail.  This
 * allows us to bail out as soon as it's apparent that compression is
 * failing and is actually resulting in a larger file.
 */
void Nu_FunnelSetMaxOutput(NuFunnel* pFunnel, uint32_t maxBytes)
{
    Assert(pFunnel != NULL);
    Assert(maxBytes > 0);

    pFunnel->outMax = maxBytes;
    if (pFunnel->outCount >= pFunnel->outMax)
        pFunnel->outMaxExceeded = true;
    else
        pFunnel->outMaxExceeded = false;
}
#endif


/*
 * Check to see if this is a high-ASCII file.  To qualify, EVERY
 * character must have its high bit set, except for spaces (0x20).
 * (The exception is courtesy Glen Bredon's "Merlin".)
 */
static Boolean Nu_CheckHighASCII(const NuFunnel* pFunnel, const uint8_t* buffer,
    uint32_t count)
{
    Boolean isHighASCII;

    Assert(buffer != NULL);
    Assert(count != 0);
    Assert(pFunnel->checkStripHighASCII);

    isHighASCII = true;
    while (count--) {
        if ((*buffer & 0x80) == 0 && *buffer != 0x20) {
            isHighASCII = false;
            break;
        }
        
        buffer++;
    }

    return isHighASCII;
}

/*
 * Table determining what's a binary character and what isn't.  It would
 * possibly be more compact to generate this from a simple description,
 * but I'm hoping static/const data will end up in the code segment and
 * save space on the heap.
 *
 * This corresponds to less-316's ISO-latin1 "8bcccbcc18b95.33b.".  This
 * may be too loose by itself; we may want to require that the lower-ASCII
 * values appear in higher proportions than the upper-ASCII values.
 * Otherwise we run the risk of converting a binary file with specific
 * properties.  (Note that "upper-ASCII" refers to umlauts and other
 * accented characters, not DOS 3.3 "high ASCII".)
 *
 * The auto-detect mechanism will never be perfect though, so there's not
 * much point in tweaking it to death.
 */
static const char gNuIsBinary[256] = {
    1, 1, 1, 1, 1, 1, 1, 1,  0, 0, 0, 1, 0, 0, 1, 1,    /* ^@-^O */
    1, 1, 1, 1, 1, 1, 1, 1,  1, 1, 1, 1, 1, 1, 1, 1,    /* ^P-^_ */
    0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,    /*   - / */
    0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,    /* 0 - ? */
    0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,    /* @ - O */
    0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,    /* P - _ */
    0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,    /* ` - o */
    0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 1,    /* p - DEL */
    1, 1, 1, 1, 1, 1, 1, 1,  1, 1, 1, 1, 1, 1, 1, 1,    /* 0x80 */
    1, 1, 1, 1, 1, 1, 1, 1,  1, 1, 1, 1, 1, 1, 1, 1,    /* 0x90 */
    0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,    /* 0xa0 */
    0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,    /* 0xb0 */
    0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,    /* 0xc0 */
    0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,    /* 0xd0 */
    0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,    /* 0xe0 */
    0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,    /* 0xf0 */
};

#define kNuMaxUpperASCII    1       /* max #of binary chars per 100 bytes */
#define kNuMinConvThreshold 40      /* min of 40 chars for auto-detect */
/*
 * Decide, based on the contents of the buffer, whether we should do an
 * EOL conversion on the data.
 *
 * We need to decide if we are looking at text data, and if so, what kind
 * of line terminator is in use.
 *
 * If we don't have enough data to make a determination, don't mess with it.
 * (Thought for the day: add a "bias" flag, based on the NuRecord fileType,
 * that causes us to handle borderline or sub-min-threshold cases more
 * reasonably.  If it's of type TXT, it's probably text.)
 *
 * We try to figure out whether it's CR, LF, or CRLF, so that we can
 * skip the CPU-intensive conversion process if it isn't necessary.
 *
 * We will also enable a "high-ASCII" stripper if requested.  This is
 * only enabled when EOL conversions are enabled.
 *
 * Returns kConvEOLOff or kConvEOLOn, and sets pFunnel->doStripHighASCII
 * if pFunnel->CheckStripHighASCII is set.
 */
static NuValue Nu_DetermineConversion(NuFunnel* pFunnel, const uint8_t* buffer,
    uint32_t count)
{
    uint32_t bufCount, numBinary, numLF, numCR;
    Boolean isHighASCII;
    uint8_t val;

    if (count < kNuMinConvThreshold)
        return kNuConvertOff;

    /*
     * Check to see if the buffer is all high-ASCII characters.  If it is,
     * we want to strip characters before we test them below.
     */
    if (pFunnel->checkStripHighASCII) {
        isHighASCII = Nu_CheckHighASCII(pFunnel, buffer, count);
        DBUG(("+++ determined isHighASCII=%d\n", isHighASCII));
    } else {
        isHighASCII = false;
        DBUG(("+++ not even checking isHighASCII\n"));
    }

    bufCount = count;
    numBinary = numLF = numCR = 0;
    while (bufCount--) {
        val = *buffer++;
        if (isHighASCII)
            val &= 0x7f;
        if (gNuIsBinary[val])
            numBinary++;
        if (val == kNuCharLF)
            numLF++;
        if (val == kNuCharCR)
            numCR++;
    }

    /* if #found is > #allowed, it's a binary file */
    if (count < 100) {
        /* use simplified check on files between kNuMinConvThreshold and 100 */
        if (numBinary > kNuMaxUpperASCII)
            return kNuConvertOff;
    } else if (numBinary > (count / 100) * kNuMaxUpperASCII)
        return kNuConvertOff;

    /*
     * If our "convert to" setting is the same as what we're converting
     * from, we can turn off the converter and speed things up.
     *
     * These are simplistic, but this is intended as an optimization.  We
     * will blow it if the input has lots of CRs and LFs scattered about,
     * and they just happen to be in equal amounts, but it's not clear
     * to me that an automatic EOL conversion makes sense on that sort
     * of file anyway.
     *
     * None of this applies if we also need to do a high-ASCII conversion.
     */
    if (isHighASCII) {
        pFunnel->doStripHighASCII = true;
    } else {
        if (numLF && !numCR)
            pFunnel->convertEOLFrom = kNuEOLLF;
        else if (!numLF && numCR)
            pFunnel->convertEOLFrom = kNuEOLCR;
        else if (numLF && numLF == numCR)
            pFunnel->convertEOLFrom = kNuEOLCRLF;
        else
            pFunnel->convertEOLFrom = kNuEOLUnknown;
    }

    return kNuConvertOn;
}

/*
 * Write a block of data to the appropriate output device.  Test for
 * excessive data, and raise "outMaxExceeded" if we overrun.
 *
 * This is either a Funnel function or a DataSink function, depending on
 * your perspective.
 */
static inline void Nu_FunnelPutBlock(NuFunnel* pFunnel, const uint8_t* buf,
    uint32_t len)
{
    Assert(pFunnel != NULL);
    Assert(pFunnel->pDataSink != NULL);
    Assert(buf != NULL);
    Assert(len > 0);

#if 0
    if (pFunnel->outMax) {
        if (pFunnel->outMaxExceeded)
            return;
        if (pFunnel->outCount + len > pFunnel->outMax) {
            pFunnel->outMaxExceeded = true;
            return;
        }
    }
    pFunnel->outCount += len;
#endif

    Nu_DataSinkPutBlock(pFunnel->pDataSink, buf, len);
}


/*
 * Output the EOL marker requested for this system.
 */
static inline void Nu_PutEOL(NuFunnel* pFunnel)
{
    uint8_t ch;

    if (pFunnel->convertEOLTo == kNuEOLCR) {
        ch = kNuCharCR;
        Nu_FunnelPutBlock(pFunnel, &ch, 1);
    } else if (pFunnel->convertEOLTo == kNuEOLLF) {
        ch = kNuCharLF;
        Nu_FunnelPutBlock(pFunnel, &ch, 1);
    } else if (pFunnel->convertEOLTo == kNuEOLCRLF) {
        ch = kNuCharCR;
        Nu_FunnelPutBlock(pFunnel, &ch, 1);
        ch = kNuCharLF;
        Nu_FunnelPutBlock(pFunnel, &ch, 1);
    } else {
        Assert(0);
    }
}

/*
 * Write a buffer of data, using the EOL conversion associated with the
 * funnel (if any).
 *
 * When converting to the system's EOL convention, we take anything
 * that looks like an EOL mark and convert it.  Doesn't matter if it's
 * CR, LF, or CRLF; all three get converted to whatever the system uses.
 */
static NuError Nu_FunnelWriteConvert(NuFunnel* pFunnel, const uint8_t* buffer,
    uint32_t count)
{
    NuError err = kNuErrNone;
    uint32_t progressCount = count;

    /*if (pFunnel->outMaxExceeded)
        return kNuErrOutMax;*/

    if (pFunnel->isFirstWrite) {
        /*
         * This is the first write/flush we've done on this Funnel.
         * Check the data we have buffered to decide whether or not
         * we want to do text conversions.
         */
        if (pFunnel->convertEOL == kNuConvertAuto) {
            pFunnel->convertEOL = Nu_DetermineConversion(pFunnel, buffer,count);
            DBUG(("+++ DetermineConversion --> %ld / %ld (%d)\n",
                pFunnel->convertEOL, pFunnel->convertEOLFrom,
                pFunnel->doStripHighASCII));

            if (pFunnel->convertEOLFrom == pFunnel->convertEOLTo) {
                DBUG(("+++ Switching redundant converter off\n"));
                pFunnel->convertEOL = kNuConvertOff;
            }
            /* put it where the progress meter can see it */
            if (pFunnel->pProgress != NULL)
                pFunnel->pProgress->expand.convertEOL = pFunnel->convertEOL;
        } else if (pFunnel->convertEOL == kNuConvertOn) {
            if (pFunnel->checkStripHighASCII) {
                /* assume this part of the buffer is representative */
                pFunnel->doStripHighASCII = Nu_CheckHighASCII(pFunnel,
                                                buffer, count);
            } else {
                Assert(!pFunnel->doStripHighASCII);
            }
            DBUG(("+++ Converter is on, convHighASCII=%d\n",
                pFunnel->doStripHighASCII));
        }
    }
    Assert(pFunnel->convertEOL != kNuConvertAuto);  /* on or off now */
    pFunnel->isFirstWrite = false;

    if (pFunnel->convertEOL == kNuConvertOff) {
        /* write it straight */
        Nu_FunnelPutBlock(pFunnel, buffer, count);
    } else {
        /* do the EOL conversion and optional high-bit stripping */
        Boolean lastCR = pFunnel->lastCR;   /* make local copy */
        uint8_t uch;
        int mask;

        if (pFunnel->doStripHighASCII)
            mask = 0x7f;
        else
            mask = 0xff;

        /*
         * We could get a significant speed improvement here by writing
         * non-EOL chars as a larger block instead of single bytes.
         */
        while (count--) {
            uch = (*buffer) & mask;

            if (uch == kNuCharCR) {
                Nu_PutEOL(pFunnel);
                lastCR = true;
            } else if (uch == kNuCharLF) {
                if (!lastCR)
                    Nu_PutEOL(pFunnel);
                lastCR = false;
            } else {
                Nu_FunnelPutBlock(pFunnel, &uch, 1);
                lastCR = false;
            }
            buffer++;
        }
        pFunnel->lastCR = lastCR;   /* save copy */

    }

    /*if (pFunnel->outMaxExceeded)
        err = kNuErrOutMax;*/

    err = Nu_DataSinkGetError(pFunnel->pDataSink);

    /* update progress counter with pre-LFCR count */
    if (err == kNuErrNone && pFunnel->pProgress != NULL)
        pFunnel->pProgress->uncompressedProgress += progressCount;

    return err;
}


/*
 * Flush any data currently in the funnel.
 */
NuError Nu_FunnelFlush(NuArchive* pArchive, NuFunnel* pFunnel)
{
    NuError err = kNuErrNone;

    if (!pFunnel->bufCount)
        goto bail;

    err = Nu_FunnelWriteConvert(pFunnel, pFunnel->buffer, pFunnel->bufCount);
    BailError(err);

    pFunnel->bufCount = 0;
    err = Nu_FunnelSendProgressUpdate(pArchive, pFunnel);
    /* fall through with error */

bail:
    return err;
}


/*
 * Write a bunch of bytes into a funnel.  They will be held in the buffer
 * if they fit, or flushed out the bottom if not.
 */
NuError Nu_FunnelWrite(NuArchive* pArchive, NuFunnel* pFunnel,
    const uint8_t* buffer, uint32_t count)
{
    NuError err = kNuErrNone;

    /*pFunnel->inCount += count;*/

    /*
     * If it will fit into the buffer, just copy it in.
     */
    if (pFunnel->bufCount + count < kNuFunnelBufSize) {
        if (count == 1)     /* minor optimization */
            *(pFunnel->buffer + pFunnel->bufCount) = *buffer;
        else
            memcpy(pFunnel->buffer + pFunnel->bufCount, buffer, count);
        pFunnel->bufCount += count;
        goto bail;
    } else {
        /*
         * Won't fit.  We have to flush what we have, and we can either
         * blow out what we were just given or put it at the start of
         * the buffer.
         */
        if (pFunnel->bufCount) {
            err = Nu_FunnelFlush(pArchive, pFunnel);
            BailError(err);
        } else {
            err = Nu_FunnelSendProgressUpdate(pArchive, pFunnel);
            BailError(err);
        }

        Assert(pFunnel->bufCount == 0);

        if (count >= kNuFunnelBufSize / 4) {
            /* it's more than 25% of the buffer, just write it now */
            err = Nu_FunnelWriteConvert(pFunnel, buffer, count);
            BailError(err);
        } else {
            memcpy(pFunnel->buffer, buffer, count);
            pFunnel->bufCount = count;
        }
        goto bail;
    }

bail:
    return err;
}


/*
 * Set the Funnel's progress state.
 */
NuError Nu_FunnelSetProgressState(NuFunnel* pFunnel, NuProgressState state)
{
    Assert(pFunnel != NULL);

    if (pFunnel->pProgress == NULL)
        return kNuErrNone;

    pFunnel->pProgress->state = state;

    return kNuErrNone;
}


/*
 * Send a progress update to the application, if they're interested.
 */
NuError Nu_FunnelSendProgressUpdate(NuArchive* pArchive, NuFunnel* pFunnel)
{
    NuProgressData* pProgress;

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

    pProgress = pFunnel->pProgress;
    if (pProgress == NULL)
        return kNuErrNone;      /* no progress meter attached */

    /* don't continue if they're not accepting progress messages */
    if (pProgress->progressFunc == NULL)
        return kNuErrNone;

    /* other than the choice of arguments, it's pretty much the same story */
    return Nu_SendInitialProgress(pArchive, pProgress);
}


/*
 * Pull the "doExpand" parameter out of the data source.
 */
Boolean Nu_FunnelGetDoExpand(NuFunnel* pFunnel)
{
    Assert(pFunnel != NULL);
    Assert(pFunnel->pDataSink != NULL);

    return Nu_DataSinkGetDoExpand(pFunnel->pDataSink);
}


/*
 * ===========================================================================
 *      NuStraw object
 * ===========================================================================
 */

/*
 * Allocate and initialize a Straw.
 */
NuError Nu_StrawNew(NuArchive* pArchive, NuDataSource* pDataSource,
    NuProgressData* pProgress, NuStraw** ppStraw)
{
    NuError err = kNuErrNone;
    NuStraw* pStraw = NULL;

    Assert(ppStraw != NULL);
    Assert(pDataSource != NULL);

    pStraw = Nu_Calloc(pArchive, sizeof(*pStraw));
    BailAlloc(pStraw);
    pStraw->pDataSource = pDataSource;
    pStraw->pProgress = pProgress;
    pStraw->lastProgress = 0;
    pStraw->lastDisplayed = 0;

bail:
    if (err != kNuErrNone)
        Nu_StrawFree(pArchive, pStraw);
    else
        *ppStraw = pStraw;
    return err;
}

/*
 * Free a Straw.
 */
NuError Nu_StrawFree(NuArchive* pArchive, NuStraw* pStraw)
{
    if (pStraw == NULL)
        return kNuErrNone;

    /* we don't own the data source or progress meter */
    Nu_Free(pArchive, pStraw);

    return kNuErrNone;
}


/*
 * Set the Straw's progress state.
 */
NuError Nu_StrawSetProgressState(NuStraw* pStraw, NuProgressState state)
{
    Assert(pStraw != NULL);
    Assert(pStraw->pProgress != NULL);

    pStraw->pProgress->state = state;

    return kNuErrNone;
}

/*
 * Send a progress update to the application, if they're interested.
 */
NuError Nu_StrawSendProgressUpdate(NuArchive* pArchive, NuStraw* pStraw)
{
    NuProgressData* pProgress;

    Assert(pArchive != NULL);
    Assert(pStraw != NULL);

    pProgress = pStraw->pProgress;
    if (pProgress == NULL)
        return kNuErrNone;      /* no progress meter attached */

    /* don't continue if they're not accepting progress messages */
    if (pProgress->progressFunc == NULL)
        return kNuErrNone;

    /* other than the choice of arguments, it's pretty much the same story */
    return Nu_SendInitialProgress(pArchive, pProgress);
}


/*
 * Read data from a straw.
 */
NuError Nu_StrawRead(NuArchive* pArchive, NuStraw* pStraw, uint8_t* buffer,
    long len)
{
    NuError err;

    Assert(pArchive != NULL);
    Assert(pStraw != NULL);
    Assert(buffer != NULL);
    Assert(len > 0);

    /*
     * No buffering going on, so this is straightforward.
     */

    err = Nu_DataSourceGetBlock(pStraw->pDataSource, buffer, len);
    BailError(err);

    /*
     * Progress updating for adding is a little more complicated than
     * for extracting.  When extracting, the funnel controls the size
     * of the output buffer, and only pushes an update when the output
     * buffer fills.  Here, we don't know how much will be asked for at
     * a time, so we have to pace the updates or we risk flooding the
     * application.
     *
     * We also have another problem: we want to indicate how much data
     * has been processed, not how much data is *about* to be processed.
     * So we have to set the percentage based on how much was requested
     * on the previous call.  (This assumes that whatever they asked for
     * last time has already been fully processed.)
     */
    if (pStraw->pProgress != NULL) {
        pStraw->pProgress->uncompressedProgress = pStraw->lastProgress;
        pStraw->lastProgress += len;

        if (!pStraw->pProgress->uncompressedProgress ||
            (pStraw->pProgress->uncompressedProgress - pStraw->lastDisplayed
                > (kNuFunnelBufSize * 3 / 4)))
        {
            err = Nu_StrawSendProgressUpdate(pArchive, pStraw);
            pStraw->lastDisplayed = pStraw->pProgress->uncompressedProgress;
            BailError(err);
        }

    }

bail:
    return err;
}


/*
 * Rewind a straw.  This rewinds the underlying data source, and resets
 * some progress counters.
 */
NuError Nu_StrawRewind(NuArchive* pArchive, NuStraw* pStraw)
{
    Assert(pStraw != NULL);
    Assert(pStraw->pDataSource != NULL);

    pStraw->lastProgress = 0;
    pStraw->lastDisplayed = 0;

    return Nu_DataSourceRewind(pStraw->pDataSource);
}

