/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Convert AppleWorks 3.0 documents.
 */
#include "StdAfx.h"
#include "AppleWorks.h"

/*
 * ===========================================================================
 *      AppleWorks WP
 * ===========================================================================
 */

/*
 * AppleWorks word processor file format, from FTN.1A.xxxx.
 *
 * The overall file format is:
 *
 *  file header
 *  array of line records
 *  $ff $ff
 *  optional tags
 */

/*
 * Decide whether or not we want to handle this file.
 */
void ReformatAWP::Examine(ReformatHolder* pHolder)
{
    ReformatHolder::ReformatApplies applies = ReformatHolder::kApplicNot;

    if (pHolder->GetFileType() == kTypeAWP)
        applies = ReformatHolder::kApplicProbably;

    pHolder->SetApplic(ReformatHolder::kReformatAWP, applies,
        ReformatHolder::kApplicNot, ReformatHolder::kApplicNot);
}

/*
 * Reformat an AppleWorks WP document.
 */
int ReformatAWP::Process(const ReformatHolder* pHolder,
    ReformatHolder::ReformatID id, ReformatHolder::ReformatPart part,
    ReformatOutput* pOutput)
{
    const uint8_t* srcPtr = pHolder->GetSourceBuf(part);
    long srcLen = pHolder->GetSourceLen(part);
    long length = srcLen;
    int retval = -1;

    bool skipRecord;
    uint8_t lineRecCode, lineRecData;

    if (srcLen > 65536)
        fUseRTF = false;

    //fUseRTF = false;
    //fShowEmbeds = false;

    /* expect header plus EOF bytes at least */
    if (srcLen <= kFileHeaderSize) {
        LOGI("  AWP truncated?");
        goto bail;
    }

    RTFBegin(kRTFFlagColorTable);

    /*
     * Grab the file header.
     */
    assert(sizeof(fFileHeader) == kFileHeaderSize);

    memcpy(&fFileHeader, srcPtr, sizeof(fFileHeader));
    srcPtr += sizeof(fFileHeader);
    length -= sizeof(fFileHeader);

    /* do some quick sanity checks */
    if (fFileHeader.seventyNine != kSeventyNine) {
        LOGI("ERROR: expected %d in signature byte, found %d",
            kSeventyNine, fFileHeader.seventyNine);
        goto bail;
    }
    if (fFileHeader.sfMinVers && fFileHeader.sfMinVers != kSFMinVers30) {
        LOGI("WARNING: unexpected value %d for sfMinVers",
            fFileHeader.sfMinVers);
        /* keep going */
    }

    InitDocState();

    /* if first line record is invalid, skip it */
    skipRecord = false;
    if (fFileHeader.sfMinVers == kSFMinVers30)
        skipRecord = true;

    /* set margins to 1.0 inches at 10cpi */
    RTFLeftMargin(10);
    RTFRightMargin(10);

    /*
     * Read the line records.
     */
    while (1) {
        if (length < 0) {
            LOGI(" AWP truncated file");
            goto bail;
        }
        lineRecData = Read8(&srcPtr, &length);
        if (length < 0) {
            LOGI(" AWP truncated file");
            goto bail;
        }
        lineRecCode = Read8(&srcPtr, &length);

        if (length < 0) {
            LOGI(" AWP truncated file");
            goto bail;
        }

        if (skipRecord) {
            skipRecord = false;
            continue;
        }

        /* end of data reached? */
        if (lineRecData == kEOFMarker && lineRecCode == kEOFMarker)
            break;

        if (ProcessLineRecord(lineRecData, lineRecCode, &srcPtr, &length) != 0)
        {
            LOGI("ProcessLineRecord failed, bailing");
            goto bail;
        }
    }


    /* 
     * Read the optional tags.
     */
    /* (nah) */

    RTFEnd();

    SetResultBuffer(pOutput);
    retval = 0;

bail:
    return retval;
}

/*
 * Initialize the DocState structure.
 */
void ReformatAWP::InitDocState(void)
{
    memset(&fDocState, 0, sizeof(fDocState));
    fDocState.line = 1;
}

/*
 * Process a line record.
 */
int ReformatAWP::ProcessLineRecord(uint8_t lineRecData, uint8_t lineRecCode,
    const uint8_t** pSrcPtr, long* pLength)
{
    int err = 0;

    //LOGI(" AWP line rec <0x%02x><0x%02x>", lineRecCode, lineRecData);

    if (lineRecCode == kLineRecordCarriageReturn) {
        /* ignore the horizontal offset for now */
        RTFNewPara();
    } else if (lineRecCode == kLineRecordText) {
        if (pLength > 0)
            err = HandleTextRecord(lineRecData, pSrcPtr, pLength);
        else 
            err = -1;
    } else if (lineRecCode >= kLineRecordCommandMin &&
               lineRecCode <= kLineRecordCommandMax)
    {
        switch (lineRecCode) {
        case kLineRecordCommandCenter:
            RTFParaCenter();
            break;
        case kLineRecordCommandRightJustify:
            RTFParaRight();
            break;
        case kLineRecordCommandUnjustify:
            RTFParaLeft();
            break;
        case kLineRecordCommandJustify:
            RTFParaJustify();
            break;
        case kLineRecordCommandLeftMargin:
            RTFLeftMargin(lineRecData);
            break;
        case kLineRecordCommandRightMargin:
            RTFRightMargin(lineRecData);
            break;

        /* we handle these by showing them in the text */
        case kLineRecordCommandPageNumber:
            if (fShowEmbeds) {
                RTFSetColor(kColorBlue);
                BufPrintf("<set-page-number %d>", lineRecData);
                RTFSetColor(kColorNone);
                RTFNewPara();
            }
            break;
        case kLineRecordCommandPageHeader:
            if (fShowEmbeds) {
                RTFSetColor(kColorBlue);
                BufPrintf("<page-header>");
                RTFSetColor(kColorNone);
                RTFNewPara();
            }
            break;
        case kLineRecordCommandPageHeaderEnd:
            if (fShowEmbeds) {
                RTFSetColor(kColorBlue);
                BufPrintf("</page-header>");
                RTFSetColor(kColorNone);
                RTFNewPara();
            }
            break;
        case kLineRecordCommandPageFooter:
            if (fShowEmbeds) {
                RTFSetColor(kColorBlue);
                BufPrintf("<page-footer>");
                RTFSetColor(kColorNone);
                RTFNewPara();
            }
            break;
        case kLineRecordCommandPageFooterEnd:
            if (fShowEmbeds) {
                RTFSetColor(kColorBlue);
                BufPrintf("</page-footer>");
                RTFSetColor(kColorNone);
                RTFNewPara();
            }
            break;
        case kLineRecordCommandNewPage:
            if (fUseRTF)
                RTFPageBreak();
            else if (fShowEmbeds) {
                RTFSetColor(kColorBlue);    // won't do anything
                BufPrintf("<page-break>");
                RTFSetColor(kColorNone);
            }
            break;

        case kLineRecordCommandPlatenWidth:
        case kLineRecordCommandCharsPerInch:
        case kLineRecordCommandProportional1:
        case kLineRecordCommandProportional2:
        case kLineRecordCommandIndent:
        case kLineRecordCommandPaperLength:
        case kLineRecordCommandTopMargin:
        case kLineRecordCommandBottomMargin:
        case kLineRecordCommandLinesPerInch:
        case kLineRecordCommandSingleSpace:
        case kLineRecordCommandDoubleSpace:
        case kLineRecordCommandTripleSpace:
        case kLineRecordCommandGroupBegin:
        case kLineRecordCommandGroupEnd:
        case kLineRecordCommandSkipLines:
        case kLineRecordCommandPauseEachPage:
        case kLineRecordCommandPauseHere:
        case kLineRecordCommandSetMarker:
        case kLineRecordCommandSetPageNumber:
        case kLineRecordCommandPageBreak:
        case kLineRecordCommandPageBreak256:
        case kLineRecordCommandPageBreakPara:
        case kLineRecordCommandPageBreakPara256:
        default:
            LOGD(" AWP cmd <0x%02x><0x%02x>", lineRecCode, lineRecData);
            break;
        }
    } else {
        /* bad command */
        LOGW("WARNING: unrecognized code 0x%02x at 0x%p", lineRecCode,
            *pSrcPtr);
        fDocState.softFailures++;
        if (fDocState.softFailures > kMaxSoftFailures) {
            LOGE("ERROR: too many failures, giving up");
            err = -1;
        }
    }

    return err;
}

/*
 * Handle a text record.  The first two bytes are flags, the rest is
 * either the text or a ruler.  Special codes may be embedded in the text.
 *
 * "lineRecData" has the number of bytes of input that we have yet to read.
 */
int ReformatAWP::HandleTextRecord(uint8_t lineRecData,
    const uint8_t** pSrcPtr, long* pLength)
{
    int err = 0;
    uint8_t tabFlags;
    uint8_t byteCountPlusCR;
    int byteCount = lineRecData;
    bool noOutput = false;
    int ic;

    tabFlags = Read8(pSrcPtr, pLength);
    byteCount--;
    byteCountPlusCR = Read8(pSrcPtr, pLength);
    byteCount--;
    if (*pLength < 0) {
        err = -1;
        goto bail;
    }

    if (byteCount <= 0) {
        LOGI("WARNING: line %ld: short line (%d)",
            fDocState.line, byteCount);
        /* this is bad, but keep going anyway */
    }

    if ((byteCountPlusCR & ~kCRatEOL) != byteCount) {
        LOGI("WARNING: line %ld: byteCount now %d, offset 3 count %d",
            fDocState.line, byteCount, byteCountPlusCR & ~kCRatEOL);
        /* not sure why this would legally happen */
    }

    if (tabFlags == kTabFlagsIsRuler)
        noOutput = true;

    while ((*pLength > 0) && (byteCount--)) {
        ic = Read8(pSrcPtr, pLength);
        if (*pLength < 0) {
            err = -1;
            goto bail;
        }

        if (noOutput)
            continue;

        if (ic < kMinTextChar) {
            switch (ic) {
            case kSpecialCharBoldBegin:
                RTFBoldOn();
                break;
            case kSpecialCharBoldEnd:
                RTFBoldOff();
                break;
            case kSpecialCharSuperscriptBegin:
                RTFSuperscriptOn();
                break;
            case kSpecialCharSuperscriptEnd:
                RTFSuperscriptOff();
                break;
            case kSpecialCharSubscriptBegin:
                RTFSubscriptOn();
                break;
            case kSpecialCharSubscriptEnd:
                RTFSubscriptOff();
                break;
            case kSpecialCharUnderlineBegin:
                RTFUnderlineOn();
                break;
            case kSpecialCharUnderlineEnd:
                RTFUnderlineOff();
                break;
            case kSpecialCharEnterKeyboard:
                if (fShowEmbeds) {
                    RTFSetColor(kColorBlue);
                    BufPrintf("<kdb-entry>");
                    RTFSetColor(kColorNone);
                }
                break;
            case kSpecialCharPrintPageNumber:
                if (fShowEmbeds) {
                    RTFSetColor(kColorBlue);
                    BufPrintf("<page#>");
                    RTFSetColor(kColorNone);
                }
                break;
            case kSpecialCharStickySpace:
                /* MSWord uses "\~", but RichEdit ignores that */
                BufPrintf(" ");
                break;
            case kSpecialCharMailMerge:
                if (fShowEmbeds) {
                    RTFSetColor(kColorBlue);
                    BufPrintf("<mail-merge>");
                    RTFSetColor(kColorNone);
                }
            case kSpecialCharPrintDate:
                if (fShowEmbeds) {
                    RTFSetColor(kColorBlue);
                    BufPrintf("<date>");
                    RTFSetColor(kColorNone);
                }
                break;
            case kSpecialCharPrintTime:
                if (fShowEmbeds) {
                    RTFSetColor(kColorBlue);
                    BufPrintf("<time>");
                    RTFSetColor(kColorNone);
                }
                break;
            case kSpecialCharTab:
                if (fUseRTF)
                    RTFTab();
                else
                    BufPrintf("\t");
                break;
            case kSpecialCharTabFill:
                /* tab fill char, not vis in doc */
                BufPrintf(" ");
                break;
            default:
                LOGI(" AWP unhandled special char 0x%02x", ic);
                if (fShowEmbeds) {
                    RTFSetColor(kColorBlue);
                    BufPrintf("^");
                    RTFSetColor(kColorNone);
                }
            }
        } else {
            if (fUseRTF)
                RTFPrintChar(ic);
            else
                BufPrintf("%c", PrintableChar(ic));
        }
    }

    /* if there's a carriage return at the end of the line, output it now */
    if (byteCountPlusCR & kCRatEOL) {
        RTFNewPara();
    }

    /* another line processed, advance the line counter */
    fDocState.line++;

bail:
    return err;
}


/*
 * ===========================================================================
 *      AppleWorks DB
 * ===========================================================================
 */

/*
 * AppleWorks database file format, from FTN.19.xxxx.
 *
 * The overall file format is:
 *
 *  variable-sized file header
 *  0 to 8 (0 to 30 in 3.0) report records, 600 bytes each
 *  variable-sized data records
 *  $ff $ff
 *  optional tags
 */

/*
 * Decide whether or not we want to handle this file.
 */
void ReformatADB::Examine(ReformatHolder* pHolder)
{
    ReformatHolder::ReformatApplies applies = ReformatHolder::kApplicNot;

    if (pHolder->GetFileType() == kTypeADB)
        applies = ReformatHolder::kApplicProbably;

    pHolder->SetApplic(ReformatHolder::kReformatADB, applies,
        ReformatHolder::kApplicNot, ReformatHolder::kApplicNot);
}

/*
 * Reformat an AppleWorks DB document.
 */
int ReformatADB::Process(const ReformatHolder* pHolder,
    ReformatHolder::ReformatID id, ReformatHolder::ReformatPart part,
    ReformatOutput* pOutput)
{
    const uint8_t* srcPtr = pHolder->GetSourceBuf(part);
    long srcLen = pHolder->GetSourceLen(part);
    long length = srcLen;
    int retval = -1;
    int headerLen, numCats, numRecs, numReports;

    fUseRTF = false;

    /* expect header plus EOF bytes at least */
    if (srcLen <= kMinHeaderLen) {
        LOGI("  ADB truncated?");
        goto bail;
    }

    headerLen = Get16LE(srcPtr);
    if (headerLen < kMinHeaderLen || headerLen > length) {
        LOGI("  ADB bad headerLen %d, file len is %d", headerLen,
            srcLen);
        goto bail;
    }

    RTFBegin();

    /* offset +035: #of categories in file */
    numCats = *(srcPtr + 35);
    if (numCats < 1 || numCats > 0x1e) {
        LOGI("  ADB GLITCH: unexpected numCats %d", numCats);
        /* keep going... */
    }
    LOGI("  ADB should be %d categories", numCats);

    /* offset +036-037: #of records in file */
    numRecs = Get16LE(srcPtr + 36) & 0x7fff;
    LOGI("  ADB should be %d records", numRecs);

    /* offset +038: #of reports in file */
    numReports = *(srcPtr + 38);
    LOGI("  ADB should be %d reports", numReports);

    /* dump category names as first record */
    const uint8_t* catPtr;
    int catCount;
    catPtr = srcPtr + 357;
    catCount = numCats;
    while (catCount--) {
        if (catCount == numCats-1)
            BufPrintf("\"");
        else
            BufPrintf(",\"");

        int nameLen = *catPtr;
        const uint8_t* namePtr = catPtr + 1;
        while (nameLen--) {
            if (*namePtr == '"')
                BufPrintf("\"\"");
            else
                BufPrintf("%c", *namePtr);
            namePtr++;
        }

        BufPrintf("\"");

        catPtr += kCatNameLen+2;
    }
    BufPrintf("\r\n");

    /*
     * Advance pointer to first data record.  The first record contains
     * "standard values".
     *
     * Each record looks like this:
     *  $00-$01: count of bytes in remainder of record
     *  $02    : control byte, one of:
     *           $01-$7f: number of following bytes for this category
     *           $81-$9e: this (minus $80) is #of categories to skip
     *           $ff    : end of record
     *
     * The data within the categories may have special meanings, e.g. if it
     * starts with $c0 it's a date record and $d4 is a time record.
     */
    int offsetToData;
    offsetToData = kOffsetToFirstCatHeader +
                numCats*kCatHeaderLen + numReports*kReportRecordLen;
    LOGI("  ADB data records begin at offset 0x%08lx", offsetToData);
    if (offsetToData >= length) {
        LOGI("  ADB GLITCH: offset >= length %ld", length);
        goto bail;
    }

    srcPtr += offsetToData;
    length -= offsetToData;

    int rr;
    for (rr = 0; rr < numRecs && length > 0; rr++) {
        int recordRem = Read16(&srcPtr, &length);
        if (rr == 0) {
            /* skip first record */
            srcPtr += recordRem;
            length -= recordRem;
            if (*(srcPtr-1) != 0xff) {
                LOGI("  ADB GLITCH: first record skipped past 0x%02x",
                    *(srcPtr-1));
                /* keep going, I guess */
            }
            continue;
        }

        int catNum = 0;

        /* scan through all categories in this record */
        int ctrl = Read8(&srcPtr, &length);
        while (ctrl != 0xff && length > 0) {
            if (ctrl >= 0x01 && ctrl <= 0x7f) {
                /* just data */
                if (catNum == 0)
                    BufPrintf("\"");
                else
                    BufPrintf(",\"");
                if (*srcPtr == 0xc0) {
                    static const char kMonths[12][4] = {
                        "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
                    };
                    /* date entry */
                    Read8(&srcPtr, &length);    // throw out the 0xc0
                    char year[2], month, day[2];
                    year[0] = Read8(&srcPtr, &length);
                    year[1] = Read8(&srcPtr, &length);
                    month = Read8(&srcPtr, &length);
                    day[0] = Read8(&srcPtr, &length);
                    day[1] = Read8(&srcPtr, &length);
                    if (day[0] == ' ')
                        day[0] = '0';
                    BufPrintf("%c%c-%s-%s%c%c",
                        day[0], day[1],
                        month >= 'A' && month <= 'L' ? kMonths[month-'A'] : "???",
                        year[0] < '7' ? "20" : "19", year[0], year[1]);
                } else if (*srcPtr == 0xd4) {
                    /* time entry */
                    Read8(&srcPtr, &length);    // throw out the 0xd4
                    char hour, minute[2];
                    hour = Read8(&srcPtr, &length);
                    minute[0] = Read8(&srcPtr, &length);
                    minute[1] = Read8(&srcPtr, &length);
                    if (hour >= 'A' && hour < 'M') {
                        if (hour == 'A')        // don't show 00:00
                            hour = 'A' + 12;
                        BufPrintf("%02d:%c%c AM",
                            hour - 'A', minute[0], minute[1]);
                    } else if (hour >= 'M' && hour <= 'X') {
                        if (hour == 'M')        // don't show 00:00
                            hour = 'M' + 12;
                        BufPrintf("%02d:%c%c PM",
                            hour - 'M', minute[0], minute[1]);
                    }
                } else {
                    while (ctrl--) {
                        uint8_t ch = Read8(&srcPtr, &length);
                        BufPrintf("%c", ch);
                        if (ch == '"')
                            BufPrintf("%c", ch);
                    }
                }
                BufPrintf("\"");
            } else if (ctrl >= 0x81 && ctrl <= 0x9e) {
                /* skip over empty categories */
                ctrl -= 0x80;
                while (ctrl--) {
                    BufPrintf(",");
                    catNum++;
                }
                catNum--;   // don't double-count this category
            } else {
                LOGI("  ADB GLITCH: invalid ctrl byte 0x%02x", ctrl);
                break;
                /* keep going anyway? */
            }

            catNum++;
            ctrl = Read8(&srcPtr, &length);
        }
        while (catNum < numCats) {
            BufPrintf(",");
            catNum++;
        }

        /* end of record */
        RTFNewPara();
    }
    LOGI("  ADB at exit rr=%d numRecs=%d", rr, numRecs);

    int checkEnd;
    checkEnd = Read16(&srcPtr, &length);
    if (checkEnd != 0xffff) {
        LOGI("  ADB GLITCH: last read returned 0x%04x", checkEnd);
    } else {
        LOGI("  ADB found EOF; success");
    }

    /* 
     * Read the optional tags.
     */
    /* (nah) */

    RTFEnd();

    SetResultBuffer(pOutput);
    pOutput->SetOutputKind(ReformatOutput::kOutputCSV);
    retval = 0;

bail:
    return retval;
}


/*
 * ===========================================================================
 *      AppleWorks SS
 * ===========================================================================
 */

/*
 * AppleWorks spreadsheet file format, from FTN.1b.xxxx.
 *
 * The overall file format is:
 *
 *  fixed-sized file header
 *  series of variable-length row records
 *      collection of cell data
 *  $ff $ff
 *  optional tags
 *
 * The cell data can take several different forms.
 */

/*
 * Decide whether or not we want to handle this file.
 */
void ReformatASP::Examine(ReformatHolder* pHolder)
{
    ReformatHolder::ReformatApplies applies = ReformatHolder::kApplicNot;

    if (pHolder->GetFileType() == kTypeASP)
        applies = ReformatHolder::kApplicProbably;

    pHolder->SetApplic(ReformatHolder::kReformatASP, applies,
        ReformatHolder::kApplicNot, ReformatHolder::kApplicNot);
}

/*
 * Reformat an AppleWorks SS document.
 */
int ReformatASP::Process(const ReformatHolder* pHolder,
    ReformatHolder::ReformatID id, ReformatHolder::ReformatPart part,
    ReformatOutput* pOutput)
{
    const uint8_t* srcPtr = pHolder->GetSourceBuf(part);
    long srcLen = pHolder->GetSourceLen(part);
    long length = srcLen;
    int retval = -1;
    const FileHeader* pFileHeader;
    bool aw30flag;

    ASSERT(sizeof(FileHeader) == kFileHeaderSize);

    fUseRTF = false;

    /* must at least have the header */
    if (length < kFileHeaderSize) {
        LOGI("  ADB truncated?");
        goto bail;
    }

    RTFBegin();

    pFileHeader = (const FileHeader*) srcPtr;
    aw30flag = false;
    if (pFileHeader->ssMinVers != 0)
        aw30flag = true;
    LOGI("  ASP ssMinVers=0x%02x, aw30flag=%d",
        pFileHeader->ssMinVers, aw30flag);

    /*
     * Advance pointer past file header.  v3.0 adds a couple of extra bytes
     * right after the end of the header that the FTN says we should just
     * ignore.
     */
    srcPtr += kFileHeaderSize;
    length -= kFileHeaderSize;
    if (aw30flag) {
        srcPtr += 2;
        length -= 2;
    }

    /*
     * Loop through the file, reading one row at a time.  There is no row
     * count because spreadsheets are sparse.
     *
     * We assume that rows are stored from top to bottom.  The count begins
     * with 1, not zero.
     */
    fCurrentRow = 1;
    while (length > 0) {
        uint16_t rowLen;
        int rowNum;

        /* row length or EOF marker */
        rowLen = Read16(&srcPtr, &length);
        if (rowLen == 0xffff) {
            LOGI("  ASP found EOF marker, we're done");
            break;
        }

        rowNum = Read16(&srcPtr, &length);
        //LOGI("  ASP  process row %d (cur=%d)", rowNum, currentRow);

        /* fill out empty rows */
        ASSERT(fCurrentRow <= rowNum);
        while (fCurrentRow < rowNum) {
            BufPrintf("\"\"\r\n");
            fCurrentRow++;
        }

        if (ProcessRow(rowNum, &srcPtr, &length) != 0)
            break;

        fCurrentRow++;
    }

    /* 
     * Read the optional tags.
     */
    /* (nah) */

    RTFEnd();

    SetResultBuffer(pOutput);
    pOutput->SetOutputKind(ReformatOutput::kOutputCSV);
    retval = 0;

bail:
    return retval;
}

/*
 * Process one row of spreadsheet data.
 */
int ReformatASP::ProcessRow(int rowNum, const uint8_t** pSrcPtr, long* pLength)
{
    uint8_t ctrl;
    bool first = true;

    fCurrentCol = 0;
    while (*pLength > 0) {
        ctrl = Read8(pSrcPtr, pLength);
        if (ctrl >= 0x01 && ctrl <= 0x7f) {
            if (!first)
                BufPrintf(",");
            else
                first = false;
            /* read cell entry contents */
            if (ctrl > *pLength) {
                LOGI("  ASP GLITCH: cell len exceeds file len (%d %d)",
                    *pLength, ctrl);
                break;
            }
            ProcessCell(*pSrcPtr, ctrl);
            (*pSrcPtr) += ctrl;
            *pLength -= ctrl;
        } else if (ctrl >= 0x81 && ctrl <= 0xfe) {
            /* skip this many columns */
            if (!first)
                BufPrintf(",");
            else
                first = false;

            ctrl -= 0x80;
            ctrl--;
            while (ctrl--) {
                BufPrintf(",");
                fCurrentCol++;
            }
        } else if (ctrl == 0xff) {
            /* end of row */
            break;
        } else {
            /* unexpected 0x00 or 0x80 */
            LOGI("  ASP GLITCH: unexpected ctrl byte 0x%02x", ctrl);
            break;
        }

        fCurrentCol++;
    }

    BufPrintf("\r\n");

    return 0;
}

/*
 * Process the contents of a single cell.
 */
void ReformatASP::ProcessCell(const uint8_t* srcPtr, long cellLength)
{
    uint8_t flag1, flag2;
    double dval;
    int i;

    BufPrintf("\"");

    flag1 = *srcPtr++;
    cellLength--;

    if (flag1 & 0x80) {     /* bit 7 set? */
        /* this is a value, not a label */
        flag2 = *srcPtr++;
        cellLength--;

        if (flag1 & 0x20) { /* bit 5 set? */
            /* this is a "value constant" */
            dval = ConvertSANEDouble(srcPtr);
            BufPrintf("%f", dval);
        } else {
            if (flag2 & 0x08) { /* bit 3 set? */
                /* this is a "value label", AW30+ only */
                /* skip over cached string result */
                if (*srcPtr >= cellLength) {
                    LOGI("  ASP GLITCH: invalid value label str len");
                    BufPrintf("GLITCH");
                } else {
                    srcPtr += *srcPtr +1;
                    /* output tokens */
                    while (cellLength > 0)
                        PrintToken(&srcPtr, &cellLength);
                }
            } else {
                /* this is a "value formula" */
                //dval = ConvertSANEDouble(srcPtr);
                /* skip over cached computation result */
                if (cellLength <= kSANELen) {
                    LOGI("  ASP GLITCH: invalid value formula len");
                    BufPrintf("GLITCH");
                } else {
                    srcPtr += kSANELen;
                    cellLength -= kSANELen;
                    /* [tokens] */
                    while (cellLength > 0)
                        PrintToken(&srcPtr, &cellLength);
                }
            }
        }
    } else {
        /* this is a label, not a value */

        if (flag1 & 0x20) { /* bit 5 set? */
            /* propagated label cell */
            for (i = 0; i < kPropCount; i++)
                BufPrintQChar(*srcPtr);
        } else {
            /* regular label cell */
            for (i = 0; i < cellLength; i++)
                BufPrintQChar(srcPtr[i]);
        }
    }

    BufPrintf("\"");
}

/*
 * Print the AppleWorks SS token pointed to by srcPtr.  Some tokens require
 * several bytes to express.
 */
void ReformatASP::PrintToken(const uint8_t** pSrcPtr, long* pLength)
{
    /* string constants; note these must NOT contain '"' chars */
    const int kTokenStart = 0xc0;
    const char* tokenTable[0x100-kTokenStart] = {
        /*0xc0*/ "@Deg",    "@Rad",     "@Pi",      "@True",
        /*0xc4*/ "@False",  "@Not",     "@IsBlank", "@IsNA",
        /*0xc8*/ "@IsError", "@Exp",    "@Ln",      "@Log",
        /*0xcc*/ "@Cos",    "@Sin",     "@Tan",     "@ACos",
        /*0xd0*/ "@ASin",   "@ATan2",   "@ATan",    "@Mod",
        /*0xd4*/ "@FV",     "@PV",      "@PMT",     "@Term",
        /*0xd8*/ "@Rate",   "@Round",   "@Or",      "@And",
        /*0xdc*/ "@Sum",    "@Avg",     "@Choose",  "@Count",
        /*0xe0*/ "@Error",  "@IRR",     "@If",      "@Int",
        /*0xe4*/ "@Lookup", "@Max",     "@Min",     "@NA",
        /*0xe8*/ "@NPV",    "@Sqrt",    "@Abs",     "",
        /*0xec*/ "<>",      ">=",       "<=",       "=",
        /*0xf0*/ ">",       "<",        ",",        "^",
        /*0xf4*/ ")",       "-",        "+",        "/",
        /*0xf8*/ "*",       "(",        "-" /*unary*/, "+" /*unary*/,
        /*0xfc*/ "...",     "",         "",         ""
    };
    uint8_t token;

    token = Read8(pSrcPtr, pLength);
    if (token < kTokenStart) {
        LOGI("  ASP GLITCH: funky token 0x%02x", token);
        return;
    }

    BufPrintf("%s", tokenTable[token - kTokenStart]);
    if (token == 0xe0 || token == 0xe7) {
        /* @Error and @NA followed by three zero bytes */
        if (*pLength < 3) {
            LOGI("  ASP GLITCH: ran off end processing tokens");
            return;
        }
        *pSrcPtr += 3;
        *pLength -= 3;
    } else if (token == 0xfd) {
        /* SANE double number */
        if (*pLength < 8) {
            LOGI("  ASP GLITCH: not enough left to grab a SANE");
            return;
        }
        double dval = ConvertSANEDouble(*pSrcPtr);
        BufPrintf("%f", dval);
        *pSrcPtr += 8;
        *pLength -= 8;
    } else if (token == 0xfe) {
        /* row, column reference (relative to current cell) */
        int row, col;
        col = Read8(pSrcPtr, pLength);
        if (col >= 128)
            col -= 256;
        row = Read16(pSrcPtr, pLength);
        if (row >= 32768)
            row -= 65536;
        BufPrintf("%s%d", PrintCol(fCurrentCol+col), fCurrentRow+row);
    } else if (token == 0xff) {
        /* Pascal string */
        int i;
        i = Read8(pSrcPtr, pLength);
        if (i > *pLength) {
            LOGI("  ASP GLITCH: string exceeds cell len");
            return;
        }
        while (i--) {
            uint8_t ch = Read8(pSrcPtr, pLength);
            BufPrintQChar(ch);
        }
    }
}

/*
 * Format the current column number into something like 'A' or 'BA'.  This
 * stores the value in fPrintColBuf and returns a pointer to it.
 */
const char* ReformatASP::PrintCol(int col)
{
    if (col < 0 || col >= 702) {
        LOGI("  ASP can't PrintCol(%d)", col);
        fPrintColBuf[0] = fPrintColBuf[1] = '?';
        fPrintColBuf[2] = '\0';
    } else if (col < 26) {
        fPrintColBuf[0] = 'A' + col;
        fPrintColBuf[1] = '\0';
    } else {
        fPrintColBuf[0] = 'A' + col / 26;
        fPrintColBuf[1] = 'A' + col % 26;
        fPrintColBuf[2] = '\0';
    }
    return fPrintColBuf;
}

/*
 * Convert a 64-bit SANE double to an x86 double.  The format is the same as
 * IEEE 754, which happily is the same used by the VC++6.0 compiler.
 *
 * Info from http://www.cs.trinity.edu/About/The_Courses/cs2322/ieee-fp.html
 * (also http://www.psc.edu/general/software/packages/ieee/ieee.html).
 *
 * -----
 * The 64-bit double format is divided into three fields as shown below:
 *
 *    1       11               52
 *  +-------------------------------------+
 *  | s |     e    |           f          |
 *  +-------------------------------------+
 *
 * The value v of the number is determined by these fields as shown in
 * the following table:
 *
 * Values of double-format numbers (64 bits) 
 * ___________________________________________________________
 * e        f         v                           class of v
 * ___________________________________________________________
 * 0<e<2047 (any)     v=(-1)s x 2(e-1023) x (1.f)  normalized
 * e=0      f!=0      v=(-1)s x 2(e-1022) x (0.f)  denormalized
 * e=0      f=0       v=(-1)s x 0                 zero
 * e=2047   f=0       v=(-1)s x infinity          infinity
 * e=2047   f!=0      v is a NaN                  NaN
 *
 * For example, the double representation (in hex notation) of 1.5 is 
 * 3FF8000000000000
 * is 
 * 3F847AE147AE147A
 * -----
 */
double ReformatASP::ConvertSANEDouble(const uint8_t* srcPtr)
{
    double newVal;
    uint8_t* dptr;
    int i;

    ASSERT(sizeof(newVal) == kSANELen);

    dptr = (uint8_t*) &newVal;
    for (i = 0; i < kSANELen; i++)
        *dptr++ = *srcPtr++;

    return newVal;
}
